square-activerecord 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/CHANGELOG +6140 -0
  2. data/README.rdoc +222 -0
  3. data/examples/associations.png +0 -0
  4. data/examples/performance.rb +179 -0
  5. data/examples/simple.rb +14 -0
  6. data/lib/active_record.rb +124 -0
  7. data/lib/active_record/aggregations.rb +277 -0
  8. data/lib/active_record/association_preload.rb +430 -0
  9. data/lib/active_record/associations.rb +2307 -0
  10. data/lib/active_record/associations/association_collection.rb +572 -0
  11. data/lib/active_record/associations/association_proxy.rb +299 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +128 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +115 -0
  17. data/lib/active_record/associations/has_one_association.rb +143 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  19. data/lib/active_record/associations/through_association_scope.rb +154 -0
  20. data/lib/active_record/attribute_methods.rb +60 -0
  21. data/lib/active_record/attribute_methods/before_type_cast.rb +30 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  23. data/lib/active_record/attribute_methods/primary_key.rb +56 -0
  24. data/lib/active_record/attribute_methods/query.rb +39 -0
  25. data/lib/active_record/attribute_methods/read.rb +145 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
  27. data/lib/active_record/attribute_methods/write.rb +43 -0
  28. data/lib/active_record/autosave_association.rb +369 -0
  29. data/lib/active_record/base.rb +1904 -0
  30. data/lib/active_record/callbacks.rb +284 -0
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -0
  32. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  33. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +333 -0
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -0
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +539 -0
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
  40. data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -0
  43. data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
  44. data/lib/active_record/counter_cache.rb +115 -0
  45. data/lib/active_record/dynamic_finder_match.rb +56 -0
  46. data/lib/active_record/dynamic_scope_match.rb +23 -0
  47. data/lib/active_record/errors.rb +172 -0
  48. data/lib/active_record/fixtures.rb +1006 -0
  49. data/lib/active_record/locale/en.yml +40 -0
  50. data/lib/active_record/locking/optimistic.rb +172 -0
  51. data/lib/active_record/locking/pessimistic.rb +55 -0
  52. data/lib/active_record/log_subscriber.rb +48 -0
  53. data/lib/active_record/migration.rb +617 -0
  54. data/lib/active_record/named_scope.rb +138 -0
  55. data/lib/active_record/nested_attributes.rb +419 -0
  56. data/lib/active_record/observer.rb +125 -0
  57. data/lib/active_record/persistence.rb +290 -0
  58. data/lib/active_record/query_cache.rb +36 -0
  59. data/lib/active_record/railtie.rb +91 -0
  60. data/lib/active_record/railties/controller_runtime.rb +38 -0
  61. data/lib/active_record/railties/databases.rake +512 -0
  62. data/lib/active_record/reflection.rb +411 -0
  63. data/lib/active_record/relation.rb +394 -0
  64. data/lib/active_record/relation/batches.rb +89 -0
  65. data/lib/active_record/relation/calculations.rb +295 -0
  66. data/lib/active_record/relation/finder_methods.rb +363 -0
  67. data/lib/active_record/relation/predicate_builder.rb +48 -0
  68. data/lib/active_record/relation/query_methods.rb +303 -0
  69. data/lib/active_record/relation/spawn_methods.rb +132 -0
  70. data/lib/active_record/schema.rb +59 -0
  71. data/lib/active_record/schema_dumper.rb +195 -0
  72. data/lib/active_record/serialization.rb +60 -0
  73. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  74. data/lib/active_record/session_store.rb +340 -0
  75. data/lib/active_record/test_case.rb +67 -0
  76. data/lib/active_record/timestamp.rb +88 -0
  77. data/lib/active_record/transactions.rb +359 -0
  78. data/lib/active_record/validations.rb +84 -0
  79. data/lib/active_record/validations/associated.rb +48 -0
  80. data/lib/active_record/validations/uniqueness.rb +190 -0
  81. data/lib/active_record/version.rb +10 -0
  82. data/lib/rails/generators/active_record.rb +19 -0
  83. data/lib/rails/generators/active_record/migration.rb +15 -0
  84. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  85. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  86. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  87. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  88. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  89. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  90. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  91. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  92. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  93. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  94. metadata +223 -0
