jsonapi-resources 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +103 -71
  4. data/Rakefile +2 -2
  5. data/jsonapi-resources.gemspec +2 -2
  6. data/lib/jsonapi-resources.rb +0 -1
  7. data/lib/jsonapi/active_record_operations_processor.rb +10 -2
  8. data/lib/jsonapi/acts_as_resource_controller.rb +26 -24
  9. data/lib/jsonapi/association.rb +50 -15
  10. data/lib/jsonapi/callbacks.rb +1 -2
  11. data/lib/jsonapi/configuration.rb +8 -24
  12. data/lib/jsonapi/error.rb +1 -2
  13. data/lib/jsonapi/error_codes.rb +3 -1
  14. data/lib/jsonapi/exceptions.rb +59 -47
  15. data/lib/jsonapi/include_directives.rb +11 -11
  16. data/lib/jsonapi/mime_types.rb +2 -2
  17. data/lib/jsonapi/operation.rb +28 -11
  18. data/lib/jsonapi/operations_processor.rb +16 -5
  19. data/lib/jsonapi/paginator.rb +19 -19
  20. data/lib/jsonapi/request.rb +175 -196
  21. data/lib/jsonapi/resource.rb +158 -105
  22. data/lib/jsonapi/resource_serializer.rb +37 -26
  23. data/lib/jsonapi/resources/version.rb +2 -2
  24. data/lib/jsonapi/response_document.rb +5 -4
  25. data/lib/jsonapi/routing_ext.rb +24 -19
  26. data/test/controllers/controller_test.rb +261 -31
  27. data/test/fixtures/active_record.rb +206 -8
  28. data/test/fixtures/book_comments.yml +2 -1
  29. data/test/fixtures/books.yml +1 -0
  30. data/test/fixtures/documents.yml +3 -0
  31. data/test/fixtures/people.yml +8 -1
  32. data/test/fixtures/pictures.yml +15 -0
  33. data/test/fixtures/products.yml +3 -0
  34. data/test/fixtures/vehicles.yml +8 -0
  35. data/test/helpers/{hash_helpers.rb → assertions.rb} +6 -1
  36. data/test/integration/requests/request_test.rb +14 -3
  37. data/test/integration/routes/routes_test.rb +47 -0
  38. data/test/test_helper.rb +27 -4
  39. data/test/unit/serializer/include_directives_test.rb +5 -0
  40. data/test/unit/serializer/polymorphic_serializer_test.rb +384 -0
  41. data/test/unit/serializer/serializer_test.rb +19 -1
  42. metadata +14 -4
@@ -6,7 +6,7 @@ module JSONAPI
6
6
 
7
7
  @@resource_types = {}
8
8
 
9
- attr :context
9
+ attr_reader :context
10
10
  attr_reader :model
11
11
 
12
12
  define_jsonapi_resources_callbacks :create,
@@ -17,6 +17,7 @@ module JSONAPI
17
17
  :replace_has_many_links,
18
18
  :create_has_one_link,
19
19
  :replace_has_one_link,
20
+ :replace_polymorphic_has_one_link,
20
21
  :remove_has_many_link,
21
22
  :remove_has_one_link,
22
23
  :replace_fields
@@ -48,9 +49,7 @@ module JSONAPI
48
49
  completed = (yield == :completed)
49
50
  end
50
51
 
51
- if @save_needed || is_new?
52
- completed = (save == :completed)
53
- end
52
+ completed = (save == :completed) if @save_needed || is_new?
54
53
  end
55
54
  end
56
55
 
@@ -81,6 +80,12 @@ module JSONAPI
81
80
  end
82
81
  end
83
82
 
83
+ def replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
84
+ change :replace_polymorphic_has_one_link do
85
+ _replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
86
+ end
87
+ end
88
+
84
89
  def remove_has_many_link(association_type, key)
85
90
  change :remove_has_many_link do
86
91
  _remove_has_many_link(association_type, key)
