daylight 0.9.0.rc2 → 0.9.0.rc3
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.
- 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
|