snusnu-dm-accepts_nested_attributes 0.0.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. data/Manifest.txt +13 -8
  2. data/Rakefile +2 -3
  3. data/TODO +5 -0
  4. data/lib/dm-accepts_nested_attributes.rb +7 -14
  5. data/lib/dm-accepts_nested_attributes/error_collecting.rb +35 -0
  6. data/lib/dm-accepts_nested_attributes/model.rb +132 -0
  7. data/lib/dm-accepts_nested_attributes/resource.rb +218 -29
  8. data/lib/dm-accepts_nested_attributes/save.rb +13 -0
  9. data/lib/dm-accepts_nested_attributes/transactional_save.rb +15 -0
  10. data/lib/dm-accepts_nested_attributes/version.rb +1 -1
  11. data/spec/fixtures/person.rb +8 -0
  12. data/spec/fixtures/profile.rb +9 -0
  13. data/spec/fixtures/project.rb +8 -0
  14. data/spec/fixtures/project_membership.rb +8 -0
  15. data/spec/fixtures/task.rb +9 -0
  16. data/spec/integration/belongs_to_spec.rb +10 -134
  17. data/spec/integration/has_1_spec.rb +9 -121
  18. data/spec/integration/has_n_spec.rb +10 -149
  19. data/spec/integration/has_n_through_spec.rb +10 -162
  20. data/spec/{shared → lib}/rspec_tmbundle_support.rb +1 -1
  21. data/spec/shared/belongs_to_spec.rb +127 -0
  22. data/spec/shared/has_1_spec.rb +103 -0
  23. data/spec/shared/has_n_spec.rb +114 -0
  24. data/spec/shared/has_n_through_spec.rb +139 -0
  25. data/spec/spec_helper.rb +12 -9
  26. data/spec/unit/accepts_nested_attributes_for_spec.rb +39 -118
  27. data/tasks/changelog.rb +18 -0
  28. data/tasks/spec.rb +0 -1
  29. metadata +19 -14
  30. data/lib/dm-accepts_nested_attributes/association_proxies.rb +0 -55
  31. data/lib/dm-accepts_nested_attributes/association_validation.rb +0 -49
  32. data/lib/dm-accepts_nested_attributes/nested_attributes.rb +0 -350
  33. data/spec/unit/resource_spec.rb +0 -174
@@ -0,0 +1,18 @@
1
+ desc 'update changelog'
2
+ task :changelog do
3
+ File.open('CHANGELOG', 'w+') do |changelog|
4
+ `git log -z --abbrev-commit`.split("\0").each do |commit|
5
+ next if commit =~ /^Merge: \d*/
6
+ ref, author, time, _, title, _, message = commit.split("\n", 7)
7
+ ref = ref[/commit ([0-9a-f]+)/, 1]
8
+ author = author[/Author: (.*)/, 1].strip
9
+ time = Time.parse(time[/Date: (.*)/, 1]).utc
10
+ title.strip!
11
+
12
+ changelog.puts "[#{ref} | #{time}] #{author}"
13
+ changelog.puts '', " * #{title}"
14
+ changelog.puts '', message.rstrip if message
15
+ changelog.puts
16
+ end
17
+ end
18
+ end
@@ -8,7 +8,6 @@ begin
8
8
  desc 'Run specifications'
9
9
  Spec::Rake::SpecTask.new(:spec) do |t|
10
10
  t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
11
- t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s).map { |f| f.to_s }
12
11
 
13
12
  begin
14
13
  gem 'rcov', '~>0.8'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snusnu-dm-accepts_nested_attributes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Martin Gamsj\xC3\xA4ger"
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-13 00:00:00 -07:00
12
+ date: 2009-06-17 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,17 +20,17 @@ dependencies:
20
20
  requirements:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: 0.9.11
23
+ version: 0.10.0
24
24
  version:
25
25
  - !ruby/object:Gem::Dependency
26
- name: addressable
26
+ name: dm-validations
27
27
  type: :runtime
28
28
  version_requirement:
29
29
  version_requirements: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 2.0.2
33
+ version: 0.10.0
34
34
  version:
35
35
  description: A DataMapper plugin that adds the possibility to perform nested model attribute assignment
36
36
  email:
@@ -52,25 +52,30 @@ files:
52
52
  - TODO
53
53
  - CHANGELOG