@@ -106,11 +111,12 @@ module JSONAPI
106
111
 
107
112
  # Override this on a resource to customize how the associated records
108
113
  # are fetched for a model. Particularly helpful for authorization.
109
- def records_for(association_name, options = {})
114
+ def records_for(association_name, _options = {})
110
115
  model.send association_name
111
116
  end
112
117
 
113
118
  private
119
+
114
120
  def save
115
121
  run_callbacks :save do
116
122
  _save
@@ -132,45 +138,43 @@ module JSONAPI
132
138
  # ```
133
139
  def _save
134
140
  unless @model.valid?
135
- raise JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
141
+ fail JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
136
142
  end
137
143
 
138
144
  if defined? @model.save
139
145
  saved = @model.save
140
- unless saved
141
- raise JSONAPI::Exceptions::SaveFailed.new
142
- end
146
+ fail JSONAPI::Exceptions::SaveFailed.new unless saved
143
147
  else
144
148
  saved = true
145
149
  end
146
150
 
147
151
  @save_needed = !saved
148
152
 
149
- return :completed
153
+ :completed
150
154
  end
151
155
 
152
156
  def _remove
153
157
  @model.destroy
154
158
 
155
- return :completed
159
+ :completed
156
160
  end
157
161
 
158
162
  def _create_has_many_links(association_type, association_key_values)
159
163
  association = self.class._associations[association_type]
160
164
 
161
165
  association_key_values.each do |association_key_value|
162
- related_resource = Resource.resource_for(self.class.module_path + association.type.to_s).find_by_key(association_key_value, context: @context)
166
+ related_resource = association.resource_klass.find_by_key(association_key_value, context: @context)
163
167
 
164
- # ToDo: Add option to skip relations that already exist instead of returning an error?
168
+ # TODO: Add option to skip relations that already exist instead of returning an error?
165
169
  relation = @model.send(association.type).where(association.primary_key => association_key_value).first
166
170
  if relation.nil?
167
171
  @model.send(association.type) << related_resource.model
168
172
  else
169
- raise JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
173
+ fail JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
170
174
  end
171
175
  end
172
176
 
173
- return :completed
177
+ :completed
174
178
  end
175
179
 
176
180
  def _replace_has_many_links(association_type, association_key_values)
@@ -179,7 +183,7 @@ module JSONAPI
179
183
  send("#{association.foreign_key}=", association_key_values)
180
184
  @save_needed = true
181
185
 
182
- return :completed
186
+ :completed
183
187
  end
184
188
 
185
189
  def _replace_has_one_link(association_type, association_key_value)
@@ -188,7 +192,18 @@ module JSONAPI
188
192
  send("#{association.foreign_key}=", association_key_value)
189
193
  @save_needed = true
190
194
 
191
- return :completed
195
+ :completed
196
+ end
197
+
198
+ def _replace_polymorphic_has_one_link(association_type, key_value, key_type)
199
+ association = self.class._associations[association_type.to_sym]
200
+
201
+ model.send("#{association.foreign_key}=", key_value)
202
+ model.send("#{association.polymorphic_type}=", key_type.to_s.classify)
203
+
204
+ @save_needed = true
205
+
206
+ :completed
192
207
  end
193
208
 
194
209
  def _remove_has_many_link(association_type, key)
@@ -196,7 +211,7 @@ module JSONAPI
196
211
 
197
212
  @model.send(association.type).delete(key)
198
213
 
199
- return :completed
214
+ :completed
200
215
  end
201
216
 
202
217
  def _remove_has_one_link(association_type)
@@ -205,7 +220,7 @@ module JSONAPI
205
220
  send("#{association.foreign_key}=", nil)
206
221
  @save_needed = true
207
222
 
208
- return :completed
223
+ :completed
209
224
  end
210
225
 
211
226
  def _replace_fields(field_data)
@@ -224,7 +239,12 @@ module JSONAPI
224
239
  if value.nil?
225
240
  remove_has_one_link(association_type)
226
241
  else
