activerecord 2.3.4 → 2.3.5
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 +8 -0
- data/Rakefile +1 -1
- data/lib/active_record/associations.rb +10 -7
- data/lib/active_record/associations/association_proxy.rb +7 -8
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
- data/lib/active_record/associations/has_one_association.rb +9 -0
- data/lib/active_record/autosave_association.rb +32 -23
- data/lib/active_record/base.rb +7 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +6 -2
- data/lib/active_record/fixtures.rb +1 -1
- data/lib/active_record/locking/optimistic.rb +0 -33
- data/lib/active_record/locking/pessimistic.rb +0 -22
- data/lib/active_record/nested_attributes.rb +101 -38
- data/lib/active_record/validations.rb +35 -35
- data/lib/active_record/version.rb +1 -1
- data/lib/activerecord.rb +1 -0
- data/test/cases/associations/has_many_associations_test.rb +12 -0
- data/test/cases/associations/has_many_through_associations_test.rb +22 -0
- data/test/cases/associations/has_one_associations_test.rb +21 -0
- data/test/cases/autosave_association_test.rb +230 -11
- data/test/cases/base_test.rb +2 -0
- data/test/cases/connection_test_mysql.rb +8 -0
- data/test/cases/fixtures_test.rb +2 -2
- data/test/cases/locking_test.rb +0 -18
- data/test/cases/nested_attributes_test.rb +109 -37
- data/test/cases/reflection_test.rb +3 -3
- data/test/cases/validations_i18n_test.rb +8 -0
- data/test/cases/validations_test.rb +37 -9
- data/test/fixtures/accounts.yml +1 -0
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/models/company.rb +10 -0
- data/test/models/pirate.rb +9 -2
- data/test/models/treasure.rb +2 -0
- data/test/schema/mysql_specific_schema.rb +12 -0
- data/test/schema/schema.rb +1 -0
- metadata +4 -4
data/CHANGELOG
CHANGED
data/Rakefile
CHANGED
@@ -192,7 +192,7 @@ spec = Gem::Specification.new do |s|
|
|
192
192
|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
193
193
|
end
|
194
194
|
|
195
|
-
s.add_dependency('activesupport', '= 2.3.
|
195
|
+
s.add_dependency('activesupport', '= 2.3.5' + PKG_BUILD)
|
196
196
|
|
197
197
|
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
|
198
198
|
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
|
@@ -275,9 +275,10 @@ module ActiveRecord
|
|
275
275
|
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
|
276
276
|
# aware of, mostly involving the saving of associated objects.
|
277
277
|
#
|
278
|
-
# Unless you
|
279
|
-
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association
|
280
|
-
#
|
278
|
+
# Unless you set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
|
279
|
+
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
|
280
|
+
# to +true+ will _always_ save the members, whereas setting it to +false+ will
|
281
|
+
# _never_ save the members.
|
281
282
|
#
|
282
283
|
# === One-to-one associations
|
283
284
|
#
|
@@ -874,7 +875,9 @@ module ActiveRecord
|
|
874
875
|
# if the real class name is Person, you'll have to specify it with this option.
|
875
876
|
# [:conditions]
|
876
877
|
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
877
|
-
# SQL fragment, such as <tt>rank = 5</tt>.
|
878
|
+
# SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
|
879
|
+
# is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create an enabled account with <tt>@company.create_account</tt>
|
880
|
+
# or <tt>@company.build_account</tt>.
|
878
881
|
# [:order]
|
879
882
|
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
880
883
|
# such as <tt>last_name, first_name DESC</tt>.
|
@@ -1324,8 +1327,8 @@ module ActiveRecord
|
|
1324
1327
|
end
|
1325
1328
|
|
1326
1329
|
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
1327
|
-
ids = (new_value || []).reject { |nid| nid.blank? }
|
1328
|
-
send("#{reflection.name}=", reflection.klass.find(ids))
|
1330
|
+
ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i)
|
1331
|
+
send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
|
1329
1332
|
end
|
1330
1333
|
end
|
1331
1334
|
end
|
@@ -1408,7 +1411,7 @@ module ActiveRecord
|
|
1408
1411
|
if reflection.options.include?(:dependent)
|
1409
1412
|
# Add polymorphic type if the :as option is present
|
1410
1413
|
dependent_conditions = []
|
1411
|
-
dependent_conditions << "#{reflection.primary_key_name} = \#{record.
|
1414
|
+
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
|
1412
1415
|
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
|
1413
1416
|
dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
|
1414
1417
|
dependent_conditions << extra_conditions if extra_conditions
|
@@ -210,15 +210,14 @@ module ActiveRecord
|
|
210
210
|
# Forwards any missing method call to the \target.
|
211
211
|
def method_missing(method, *args)
|
212
212
|
if load_target
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
213
|
+
if @target.respond_to?(method)
|
214
|
+
if block_given?
|
215
|
+
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
216
|
+
else
|
217
|
+
@target.send(method, *args)
|
218
|
+
end
|
220
219
|
else
|
221
|
-
|
220
|
+
super
|
222
221
|
end
|
223
222
|
end
|
224
223
|
end
|
@@ -24,8 +24,8 @@ module ActiveRecord
|
|
24
24
|
|
25
25
|
def has_primary_key?
|
26
26
|
return @has_primary_key unless @has_primary_key.nil?
|
27
|
-
@has_primary_key = (
|
28
|
-
|
27
|
+
@has_primary_key = (@owner.connection.supports_primary_key? &&
|
28
|
+
@owner.connection.primary_key(@reflection.options[:join_table]))
|
29
29
|
end
|
30
30
|
|
31
31
|
protected
|
@@ -8,18 +8,21 @@ module ActiveRecord
|
|
8
8
|
|
9
9
|
def create(attrs = {}, replace_existing = true)
|
10
10
|
new_record(replace_existing) do |reflection|
|
11
|
+
attrs = merge_with_conditions(attrs)
|
11
12
|
reflection.create_association(attrs)
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
15
16
|
def create!(attrs = {}, replace_existing = true)
|
16
17
|
new_record(replace_existing) do |reflection|
|
18
|
+
attrs = merge_with_conditions(attrs)
|
17
19
|
reflection.create_association!(attrs)
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
23
|
def build(attrs = {}, replace_existing = true)
|
22
24
|
new_record(replace_existing) do |reflection|
|
25
|
+
attrs = merge_with_conditions(attrs)
|
23
26
|
reflection.build_association(attrs)
|
24
27
|
end
|
25
28
|
end
|
@@ -119,6 +122,12 @@ module ActiveRecord
|
|
119
122
|
|
120
123
|
record
|
121
124
|
end
|
125
|
+
|
126
|
+
def merge_with_conditions(attrs={})
|
127
|
+
attrs ||= {}
|
128
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
129
|
+
attrs
|
130
|
+
end
|
122
131
|
end
|
123
132
|
end
|
124
133
|
end
|
@@ -159,7 +159,7 @@ module ActiveRecord
|
|
159
159
|
def add_autosave_association_callbacks(reflection)
|
160
160
|
save_method = "autosave_associated_records_for_#{reflection.name}"
|
161
161
|
validation_method = "validate_associated_records_for_#{reflection.name}"
|
162
|
-
validate
|
162
|
+
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
|
163
163
|
|
164
164
|
case reflection.macro
|
165
165
|
when :has_many, :has_and_belongs_to_many
|
@@ -170,7 +170,10 @@ module ActiveRecord
|
|
170
170
|
after_create save_method
|
171
171
|
after_update save_method
|
172
172
|
|
173
|
-
|
173
|
+
if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
|
174
|
+
define_method(validation_method) { validate_collection_association(reflection) }
|
175
|
+
validate validation_method
|
176
|
+
end
|
174
177
|
else
|
175
178
|
case reflection.macro
|
176
179
|
when :has_one
|
@@ -180,7 +183,11 @@ module ActiveRecord
|
|
180
183
|
define_method(save_method) { save_belongs_to_association(reflection) }
|
181
184
|
before_save save_method
|
182
185
|
end
|
183
|
-
|
186
|
+
|
187
|
+
if force_validation
|
188
|
+
define_method(validation_method) { validate_single_association(reflection) }
|
189
|
+
validate validation_method
|
190
|
+
end
|
184
191
|
end
|
185
192
|
end
|
186
193
|
end
|
@@ -224,10 +231,8 @@ module ActiveRecord
|
|
224
231
|
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
225
232
|
# turned on for the association specified by +reflection+.
|
226
233
|
def validate_single_association(reflection)
|
227
|
-
if
|
228
|
-
|
229
|
-
association_valid?(reflection, association)
|
230
|
-
end
|
234
|
+
if (association = association_instance_get(reflection.name)) && !association.target.nil?
|
235
|
+
association_valid?(reflection, association)
|
231
236
|
end
|
232
237
|
end
|
233
238
|
|
@@ -235,7 +240,7 @@ module ActiveRecord
|
|
235
240
|
# <tt>:autosave</tt> is turned on for the association specified by
|
236
241
|
# +reflection+.
|
237
242
|
def validate_collection_association(reflection)
|
238
|
-
if
|
243
|
+
if association = association_instance_get(reflection.name)
|
239
244
|
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
|
240
245
|
records.each { |record| association_valid?(reflection, record) }
|
241
246
|
end
|
@@ -244,16 +249,15 @@ module ActiveRecord
|
|
244
249
|
|
245
250
|
# Returns whether or not the association is valid and applies any errors to
|
246
251
|
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
247
|
-
# enabled records if they're marked_for_destruction
|
252
|
+
# enabled records if they're marked_for_destruction? or destroyed.
|
248
253
|
def association_valid?(reflection, association)
|
254
|
+
return true if association.destroyed? || association.marked_for_destruction?
|
255
|
+
|
249
256
|
unless valid = association.valid?
|
250
257
|
if reflection.options[:autosave]
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
error.attribute = "#{reflection.name}_#{attribute}"
|
255
|
-
errors.add(error) unless errors.on(error.attribute)
|
256
|
-
end
|
258
|
+
association.errors.each_error do |attribute, error|
|
259
|
+
attribute = "#{reflection.name}.#{attribute}"
|
260
|
+
errors.add(attribute, error.dup) unless errors.on(attribute)
|
257
261
|
end
|
258
262
|
else
|
259
263
|
errors.add(reflection.name)
|
@@ -283,9 +287,11 @@ module ActiveRecord
|
|
283
287
|
|
284
288
|
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
|
285
289
|
records.each do |record|
|
290
|
+
next if record.destroyed?
|
291
|
+
|
286
292
|
if autosave && record.marked_for_destruction?
|
287
293
|
association.destroy(record)
|
288
|
-
elsif @new_record_before_save || record.new_record?
|
294
|
+
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
289
295
|
if autosave
|
290
296
|
association.send(:insert_record, record, false, false)
|
291
297
|
else
|
@@ -311,14 +317,17 @@ module ActiveRecord
|
|
311
317
|
# This all happens inside a transaction, _if_ the Transactions module is included into
|
312
318
|
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
313
319
|
def save_has_one_association(reflection)
|
314
|
-
if (association = association_instance_get(reflection.name)) && !association.target.nil?
|
320
|
+
if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
|
315
321
|
autosave = reflection.options[:autosave]
|
316
322
|
|
317
323
|
if autosave && association.marked_for_destruction?
|
318
324
|
association.destroy
|
319
|
-
|
320
|
-
|
321
|
-
association.
|
325
|
+
else
|
326
|
+
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
327
|
+
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
328
|
+
association[reflection.primary_key_name] = key
|
329
|
+
association.save(!autosave)
|
330
|
+
end
|
322
331
|
end
|
323
332
|
end
|
324
333
|
end
|
@@ -332,12 +341,12 @@ module ActiveRecord
|
|
332
341
|
# This all happens inside a transaction, _if_ the Transactions module is included into
|
333
342
|
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
334
343
|
def save_belongs_to_association(reflection)
|
335
|
-
if association = association_instance_get(reflection.name)
|
344
|
+
if (association = association_instance_get(reflection.name)) && !association.destroyed?
|
336
345
|
autosave = reflection.options[:autosave]
|
337
346
|
|
338
347
|
if autosave && association.marked_for_destruction?
|
339
348
|
association.destroy
|
340
|
-
|
349
|
+
elsif autosave != false
|
341
350
|
association.save(!autosave) if association.new_record? || autosave
|
342
351
|
|
343
352
|
if association.updated?
|
@@ -352,4 +361,4 @@ module ActiveRecord
|
|
352
361
|
end
|
353
362
|
end
|
354
363
|
end
|
355
|
-
end
|
364
|
+
end
|
data/lib/active_record/base.rb
CHANGED
@@ -2567,6 +2567,7 @@ module ActiveRecord #:nodoc:
|
|
2567
2567
|
# options, use <tt>#destroy</tt>.
|
2568
2568
|
def delete
|
2569
2569
|
self.class.delete(id) unless new_record?
|
2570
|
+
@destroyed = true
|
2570
2571
|
freeze
|
2571
2572
|
end
|
2572
2573
|
|
@@ -2581,6 +2582,7 @@ module ActiveRecord #:nodoc:
|
|
2581
2582
|
)
|
2582
2583
|
end
|
2583
2584
|
|
2585
|
+
@destroyed = true
|
2584
2586
|
freeze
|
2585
2587
|
end
|
2586
2588
|
|
@@ -2840,6 +2842,11 @@ module ActiveRecord #:nodoc:
|
|
2840
2842
|
@attributes.frozen?
|
2841
2843
|
end
|
2842
2844
|
|
2845
|
+
# Returns +true+ if the record has been destroyed.
|
2846
|
+
def destroyed?
|
2847
|
+
@destroyed
|
2848
|
+
end
|
2849
|
+
|
2843
2850
|
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
|
2844
2851
|
# attributes will be marked as read only since they cannot be saved.
|
2845
2852
|
def readonly?
|
@@ -7,7 +7,8 @@ module MysqlCompat #:nodoc:
|
|
7
7
|
raise 'Mysql not loaded' unless defined?(::Mysql)
|
8
8
|
|
9
9
|
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
|
10
|
-
return if target.instance_methods.include?('all_hashes')
|
10
|
+
return if target.instance_methods.include?('all_hashes') ||
|
11
|
+
target.instance_methods.include?(:all_hashes)
|
11
12
|
|
12
13
|
# Ruby driver has a version string and returns null values in each_hash
|
13
14
|
# C driver >= 2.7 returns null values in each_hash
|
@@ -63,12 +64,15 @@ module ActiveRecord
|
|
63
64
|
raise
|
64
65
|
end
|
65
66
|
end
|
67
|
+
|
66
68
|
MysqlCompat.define_all_hashes_method!
|
67
69
|
|
68
70
|
mysql = Mysql.init
|
69
71
|
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
70
72
|
|
71
|
-
|
73
|
+
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
|
74
|
+
options = [host, username, password, database, port, socket, default_flags]
|
75
|
+
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
@@ -23,16 +23,6 @@ module ActiveRecord
|
|
23
23
|
# p2.first_name = "should fail"
|
24
24
|
# p2.save # Raises a ActiveRecord::StaleObjectError
|
25
25
|
#
|
26
|
-
# Optimistic locking will also check for stale data when objects are destroyed. Example:
|
27
|
-
#
|
28
|
-
# p1 = Person.find(1)
|
29
|
-
# p2 = Person.find(1)
|
30
|
-
#
|
31
|
-
# p1.first_name = "Michael"
|
32
|
-
# p1.save
|
33
|
-
#
|
34
|
-
# p2.destroy # Raises a ActiveRecord::StaleObjectError
|
35
|
-
#
|
36
26
|
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
37
27
|
# or otherwise apply the business logic needed to resolve the conflict.
|
38
28
|
#
|
@@ -49,7 +39,6 @@ module ActiveRecord
|
|
49
39
|
base.lock_optimistically = true
|
50
40
|
|
51
41
|
base.alias_method_chain :update, :lock
|
52
|
-
base.alias_method_chain :destroy, :lock
|
53
42
|
base.alias_method_chain :attributes_from_column_definition, :lock
|
54
43
|
|
55
44
|
class << base
|
@@ -109,28 +98,6 @@ module ActiveRecord
|
|
109
98
|
end
|
110
99
|
end
|
111
100
|
|
112
|
-
def destroy_with_lock #:nodoc:
|
113
|
-
return destroy_without_lock unless locking_enabled?
|
114
|
-
|
115
|
-
unless new_record?
|
116
|
-
lock_col = self.class.locking_column
|
117
|
-
previous_value = send(lock_col).to_i
|
118
|
-
|
119
|
-
affected_rows = connection.delete(
|
120
|
-
"DELETE FROM #{self.class.quoted_table_name} " +
|
121
|
-
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
|
122
|
-
"AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
|
123
|
-
"#{self.class.name} Destroy"
|
124
|
-
)
|
125
|
-
|
126
|
-
unless affected_rows == 1
|
127
|
-
raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
freeze
|
132
|
-
end
|
133
|
-
|
134
101
|
module ClassMethods
|
135
102
|
DEFAULT_LOCKING_COLUMN = 'lock_version'
|
136
103
|
|
@@ -1,25 +1,3 @@
|
|
1
|
-
# Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
|
2
|
-
#
|
3
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
# a copy of this software and associated documentation files (the
|
5
|
-
# "Software"), to deal in the Software without restriction, including
|
6
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
# permit persons to whom the Software is furnished to do so, subject
|
9
|
-
# to the following conditions:
|
10
|
-
#
|
11
|
-
# The above copyright notice and this permission notice shall be
|
12
|
-
# included in all copies or substantial portions of the Software.
|
13
|
-
#
|
14
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
-
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
18
|
-
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
19
|
-
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
-
|
22
|
-
|
23
1
|
module ActiveRecord
|
24
2
|
module Locking
|
25
3
|
# Locking::Pessimistic provides support for row-level locking using
|
@@ -1,9 +1,12 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module NestedAttributes #:nodoc:
|
3
|
+
class TooManyRecords < ActiveRecordError
|
4
|
+
end
|
5
|
+
|
3
6
|
def self.included(base)
|
4
7
|
base.extend(ClassMethods)
|
5
|
-
base.class_inheritable_accessor :
|
6
|
-
base.
|
8
|
+
base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
|
9
|
+
base.nested_attributes_options = {}
|
7
10
|
end
|
8
11
|
|
9
12
|
# == Nested Attributes
|
@@ -62,10 +65,10 @@ module ActiveRecord
|
|
62
65
|
# accepts_nested_attributes_for :avatar, :allow_destroy => true
|
63
66
|
# end
|
64
67
|
#
|
65
|
-
# Now, when you add the <tt>
|
68
|
+
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
|
66
69
|
# value that evaluates to +true+, you will destroy the associated model:
|
67
70
|
#
|
68
|
-
# member.avatar_attributes = { :id => '2', :
|
71
|
+
# member.avatar_attributes = { :id => '2', :_destroy => '1' }
|
69
72
|
# member.avatar.marked_for_destruction? # => true
|
70
73
|
# member.save
|
71
74
|
# member.avatar #=> nil
|
@@ -85,14 +88,14 @@ module ActiveRecord
|
|
85
88
|
# the attribute hash.
|
86
89
|
#
|
87
90
|
# For each hash that does _not_ have an <tt>id</tt> key a new record will
|
88
|
-
# be instantiated, unless the hash also contains a <tt>
|
91
|
+
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
|
89
92
|
# that evaluates to +true+.
|
90
93
|
#
|
91
94
|
# params = { :member => {
|
92
95
|
# :name => 'joe', :posts_attributes => [
|
93
96
|
# { :title => 'Kari, the awesome Ruby documentation browser!' },
|
94
97
|
# { :title => 'The egalitarian assumption of the modern citizen' },
|
95
|
-
# { :title => '', :
|
98
|
+
# { :title => '', :_destroy => '1' } # this will be ignored
|
96
99
|
# ]
|
97
100
|
# }}
|
98
101
|
#
|
@@ -123,6 +126,22 @@ module ActiveRecord
|
|
123
126
|
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
|
124
127
|
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
|
125
128
|
#
|
129
|
+
# Alternatively, :reject_if also accepts a symbol for using methods:
|
130
|
+
#
|
131
|
+
# class Member < ActiveRecord::Base
|
132
|
+
# has_many :posts
|
133
|
+
# accepts_nested_attributes_for :posts, :reject_if => :new_record?
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# class Member < ActiveRecord::Base
|
137
|
+
# has_many :posts
|
138
|
+
# accepts_nested_attributes_for :posts, :reject_if => :reject_posts
|
139
|
+
#
|
140
|
+
# def reject_posts(attributed)
|
141
|
+
# attributed['title].blank?
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
#
|
126
145
|
# If the hash contains an <tt>id</tt> key that matches an already
|
127
146
|
# associated record, the matching record will be modified:
|
128
147
|
#
|
@@ -140,7 +159,7 @@ module ActiveRecord
|
|
140
159
|
# By default the associated records are protected from being destroyed. If
|
141
160
|
# you want to destroy any of the associated records through the attributes
|
142
161
|
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
|
143
|
-
# option. This will allow you to also use the <tt>
|
162
|
+
# option. This will allow you to also use the <tt>_destroy</tt> key to
|
144
163
|
# destroy existing records:
|
145
164
|
#
|
146
165
|
# class Member < ActiveRecord::Base
|
@@ -149,7 +168,7 @@ module ActiveRecord
|
|
149
168
|
# end
|
150
169
|
#
|
151
170
|
# params = { :member => {
|
152
|
-
# :posts_attributes => [{ :id => '2', :
|
171
|
+
# :posts_attributes => [{ :id => '2', :_destroy => '1' }]
|
153
172
|
# }}
|
154
173
|
#
|
155
174
|
# member.attributes = params['member']
|
@@ -172,14 +191,23 @@ module ActiveRecord
|
|
172
191
|
# Supported options:
|
173
192
|
# [:allow_destroy]
|
174
193
|
# If true, destroys any members from the attributes hash with a
|
175
|
-
# <tt>
|
194
|
+
# <tt>_destroy</tt> key and a value that evaluates to +true+
|
176
195
|
# (eg. 1, '1', true, or 'true'). This option is off by default.
|
177
196
|
# [:reject_if]
|
178
|
-
# Allows you to specify a Proc
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
197
|
+
# Allows you to specify a Proc or a Symbol pointing to a method
|
198
|
+
# that checks whether a record should be built for a certain attribute
|
199
|
+
# hash. The hash is passed to the supplied Proc or the method
|
200
|
+
# and it should return either +true+ or +false+. When no :reject_if
|
201
|
+
# is specified, a record will be built for all attribute hashes that
|
202
|
+
# do not have a <tt>_destroy</tt> value that evaluates to true.
|
203
|
+
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
|
204
|
+
# that will reject a record where all the attributes are blank.
|
205
|
+
# [:limit]
|
206
|
+
# Allows you to specify the maximum number of the associated records that
|
207
|
+
# can be processes with the nested attributes. If the size of the
|
208
|
+
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
209
|
+
# exception is raised. If omitted, any number associations can be processed.
|
210
|
+
# Note that the :limit option is only applicable to one-to-many associations.
|
183
211
|
#
|
184
212
|
# Examples:
|
185
213
|
# # creates avatar_attributes=
|
@@ -189,7 +217,7 @@ module ActiveRecord
|
|
189
217
|
def accepts_nested_attributes_for(*attr_names)
|
190
218
|
options = { :allow_destroy => false }
|
191
219
|
options.update(attr_names.extract_options!)
|
192
|
-
options.assert_valid_keys(:allow_destroy, :reject_if)
|
220
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
|
193
221
|
|
194
222
|
attr_names.each do |association_name|
|
195
223
|
if reflection = reflect_on_association(association_name)
|
@@ -201,16 +229,18 @@ module ActiveRecord
|
|
201
229
|
end
|
202
230
|
|
203
231
|
reflection.options[:autosave] = true
|
204
|
-
self.
|
232
|
+
self.nested_attributes_options[association_name.to_sym] = options
|
205
233
|
|
206
234
|
# def pirate_attributes=(attributes)
|
207
235
|
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
|
208
236
|
# end
|
209
237
|
class_eval %{
|
210
238
|
def #{association_name}_attributes=(attributes)
|
211
|
-
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes
|
239
|
+
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
212
240
|
end
|
213
241
|
}, __FILE__, __LINE__
|
242
|
+
|
243
|
+
add_autosave_association_callbacks(reflection)
|
214
244
|
else
|
215
245
|
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
216
246
|
end
|
@@ -223,15 +253,25 @@ module ActiveRecord
|
|
223
253
|
# destruction of this association.
|
224
254
|
#
|
225
255
|
# See ActionView::Helpers::FormHelper::fields_for for more info.
|
226
|
-
def
|
256
|
+
def _destroy
|
227
257
|
marked_for_destruction?
|
228
258
|
end
|
229
259
|
|
260
|
+
# Deal with deprecated _delete.
|
261
|
+
#
|
262
|
+
def _delete #:nodoc:
|
263
|
+
ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
|
264
|
+
_destroy
|
265
|
+
end
|
266
|
+
|
230
267
|
private
|
231
268
|
|
232
269
|
# Attribute hash keys that should not be assigned as normal attributes.
|
233
270
|
# These hash keys are nested attributes implementation details.
|
234
|
-
|
271
|
+
#
|
272
|
+
# TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
|
273
|
+
# removed.
|
274
|
+
UNASSIGNABLE_KEYS = %w( id _destroy _delete )
|
235
275
|
|
236
276
|
# Assigns the given attributes to the association.
|
237
277
|
#
|
@@ -240,17 +280,23 @@ module ActiveRecord
|
|
240
280
|
# record will be built.
|
241
281
|
#
|
242
282
|
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
243
|
-
# <tt>:
|
283
|
+
# <tt>:_destroy</tt> key set to a truthy value, then the existing record
|
244
284
|
# will be marked for destruction.
|
245
|
-
def assign_nested_attributes_for_one_to_one_association(association_name, attributes
|
246
|
-
|
285
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
286
|
+
options = self.nested_attributes_options[association_name]
|
287
|
+
attributes = attributes.with_indifferent_access
|
247
288
|
|
248
289
|
if attributes['id'].blank?
|
249
290
|
unless reject_new_record?(association_name, attributes)
|
250
|
-
|
291
|
+
method = "build_#{association_name}"
|
292
|
+
if respond_to?(method)
|
293
|
+
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
294
|
+
else
|
295
|
+
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
296
|
+
end
|
251
297
|
end
|
252
298
|
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
|
253
|
-
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
|
299
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
254
300
|
end
|
255
301
|
end
|
256
302
|
|
@@ -259,7 +305,7 @@ module ActiveRecord
|
|
259
305
|
# Hashes with an <tt>:id</tt> value matching an existing associated record
|
260
306
|
# will update that record. Hashes without an <tt>:id</tt> value will build
|
261
307
|
# a new record for the association. Hashes with a matching <tt>:id</tt>
|
262
|
-
# value and a <tt>:
|
308
|
+
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
|
263
309
|
# matched record for destruction.
|
264
310
|
#
|
265
311
|
# For example:
|
@@ -267,7 +313,7 @@ module ActiveRecord
|
|
267
313
|
# assign_nested_attributes_for_collection_association(:people, {
|
268
314
|
# '1' => { :id => '1', :name => 'Peter' },
|
269
315
|
# '2' => { :name => 'John' },
|
270
|
-
# '3' => { :id => '2', :
|
316
|
+
# '3' => { :id => '2', :_destroy => true }
|
271
317
|
# })
|
272
318
|
#
|
273
319
|
# Will update the name of the Person with ID 1, build a new associated
|
@@ -279,51 +325,68 @@ module ActiveRecord
|
|
279
325
|
# assign_nested_attributes_for_collection_association(:people, [
|
280
326
|
# { :id => '1', :name => 'Peter' },
|
281
327
|
# { :name => 'John' },
|
282
|
-
# { :id => '2', :
|
328
|
+
# { :id => '2', :_destroy => true }
|
283
329
|
# ])
|
284
|
-
def assign_nested_attributes_for_collection_association(association_name, attributes_collection
|
330
|
+
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
331
|
+
options = self.nested_attributes_options[association_name]
|
332
|
+
|
285
333
|
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
286
334
|
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
287
335
|
end
|
288
336
|
|
337
|
+
if options[:limit] && attributes_collection.size > options[:limit]
|
338
|
+
raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
|
339
|
+
end
|
340
|
+
|
289
341
|
if attributes_collection.is_a? Hash
|
290
342
|
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
291
343
|
end
|
292
344
|
|
293
345
|
attributes_collection.each do |attributes|
|
294
|
-
attributes = attributes.
|
346
|
+
attributes = attributes.with_indifferent_access
|
295
347
|
|
296
348
|
if attributes['id'].blank?
|
297
349
|
unless reject_new_record?(association_name, attributes)
|
298
350
|
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
299
351
|
end
|
300
352
|
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
|
301
|
-
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
|
353
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
302
354
|
end
|
303
355
|
end
|
304
356
|
end
|
305
357
|
|
306
358
|
# Updates a record with the +attributes+ or marks it for destruction if
|
307
|
-
# +allow_destroy+ is +true+ and
|
359
|
+
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
|
308
360
|
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
|
309
|
-
if
|
361
|
+
if has_destroy_flag?(attributes) && allow_destroy
|
310
362
|
record.mark_for_destruction
|
311
363
|
else
|
312
364
|
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
313
365
|
end
|
314
366
|
end
|
315
367
|
|
316
|
-
# Determines if a hash contains a truthy
|
317
|
-
def
|
318
|
-
ConnectionAdapters::Column.value_to_boolean
|
368
|
+
# Determines if a hash contains a truthy _destroy key.
|
369
|
+
def has_destroy_flag?(hash)
|
370
|
+
ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
|
371
|
+
ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
|
319
372
|
end
|
320
373
|
|
321
374
|
# Determines if a new record should be build by checking for
|
322
|
-
#
|
375
|
+
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
|
323
376
|
# association and evaluates to +true+.
|
324
377
|
def reject_new_record?(association_name, attributes)
|
325
|
-
|
326
|
-
|
378
|
+
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
379
|
+
end
|
380
|
+
|
381
|
+
def call_reject_if(association_name, attributes)
|
382
|
+
callback = self.nested_attributes_options[association_name][:reject_if]
|
383
|
+
|
384
|
+
case callback
|
385
|
+
when Symbol
|
386
|
+
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
387
|
+
when Proc
|
388
|
+
callback.try(:call, attributes)
|
389
|
+
end
|
327
390
|
end
|
328
391
|
end
|
329
392
|
end
|