54
54
  - lib/dm-accepts_nested_attributes.rb
55
- - lib/dm-accepts_nested_attributes/association_proxies.rb
56
- - lib/dm-accepts_nested_attributes/association_validation.rb
57
- - lib/dm-accepts_nested_attributes/nested_attributes.rb
55
+ - lib/dm-accepts_nested_attributes/error_collecting.rb
56
+ - lib/dm-accepts_nested_attributes/model.rb
58
57
  - lib/dm-accepts_nested_attributes/resource.rb
58
+ - lib/dm-accepts_nested_attributes/save.rb
59
+ - lib/dm-accepts_nested_attributes/transactional_save.rb
59
60
  - lib/dm-accepts_nested_attributes/version.rb
61
+ - spec/spec.opts
62
+ - spec/spec_helper.rb
63
+ - spec/lib/rspec_tmbundle_support.rb
60
64
  - spec/fixtures/person.rb
61
65
  - spec/fixtures/profile.rb
62
66
  - spec/fixtures/project.rb
63
67
  - spec/fixtures/project_membership.rb
64
68
  - spec/fixtures/task.rb
69
+ - spec/shared/belongs_to_spec.rb
70
+ - spec/shared/has_1_spec.rb
71
+ - spec/shared/has_n_spec.rb
72
+ - spec/shared/has_n_through_spec.rb
73
+ - spec/unit/accepts_nested_attributes_for_spec.rb
65
74
  - spec/integration/belongs_to_spec.rb
66
75
  - spec/integration/has_1_spec.rb
67
76
  - spec/integration/has_n_spec.rb
68
77
  - spec/integration/has_n_through_spec.rb
69
- - spec/shared/rspec_tmbundle_support.rb
70
- - spec/spec.opts
71
- - spec/spec_helper.rb
72
- - spec/unit/accepts_nested_attributes_for_spec.rb
73
- - spec/unit/resource_spec.rb
78
+ - tasks/changelog.rb
74
79
  - tasks/gemspec.rb
75
80
  - tasks/hoe.rb
76
81
  - tasks/install.rb