227
- replace_has_one_link(association_type, value)
242
+ case value
243
+ when Hash
244
+ replace_polymorphic_has_one_link(association_type.to_s, value.fetch(:id), value.fetch(:type))
245
+ else
246
+ replace_has_one_link(association_type, value)
247
+ end
228
248
  end
229
249
  end if field_data[:has_one]
230
250
 
@@ -232,7 +252,7 @@ module JSONAPI
232
252
  replace_has_many_links(association_type, values)
233
253
  end if field_data[:has_many]
234
254
 
235
- return :completed
255
+ :completed
236
256
  end
237
257
 
238
258
  class << self
@@ -253,15 +273,15 @@ module JSONAPI
253
273
  resource_name = JSONAPI::Resource._resource_name_from_type(type)
254
274
  resource = resource_name.safe_constantize if resource_name
255
275
  if resource.nil?
256
- raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
276
+ fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
257
277
  end
258
278
  resource
259
279
  end
260
280
 
261
- attr_accessor :_attributes, :_associations, :_allowed_filters , :_type, :_paginator
281
+ attr_accessor :_attributes, :_associations, :_allowed_filters, :_type, :_paginator
262
282
 
263
283
  def create(context)
264
- self.new(self.create_model, context)
284
+ new(create_model, context)
265
285
  end
266
286
 
267
287
  def create_model
@@ -303,7 +323,7 @@ module JSONAPI
303
323
  end
304
324
 
305
325
  def default_attribute_options
306
- {format: :default}
326
+ { format: :default }
307
327
  end
308
328
 
309
329
  def has_one(*attrs)
@@ -319,7 +339,7 @@ module JSONAPI
319
339
  end
320
340
 
321
341
  def filters(*attrs)
322
- @_allowed_filters.merge!(attrs.inject( Hash.new ) { |h, attr| h[attr] = {}; h })
342
+ @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
323
343
  end
324
344
 
325
345
  def filter(attr, *args)
@@ -334,11 +354,11 @@ module JSONAPI
334
354
  # :nocov:
335
355
  def method_missing(method, *args)
336
356
  if method.to_s.match /createable_fields/
337
- ActiveSupport::Deprecation.warn("`createable_fields` is deprecated, please use `creatable_fields` instead")
338
- self.creatable_fields(*args)
357
+ ActiveSupport::Deprecation.warn('`createable_fields` is deprecated, please use `creatable_fields` instead')
358
+ creatable_fields(*args)
339
359
  elsif method.to_s.match /updateable_fields/
340
- ActiveSupport::Deprecation.warn("`updateable_fields` is deprecated, please use `updatable_fields` instead")
341
- self.updatable_fields(*args)
360
+ ActiveSupport::Deprecation.warn('`updateable_fields` is deprecated, please use `updatable_fields` instead')
361
+ updatable_fields(*args)
342
362
  else
343
363
  super
344
364
  end
@@ -346,17 +366,17 @@ module JSONAPI
346
366
  # :nocov:
347
367
 
348
368
  # Override in your resource to filter the updatable keys
349
- def updatable_fields(context = nil)
369
+ def updatable_fields(_context = nil)
350
370
  _updatable_associations | _attributes.keys - [:id]
351
371
  end
352
372
 
353
373
  # Override in your resource to filter the creatable keys
354
- def creatable_fields(context = nil)
374
+ def creatable_fields(_context = nil)
355
375
  _updatable_associations | _attributes.keys
356
376
  end
357
377
 
358
378
  # Override in your resource to filter the sortable keys
359
- def sortable_fields(context = nil)
379
+ def sortable_fields(_context = nil)
360
380
  _attributes.keys
361
381
  end
362
382
 
@@ -364,15 +384,38 @@ module JSONAPI
364
384
  _associations.keys | _attributes.keys
365
385
  end
366
386
 
