daylight 0.9.0.rc2 → 0.9.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/usage.md +37 -33
- data/lib/daylight.rb +1 -0
- data/lib/daylight/api.rb +29 -1
- data/lib/daylight/association_persistance.rb +57 -0
- data/lib/daylight/associations.rb +24 -12
- data/lib/daylight/read_only.rb +5 -4
- data/lib/daylight/resource_proxy.rb +36 -17
- data/lib/daylight/version.rb +1 -1
- data/rails/daylight/api_controller.rb +15 -3
- data/rails/extensions/nested_attributes_ext.rb +58 -12
- data/spec/lib/daylight/api_spec.rb +38 -3
- data/spec/lib/daylight/association_persistance_spec.rb +130 -0
- data/spec/lib/daylight/associations_spec.rb +3 -28
- data/spec/lib/daylight/resource_proxy_spec.rb +14 -7
- data/spec/rails/daylight/api_controller_spec.rb +1 -1
- data/spec/rails/extensions/nested_attributes_ext_spec.rb +164 -62
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb3cda8546b99d74c72bf1af1ca14aabb51f3d8d
|
4
|
+
data.tar.gz: 41ad2d678d9aa293cc766179e2441789b7b351be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75780f4b059adb79463f13b0b41e01ae105ecdcad262ca59f133d4624c0fe5557bcc927f9f8a341961d6be8bba5a1720fbd70af0e749202ee8d2aca704b95a99
|
7
|
+
data.tar.gz: 438f45ccd0e69581e5c2217d989bfe0b9fa238c8665d184632a76cd9055c6a83cb3df5257c7376c4a968b1904b5e61685f2dc2eb38ac7f97776437222f16edbd
|
data/doc/usage.md
CHANGED
@@ -365,8 +365,9 @@ server-side.
|
|
365
365
|
Daylight adds additional functionality directly on the association:
|
366
366
|
* add new resources
|
367
367
|
* update existing resources
|
368
|
-
* add a
|
368
|
+
* add a resource to a collection
|
369
369
|
* associate two existing resources
|
370
|
+
* delete from an association
|
370
371
|
|
371
372
|
Currently, `ActiveResource` will only let you associate a resource by setting
|
372
373
|
the `foreign_key` directly on a model.
|
@@ -485,48 +486,27 @@ You can also add a nested object to an existing collection:
|
|
485
486
|
|
486
487
|
#### Updating a Nested Resource
|
487
488
|
|
488
|
-
Updates to nested resources are
|
489
|
-
You must save the nested resources directly:
|
489
|
+
Updates to nested resources are saved by saving the parent resource.
|
490
490
|
|
491
491
|
````ruby
|
492
492
|
post = API::Post.first
|
493
493
|
post.author.full_name = "Reid MacDonald"
|
494
|
-
post.
|
495
|
-
|
496
|
-
post = API::Post.first
|
497
|
-
post.author.full_name #=> "Reid MacDonald"
|
498
|
-
````
|
499
|
-
|
500
|
-
This is the same as saying:
|
501
|
-
|
502
|
-
````ruby
|
503
|
-
post = API::Post.first
|
504
|
-
|
505
|
-
author = post.author
|
506
|
-
author.full_name = "Reid MacDonald"
|
507
|
-
author.save #=> true
|
494
|
+
post.save #=> true
|
508
495
|
|
509
496
|
post = API::Post.first
|
510
497
|
post.author.full_name #=> "Reid MacDonald"
|
511
498
|
````
|
512
|
-
|
513
499
|
The same is true of nested objects in collections:
|
514
500
|
|
515
501
|
````ruby
|
516
502
|
post = API::Post.first
|
517
|
-
|
518
|
-
|
519
|
-
first_comment.message = "First!"
|
520
|
-
first_comment.save #=> true
|
503
|
+
post.comments.first.message = "First!"
|
504
|
+
post.save #=> true
|
521
505
|
|
522
506
|
post = API::Post.first
|
523
507
|
post.comments.first.message #=> "First!"
|
524
508
|
````
|
525
509
|
|
526
|
-
> FUTURE [#5](https://github.com/att-cloud/daylight/issues/5):
|
527
|
-
> Updates to the associated nested resource do not get saved when the parent
|
528
|
-
> resources are saved and they should be.
|
529
|
-
|
530
510
|
#### Associating an Existing Nested Resources
|
531
511
|
|
532
512
|
Associating using an existing nested records is possible with Daylight. The
|
@@ -557,8 +537,31 @@ This also will work to add to a collection on a new or existing resource:
|
|
557
537
|
post.commenters.find {|c| c.username == 'reidmix'} # #<API::V1::User:0x007fe2cfc45ce8 ..>
|
558
538
|
````
|
559
539
|
|
560
|
-
|
561
|
-
|
540
|
+
#### Deleting Nested Resources
|
541
|
+
|
542
|
+
A nested resource can be removed from a collection:
|
543
|
+
|
544
|
+
````ruby
|
545
|
+
post = API::Post.first
|
546
|
+
post.comments.count #=> 4
|
547
|
+
post.comments.shift
|
548
|
+
post.save #=> true
|
549
|
+
|
550
|
+
post = API::Post.first
|
551
|
+
post.comments.count #=> 3
|
552
|
+
````
|
553
|
+
|
554
|
+
A collection can also be reset:
|
555
|
+
|
556
|
+
````ruby
|
557
|
+
post = API::Post.first
|
558
|
+
post.comments.count #=> 4
|
559
|
+
post.comments = []
|
560
|
+
post.save #=> true
|
561
|
+
|
562
|
+
post = API::Post.first
|
563
|
+
post.comments.count #=> 0
|
564
|
+
````
|
562
565
|
|
563
566
|
### More Chaining
|
564
567
|
|
@@ -788,17 +791,18 @@ When saving this model from the client errors will be exposed similar to
|
|
788
791
|
|
789
792
|
With the introduction of and use of
|
790
793
|
[Strong Parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)
|
791
|
-
unpermitted or missing attributes
|
794
|
+
unpermitted or missing attributes can be detected if `action_on_unpermitted_parameters` is set to `:raise`
|
795
|
+
in configuration:
|
792
796
|
|
793
|
-
|
794
|
-
|
795
|
-
|
797
|
+
````ruby
|
798
|
+
config.action_controller.action_on_unpermitted_parameters = :raise
|
799
|
+
````
|
796
800
|
|
797
801
|
Lets say `created_at` is not permitted on the `PostController`:
|
798
802
|
````ruby
|
799
803
|
post = API::Post.new(created_at: Time.now)
|
800
804
|
post.save # => false
|
801
|
-
post.errors.messages # => {
|
805
|
+
post.errors.messages # => {'created_at'=>['unpermitted parameter']}
|
802
806
|
````
|
803
807
|
|
804
808
|
### Bad Requests
|
data/lib/daylight.rb
CHANGED
data/lib/daylight/api.rb
CHANGED
@@ -44,6 +44,7 @@ class Daylight::API < ActiveResource::Base
|
|
44
44
|
include Daylight::ReadOnly
|
45
45
|
include Daylight::Refinements
|
46
46
|
include Daylight::Associations
|
47
|
+
prepend Daylight::AssociationPersistance
|
47
48
|
|
48
49
|
class << self
|
49
50
|
attr_reader :version, :versions, :namespace
|
@@ -186,7 +187,7 @@ class Daylight::API < ActiveResource::Base
|
|
186
187
|
end
|
187
188
|
end
|
188
189
|
|
189
|
-
attr_reader :metadata
|
190
|
+
attr_reader :metadata, :hashcode, :association_hashcodes
|
190
191
|
|
191
192
|
##
|
192
193
|
# Extends ActiveResource to allow for saving metadata from the responses on
|
@@ -199,6 +200,9 @@ class Daylight::API < ActiveResource::Base
|
|
199
200
|
extract_metadata!(attributes)
|
200
201
|
|
201
202
|
super
|
203
|
+
|
204
|
+
@association_hashcodes = {}.with_indifferent_access
|
205
|
+
@hashcode = self.attributes.hash
|
202
206
|
end
|
203
207
|
|
204
208
|
##
|
@@ -269,6 +273,30 @@ class Daylight::API < ActiveResource::Base
|
|
269
273
|
end
|
270
274
|
end
|
271
275
|
|
276
|
+
##
|
277
|
+
# Load object(s) for a reflection name from the given values which could be
|
278
|
+
# an Array or a Hash
|
279
|
+
|
280
|
+
def load_attributes_for(name, value)
|
281
|
+
case value
|
282
|
+
when Array
|
283
|
+
resource = nil
|
284
|
+
value.map do |attrs|
|
285
|
+
if attrs.is_a?(Hash)
|
286
|
+
resource ||= find_or_create_resource_for_collection(name)
|
287
|
+
resource.new(attrs, true)
|
288
|
+
else
|
289
|
+
attrs.duplicable? ? attrs.dup : attrs
|
290
|
+
end
|
291
|
+
end
|
292
|
+
when Hash
|
293
|
+
resource = find_or_create_resource_for(name)
|
294
|
+
resource.new(value, true)
|
295
|
+
else
|
296
|
+
value.duplicable? ? value.dup : value
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
272
300
|
private
|
273
301
|
|
274
302
|
##
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Daylight::AssociationPersistance
|
2
|
+
|
3
|
+
# has our attributes changed since we were loaded?
|
4
|
+
def changed?
|
5
|
+
new? || hashcode != attributes.hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def serializable_hash(options=nil)
|
9
|
+
super((options || {}).reverse_merge(include: association_includes))
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
##
|
15
|
+
# returns nil if no changes (ourself or our children)
|
16
|
+
# returns empty hash if we've changed, but our children haven't
|
17
|
+
# returns include key if some of our children have changed
|
18
|
+
#
|
19
|
+
# { include: { post: { include: { comment: {} } } }
|
20
|
+
|
21
|
+
def association_includes
|
22
|
+
include_hash = {}
|
23
|
+
|
24
|
+
self.class.reflection_names.each do |reflection_name|
|
25
|
+
association = instance_variable_get("@#{reflection_name}")
|
26
|
+
reflection_attribute_name = "#{reflection_name}_attributes"
|
27
|
+
# ignore associations that have not been set
|
28
|
+
next unless association
|
29
|
+
|
30
|
+
# recurse into the child(ren)
|
31
|
+
child_include_hash =
|
32
|
+
if association.respond_to?(:to_ary)
|
33
|
+
# merge all the includes from all the children
|
34
|
+
children_includes = association.to_ary.map {|child| child.association_includes }.compact
|
35
|
+
children_includes.reduce(:merge) if children_includes.present?
|
36
|
+
else
|
37
|
+
association.association_includes
|
38
|
+
end
|
39
|
+
|
40
|
+
if child_include_hash.present?
|
41
|
+
include_hash[reflection_attribute_name] = {include: child_include_hash}
|
42
|
+
elsif child_include_hash || changed_associations.include?(reflection_name)
|
43
|
+
include_hash[reflection_attribute_name] = {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
include_hash if changed? || include_hash.present?
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# list of associations that have been modified
|
52
|
+
#
|
53
|
+
def changed_associations
|
54
|
+
association_hashcodes.select {|association, code| send(association).hash != code }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -73,7 +73,6 @@ module Daylight::Associations
|
|
73
73
|
# define setter that places the value directly in the attributes using
|
74
74
|
# the nested_attributes functionality server-side
|
75
75
|
define_method "#{reflection.name}=" do |value|
|
76
|
-
self.attributes[nested_attribute_key] = value
|
77
76
|
instance_variable_set(:"@#{reflection.name}", value)
|
78
77
|
end
|
79
78
|
|
@@ -93,15 +92,20 @@ module Daylight::Associations
|
|
93
92
|
# ActiveResource::Associations#belongs_to
|
94
93
|
|
95
94
|
def belongs_to name, options={}
|
96
|
-
|
97
|
-
|
95
|
+
create_reflection(:belongs_to, name, options).tap do |reflection|
|
96
|
+
|
97
|
+
nested_attribute_key = "#{reflection.name}_attributes"
|
98
|
+
|
99
|
+
# setup the resource_proxy to fetch the results
|
100
|
+
define_cached_method reflection.name, cache_key: nested_attribute_key do
|
101
|
+
reflection.klass.find(send(reflection.foreign_key))
|
102
|
+
end
|
98
103
|
|
99
104
|
# Defines a setter caching the value in an instance variable for later
|
100
105
|
# retrieval. Stash value directly in the attributes using the
|
101
106
|
# nested_attributes functionality server-side.
|
102
107
|
define_method "#{reflection.name}=" do |value|
|
103
108
|
attributes[reflection.foreign_key] = value.id # set the foreign key
|
104
|
-
attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
|
105
109
|
instance_variable_set(:"@#{reflection.name}", value) # set the cached value
|
106
110
|
end
|
107
111
|
end
|
@@ -198,7 +202,6 @@ module Daylight::Associations
|
|
198
202
|
end
|
199
203
|
|
200
204
|
define_method "#{reflection.name}=" do |value|
|
201
|
-
attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
|
202
205
|
value.attributes[:"#{self.class.element_name}_id"] = self.id
|
203
206
|
instance_variable_set(:"@#{reflection.name}", value) # set the cached value
|
204
207
|
end
|
@@ -233,14 +236,23 @@ module Daylight::Associations
|
|
233
236
|
cache_key = options[:cache_key] || method_name
|
234
237
|
attributes = options.has_key?(:index) ? @attributes[options[:index]] : @attributes
|
235
238
|
|
236
|
-
if instance_variable_defined?(ivar_name)
|
237
|
-
|
238
|
-
|
239
|
-
attributes
|
240
|
-
|
241
|
-
|
242
|
-
|
239
|
+
return instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
|
240
|
+
|
241
|
+
value =
|
242
|
+
if attributes.include?(cache_key)
|
243
|
+
load_attributes_for(method_name, attributes[cache_key])
|
244
|
+
else
|
245
|
+
send(uncached_method_name)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Track of the association hashcode for changes
|
249
|
+
association_hashcodes[method_name] = value.hash
|
250
|
+
|
251
|
+
instance_variable_set ivar_name, value
|
243
252
|
end
|
253
|
+
|
254
|
+
# alias our wrapper so calls to the attributes work
|
255
|
+
alias_method "#{method_name}_attributes", method_name
|
244
256
|
end
|
245
257
|
|
246
258
|
end
|
data/lib/daylight/read_only.rb
CHANGED
@@ -23,14 +23,14 @@ module Daylight::ReadOnly
|
|
23
23
|
end
|
24
24
|
|
25
25
|
##
|
26
|
-
# Adds API specific options when generating
|
26
|
+
# Adds API specific options when generating a serializable hash.
|
27
27
|
# Removes read_only attributes for requests.
|
28
28
|
#
|
29
29
|
# See
|
30
30
|
# except_read_only
|
31
31
|
|
32
|
-
def
|
33
|
-
super(except_read_only(options))
|
32
|
+
def serializable_hash(options=nil)
|
33
|
+
super(except_read_only(options || {}))
|
34
34
|
end
|
35
35
|
|
36
36
|
##
|
@@ -44,6 +44,7 @@ module Daylight::ReadOnly
|
|
44
44
|
super(except_read_only(options))
|
45
45
|
end
|
46
46
|
|
47
|
+
|
47
48
|
##
|
48
49
|
# Writers for read_only attributes are not included as methods
|
49
50
|
#--
|
@@ -75,7 +76,7 @@ module Daylight::ReadOnly
|
|
75
76
|
##
|
76
77
|
# Ensures that read_only attributes are merged in with `:except` options.
|
77
78
|
|
78
|
-
def except_read_only options
|
79
|
+
def except_read_only options={}
|
79
80
|
options.merge(except: (options[:except]||[]).push(*read_only))
|
80
81
|
end
|
81
82
|
|
@@ -18,17 +18,6 @@ class Daylight::ResourceProxy
|
|
18
18
|
@association_name, @association_resource = association.first
|
19
19
|
end
|
20
20
|
|
21
|
-
##
|
22
|
-
# Delegates appending to the association resource when available
|
23
|
-
|
24
|
-
def << value
|
25
|
-
if association_resource
|
26
|
-
association_resource.send("#{association_name}=", records.elements << value)
|
27
|
-
else
|
28
|
-
raise NoMethodError, "undefined method `<<' for #{self}"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
21
|
##
|
33
22
|
# Sets `from` URL on a request
|
34
23
|
def from from
|
@@ -118,11 +107,16 @@ class Daylight::ResourceProxy
|
|
118
107
|
end
|
119
108
|
|
120
109
|
##
|
121
|
-
#
|
110
|
+
# If loaded, return the first element, otherwise -
|
111
|
+
# sets the limit to the current parameters, and fetches the first result.
|
122
112
|
# Immediately issues the request to the API.
|
123
113
|
|
124
114
|
def first
|
125
|
-
|
115
|
+
if loaded?
|
116
|
+
to_a.first
|
117
|
+
else
|
118
|
+
limit(1).to_a.first
|
119
|
+
end
|
126
120
|
end
|
127
121
|
|
128
122
|
##
|
@@ -183,8 +177,7 @@ class Daylight::ResourceProxy
|
|
183
177
|
|
184
178
|
##
|
185
179
|
# Will attempt to fulfill the method if it exists on the resource or if it
|
186
|
-
# exists on an Array.
|
187
|
-
# execution.
|
180
|
+
# exists on an Array. Delegates the method on for subsequent execution.
|
188
181
|
|
189
182
|
def method_missing(method_name, *args, &block)
|
190
183
|
if resource_class.respond_to?(method_name)
|
@@ -193,8 +186,9 @@ class Daylight::ResourceProxy
|
|
193
186
|
end
|
194
187
|
resource_class.send(method_name, *args, &block)
|
195
188
|
elsif Array.method_defined?(method_name)
|
196
|
-
|
197
|
-
|
189
|
+
wrap_array_method(method_name)
|
190
|
+
# resend call to newly wrapped method
|
191
|
+
send(method_name, *args, &block)
|
198
192
|
else
|
199
193
|
super
|
200
194
|
end
|
@@ -223,4 +217,29 @@ class Daylight::ResourceProxy
|
|
223
217
|
@records = nil
|
224
218
|
self
|
225
219
|
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
##
|
224
|
+
# Create a wrapper method around a called array method that updates the assocation
|
225
|
+
# resource if the array has changed.
|
226
|
+
# This way we can propagate those changes to the server when the client model is saved.
|
227
|
+
|
228
|
+
def wrap_array_method(method_name)
|
229
|
+
self.class.send(:define_method, method_name) do |*args, &block|
|
230
|
+
array = records.elements
|
231
|
+
count_before = array.count
|
232
|
+
response = array.send(method_name, *args, &block)
|
233
|
+
# update the association if the array has changed
|
234
|
+
association_resource.send("#{association_name}=", array) if association_name && count_before != array.count
|
235
|
+
response
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# has this proxy loaded its data yet?
|
241
|
+
|
242
|
+
def loaded?
|
243
|
+
@records.present?
|
244
|
+
end
|
226
245
|
end
|
data/lib/daylight/version.rb
CHANGED
@@ -49,6 +49,8 @@ class Daylight::APIController < ApplicationController
|
|
49
49
|
include Daylight::Helpers
|
50
50
|
include VersionedUrlFor
|
51
51
|
|
52
|
+
ALLOWED_WHERE_PARAMS = [:id, :order, :limit, :offset, :associated, :remoted, :format].freeze
|
53
|
+
|
52
54
|
API_ACTIONS = [:index, :create, :show, :update, :destroy, :associated, :remoted].freeze
|
53
55
|
class_attribute :record_name, :collection_name, :model_name, instance_predicate: false
|
54
56
|
|
@@ -199,6 +201,16 @@ class Daylight::APIController < ApplicationController
|
|
199
201
|
respond_to?(model_params_name, true) ? send(model_params_name) : params[model_key]
|
200
202
|
end
|
201
203
|
|
204
|
+
##
|
205
|
+
# Permits known parameters for quering
|
206
|
+
# This has become necessary as of Rails 4.0.9 and 4.1.5 because of this security fix:
|
207
|
+
# https://groups.google.com/forum/#!topic/rubyonrails-security/M4chq5Sb540
|
208
|
+
def where_params
|
209
|
+
params.permit(*ALLOWED_WHERE_PARAMS,
|
210
|
+
filters: params[:filters].try(:keys),
|
211
|
+
scopes: [])
|
212
|
+
end
|
213
|
+
|
202
214
|
##
|
203
215
|
# Instance-level delegate of the `primary_key` method
|
204
216
|
#
|
@@ -227,7 +239,7 @@ class Daylight::APIController < ApplicationController
|
|
227
239
|
# See:
|
228
240
|
# Daylight::Refiners.refine_by
|
229
241
|
def index
|
230
|
-
render json: self.collection = model.refine_by(
|
242
|
+
render json: self.collection = model.refine_by(where_params)
|
231
243
|
end
|
232
244
|
|
233
245
|
##
|
@@ -330,7 +342,7 @@ class Daylight::APIController < ApplicationController
|
|
330
342
|
# Daylight::Helpers.associated_params
|
331
343
|
# RouteOptions
|
332
344
|
def associated
|
333
|
-
render json: self.collection = model.associated(
|
345
|
+
render json: self.collection = model.associated(where_params), root: associated_params
|
334
346
|
end
|
335
347
|
|
336
348
|
##
|
@@ -350,6 +362,6 @@ class Daylight::APIController < ApplicationController
|
|
350
362
|
# Daylight::Helpers.remoted_params
|
351
363
|
# RouteOptions
|
352
364
|
def remoted
|
353
|
-
render json: self.collection = model.remoted(
|
365
|
+
render json: self.collection = model.remoted(where_params), root: remoted_params
|
354
366
|
end
|
355
367
|
end
|
@@ -19,7 +19,10 @@ module NestedAttributesExt
|
|
19
19
|
def assign_nested_attributes_for_collection_association association_name, attributes_collection
|
20
20
|
return if attributes_collection.nil?
|
21
21
|
|
22
|
+
return if is_collection_multilevel?(association_name)
|
23
|
+
|
22
24
|
associate_existing_records(association_name, attributes_collection)
|
25
|
+
unassociate_missing_records(association_name, attributes_collection)
|
23
26
|
|
24
27
|
super
|
25
28
|
end
|
@@ -63,30 +66,73 @@ module NestedAttributesExt
|
|
63
66
|
return if attribute_ids.empty?
|
64
67
|
|
65
68
|
association = association(association_name)
|
66
|
-
|
67
|
-
|
68
|
-
# get known existing ids on the association
|
69
|
-
existing_record_ids = if association.loaded?
|
70
|
-
association.target.map(&primary_key)
|
71
|
-
else
|
72
|
-
association.scope.where(primary_key => attribute_ids).pluck(primary_key)
|
73
|
-
end
|
69
|
+
foreign_key = association.reflection.foreign_key
|
74
70
|
|
75
|
-
# unassociated records are those
|
76
|
-
unassociated_record_ids = attribute_ids.map(&:to_s) -
|
71
|
+
# unassociated records ids are those not existing in the association ids
|
72
|
+
unassociated_record_ids = attribute_ids.map(&:to_s) - association.ids_reader.map(&:to_s)
|
77
73
|
|
78
74
|
# we are about to set all foreign_keys, remove any foreign_key references in
|
79
75
|
# unassigned records attributes so they don't get clobbered
|
80
76
|
attributes_collection.map do |a|
|
81
77
|
if unassociated_record_ids.include?((a['id'] || a[:id]).to_s)
|
82
|
-
|
83
|
-
a.delete(key) || a.delete(key.to_sym)
|
78
|
+
a.delete(foreign_key) || a.delete(foreign_key.to_sym)
|
84
79
|
end
|
85
80
|
end
|
86
81
|
|
87
82
|
# concat the unassociated records to the association
|
88
83
|
association.concat(association.klass.find(unassociated_record_ids))
|
89
84
|
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Determines removed records from existing records on the association and sets their
|
88
|
+
# foreign keys to NULL
|
89
|
+
def unassociate_missing_records(association_name, attributes_collection)
|
90
|
+
# determine existing records, bail if there are none specified by 'id'
|
91
|
+
attribute_ids = attributes_collection.map {|a| (a['id'] || a[:id]) }.compact
|
92
|
+
|
93
|
+
association = association(association_name)
|
94
|
+
|
95
|
+
# removed records are those that are not part of existing in the association
|
96
|
+
removed_record_ids = association.ids_reader.map(&:to_s) - attribute_ids.map(&:to_s)
|
97
|
+
|
98
|
+
# remove the records from the association
|
99
|
+
association.delete(*removed_record_ids) unless removed_record_ids.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# returns true if the collection is a has_many :through or has_and_belongs_to_many
|
104
|
+
# association.
|
105
|
+
#
|
106
|
+
|
107
|
+
def is_collection_multilevel?(association_name)
|
108
|
+
association = association(association_name)
|
109
|
+
|
110
|
+
type = has_many_type(association)
|
111
|
+
return false unless type
|
112
|
+
|
113
|
+
logger.error <<-ERROR
|
114
|
+
Attempt to modify "#{association_name}" collection on #{self.class.name}.
|
115
|
+
Ignoring modification for #{type} used with
|
116
|
+
accepts_nested_attributes_for because it causes unexpected results.
|
117
|
+
ERROR
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Return a description of the association if it is a has_and_belongs_to_many
|
122
|
+
# or a has_many :through.
|
123
|
+
#
|
124
|
+
# Takes differences between Rails 4.0 and 4.1 into account.
|
125
|
+
|
126
|
+
def has_many_type(association)
|
127
|
+
reflection = association.reflection
|
128
|
+
if reflection.try(:has_and_belongs_to_many?) ||
|
129
|
+
(reflection.try(:parent_reflection) &&
|
130
|
+
reflection.parent_reflection.last.try(:macro) == :has_and_belongs_to_many)
|
131
|
+
'has_and_belongs_to_many'
|
132
|
+
elsif reflection.options.has_key?(:through)
|
133
|
+
'has_many :through'
|
134
|
+
end
|
135
|
+
end
|
90
136
|
end
|
91
137
|
|
92
138
|
ActiveRecord::Base.class_eval do
|
@@ -119,13 +119,14 @@ describe Daylight::API do
|
|
119
119
|
|
120
120
|
it "does not objectify a known reflection's attributes" do
|
121
121
|
test = TestDescendant.find(1)
|
122
|
-
test.child_attributes['id'].should == 2
|
122
|
+
test.attributes['child_attributes']['id'].should == 2
|
123
123
|
end
|
124
124
|
|
125
125
|
it "objectifies hashes within a known reflection's attributes" do
|
126
126
|
test = TestDescendant.find(1)
|
127
|
-
test.child_attributes['toy']
|
128
|
-
|
127
|
+
toy = test.attributes['child_attributes']['toy']
|
128
|
+
toy.should be_kind_of(ActiveResource::Base)
|
129
|
+
toy.attributes.should == {'id' => 5, 'name' => 'slinky'}
|
129
130
|
end
|
130
131
|
|
131
132
|
it "still objectifies other attributes" do
|
@@ -188,4 +189,38 @@ describe Daylight::API do
|
|
188
189
|
test.metadata.should be_present
|
189
190
|
end
|
190
191
|
end
|
192
|
+
|
193
|
+
describe :load_attributes_for do
|
194
|
+
let(:test) { TestDescendant.new }
|
195
|
+
|
196
|
+
it 'creates a resource from a Hash based on the name' do
|
197
|
+
obj = test.send(:load_attributes_for, :test_descendant, {name: 'foo'})
|
198
|
+
obj.should be_instance_of(TestDescendant)
|
199
|
+
obj.name.should == 'foo'
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'creates a resource for each Hash in an array' do
|
203
|
+
obj = test.send(:load_attributes_for, :test_descendants, [{name: 'foo'}, {name: 'bar'}])
|
204
|
+
obj.size.should == 2
|
205
|
+
obj.first.should be_instance_of(TestDescendant)
|
206
|
+
obj.first.name.should == 'foo'
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'dups each object in a given array if it is not a Hash' do
|
210
|
+
data = %w[one two three]
|
211
|
+
obj = test.send(:load_attributes_for, :test_descendants, data)
|
212
|
+
obj.size.should == 3
|
213
|
+
obj.first.should be_instance_of(String)
|
214
|
+
obj.first.should == 'one'
|
215
|
+
obj.object_id.should_not == data.first.object_id
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'otherwise creates a dup of the given value' do
|
219
|
+
my_string = 'my string'
|
220
|
+
obj = test.send(:load_attributes_for, :test_descendant, my_string)
|
221
|
+
obj.should be_instance_of(String)
|
222
|
+
obj.should == 'my string'
|
223
|
+
obj.object_id.should_not == my_string.object_id
|
224
|
+
end
|
225
|
+
end
|
191
226
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Daylight::AssociationPersistance do
|
4
|
+
|
5
|
+
class GrandchildPersistanceTestClass < Daylight::API
|
6
|
+
end
|
7
|
+
|
8
|
+
class RelatedPersistanceTestClass < Daylight::API
|
9
|
+
has_many :grandchildren, class_name: GrandchildPersistanceTestClass
|
10
|
+
end
|
11
|
+
|
12
|
+
class PersistanceTestClass < Daylight::API
|
13
|
+
has_many :children, class_name: 'RelatedPersistanceTestClass'
|
14
|
+
belongs_to :parent, class_name: 'RelatedPersistanceTestClass'
|
15
|
+
|
16
|
+
def parent_id ; 1 ; end
|
17
|
+
end
|
18
|
+
|
19
|
+
before do
|
20
|
+
data = {id: 1, name: 'test'}
|
21
|
+
stub_request(:get, %r{#{PersistanceTestClass.element_path(1)}}).to_return(body: data.to_json)
|
22
|
+
stub_request(:get, %r{#{RelatedPersistanceTestClass.element_path(1)}}).to_return(body: data.to_json)
|
23
|
+
stub_request(:get, %r{#{RelatedPersistanceTestClass.element_path(2)}}).to_return(body: data.merge(id:2).to_json)
|
24
|
+
stub_request(:get, %r{/persistance_test_classes/1/children\.json}).to_return(body: [data, data].to_json)
|
25
|
+
stub_request(:get, %r{/related_persistance_test_classes/1/grandchildren\.json}).to_return(body: [data, data].to_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:object) { PersistanceTestClass.find(1) }
|
29
|
+
|
30
|
+
describe :changed? do
|
31
|
+
|
32
|
+
it 'returns false if the object is not modified' do
|
33
|
+
object.changed?.should be_false
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns true if the object has a field modified' do
|
37
|
+
object.name = 'this is a change'
|
38
|
+
|
39
|
+
object.changed?.should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns true if the object has a modified association' do
|
43
|
+
object.parent = RelatedPersistanceTestClass.new
|
44
|
+
|
45
|
+
object.changed?.should be_true
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'returns true if the object is new' do
|
49
|
+
PersistanceTestClass.new.changed?.should be_true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe :include_child_updates do
|
54
|
+
it 'does not include associations if has not been loaded' do
|
55
|
+
hash = object.serializable_hash
|
56
|
+
hash['parent_attributes'].should be_nil
|
57
|
+
hash['children_attributes'].should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'includes the single belongs_to/has_one assocation if it has changed' do
|
61
|
+
object.parent.should_not be_nil
|
62
|
+
object.parent.name = 'updated name'
|
63
|
+
hash = object.serializable_hash
|
64
|
+
hash['parent_attributes'].should be_present
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'includes all children in a has_many type relation if any of them have changed' do
|
68
|
+
object.children.should be_present
|
69
|
+
object.children[0].name = 'updated name'
|
70
|
+
|
71
|
+
hash = object.serializable_hash
|
72
|
+
hash['children_attributes'].count.should == 2
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'does not include the association attribute if nothing has changed in the collection' do
|
76
|
+
object.children.should be_present
|
77
|
+
|
78
|
+
hash = object.serializable_hash
|
79
|
+
hash.should_not have_key('children_attributes')
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'does not include the single child if it has not changed' do
|
83
|
+
object.parent.should be_present
|
84
|
+
|
85
|
+
hash = object.serializable_hash
|
86
|
+
hash.should_not have_key('parent_attributes')
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'still includes associations when there is a new child object' do
|
90
|
+
object.children.should be_present
|
91
|
+
object.children << RelatedPersistanceTestClass.new
|
92
|
+
object.children.count.should == 3
|
93
|
+
|
94
|
+
hash = object.serializable_hash
|
95
|
+
hash['children_attributes'].count.should == 3
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'still includes associations when an additional persisted child object is added' do
|
99
|
+
object.children.should be_present
|
100
|
+
object.children << RelatedPersistanceTestClass.find(1)
|
101
|
+
object.children.count.should == 3
|
102
|
+
|
103
|
+
hash = object.serializable_hash
|
104
|
+
hash['children_attributes'].count.should == 3
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'still includes has_one associations when persisted child object is set' do
|
108
|
+
object.parent.should be_present
|
109
|
+
object.parent = RelatedPersistanceTestClass.find(2)
|
110
|
+
|
111
|
+
hash = object.serializable_hash
|
112
|
+
hash['parent_attributes'].should be_present
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'does not include has_one associations when it is replaced with the same object' do
|
116
|
+
object.parent.should be_present
|
117
|
+
object.parent = RelatedPersistanceTestClass.find(1)
|
118
|
+
|
119
|
+
hash = object.serializable_hash
|
120
|
+
hash['parent_attributes'].should_not be_present
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'handles grandchild modifications' do
|
124
|
+
object.children[0].grandchildren[0].name = 'updated name'
|
125
|
+
hash = object.serializable_hash
|
126
|
+
hash['children_attributes'][0]['grandchildren_attributes'][0]['name'].should == 'updated name'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -53,12 +53,6 @@ describe Daylight::Associations do
|
|
53
53
|
collection.should be_a Array
|
54
54
|
end
|
55
55
|
|
56
|
-
it "sets the associations directly the attributes hash" do
|
57
|
-
new_resource.related_test_classes = ["associated instances"]
|
58
|
-
|
59
|
-
new_resource.attributes['related_test_classes_attributes'].should == ["associated instances"]
|
60
|
-
end
|
61
|
-
|
62
56
|
it "fetches the stored associations out of the attributes when they are set" do
|
63
57
|
new_resource.related_test_classes = ["associated instances"]
|
64
58
|
|
@@ -107,12 +101,6 @@ describe Daylight::Associations do
|
|
107
101
|
proxy.to_params[:filters].should == {wibble: 'wobble'}
|
108
102
|
end
|
109
103
|
|
110
|
-
it "sets the associations directly the attributes hash" do
|
111
|
-
existing_resource.related_test_classes = ["associated instances"]
|
112
|
-
|
113
|
-
existing_resource.attributes['related_test_classes_attributes'].should == ["associated instances"]
|
114
|
-
end
|
115
|
-
|
116
104
|
it "fetches the stored associations out of the attributes when they exist" do
|
117
105
|
existing_resource.related_test_classes = ["associated instances"]
|
118
106
|
|
@@ -150,13 +138,6 @@ describe Daylight::Associations do
|
|
150
138
|
|
151
139
|
resource.attributes['parent_id'].should == 789
|
152
140
|
end
|
153
|
-
|
154
|
-
it 'sets the parent directly in the nested attributes hash' do
|
155
|
-
resource = AssociationsTestClass.find(1)
|
156
|
-
resource.parent = RelatedTestClass.new(id: 789, name: 'new parent')
|
157
|
-
|
158
|
-
resource.attributes['parent_attributes'].should == resource.parent
|
159
|
-
end
|
160
141
|
end
|
161
142
|
|
162
143
|
describe :belongs_to_through do
|
@@ -167,6 +148,7 @@ describe Daylight::Associations do
|
|
167
148
|
parent_id: 456, # ignored because of parent_id method
|
168
149
|
parent_attributes: {
|
169
150
|
id: 456,
|
151
|
+
name: 'nested',
|
170
152
|
grandparent_id: 3
|
171
153
|
}
|
172
154
|
}
|
@@ -190,12 +172,12 @@ describe Daylight::Associations do
|
|
190
172
|
stub_request(:get, %r{#{RelatedTestClass.element_path(3)}}).to_return(body: related_data.merge(id: 3).to_json)
|
191
173
|
end
|
192
174
|
|
193
|
-
it '
|
175
|
+
it 'loads the parent object from the original response' do
|
194
176
|
resource = AssociationsTestClass.find(1)
|
195
177
|
|
196
178
|
resource.parent.should_not be_nil
|
197
179
|
resource.parent.id.should == 456
|
198
|
-
resource.parent.name.should == '
|
180
|
+
resource.parent.name.should == 'nested'
|
199
181
|
end
|
200
182
|
|
201
183
|
it 'fetches the "through" object' do
|
@@ -260,13 +242,6 @@ describe Daylight::Associations do
|
|
260
242
|
|
261
243
|
resource.associate.associations_test_class_id.should == resource.id
|
262
244
|
end
|
263
|
-
|
264
|
-
it 'sets the associate directly in the nested attributes hash' do
|
265
|
-
resource = AssociationsTestClass.find(1)
|
266
|
-
resource.associate = RelatedTestClass.new(id: 333, name: 'Rik Mayall')
|
267
|
-
|
268
|
-
resource.attributes['associate_attributes'].should == resource.associate
|
269
|
-
end
|
270
245
|
end
|
271
246
|
|
272
247
|
describe :remote do
|
@@ -50,16 +50,10 @@ describe Daylight::ResourceProxy do
|
|
50
50
|
it "still thrown when chaining" do
|
51
51
|
expect { ProxyTestClass.foo.not_a_method }.to raise_error(NoMethodError)
|
52
52
|
end
|
53
|
-
|
54
|
-
it "still thrown when appending to scopes" do
|
55
|
-
ProxyTestClass.foo.association_resource.should be_nil
|
56
|
-
|
57
|
-
expect { ProxyTestClass.foo << ['foo class'] }.to raise_error(NoMethodError)
|
58
|
-
end
|
59
53
|
end
|
60
54
|
|
61
55
|
describe "Array methods" do
|
62
|
-
it "
|
56
|
+
it "supports through generated methods" do
|
63
57
|
mock = ProxyTestClass.new(name: 'three')
|
64
58
|
results = ProxyTestClass.foo
|
65
59
|
|
@@ -83,6 +77,19 @@ describe Daylight::ResourceProxy do
|
|
83
77
|
it "supported through Enumerable methods" do
|
84
78
|
ProxyTestClass.foo.map {|f| f.name}.should == %w[one two]
|
85
79
|
end
|
80
|
+
|
81
|
+
it "pulls the first off of the array if it's already loaded" do
|
82
|
+
result = ProxyTestClass.foo
|
83
|
+
result.instance_variable_set('@records', [:yay, :boo])
|
84
|
+
|
85
|
+
result.first.should == :yay
|
86
|
+
end
|
87
|
+
|
88
|
+
it "does a request for first if it hasn't been loaded" do
|
89
|
+
result = ProxyTestClass.foo
|
90
|
+
|
91
|
+
result.first.name.should == 'one'
|
92
|
+
end
|
86
93
|
end
|
87
94
|
|
88
95
|
describe "results fetch" do
|
@@ -6,8 +6,9 @@ class NoNestedAttributesTest < ActiveRecord::Base; end
|
|
6
6
|
# test cyclic autosave problem that AutosaveAssociataionFix resolves
|
7
7
|
class NestedAttributeTest < ActiveRecord::Base
|
8
8
|
has_many :collection, class_name: 'AssocNestedAttributeTest', inverse_of: :nested_attribute_test
|
9
|
+
has_many :through_collection, class_name: 'SingleNestedAttributeTest', through: :collection, source: :single_nested_attribute_test
|
9
10
|
|
10
|
-
accepts_nested_attributes_for :collection
|
11
|
+
accepts_nested_attributes_for :collection, :through_collection
|
11
12
|
end
|
12
13
|
|
13
14
|
class SingleNestedAttributeTest < ActiveRecord::Base
|
@@ -23,6 +24,16 @@ class AssocNestedAttributeTest < ActiveRecord::Base
|
|
23
24
|
accepts_nested_attributes_for :nested_attribute_test, :single_nested_attribute_test
|
24
25
|
end
|
25
26
|
|
27
|
+
class HabtmParentNestedAttributeTest < ActiveRecord::Base
|
28
|
+
has_and_belongs_to_many :foo, class_name: 'HabtmChildNestedAttributeTest'
|
29
|
+
|
30
|
+
accepts_nested_attributes_for :foo
|
31
|
+
end
|
32
|
+
|
33
|
+
class HabtmChildNestedAttributeTest < ActiveRecord::Base
|
34
|
+
has_and_belongs_to_many :foo, class_name: 'HabtmParentNestedAttributeTest'
|
35
|
+
end
|
36
|
+
|
26
37
|
describe NestedAttributesExt, type: [:model] do
|
27
38
|
|
28
39
|
migrate do
|
@@ -39,6 +50,19 @@ describe NestedAttributesExt, type: [:model] do
|
|
39
50
|
t.references :nested_attribute_test
|
40
51
|
t.references :single_nested_attribute_test
|
41
52
|
end
|
53
|
+
|
54
|
+
create_table :habtm_parent_nested_attribute_tests do |t|
|
55
|
+
t.string :name
|
56
|
+
end
|
57
|
+
|
58
|
+
create_table :habtm_child_nested_attribute_tests do |t|
|
59
|
+
t.string :name
|
60
|
+
end
|
61
|
+
|
62
|
+
create_table :habtm_child_nested_attribute_tests_parent_nested_attribute_tests do |t|
|
63
|
+
t.belongs_to :habtm_parent_nested_attribute_test
|
64
|
+
t.belongs_to :habtm_child_nested_attribute_test
|
65
|
+
end
|
42
66
|
end
|
43
67
|
|
44
68
|
before(:all) do
|
@@ -54,6 +78,14 @@ describe NestedAttributesExt, type: [:model] do
|
|
54
78
|
factory :assoc_nested_attribute_test do
|
55
79
|
name { Faker::Name.name }
|
56
80
|
end
|
81
|
+
|
82
|
+
factory :habtm_child_nested_attribute_test do
|
83
|
+
name { Faker::Name.name }
|
84
|
+
end
|
85
|
+
|
86
|
+
factory :habtm_parent_nested_attribute_test do
|
87
|
+
name { Faker::Name.name }
|
88
|
+
end
|
57
89
|
end
|
58
90
|
end
|
59
91
|
|
@@ -77,101 +109,171 @@ describe NestedAttributesExt, type: [:model] do
|
|
77
109
|
end
|
78
110
|
|
79
111
|
|
80
|
-
describe '
|
81
|
-
let(:record) { create(:nested_attribute_test) }
|
82
|
-
let(:member) { create(:assoc_nested_attribute_test) }
|
112
|
+
describe 'associate' do
|
83
113
|
|
84
|
-
|
85
|
-
record
|
114
|
+
describe 'has_many' do
|
115
|
+
let(:record) { create(:nested_attribute_test) }
|
116
|
+
let(:member) { create(:assoc_nested_attribute_test) }
|
86
117
|
|
87
|
-
|
88
|
-
|
89
|
-
end
|
118
|
+
it "ignores nil values" do
|
119
|
+
record.collection_attributes = nil
|
90
120
|
|
91
|
-
|
92
|
-
|
121
|
+
lambda { record.save! }.should_not raise_error
|
122
|
+
record.collection.should == []
|
123
|
+
end
|
93
124
|
|
94
|
-
|
125
|
+
it "continues to create and associate new records" do
|
126
|
+
record.collection_attributes = [{name: 'foo'}, {name: 'bar'}]
|
95
127
|
|
96
|
-
|
97
|
-
names.should include('foo')
|
98
|
-
names.should include('bar')
|
99
|
-
end
|
128
|
+
lambda { record.save! }.should_not raise_error
|
100
129
|
|
101
|
-
|
102
|
-
|
103
|
-
|
130
|
+
names = record.collection.map(&:name)
|
131
|
+
names.should include('foo')
|
132
|
+
names.should include('bar')
|
133
|
+
end
|
104
134
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
135
|
+
it "continues to update assoicated records" do
|
136
|
+
record.collection << member
|
137
|
+
record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
|
138
|
+
|
139
|
+
lambda { record.save! }.should_not raise_error
|
140
|
+
record.collection.size.should == 1
|
141
|
+
record.collection.first.name.should == 'Foozle Cumberbunch'
|
142
|
+
end
|
109
143
|
|
110
|
-
|
111
|
-
|
112
|
-
|
144
|
+
# this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
|
145
|
+
it "associates existing records" do
|
146
|
+
record.collection_attributes = [{id: member.id}]
|
113
147
|
|
114
|
-
|
115
|
-
|
116
|
-
|
148
|
+
lambda { record.save! }.should_not raise_error
|
149
|
+
record.collection.size.should == 1
|
150
|
+
record.collection.first.id.should == member.id
|
151
|
+
end
|
152
|
+
|
153
|
+
# this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
|
154
|
+
it "keeps association for existing records that are already assoicated" do
|
155
|
+
record.collection << member
|
156
|
+
record.collection_attributes = [{id: member.id}]
|
157
|
+
|
158
|
+
lambda { record.save! }.should_not raise_error
|
159
|
+
record.collection.size.should == 1
|
160
|
+
record.collection.first.id.should == member.id
|
161
|
+
end
|
162
|
+
|
163
|
+
it "updates records that were just associated" do
|
164
|
+
record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
|
165
|
+
|
166
|
+
lambda { record.save! }.should_not raise_error
|
167
|
+
record.collection.size.should == 1
|
168
|
+
record.collection.first.name.should == 'Foozle Cumberbunch'
|
169
|
+
end
|
170
|
+
|
171
|
+
it "ignores foreign key updates" do
|
172
|
+
different_foreign_key = record.id + 1
|
173
|
+
record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch', nested_attribute_test_id: different_foreign_key}]
|
174
|
+
|
175
|
+
lambda { record.save! }.should_not raise_error
|
176
|
+
|
177
|
+
record.collection.size.should == 1
|
178
|
+
record.collection.first.nested_attribute_test_id.should == record.id
|
179
|
+
end
|
180
|
+
|
181
|
+
# this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
|
182
|
+
it "updates previsoulsy associated records" do
|
183
|
+
record.collection << member
|
184
|
+
|
185
|
+
lambda { record.update!(collection_attributes: [{id: member.id, name: 'Foozle Cumberbunch'}]) }.should_not raise_error
|
186
|
+
|
187
|
+
record.collection.size.should == 1
|
188
|
+
record.collection.first.name.should == 'Foozle Cumberbunch'
|
189
|
+
end
|
117
190
|
end
|
118
191
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
record.collection_attributes = [{id: member.id}]
|
192
|
+
describe 'has_one' do
|
193
|
+
let(:record) { create(:single_nested_attribute_test) }
|
194
|
+
let(:member) { create(:assoc_nested_attribute_test) }
|
123
195
|
|
124
|
-
|
125
|
-
|
126
|
-
|
196
|
+
it "ignores nil values" do
|
197
|
+
record.single = nil
|
198
|
+
|
199
|
+
lambda { record.save! }.should_not raise_error
|
200
|
+
record.single.should be_nil
|
201
|
+
end
|
202
|
+
|
203
|
+
it "continues to create and associate new records" do
|
204
|
+
record.single_attributes = {name: 'wibble'}
|
205
|
+
|
206
|
+
lambda { record.save! }.should_not raise_error
|
207
|
+
|
208
|
+
record.single.name.should == 'wibble'
|
209
|
+
end
|
127
210
|
end
|
211
|
+
end
|
128
212
|
|
129
|
-
|
130
|
-
record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
|
213
|
+
describe 'unassociating records missing from the attributes collection' do
|
131
214
|
|
132
|
-
|
133
|
-
|
134
|
-
|
215
|
+
let(:single1) { create(:single_nested_attribute_test) }
|
216
|
+
let(:single2) { create(:single_nested_attribute_test) }
|
217
|
+
let(:assoc1) do
|
218
|
+
create(:assoc_nested_attribute_test) {|record| record.single_nested_attribute_test = single1 }
|
219
|
+
end
|
220
|
+
let(:assoc2) do
|
221
|
+
create(:assoc_nested_attribute_test) {|record| record.single_nested_attribute_test = single2 }
|
135
222
|
end
|
136
223
|
|
137
|
-
|
138
|
-
|
139
|
-
|
224
|
+
let(:record) do
|
225
|
+
record = create(:nested_attribute_test) {|r| r.collection = [assoc1, assoc2] }
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'handles has_many relationships' do
|
229
|
+
record.collection_attributes = [assoc2.as_json]
|
140
230
|
|
141
231
|
lambda { record.save! }.should_not raise_error
|
142
232
|
|
143
|
-
record.collection.
|
144
|
-
|
233
|
+
record.reload.collection.map(&:id).should == [assoc2.id]
|
234
|
+
|
235
|
+
assoc1.reload.nested_attribute_test.should be_nil
|
236
|
+
assoc2.reload.nested_attribute_test.should == record
|
145
237
|
end
|
146
238
|
|
147
|
-
|
148
|
-
|
149
|
-
|
239
|
+
it 'ignores has_many through' do
|
240
|
+
assoc2.single_nested_attribute_test.id.should == single2.id
|
241
|
+
|
242
|
+
record.through_collection.count.should == 2
|
150
243
|
|
151
|
-
|
244
|
+
record.through_collection_attributes = [single1.as_json]
|
152
245
|
|
153
|
-
record.
|
154
|
-
|
246
|
+
lambda { record.save! }.should_not raise_error
|
247
|
+
|
248
|
+
record.reload.through_collection.count.should == 2
|
155
249
|
end
|
156
|
-
end
|
157
250
|
|
158
|
-
|
159
|
-
|
160
|
-
|
251
|
+
it 'ignores habtm' do
|
252
|
+
test = create(:habtm_parent_nested_attribute_test)
|
253
|
+
test.foo << create(:habtm_child_nested_attribute_test)
|
254
|
+
test.foo << create(:habtm_child_nested_attribute_test)
|
255
|
+
test.save!
|
256
|
+
|
257
|
+
test.reload.foo.count.should == 2
|
161
258
|
|
162
|
-
|
163
|
-
record.single = nil
|
259
|
+
test.foo_attributes = [test.foo.first.as_json]
|
164
260
|
|
165
261
|
lambda { record.save! }.should_not raise_error
|
166
|
-
|
262
|
+
|
263
|
+
test.reload.foo.count.should == 2
|
167
264
|
end
|
168
265
|
|
169
|
-
it
|
170
|
-
record.
|
266
|
+
it 'allows removing all things from a collection' do
|
267
|
+
record.collection_attributes = []
|
171
268
|
|
172
269
|
lambda { record.save! }.should_not raise_error
|
173
270
|
|
174
|
-
record.
|
271
|
+
record.reload.collection.should be_empty
|
272
|
+
|
273
|
+
assoc1.reload.nested_attribute_test.should be_nil
|
274
|
+
assoc2.reload.nested_attribute_test.should be_nil
|
175
275
|
end
|
276
|
+
|
176
277
|
end
|
278
|
+
|
177
279
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: daylight
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.0.
|
4
|
+
version: 0.9.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid MacDonald
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-08-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activeresource
|
@@ -224,6 +224,7 @@ files:
|
|
224
224
|
- config/routes.rb
|
225
225
|
- lib/daylight.rb
|
226
226
|
- lib/daylight/api.rb
|
227
|
+
- lib/daylight/association_persistance.rb
|
227
228
|
- lib/daylight/associations.rb
|
228
229
|
- lib/daylight/client_reloader.rb
|
229
230
|
- lib/daylight/collection.rb
|
@@ -308,6 +309,7 @@ files:
|
|
308
309
|
- spec/dummy/public/favicon.ico
|
309
310
|
- spec/helpers/documentation_helper_spec.rb
|
310
311
|
- spec/lib/daylight/api_spec.rb
|
312
|
+
- spec/lib/daylight/association_persistance_spec.rb
|
311
313
|
- spec/lib/daylight/associations_spec.rb
|
312
314
|
- spec/lib/daylight/collection_spec.rb
|
313
315
|
- spec/lib/daylight/errors_spec.rb
|
@@ -398,6 +400,7 @@ test_files:
|
|
398
400
|
- spec/dummy/public/favicon.ico
|
399
401
|
- spec/helpers/documentation_helper_spec.rb
|
400
402
|
- spec/lib/daylight/api_spec.rb
|
403
|
+
- spec/lib/daylight/association_persistance_spec.rb
|
401
404
|
- spec/lib/daylight/associations_spec.rb
|
402
405
|
- spec/lib/daylight/collection_spec.rb
|
403
406
|
- spec/lib/daylight/errors_spec.rb
|