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
@@ -37,7 +37,7 @@ en:
37
37
  # blank: "This is a custom blank message for User login"
38
38
  # Will define custom blank validation message for User model and
39
39
  # custom blank validation message for login attribute of User model.
40
- models:
40
+ #models:
41
41
 
42
42
  # Translate model names. Used in Model.human_name().
43
43
  #models:
@@ -23,6 +23,16 @@ 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
+ #
26
36
  # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
27
37
  # or otherwise apply the business logic needed to resolve the conflict.
28
38
  #
@@ -39,6 +49,7 @@ module ActiveRecord
39
49
  base.lock_optimistically = true
40
50
 
41
51
  base.alias_method_chain :update, :lock
52
+ base.alias_method_chain :destroy, :lock
42
53
  base.alias_method_chain :attributes_from_column_definition, :lock
43
54
 
44
55
  class << base
@@ -98,6 +109,28 @@ module ActiveRecord
98
109
  end
99
110
  end
100
111
 
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
+
101
134
  module ClassMethods
102
135
  DEFAULT_LOCKING_COLUMN = 'lock_version'
103
136
 
@@ -130,7 +130,9 @@ module ActiveRecord
130
130
  # To run migrations against the currently configured database, use
131
131
  # <tt>rake db:migrate</tt>. This will update the database by running all of the
132
132
  # pending migrations, creating the <tt>schema_migrations</tt> table
133
- # (see "About the schema_migrations table" section below) if missing.
133
+ # (see "About the schema_migrations table" section below) if missing. It will also
134
+ # invoke the db:schema:dump task, which will update your db/schema.rb file
135
+ # to match the structure of your database.
134
136
  #
135
137
  # To roll the database back to a previous migration version, use
136
138
  # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
@@ -336,6 +338,10 @@ module ActiveRecord
336
338
  self.verbose = save
337
339
  end
338
340
 
341
+ def connection
342
+ ActiveRecord::Base.connection
343
+ end
344
+
339
345
  def method_missing(method, *arguments, &block)
340
346
  arg_list = arguments.map(&:inspect) * ', '
341
347
 
@@ -343,7 +349,7 @@ module ActiveRecord
343
349
  unless arguments.empty? || method == :execute
344
350
  arguments[0] = Migrator.proper_table_name(arguments.first)
345
351
  end
346
- ActiveRecord::Base.connection.send(method, *arguments, &block)
352
+ connection.send(method, *arguments, &block)
347
353
  end
348
354
  end
349
355
  end
@@ -1,11 +1,12 @@
1
1
  module ActiveRecord
2
2
  module NamedScope
3
- # All subclasses of ActiveRecord::Base have two named \scopes:
4
- # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
3
+ # All subclasses of ActiveRecord::Base have one named scope:
5
4
  # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
6
5
  #
7
6
  # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
8
7
  # intermediate values (scopes) around as first-class objects is convenient.
8
+ #
9
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
9
10
  def self.included(base)
10
11
  base.class_eval do
11
12
  extend ClassMethods
@@ -39,7 +40,7 @@ module ActiveRecord
39
40
  # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
40
41
  # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
41
42
  #
42
- # All \scopes are available as class methods on the ActiveRecord::Base descendent upon which the \scopes were defined. But they are also available to
43
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which the \scopes were defined. But they are also available to
43
44
  # <tt>has_many</tt> associations. If,
44
45
  #
45
46
  # class Person < ActiveRecord::Base
@@ -88,7 +89,12 @@ module ActiveRecord
88
89
  when Hash
89
90
  options
90
91
  when Proc
91
- options.call(*args)
92
+ case parent_scope
93
+ when Scope
94
+ with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
95
+ else
96
+ options.call(*args)
97
+ end
92
98
  end, &block)
93
99
  end
94
100
  (class << self; self end).instance_eval do
@@ -98,7 +104,7 @@ module ActiveRecord
98
104
  end
99
105
  end
100
106
  end
101
-
107
+
102
108
  class Scope