367
- def apply_includes(records, directives)
368
- records = records.includes(*directives.model_includes) if directives
387
+ def resolve_association_names_to_relations(resource_klass, model_includes, options = {})
388
+ case model_includes
389
+ when Array
390
+ return model_includes.map do |value|
391
+ resolve_association_names_to_relations(resource_klass, value, options)
392
+ end
393
+ when Hash
394
+ model_includes.keys.each do |key|
395
+ association = resource_klass._associations[key]
396
+ value = model_includes[key]
397
+ model_includes.delete(key)
398
+ model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options)
399
+ end
400
+ return model_includes
401
+ when Symbol
402
+ association = resource_klass._associations[model_includes]
403
+ return association.relation_name(options)
404
+ end
405
+ end
406
+
407
+ def apply_includes(records, options = {})
408
+ include_directives = options[:include_directives]
409
+ if include_directives
410
+ model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options)
411
+ records = records.includes(model_includes)
412
+ end
413
+
369
414
  records
370
415
  end
371
416
 
372
417
  def apply_pagination(records, paginator, order_options)
373
- if paginator
374
- records = paginator.apply(records, order_options)
375
- end
418
+ records = paginator.apply(records, order_options) if paginator
376
419
  records
377
420
  end
378
421
 
@@ -384,7 +427,7 @@ module JSONAPI
384
427
  end
385
428
  end
386
429
 
387
- def apply_filter(records, filter, value, options = {})
430
+ def apply_filter(records, filter, value, _options = {})
388
431
  records.where(filter => value)
389
432
  end
390
433
 
@@ -395,7 +438,7 @@ module JSONAPI
395
438
  filters.each do |filter, value|
396
439
  if _associations.include?(filter)
397
440
  if _associations[filter].is_a?(JSONAPI::Association::HasMany)
398
- required_includes.push(filter)
441
+ required_includes.push(filter.to_s)
399
442
  records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
400
443
  else
401
444
  records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
@@ -407,20 +450,16 @@ module JSONAPI
407
450
  end
408
451
 
409
452
  if required_includes.any?
410
- records.includes(required_includes)
411
- elsif records.respond_to? :to_ary
412
- records
413
- else
414
- records.all
453
+ records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
415
454
  end
455
+
456
+ records
416
457
  end
417
458
 
418
459
  def filter_records(filters, options)
419
- include_directives = options[:include_directives]
420
-
421
460
  records = records(options)
422
- records = apply_includes(records, include_directives)
423
- apply_filters(records, filters, options)
461
+ records = apply_filters(records, filters, options)
462
+ apply_includes(records, options)
424
463
  end
425
464
 
426
465
  def sort_records(records, order_options)
@@ -445,27 +484,24 @@ module JSONAPI
445
484
 
446
485
  resources = []
447
486
  records.each do |model|
448
- resources.push self.new(model, context)
487
+ resources.push new(model, context)
449
488
  end
450
489
 
451
- return resources
490
+ resources
452
491
  end
453
492
 
454
493
  def find_by_key(key, options = {})
455
494
  context = options[:context]
456
- include_directives = options[:include_directives]
457
495
  records = records(options)
458
- records = apply_includes(records, include_directives)
496
+ records = apply_includes(records, options)
459
497
  model = records.where({_primary_key => key}).first
460
- if model.nil?
461
- raise JSONAPI::Exceptions::RecordNotFound.new(key)
462
- end
463
- self.new(model, context)
498
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
499
+ new(model, context)
464
500
  end
465
501
 
466
502
  # Override this method if you want to customize the relation for
467
503
  # finder methods (find, find_by_key)
468
- def records(options = {})
504
+ def records(_options = {})
469
505
  _model_class
470
506
  end
471
507
 
@@ -494,7 +530,7 @@ module JSONAPI
494
530
  end
495
531
 
496
532
  # override to allow for key processing and checking
497
- def verify_key(key, context = nil)
533
+ def verify_key(key, _context = nil)
498
534
  key && Integer(key)
499
535
  rescue
500
536
  raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
@@ -508,13 +544,13 @@ module JSONAPI
508
544
  end