@@ -0,0 +1,138 @@
1
+ require 'active_support/core_ext/array'
2
+ require 'active_support/core_ext/hash/except'
3
+ require 'active_support/core_ext/kernel/singleton_class'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module ActiveRecord
7
+ # = Active Record Named \Scopes
8
+ module NamedScope
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ # Returns an anonymous \scope.
13
+ #
14
+ # posts = Post.scoped
15
+ # posts.size # Fires "select count(*) from posts" and returns the count
16
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
17
+ #
18
+ # fruits = Fruit.scoped
19
+ # fruits = fruits.where(:colour => 'red') if options[:red_only]
20
+ # fruits = fruits.limit(10) if limited?
21
+ #
22
+ # Anonymous \scopes tend to be useful when procedurally generating complex
23
+ # queries, where passing intermediate values (\scopes) around as first-class
24
+ # objects is convenient.
25
+ #
26
+ # You can define a \scope that applies to all finders using
27
+ # ActiveRecord::Base.default_scope.
28
+ def scoped(options = nil)
29
+ if options
30
+ scoped.apply_finder_options(options)
31
+ else
32
+ current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone
33
+ end
34
+ end
35
+
36
+ def scopes
37
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
38
+ end
39
+
40
+ # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
41
+ # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
42
+ #
43
+ # class Shirt < ActiveRecord::Base
44
+ # scope :red, where(:color => 'red')
45
+ # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
46
+ # end
47
+ #
48
+ # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
49
+ # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
50
+ #
51
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
52
+ # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
53
+ # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
54
+ # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
55
+ # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
56
+ # all behave as if Shirt.red really was an Array.
57
+ #
58
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
59
+ # all shirts that are both red and dry clean only.
60
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
61
+ # returns the number of garments for which these criteria obtain. Similarly with
62
+ # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
63
+ #
64
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
65
+ # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
66
+ #
67
+ # class Person < ActiveRecord::Base
68
+ # has_many :shirts
69
+ # end
70
+ #
71
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
72
+ # only shirts.
73
+ #
74
+ # Named \scopes can also be procedural:
75
+ #
76
+ # class Shirt < ActiveRecord::Base
77
+ # scope :colored, lambda {|color| where(:color => color) }
78
+ # end
79
+ #
80
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
81
+ #
82
+ # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
83
+ #
84
+ # class Shirt < ActiveRecord::Base
85
+ # scope :red, where(:color => 'red') do
86
+ # def dom_id
87
+ # 'red_shirts'
88
+ # end
89
+ # end
90
+ # end
91
+ #
92
+ # Scopes can also be used while creating/building a record.
93
+ #
94
+ # class Article < ActiveRecord::Base
95
+ # scope :published, where(:published => true)
96
+ # end
97
+ #
98
+ # Article.published.new.published # => true
99
+ # Article.published.create.published # => true
100
+ def scope(name, scope_options = {}, &block)
101
+ name = name.to_sym
102
+ valid_scope_name?(name)
103
+
104
+ extension = Module.new(&block) if block_given?
105
+
106
+ scopes[name] = lambda do |*args|
107
+ options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options
108
+
109
+ relation = if options.is_a?(Hash)
110
+ scoped.apply_finder_options(options)
111
+ elsif options
112
+ scoped.merge(options)
113
+ else
114
+ scoped
115
+ end
116
+
117
+ extension ? relation.extending(extension) : relation
118
+ end
119
+
120
+ singleton_class.send(:redefine_method, name, &scopes[name])
121
+ end
122
+
123
+ def named_scope(*args, &block)
124
+ ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller)
125
+ scope(*args, &block)
126
+ end
127
+
128
+ protected
129
+
130
+ def valid_scope_name?(name)
131
+ if !scopes[name] && respond_to?(name, true)
132
+ logger.warn "Creating scope :#{name}. " \
133
+ "Overwriting existing method #{self.name}.#{name}."
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,419 @@
1
+ require 'active_support/core_ext/hash/except'
2
+ require 'active_support/core_ext/object/try'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
6
+ module ActiveRecord
7
+ module NestedAttributes #:nodoc:
8
+ class TooManyRecords < ActiveRecordError
9
+ end
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ class_inheritable_accessor :nested_attributes_options, :instance_writer => false
15
+ self.nested_attributes_options = {}
16
+ end
17
+
18
+ # = Active Record Nested Attributes
19
+ #
20
+ # Nested attributes allow you to save attributes on associated records
21
+ # through the parent. By default nested attribute updating is turned off,
22
+ # you can enable it using the accepts_nested_attributes_for class method.
23
+ # When you enable nested attributes an attribute writer is defined on
24
+ # the model.
25
+ #
26
+ # The attribute writer is named after the association, which means that
27
+ # in the following example, two new methods are added to your model:
28
+ #
29
+ # <tt>author_attributes=(attributes)</tt> and
30
+ # <tt>pages_attributes=(attributes)</tt>.
31
+ #
32
+ # class Book < ActiveRecord::Base
33
+ # has_one :author
34
+ # has_many :pages
35
+ #
36
+ # accepts_nested_attributes_for :author, :pages
37
+ # end
38
+ #
39
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
40
+ # association that accepts_nested_attributes_for is used for.
41
+ #
42
+ # === One-to-one
43
+ #
44
+ # Consider a Member model that has one Avatar:
45
+ #
46
+ # class Member < ActiveRecord::Base
47
+ # has_one :avatar
48
+ # accepts_nested_attributes_for :avatar
49
+ # end
50
+ #
51
+ # Enabling nested attributes on a one-to-one association allows you to
52
+ # create the member and avatar in one go:
53
+ #
54
+ # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
55
+ # member = Member.create(params[:member])
56
+ # member.avatar.id # => 2
57
+ # member.avatar.icon # => 'smiling'
58
+ #
59
+ # It also allows you to update the avatar through the member:
60
+ #
61
+ # params = { :member => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
62
+ # member.update_attributes params[:member]
63
+ # member.avatar.icon # => 'sad'
64
+ #
65
+ # By default you will only be able to set and update attributes on the
66
+ # associated model. If you want to destroy the associated model through the
67
+ # attributes hash, you have to enable it first using the
68
+ # <tt>:allow_destroy</tt> option.
69
+ #
70
+ # class Member < ActiveRecord::Base
71
+ # has_one :avatar
72
+ # accepts_nested_attributes_for :avatar, :allow_destroy => true
73
+ # end
74
+ #
75
+ # Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
76
+ # value that evaluates to +true+, you will destroy the associated model:
77
+ #
78
+ # member.avatar_attributes = { :id => '2', :_destroy => '1' }
79
+ # member.avatar.marked_for_destruction? # => true
80
+ # member.save
81
+ # member.reload.avatar # => nil
82
+ #
83
+ # Note that the model will _not_ be destroyed until the parent is saved.
84
+ #
85
+ # === One-to-many
86
+ #
87
+ # Consider a member that has a number of posts:
88
+ #
89
+ # class Member < ActiveRecord::Base
90
+ # has_many :posts
91
+ # accepts_nested_attributes_for :posts
92
+ # end
93
+ #
94
+ # You can now set or update attributes on an associated post model through
95
+ # the attribute hash.
96
+ #
97
+ # For each hash that does _not_ have an <tt>id</tt> key a new record will
98
+ # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
99
+ # that evaluates to +true+.
100
+ #
101
+ # params = { :member => {
102
+ # :name => 'joe', :posts_attributes => [
103
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
104
+ # { :title => 'The egalitarian assumption of the modern citizen' },
105
+ # { :title => '', :_destroy => '1' } # this will be ignored
106
+ # ]
107
+ # }}
108
+ #
109
+ # member = Member.create(params['member'])
110
+ # member.posts.length # => 2
111
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
112
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
113
+ #
114
+ # You may also set a :reject_if proc to silently ignore any new record
115
+ # hashes if they fail to pass your criteria. For example, the previous
116
+ # example could be rewritten as:
117
+ #
118
+ # class Member < ActiveRecord::Base
119
+ # has_many :posts
120
+ # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
121
+ # end
122
+ #
123
+ # params = { :member => {
124
+ # :name => 'joe', :posts_attributes => [
125
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
126
+ # { :title => 'The egalitarian assumption of the modern citizen' },
127
+ # { :title => '' } # this will be ignored because of the :reject_if proc
128
+ # ]
129
+ # }}
130
+ #
131
+ # member = Member.create(params['member'])
132
+ # member.posts.length # => 2
133
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
134
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
135
+ #
136
+ # Alternatively, :reject_if also accepts a symbol for using methods:
137
+ #
138
+ # class Member < ActiveRecord::Base
139
+ # has_many :posts
140
+ # accepts_nested_attributes_for :posts, :reject_if => :new_record?
141
+ # end
142
+ #
143
+ # class Member < ActiveRecord::Base
144
+ # has_many :posts
145
+ # accepts_nested_attributes_for :posts, :reject_if => :reject_posts
146
+ #
147
+ # def reject_posts(attributed)
148
+ # attributed['title'].blank?
149
+ # end
150
+ # end
151
+ #
152
+ # If the hash contains an <tt>id</tt> key that matches an already
153
+ # associated record, the matching record will be modified:
154
+ #
155
+ # member.attributes = {
156
+ # :name => 'Joe',
157
+ # :posts_attributes => [
158
+ # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
159
+ # { :id => 2, :title => '[UPDATED] other post' }
160
+ # ]
161
+ # }
162
+ #
163
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
164
+ # member.posts.second.title # => '[UPDATED] other post'
165
+ #
166
+ # By default the associated records are protected from being destroyed. If
167
+ # you want to destroy any of the associated records through the attributes
168
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
169
+ # option. This will allow you to also use the <tt>_destroy</tt> key to
170
+ # destroy existing records:
171
+ #
172
+ # class Member < ActiveRecord::Base
173
+ # has_many :posts
174
+ # accepts_nested_attributes_for :posts, :allow_destroy => true
175
+ # end
176
+ #
177
+ # params = { :member => {
178
+ # :posts_attributes => [{ :id => '2', :_destroy => '1' }]
179
+ # }}
180
+ #
181
+ # member.attributes = params['member']
182
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
183
+ # member.posts.length # => 2
184
+ # member.save
185
+ # member.reload.posts.length # => 1
186
+ #
187
+ # === Saving
188
+ #
189
+ # All changes to models, including the destruction of those marked for
190
+ # destruction, are saved and destroyed automatically and atomically when
191
+ # the parent model is saved. This happens inside the transaction initiated
192
+ # by the parents save method. See ActiveRecord::AutosaveAssociation.
193
+ module ClassMethods
194
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
195
+
196
+ # Defines an attributes writer for the specified association(s). If you
197
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
198
+ # will need to add the attribute writer to the allowed list.
199
+ #
200
+ # Supported options:
201
+ # [:allow_destroy]
202
+ # If true, destroys any members from the attributes hash with a
203
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
204
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
205
+ # [:reject_if]
206
+ # Allows you to specify a Proc or a Symbol pointing to a method
207
+ # that checks whether a record should be built for a certain attribute
208
+ # hash. The hash is passed to the supplied Proc or the method
209
+ # and it should return either +true+ or +false+. When no :reject_if
210
+ # is specified, a record will be built for all attribute hashes that
211
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
212
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
213
+ # that will reject a record where all the attributes are blank.
214
+ # [:limit]
215
+ # Allows you to specify the maximum number of the associated records that
216
+ # can be processed with the nested attributes. If the size of the
217
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
218
+ # exception is raised. If omitted, any number associations can be processed.
219
+ # Note that the :limit option is only applicable to one-to-many associations.
220
+ # [:update_only]
221
+ # Allows you to specify that an existing record may only be updated.
222
+ # A new record may only be created when there is no existing record.
223
+ # This option only works for one-to-one associations and is ignored for
224
+ # collection associations. This option is off by default.
225
+ #
226
+ # Examples:
227
+ # # creates avatar_attributes=
228
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
229
+ # # creates avatar_attributes=
230
+ # accepts_nested_attributes_for :avatar, :reject_if => :all_blank
231
+ # # creates avatar_attributes= and posts_attributes=
232
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
233
+ def accepts_nested_attributes_for(*attr_names)
234
+ options = { :allow_destroy => false, :update_only => false }
235
+ options.update(attr_names.extract_options!)
236
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
237
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
238
+
239
+ attr_names.each do |association_name|
240
+ if reflection = reflect_on_association(association_name)
241
+ reflection.options[:autosave] = true
242
+ add_autosave_association_callbacks(reflection)
243
+ nested_attributes_options[association_name.to_sym] = options
244
+ type = (reflection.collection? ? :collection : :one_to_one)
245
+
246
+ # def pirate_attributes=(attributes)
247
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
248
+ # end
249
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
250
+ if method_defined?(:#{association_name}_attributes=)
251
+ remove_method(:#{association_name}_attributes=)
252
+ end
253
+ def #{association_name}_attributes=(attributes)
254
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
255
+ end
256
+ eoruby
257
+ else
258
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
265
+ # used in conjunction with fields_for to build a form element for the
266
+ # destruction of this association.
267
+ #
268
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
269
+ def _destroy
270
+ marked_for_destruction?
271
+ end
272
+
273
+ private
274
+
275
+ # Attribute hash keys that should not be assigned as normal attributes.
276
+ # These hash keys are nested attributes implementation details.
277
+ UNASSIGNABLE_KEYS = %w( id _destroy )
278
+
279
+ # Assigns the given attributes to the association.
280
+ #
281
+ # If update_only is false and the given attributes include an <tt>:id</tt>
282
+ # that matches the existing record's id, then the existing record will be
283
+ # modified. If update_only is true, a new record is only created when no
284
+ # object exists. Otherwise a new record will be built.
285
+ #
286
+ # If the given attributes include a matching <tt>:id</tt> attribute, or
287
+ # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
288
+ # then the existing record will be marked for destruction.
289
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
290
+ options = nested_attributes_options[association_name]
291
+ attributes = attributes.with_indifferent_access
292
+ check_existing_record = (options[:update_only] || !attributes['id'].blank?)
293
+
294
+ if check_existing_record && (record = send(association_name)) &&
295
+ (options[:update_only] || record.id.to_s == attributes['id'].to_s)
296
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
297
+
298
+ elsif !attributes['id'].blank?
299
+ raise_nested_attributes_record_not_found(association_name, attributes['id'])
300
+
301
+ elsif !reject_new_record?(association_name, attributes)
302
+ method = "build_#{association_name}"
303
+ if respond_to?(method)
304
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
305
+ else
306
+ raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
307
+ end
308
+ end
309
+ end
310
+
311
+ # Assigns the given attributes to the collection association.
312
+ #
313
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
314
+ # will update that record. Hashes without an <tt>:id</tt> value will build
315
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
316
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
317
+ # matched record for destruction.
318
+ #
319
+ # For example:
320
+ #
321
+ # assign_nested_attributes_for_collection_association(:people, {
322
+ # '1' => { :id => '1', :name => 'Peter' },
323
+ # '2' => { :name => 'John' },
324
+ # '3' => { :id => '2', :_destroy => true }
325
+ # })
326
+ #
327
+ # Will update the name of the Person with ID 1, build a new associated
328
+ # person with the name `John', and mark the associated Person with ID 2
329
+ # for destruction.
330
+ #
331
+ # Also accepts an Array of attribute hashes:
332
+ #
333
+ # assign_nested_attributes_for_collection_association(:people, [
334
+ # { :id => '1', :name => 'Peter' },
335
+ # { :name => 'John' },
336
+ # { :id => '2', :_destroy => true }
337
+ # ])
338
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
339
+ options = nested_attributes_options[association_name]
340
+
341
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
342
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
343
+ end
344
+
345
+ if options[:limit] && attributes_collection.size > options[:limit]
346
+ raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
347
+ end
348
+
349
+ if attributes_collection.is_a? Hash
350
+ keys = attributes_collection.keys
351
+ attributes_collection = if keys.include?('id') || keys.include?(:id)
352
+ Array.wrap(attributes_collection)
353
+ else
354
+ attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
355
+ end
356
+ end
357
+
358
+ association = send(association_name)
359
+
360
+ existing_records = if association.loaded?
361
+ association.to_a
362
+ else
363
+ attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
364
+ attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : []
365
+ end
366
+
367
+ attributes_collection.each do |attributes|
368
+ attributes = attributes.with_indifferent_access
369
+
370
+ if attributes['id'].blank?
371
+ unless reject_new_record?(association_name, attributes)
372
+ association.build(attributes.except(*UNASSIGNABLE_KEYS))
373
+ end
374
+
375
+ elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
376
+ association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes)
377
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
378
+
379
+ else
380
+ raise_nested_attributes_record_not_found(association_name, attributes['id'])
381
+ end
382
+ end
383
+ end
384
+
385
+ # Updates a record with the +attributes+ or marks it for destruction if
386
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
387
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
388
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
389
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
390
+ end
391
+
392
+ # Determines if a hash contains a truthy _destroy key.
393
+ def has_destroy_flag?(hash)
394
+ ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
395
+ end
396
+
397
+ # Determines if a new record should be build by checking for
398
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
399
+ # association and evaluates to +true+.
400
+ def reject_new_record?(association_name, attributes)
401
+ has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
402
+ end
403
+
404
+ def call_reject_if(association_name, attributes)
405
+ return false if has_destroy_flag?(attributes)
406
+ case callback = nested_attributes_options[association_name][:reject_if]
407
+ when Symbol
408
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
409
+ when Proc
410
+ callback.call(attributes)
411
+ end
412
+ end
413
+
414
+ def raise_nested_attributes_record_not_found(association_name, record_id)
415
+ reflection = self.class.reflect_on_association(association_name)
416
+ raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
417
+ end
418
+ end
419
+ end