103
109
  attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
104
110
  NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
@@ -111,6 +117,7 @@ module ActiveRecord
111
117
  delegate :scopes, :with_scope, :to => :proxy_scope
112
118
 
113
119
  def initialize(proxy_scope, options, &block)
120
+ options ||= {}
114
121
  [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
115
122
  extend Module.new(&block) if block_given?
116
123
  unless Scope === proxy_scope
@@ -169,7 +176,7 @@ module ActiveRecord
169
176
  if scopes.include?(method)
170
177
  scopes[method].call(self, *args)
171
178
  else
172
- with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
179
+ with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
173
180
  method = :new if method == :build
174
181
  if current_scoped_methods_when_defined
175
182
  with_scope current_scoped_methods_when_defined do
@@ -0,0 +1,329 @@
1
+ module ActiveRecord
2
+ module NestedAttributes #:nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
6
+ base.reject_new_nested_attributes_procs = {}
7
+ end
8
+
9
+ # == Nested Attributes
10
+ #
11
+ # Nested attributes allow you to save attributes on associated records
12
+ # through the parent. By default nested attribute updating is turned off,
13
+ # you can enable it using the accepts_nested_attributes_for class method.
14
+ # When you enable nested attributes an attribute writer is defined on
15
+ # the model.
16
+ #
17
+ # The attribute writer is named after the association, which means that
18
+ # in the following example, two new methods are added to your model:
19
+ # <tt>author_attributes=(attributes)</tt> and
20
+ # <tt>pages_attributes=(attributes)</tt>.
21
+ #
22
+ # class Book < ActiveRecord::Base
23
+ # has_one :author
24
+ # has_many :pages
25
+ #
26
+ # accepts_nested_attributes_for :author, :pages
27
+ # end
28
+ #
29
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
30
+ # association that accepts_nested_attributes_for is used for.
31
+ #
32
+ # === One-to-one
33
+ #
34
+ # Consider a Member model that has one Avatar:
35
+ #
36
+ # class Member < ActiveRecord::Base
37
+ # has_one :avatar
38
+ # accepts_nested_attributes_for :avatar
39
+ # end
40
+ #
41
+ # Enabling nested attributes on a one-to-one association allows you to
42
+ # create the member and avatar in one go:
43
+ #
44
+ # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
45
+ # member = Member.create(params)
46
+ # member.avatar.id # => 2
47
+ # member.avatar.icon # => 'smiling'
48
+ #
49
+ # It also allows you to update the avatar through the member:
50
+ #
51
+ # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
52
+ # member.update_attributes params['member']
53
+ # member.avatar.icon # => 'sad'
54
+ #
55
+ # By default you will only be able to set and update attributes on the
56
+ # associated model. If you want to destroy the associated model through the
57
+ # attributes hash, you have to enable it first using the
58
+ # <tt>:allow_destroy</tt> option.
59
+ #
60
+ # class Member < ActiveRecord::Base
61
+ # has_one :avatar
62
+ # accepts_nested_attributes_for :avatar, :allow_destroy => true
63
+ # end
64
+ #
65
+ # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
66
+ # value that evaluates to +true+, you will destroy the associated model:
67
+ #
68
+ # member.avatar_attributes = { :id => '2', :_delete => '1' }
69
+ # member.avatar.marked_for_destruction? # => true
70
+ # member.save
71
+ # member.avatar #=> nil
72
+ #
73
+ # Note that the model will _not_ be destroyed until the parent is saved.
74
+ #
75
+ # === One-to-many
76
+ #
77
+ # Consider a member that has a number of posts:
78
+ #
79
+ # class Member < ActiveRecord::Base
80
+ # has_many :posts
81
+ # accepts_nested_attributes_for :posts
82
+ # end
83
+ #
84
+ # You can now set or update attributes on an associated post model through
85
+ # the attribute hash.
86
+ #
87
+ # 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>_delete</tt> key
89
+ # that evaluates to +true+.
90
+ #
91
+ # params = { :member => {
92
+ # :name => 'joe', :posts_attributes => [
93
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
94
+ # { :title => 'The egalitarian assumption of the modern citizen' },
95
+ # { :title => '', :_delete => '1' } # this will be ignored
96
+ # ]
97
+ # }}
98
+ #
99
+ # member = Member.create(params['member'])
100
+ # member.posts.length # => 2
101
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
102
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
103
+ #
104
+ # You may also set a :reject_if proc to silently ignore any new record
105
+ # hashes if they fail to pass your criteria. For example, the previous
106
+ # example could be rewritten as:
107
+ #
108
+ # class Member < ActiveRecord::Base
109
+ # has_many :posts
110
+ # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
111
+ # end
112
+ #
113
+ # params = { :member => {
114
+ # :name => 'joe', :posts_attributes => [
115
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
116
+ # { :title => 'The egalitarian assumption of the modern citizen' },
117
+ # { :title => '' } # this will be ignored because of the :reject_if proc
118
+ # ]
119
+ # }}
120
+ #
121
+ # member = Member.create(params['member'])
122
+ # member.posts.length # => 2
123
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
124
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
125
+ #
126
+ # If the hash contains an <tt>id</tt> key that matches an already
127
+ # associated record, the matching record will be modified:
128
+ #
129
+ # member.attributes = {
130
+ # :name => 'Joe',
131
+ # :posts_attributes => [
132
+ # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
133
+ # { :id => 2, :title => '[UPDATED] other post' }
134
+ # ]
135
+ # }
136
+ #
137
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
138
+ # member.posts.second.title # => '[UPDATED] other post'
139
+ #
140
+ # By default the associated records are protected from being destroyed. If
141
+ # you want to destroy any of the associated records through the attributes
142
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
143
+ # option. This will allow you to also use the <tt>_delete</tt> key to
144
+ # destroy existing records:
145
+ #
146
+ # class Member < ActiveRecord::Base
147
+ # has_many :posts
148
+ # accepts_nested_attributes_for :posts, :allow_destroy => true
149
+ # end
150
+ #
151
+ # params = { :member => {
152
+ # :posts_attributes => [{ :id => '2', :_delete => '1' }]
153
+ # }}
154
+ #
155
+ # member.attributes = params['member']
156
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
157
+ # member.posts.length #=> 2
158
+ # member.save
159
+ # member.posts.length # => 1
160
+ #
161
+ # === Saving
162
+ #
163
+ # All changes to models, including the destruction of those marked for
164
+ # destruction, are saved and destroyed automatically and atomically when
165
+ # the parent model is saved. This happens inside the transaction initiated
166
+ # by the parents save method. See ActiveRecord::AutosaveAssociation.
167
+ module ClassMethods
168
+ # Defines an attributes writer for the specified association(s). If you
169
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
170
+ # will need to add the attribute writer to the allowed list.
171
+ #
172
+ # Supported options:
173
+ # [:allow_destroy]
174
+ # If true, destroys any members from the attributes hash with a
175
+ # <tt>_delete</tt> key and a value that evaluates to +true+
176
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
177
+ # [:reject_if]
178
+ # Allows you to specify a Proc that checks whether a record should be
179
+ # built for a certain attribute hash. The hash is passed to the Proc
180
+ # and the Proc should return either +true+ or +false+. When no Proc
181
+ # is specified a record will be built for all attribute hashes that
182
+ # do not have a <tt>_delete</tt> that evaluates to true.
183
+ #
184
+ # Examples:
185
+ # # creates avatar_attributes=
186
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
187
+ # # creates avatar_attributes= and posts_attributes=
188
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
189
+ def accepts_nested_attributes_for(*attr_names)
190
+ options = { :allow_destroy => false }
191
+ options.update(attr_names.extract_options!)
192
+ options.assert_valid_keys(:allow_destroy, :reject_if)
193
+
194
+ attr_names.each do |association_name|
195
+ if reflection = reflect_on_association(association_name)
196
+ type = case reflection.macro
197
+ when :has_one, :belongs_to
198
+ :one_to_one
199
+ when :has_many, :has_and_belongs_to_many
200
+ :collection
201
+ end
202
+
203
+ reflection.options[:autosave] = true
204
+ self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
205
+
206
+ # def pirate_attributes=(attributes)
207
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
208
+ # end
209
+ class_eval %{
210
+ def #{association_name}_attributes=(attributes)
211
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
212
+ end
213
+ }, __FILE__, __LINE__
214
+ else
215
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
222
+ # used in conjunction with fields_for to build a form element for the
223
+ # destruction of this association.
224
+ #
225
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
226
+ def _delete
227
+ marked_for_destruction?
228
+ end
229
+
230
+ private
231
+
232
+ # Attribute hash keys that should not be assigned as normal attributes.
233
+ # These hash keys are nested attributes implementation details.
234
+ UNASSIGNABLE_KEYS = %w{ id _delete }
235
+
236
+ # Assigns the given attributes to the association.
237
+ #
238
+ # If the given attributes include an <tt>:id</tt> that matches the existing
239
+ # record’s id, then the existing record will be modified. Otherwise a new
240
+ # record will be built.
241
+ #
242
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
243
+ # <tt>:_delete</tt> key set to a truthy value, then the existing record
244
+ # will be marked for destruction.
245
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
246
+ attributes = attributes.stringify_keys
247
+
248
+ if attributes['id'].blank?
249
+ unless reject_new_record?(association_name, attributes)
250
+ send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
251
+ end
252
+ 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)
254
+ end
255
+ end
256
+
257
+ # Assigns the given attributes to the collection association.
258
+ #
259
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
260
+ # will update that record. Hashes without an <tt>:id</tt> value will build
261
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
262
+ # value and a <tt>:_delete</tt> key set to a truthy value will mark the
263
+ # matched record for destruction.
264
+ #
265
+ # For example:
266
+ #
267
+ # assign_nested_attributes_for_collection_association(:people, {
268
+ # '1' => { :id => '1', :name => 'Peter' },
269
+ # '2' => { :name => 'John' },
270
+ # '3' => { :id => '2', :_delete => true }
271
+ # })
272
+ #
273
+ # Will update the name of the Person with ID 1, build a new associated
274
+ # person with the name `John', and mark the associatied Person with ID 2
275
+ # for destruction.
276
+ #
277
+ # Also accepts an Array of attribute hashes:
278
+ #
279
+ # assign_nested_attributes_for_collection_association(:people, [
280
+ # { :id => '1', :name => 'Peter' },
281
+ # { :name => 'John' },
282
+ # { :id => '2', :_delete => true }
283
+ # ])
284
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
285
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
286
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
287
+ end
288
+
289
+ if attributes_collection.is_a? Hash
290
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
291
+ end
292
+
293
+ attributes_collection.each do |attributes|
294
+ attributes = attributes.stringify_keys
295
+
296
+ if attributes['id'].blank?
297
+ unless reject_new_record?(association_name, attributes)
298
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
299
+ end
300
+ 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)
302
+ end
303
+ end
304
+ end
305
+
306
+ # Updates a record with the +attributes+ or marks it for destruction if
307
+ # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
308
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
309
+ if has_delete_flag?(attributes) && allow_destroy
310
+ record.mark_for_destruction
311
+ else
312
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
313
+ end
314
+ end
315
+
316
+ # Determines if a hash contains a truthy _delete key.
317
+ def has_delete_flag?(hash)
318
+ ConnectionAdapters::Column.value_to_boolean hash['_delete']
319
+ end
320
+
321
+ # Determines if a new record should be build by checking for
322
+ # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
323
+ # association and evaluates to +true+.
324
+ def reject_new_record?(association_name, attributes)
325
+ has_delete_flag?(attributes) ||
326
+ self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes)
327
+ end
328
+ end
329
+ end