509
545
 
510
546
  # override to allow for custom filters
511
- def verify_custom_filter(filter, value, context = nil)
512
- return filter, value
547
+ def verify_custom_filter(filter, value, _context = nil)
548
+ [filter, value]
513
549
  end
514
550
 
515
551
  # override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys
516
- def verify_association_filter(filter, raw, context = nil)
517
- return filter, raw
552
+ def verify_association_filter(filter, raw, _context = nil)
553
+ [filter, raw]
518
554
  end
519
555
 
520
556
  # quasi private class methods
@@ -523,12 +559,12 @@ module JSONAPI
523
559
  end
524
560
 
525
561
  def _updatable_associations
526
- @_associations.map { |key, association| key }
562
+ @_associations.map { |key, _association| key }
527
563
  end
528
564
 
529
565
  def _has_association?(type)
530
566
  type = type.to_s
531
- @_associations.has_key?(type.singularize.to_sym) || @_associations.has_key?(type.pluralize.to_sym)
567
+ @_associations.key?(type.singularize.to_sym) || @_associations.key?(type.pluralize.to_sym)
532
568
  end
533
569
 
534
570
  def _association(type)
@@ -537,7 +573,7 @@ module JSONAPI
537
573
  end
538
574
 
539
575
  def _model_name
540
- @_model_name ||= self.name.demodulize.sub(/Resource$/, '')
576
+ @_model_name ||= name.demodulize.sub(/Resource$/, '')
541
577
  end
542
578
 
543
579
  def _primary_key
@@ -549,13 +585,13 @@ module JSONAPI
549
585
  end
550
586
 
551
587
  def _allowed_filters
552
- !@_allowed_filters.nil? ? @_allowed_filters : { :id => {} }
588
+ !@_allowed_filters.nil? ? @_allowed_filters : { id: {} }
553
589
  end
554
590
 
555
591
  def _resource_name_from_type(type)
556
592
  class_name = @@resource_types[type]
557
593
  if class_name.nil?
558
- class_name = "#{type.to_s.singularize}_resource".camelize
594
+ class_name = "#{type.to_s.underscore.singularize}_resource".camelize
559
595
  @@resource_types[type] = class_name
560
596
  end
561
597
  return class_name
@@ -578,19 +614,20 @@ module JSONAPI
578
614
  end
579
615
 
580
616
  def module_path
581
- @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
617
+ @module_path ||= name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
582
618
  end
583
619
 
584
620
  def construct_order_options(sort_params)
585
621
  return {} unless sort_params
586
622
 
587
- sort_params.each_with_object({}) { |sort, order_hash|
623
+ sort_params.each_with_object({}) do |sort, order_hash|
588
624
  field = sort[:field] == 'id' ? _primary_key : sort[:field]
589
625
  order_hash[field] = sort[:direction]
590
- }
626
+ end
591
627
  end
592
628
 
593
629
  private
630
+
594
631
  def check_reserved_resource_name(type, name)
595
632
  if [:ids, :types, :hrefs, :links].include?(type)
596
633
  warn "[NAME COLLISION] `#{name}` is a reserved resource name."
@@ -618,63 +655,79 @@ module JSONAPI
618
655
 
619
656
  attrs.each do |attr|
620
657
  check_reserved_association_name(attr)
658
+ @_associations[attr] = association = klass.new(attr, options)
621
659
 
622
- @_associations[attr] = klass.new(attr, options)
660
+ associated_records_method_name = case association
661
+ when JSONAPI::Association::HasOne then "record_for_#{attr}"
662
+ when JSONAPI::Association::HasMany then "records_for_#{attr}"
663
+ end
623
664
 
624
- foreign_key = @_associations[attr].foreign_key
625
-
626
- define_method foreign_key do
627
- @model.method(foreign_key).call
628
- end unless method_defined?(foreign_key)
665
+ foreign_key = association.foreign_key
629
666
 
630
667
  define_method "#{foreign_key}=" do |value|