@@ -1,55 +0,0 @@
1
- module DataMapper
2
- module Associations
3
-
4
- module ManyToOne
5
-
6
- class Proxy
7
-
8
- def save
9
-
10
- return false if @parent.nil?
11
-
12
- # original dm-core-0.9.11 code:
13
- # return true unless parent.new_record?
14
-
15
- # and the backwards compatible extension to it (allows update of belongs_to model)
16
- if !parent.new_record? && !@relationship.child_model.autosave_associations.key?(@relationship.name)
17
- return true
18
- end
19
-
20
- @relationship.with_repository(parent) do
21
- result = parent.marked_for_destruction? ? parent.destroy : parent.save
22
- @relationship.child_key.set(@child, @relationship.parent_key.get(parent)) if result
23
- result
24
- end
25
-
26
- end
27
-
28
- end
29
-
30
- end
31
-
32
-
33
- module OneToMany
34
-
35
- class Proxy
36
-
37
- private
38
-
39
- def save_resource(resource, parent = @parent)
40
- @relationship.with_repository(resource) do |r|
41
- if parent.nil? && resource.model.respond_to?(:many_to_many)
42
- resource.destroy
43
- else
44
- @relationship.attach_parent(resource, parent)
45
- resource.marked_for_destruction? ? resource.destroy : resource.save
46
- end
47
- end
48
- end
49
-
50
- end
51
-
52
- end
53
-
54
- end
55
- end
@@ -1,49 +0,0 @@
1
- module DataMapper
2
- module NestedAttributes
3
-
4
- module AssociationValidation
5
-
6
- def save_child_associations(saved, context)
7
- return super if context.nil? # preserve save! behavior
8
- child_associations.each do |a|
9
- if a.respond_to?(:valid?)
10
- a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
11
- else
12
- self.errors.add(:general, "child association is missing")
13
- end
14
- saved |= a.save
15
- end
16
- saved
17
- end
18
-
19
- def save_self
20
- self.valid? && super
21
- end
22
-
23
- def save_parent_associations(saved, context)
24
- parent_associations.each do |a|
25
- if a.respond_to?(:each)
26
- a.each do |r|
27
- r.errors.each { |e| self.errors.add(:general, e) } unless r.valid?(context)
28
- end
29
- else
30
- a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
31
- end
32
- saved |= a.save
33
- end
34
- saved
35
- end
36
-
37
- # everything works the same if this method isn't overwritten with a no-op
38
- # however, i suspect that this is the case because the registered before(:save) hook
39
- # somehow gets lost when overwriting Resource#save here in this module.
40
- # I'll leave it in for now, to make the purpose clear
41
-
42
- def check_validations(context = :default)
43
- true # no-op, validations are checked inside #save
44
- end
45
-
46
- end
47
-
48
- end
49
- end
@@ -1,350 +0,0 @@
1
- module DataMapper
2
- module NestedAttributes
3
-
4
- module ClassMethods
5
-
6
- def autosave_associations
7
- @autosave_associations ||= {}
8
- end
9
-
10
- # Defines an attributes reader and writer for the specified association(s).
11
- # If you are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>,
12
- # then you will need to add the attribute writer to the allowed list.
13
- #
14
- # After any params are passed to the attributes writer they are available
15
- # via the attributes reader (they are stored in an instance variable of
16
- # the same name). The attributes reader returns nil if the attributes
17
- # writer has not been called.
18
- #
19
- # Supported options:
20
- # [:allow_destroy]
21
- # If true, destroys any members from the attributes hash with a
22
- # <tt>_delete</tt> key and a value that evaluates to +true+
23
- # (eg. 1, '1', true, or 'true'). This option is off by default.
24
- # [:reject_if]
25
- # Allows you to specify a Proc that checks whether a record should be
26
- # built for a certain attribute hash. The hash is passed to the Proc
27
- # and the Proc should return either +true+ or +false+. When no Proc
28
- # is specified a record will be built for all attribute hashes that
29
- # do not have a <tt>_delete</tt> that evaluates to true.
30
- #
31
- # Examples:
32
- # # creates avatar_attributes
33
- # # creates avatar_attributes=
34
- # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
35
- # # creates avatar_attributes and posts_attributes
36
- # # creates avatar_attributes= and posts_attributes=
37
- # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
38
- def accepts_nested_attributes_for(association_name, options = {})
39
-
40
- assert_kind_of 'association_name', association_name, Symbol, String
41
- assert_kind_of 'options', options, Hash
42
-
43
- options = { :allow_destroy => false }.update(options)
44
-
45
- # raises if the specified option keys aren't valid
46
- assert_valid_autosave_options(options)
47
-
48
- # raises if the specified association doesn't exist
49
- # we don't need the return value here, just the check
50
- # ------------------------------------------------------
51
- # also, when using the return value from this call to
52
- # replace association_name with association.name,
53
- # has(1, :through) are broken, because they seem to have
54
- # a different name
55
-
56
- association_for_name(association_name)
57
-
58
-
59
- # should be safe to go on
60
-
61
- include InstanceMethods
62
-
63
- if ::DataMapper.const_defined?('Validate')
64
-
65
- require Pathname(__FILE__).dirname.expand_path + 'association_validation'
66
-
67
- include AssociationValidation
68
-
69
- end
70
-
71
- autosave_associations[association_name] = options
72
-
73
- type = nr_of_possible_child_instances(association_name) > 1 ? :collection : :one_to_one
74
-
75
- class_eval %{
76
-
77
- def save(context = :default)
78
- saved = false # preserve Resource#save api contract
79
- transaction { |t| t.rollback unless saved = super }
80
- saved
81
- end
82
-
83
- def #{association_name}_attributes
84
- @#{association_name}_attributes
85
- end
86
-
87
- def #{association_name}_attributes=(attributes)
88
- attributes = sanitize_nested_attributes(attributes)
89
- @#{association_name}_attributes = attributes
90
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
91
- end
92
-
93
- if association_type(:#{association_name}) == :many_to_one || association_type(:#{association_name}) == :one_to_one
94
-
95
- def get_#{association_name}
96
- #{association_name.to_s} || self.class.associated_model_for_name(:#{association_name}).new
97
- end
98
-
99
- end
100
-
101
- }, __FILE__, __LINE__ + 1
102
-
103
- end
104
-
105
- def reject_new_nested_attributes_proc_for(association_name)
106
- autosave_associations[association_name] ? autosave_associations[association_name][:reject_if] : nil
107
- end
108
-
109
-
110
- # utility methods
111
-
112
- def nr_of_possible_child_instances(association_name, repository = :default)
113
- # belongs_to seems to generate no options[:max]
114
- association_for_name(association_name, repository).options[:max] || 1
115
- end
116
-
117
- # i have the feeling this should be refactored
118
- def associated_model_for_name(association_name, repository = :default)
119
- a = association_for_name(association_name, repository)
120
- case association_type(association_name)
121
- when :many_to_one
122
- a.parent_model
123
- when :one_to_one
124
- a.child_model
125
- when :one_to_many
126
- a.child_model
127
- when :many_to_many
128
- Object.full_const_get(a.options[:child_model])
129
- else
130
- raise ArgumentError, "Unknown association type #{a.inspect}"
131
- end
132
- end
133
-
134
- # maybe this should be provided by dm-core somehow
135
- # DataMapper::Association::Relationship would be a place maybe?
136
- def association_type(association_name)
137
- a = association_for_name(association_name)
138
- if a.options[:max].nil? # belongs_to
139
- :many_to_one
140
- elsif a.options[:max] == 1 # has(1)
141
- :one_to_one
142
- elsif a.options[:max] > 1 && !a.is_a?(DataMapper::Associations::RelationshipChain) # has(n)
143
- :one_to_many
144
- elsif a.is_a?(DataMapper::Associations::RelationshipChain) # has(n, :through) MUST be checked after has(n) here
145
- :many_to_many
146
- else
147
- raise ArgumentError, "Unknown association type #{a.inspect}"
148
- end
149
- end
150
-
151
- # avoid nil access by always going through this
152
- # this method raises if the association named name is not established in this model
153
- def association_for_name(name, repository = :default)
154
- association = self.relationships(repository)[name]
155
- # TODO think about using a specific Error class like UnknownAssociationError
156
- raise(ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}") unless association
157
- association
158
- end
159
-
160
- private
161
-
162
- # think about storing valid options in a classlevel constant
163
- def assert_valid_autosave_options(options)
164
- unless options.all? { |k,v| [ :allow_destroy, :reject_if ].include?(k) }
165
- raise ArgumentError, 'accepts_nested_attributes_for only takes :allow_destroy and :reject_if as options'
166
- end
167
- end
168
-
169
- end
170
-
171
-
172
- module InstanceMethods
173
-
174
- # This method can be used to remove ambiguities from the passed attributes.
175
- # Consider a situation with a belongs_to association where both a valid value
176
- # for the foreign_key attribute *and* nested_attributes for a new record are
177
- # present (i.e. item_type_id and item_type_attributes are present).
178
- # Also see http://is.gd/sz2d on the rails-core ml for a discussion on this.
179
- # The basic idea is, that there should be a well defined behavior for what
180
- # exactly happens when such a situation occurs. I'm currently in favor for
181
- # using the foreign_key if it is present, but this probably needs more thinking.
182
- # For now, this method basically is a no-op, but at least it provides a hook where
183
- # everyone can perform it's own sanitization (just overwrite this method)
184
- def sanitize_nested_attributes(attrs)
185
- attrs
186
- end
187
-
188
- # returns nil if no resource has been associated yet
189
- def associated_instance_get(association_name, repository = :default)
190
- send(self.class.association_for_name(association_name, repository).name)
191
- end
192
-
193
-
194
- private
195
-
196
- # Attribute hash keys that should not be assigned as normal attributes.
197
- # These hash keys are nested attributes implementation details.
198
- UNASSIGNABLE_KEYS = [ :id, :_delete ]
199
-
200
-
201
- # Assigns the given attributes to the association.
202
- #
203
- # If the given attributes include an <tt>:id</tt> that matches the existing
204
- # record’s id, then the existing record will be modified. Otherwise a new
205
- # record will be built.
206
- #
207
- # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
208
- # <tt>:_delete</tt> key set to a truthy value, then the existing record
209
- # will be marked for destruction.
210
- def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
211
- if attributes[:id].blank?
212
- unless reject_new_record?(association_name, attributes)
213
- model = self.class.associated_model_for_name(association_name)
214
- send("#{association_name}=", model.new(attributes.except(*UNASSIGNABLE_KEYS)))
215
- end
216
- else (existing_record = associated_instance_get(association_name)) && existing_record.id.to_s == attributes[:id].to_s
217
- assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
218
- end
219
- end
220
-
221
- # Assigns the given attributes to the collection association.
222
- #
223
- # Hashes with an <tt>:id</tt> value matching an existing associated record
224
- # will update that record. Hashes without an <tt>:id</tt> value will build
225
- # a new record for the association. Hashes with a matching <tt>:id</tt>
226
- # value and a <tt>:_delete</tt> key set to a truthy value will mark the
227
- # matched record for destruction.
228
- #
229
- # For example:
230
- #
231
- # assign_nested_attributes_for_collection_association(:people, {
232
- # '1' => { :id => '1', :name => 'Peter' },
233
- # '2' => { :name => 'John' },
234
- # '3' => { :id => '2', :_delete => true }
235
- # })
236
- #
237
- # Will update the name of the Person with ID 1, build a new associated
238
- # person with the name `John', and mark the associatied Person with ID 2
239
- # for destruction.
240
- #
241
- # Also accepts an Array of attribute hashes:
242
- #
243
- # assign_nested_attributes_for_collection_association(:people, [
244
- # { :id => '1', :name => 'Peter' },
245
- # { :name => 'John' },
246
- # { :id => '2', :_delete => true }
247
- # ])
248
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
249
-
250
- assert_kind_of 'association_name', association_name, Symbol
251
- assert_kind_of 'attributes_collection', attributes_collection, Hash, Array
252
-
253
- if attributes_collection.is_a? Hash
254
- attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
255
- end
256
-
257
- attributes_collection.each do |attributes|
258
- if attributes[:id].blank?
259
- unless reject_new_record?(association_name, attributes)
260
- case self.class.association_type(association_name)
261
- when :one_to_many
262
- build_new_has_n_association(association_name, attributes)
263
- when :many_to_many
264
- build_new_has_n_through_association(association_name, attributes)
265
- end
266
- end
267
- elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes[:id].to_s }
268
- assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
269
- end
270
- end
271
-
272
- end
273
-
274
- def build_new_has_n_association(association_name, attributes)
275
- send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
276
- end
277
-
278
- def build_new_has_n_through_association(association_name, attributes)
279
- # fetch the association to have the information ready
280
- association = self.class.association_for_name(association_name)
281
-
282
- # do what's done in dm-core/specs/integration/association_through_spec.rb
283
-
284
- # explicitly build the join entry and assign it to the join association
285
- join_entry = self.class.associated_model_for_name(association.name).new
286
- self.send(association.name) << join_entry
287
- self.save
288
- # explicitly build the child entry and assign the join entry to its join association
289
- child_entry = self.class.associated_model_for_name(association_name).new(attributes)
290
- child_entry.send(association.name) << join_entry
291
- child_entry.save
292
- end
293
-
294
- # Updates a record with the +attributes+ or marks it for destruction if
295
- # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
296
- def assign_to_or_mark_for_destruction(association_name, record, attributes, allow_destroy)
297
- if has_delete_flag?(attributes) && allow_destroy
298
- if self.class.association_type(association_name) == :many_to_many
299
- # destroy the join record
300
- record.send(self.class.association_for_name(association_name).name).destroy!
301
- # destroy the child record
302
- record.destroy
303
- else
304
- record.mark_for_destruction
305
- end
306
- else
307
- record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
308
- if self.class.association_type(association_name) == :many_to_many
309
- record.save
310
- end
311
- end
312
- end
313
-
314
- # Determines if a hash contains a truthy _delete key.
315
- def has_delete_flag?(hash)
316
- # TODO find out if this activerecord code needs to be ported
317
- # ConnectionAdapters::Column.value_to_boolean hash['_delete']
318
- 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
- guard = self.class.reject_new_nested_attributes_proc_for(association_name)
326
- has_delete_flag?(attributes) || (guard.respond_to?(:call) && guard.call(attributes))
327
- end
328
-
329
- end
330
-
331
- module CommonInstanceMethods
332
-
333
- # Reloads the attributes of the object as usual and removes a mark for destruction.
334
- def reload
335
- @marked_for_destruction = false
336
- super
337
- end
338
-
339
- def marked_for_destruction?
340
- @marked_for_destruction
341
- end
342
-
343
- def mark_for_destruction
344
- @marked_for_destruction = true
345
- end
346
-
347
- end
348
-
349
- end
350
- end