631
668
  @model.method("#{foreign_key}=").call(value)
632
669
  end unless method_defined?("#{foreign_key}=")
633
670
 
634
- associated_records_method_name = case @_associations[attr]
635
- when JSONAPI::Association::HasOne then "record_for_#{attr}"
636
- when JSONAPI::Association::HasMany then "records_for_#{attr}"
637
- end
638
-
639
- define_method associated_records_method_name do |options={}|
640
- records_for(attr, options)
671
+ define_method associated_records_method_name do |options = {}|
672
+ relation_name = association.relation_name(options.merge({context: @context}))
673
+ records_for(relation_name, options)
641
674
  end unless method_defined?(associated_records_method_name)
642
675
 
643
- if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
644
- define_method attr do
645
- type_name = self.class._associations[attr].type.to_s
646
- resource_class = Resource.resource_for(self.class.module_path + type_name)
647
- if resource_class
676
+ if association.is_a?(JSONAPI::Association::HasOne)
677
+ define_method foreign_key do
678
+ @model.method(foreign_key).call
679
+ end unless method_defined?(foreign_key)
680
+
681
+ define_method attr do |options = {}|
682
+ if association.polymorphic?
648
683
  associated_model = public_send(associated_records_method_name)
649
- return associated_model ? resource_class.new(associated_model, @context) : nil
684
+ resource_klass = Resource.resource_for(self.class.module_path + associated_model.class.to_s.underscore) if associated_model
685
+ return resource_klass.new(associated_model, @context) if resource_klass
686
+ else
687
+ resource_klass = association.resource_klass
688
+ if resource_klass
689
+ associated_model = public_send(associated_records_method_name)
690
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
691
+ end
650
692
  end
651
693
  end unless method_defined?(attr)
652
- elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
694
+ elsif association.is_a?(JSONAPI::Association::HasMany)
695
+ define_method foreign_key do
696
+ records = public_send(associated_records_method_name)
697
+ return records.collect do |record|
698
+ record.send(association.resource_klass._primary_key)
699
+ end
700
+ end unless method_defined?(foreign_key)
653
701
  define_method attr do |options = {}|
654
- type_name = self.class._associations[attr].type.to_s
655
- resource_class = Resource.resource_for(self.class.module_path + type_name)
702
+ resource_klass = association.resource_klass
703
+ records = public_send(associated_records_method_name)
704
+
656
705
  filters = options.fetch(:filters, {})
706
+ unless filters.nil? || filters.empty?
707
+ records = resource_klass.apply_filters(records, filters, options)
708
+ end
709
+
657
710
  sort_criteria = options.fetch(:sort_criteria, {})
658
- paginator = options[:paginator]
711
+ unless sort_criteria.nil? || sort_criteria.empty?
712
+ order_options = self.class.construct_order_options(sort_criteria)
713
+ records = resource_klass.apply_sort(records, order_options)
714
+ end
659
715
 
660
- resources = []
716
+ paginator = options[:paginator]
717
+ if paginator
718
+ records = resource_klass.apply_pagination(records, paginator, order_options)
719
+ end
661
720
 
662
- if resource_class
663
- records = public_send(associated_records_method_name)
664
- records = resource_class.apply_filters(records, filters, options)
665
- order_options = self.class.construct_order_options(sort_criteria)
666
- records = resource_class.apply_sort(records, order_options)
667
- records = resource_class.apply_pagination(records, paginator, order_options)
668
- records.each do |record|
669
- resources.push resource_class.new(record, @context)
721
+ return records.collect do |record|
722
+ if association.polymorphic?
723
+ resource_klass = Resource.resource_for(self.class.module_path + record.class.to_s.underscore)
670
724
  end
725
+ resource_klass.new(record, @context)
671
726
  end
672
- return resources
673
727
  end unless method_defined?(attr)
674
728
  end
675
729
  end
676
730
  end
677
731
  end
678
-
679
732
  end
680
733
  end