rest_framework 0.9.15 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +1 -2
- data/app/views/rest_framework/_head.html.erb +3 -2
- data/app/views/rest_framework/_heading.html.erb +1 -1
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +3 -3
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/lib/rest_framework/filters/base_filter.rb +1 -1
- data/lib/rest_framework/filters/{model_ordering_filter.rb → ordering_filter.rb} +6 -3
- data/lib/rest_framework/filters/{model_query_filter.rb → query_filter.rb} +10 -7
- data/lib/rest_framework/filters/ransack_filter.rb +1 -1
- data/lib/rest_framework/filters/{model_search_filter.rb → search_filter.rb} +6 -4
- data/lib/rest_framework/filters.rb +3 -3
- data/lib/rest_framework/mixins/base_controller_mixin.rb +42 -62
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +1 -1
- data/lib/rest_framework/mixins/model_controller_mixin.rb +117 -90
- data/lib/rest_framework/serializers/native_serializer.rb +16 -5
- data/lib/rest_framework/utils.rb +38 -18
- data/lib/rest_framework.rb +16 -16
- data/vendor/assets/javascripts/rest_framework/external.min.js +618 -575
- data/vendor/assets/stylesheets/rest_framework/external.min.css +12 -19
- metadata +8 -8
@@ -2,6 +2,8 @@
|
|
2
2
|
module RESTFramework::Mixins::BaseModelControllerMixin
|
3
3
|
BASE64_REGEX = /data:(.*);base64,(.*)/
|
4
4
|
BASE64_TRANSLATE = ->(field, value) {
|
5
|
+
return value unless BASE64_REGEX.match?(value)
|
6
|
+
|
5
7
|
_, content_type, payload = value.match(BASE64_REGEX).to_a
|
6
8
|
return {
|
7
9
|
io: StringIO.new(Base64.decode64(payload)),
|
@@ -45,14 +47,24 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
45
47
|
native_serializer_associations_limit_query_param: "associations_limit",
|
46
48
|
native_serializer_include_associations_count: false,
|
47
49
|
|
48
|
-
# Attributes for
|
49
|
-
|
50
|
+
# Attributes for filtering, ordering, and searching.
|
51
|
+
filter_backends: [
|
52
|
+
RESTFramework::QueryFilter,
|
53
|
+
RESTFramework::OrderingFilter,
|
54
|
+
RESTFramework::SearchFilter,
|
55
|
+
],
|
56
|
+
filter_recordset_before_find: true,
|
57
|
+
filter_fields: nil,
|
50
58
|
ordering_fields: nil,
|
51
59
|
ordering_query_param: "ordering",
|
52
60
|
ordering_no_reorder: false,
|
53
61
|
search_fields: nil,
|
54
62
|
search_query_param: "search",
|
55
63
|
search_ilike: false,
|
64
|
+
ransack_options: nil,
|
65
|
+
ransack_query_param: "q",
|
66
|
+
ransack_distinct: true,
|
67
|
+
ransack_distinct_query_param: "distinct",
|
56
68
|
|
57
69
|
# Options for association assignment.
|
58
70
|
permit_id_assignment: true,
|
@@ -60,21 +72,11 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
60
72
|
|
61
73
|
# Option for `recordset.create` vs `Model.create` behavior.
|
62
74
|
create_from_recordset: true,
|
63
|
-
|
64
|
-
# Control if filtering is done before find.
|
65
|
-
filter_recordset_before_find: true,
|
66
|
-
|
67
|
-
# Options for `ransack` filtering.
|
68
|
-
ransack_options: nil,
|
69
|
-
ransack_query_param: "q",
|
70
|
-
ransack_distinct: true,
|
71
|
-
ransack_distinct_query_param: "distinct",
|
72
75
|
}
|
73
76
|
|
74
77
|
module ClassMethods
|
75
78
|
IGNORE_VALIDATORS_WITH_KEYS = [:if, :unless].freeze
|
76
79
|
|
77
|
-
# Get the model for this controller.
|
78
80
|
def get_model
|
79
81
|
return @model if @model
|
80
82
|
return (@model = self.model) if self.model
|
@@ -88,8 +90,8 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
88
90
|
raise RESTFramework::UnknownModelError, self
|
89
91
|
end
|
90
92
|
|
91
|
-
# Override
|
92
|
-
def
|
93
|
+
# Override to include ActiveRecord i18n-translated column names.
|
94
|
+
def label_for(s)
|
93
95
|
return self.get_model.human_attribute_name(s, default: super)
|
94
96
|
end
|
95
97
|
|
@@ -101,13 +103,20 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
101
103
|
# If fields is a hash, then parse it.
|
102
104
|
if input_fields.is_a?(Hash)
|
103
105
|
return RESTFramework::Utils.parse_fields_hash(
|
104
|
-
input_fields,
|
106
|
+
input_fields,
|
107
|
+
self.get_model,
|
108
|
+
exclude_associations: self.exclude_associations,
|
109
|
+
action_text: self.enable_action_text,
|
110
|
+
active_storage: self.enable_active_storage,
|
105
111
|
)
|
106
112
|
elsif !input_fields
|
107
113
|
# Otherwise, if fields is nil, then fallback to columns.
|
108
114
|
model = self.get_model
|
109
115
|
return model ? RESTFramework::Utils.fields_for(
|
110
|
-
model,
|
116
|
+
model,
|
117
|
+
exclude_associations: self.exclude_associations,
|
118
|
+
action_text: self.enable_action_text,
|
119
|
+
active_storage: self.enable_active_storage,
|
111
120
|
) : []
|
112
121
|
elsif input_fields
|
113
122
|
input_fields = input_fields.map(&:to_s)
|
@@ -117,10 +126,10 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
117
126
|
end
|
118
127
|
|
119
128
|
# Get a field's config, including defaults.
|
120
|
-
def
|
129
|
+
def field_config_for(f)
|
121
130
|
f = f.to_sym
|
122
|
-
@
|
123
|
-
return @
|
131
|
+
@_field_config_for ||= {}
|
132
|
+
return @_field_config_for[f] if @_field_config_for[f]
|
124
133
|
|
125
134
|
config = self.field_config&.dig(f) || {}
|
126
135
|
|
@@ -147,7 +156,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
147
156
|
}.to_h.compact.presence
|
148
157
|
end
|
149
158
|
|
150
|
-
return @
|
159
|
+
return @_field_config_for[f] = config.compact
|
151
160
|
end
|
152
161
|
|
153
162
|
# Get metadata about the resource's fields.
|
@@ -173,7 +182,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
173
182
|
metadata = {
|
174
183
|
type: nil,
|
175
184
|
kind: nil,
|
176
|
-
label: self.
|
185
|
+
label: self.label_for(f),
|
177
186
|
primary_key: nil,
|
178
187
|
required: nil,
|
179
188
|
read_only: nil,
|
@@ -305,14 +314,14 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
305
314
|
end
|
306
315
|
|
307
316
|
# Serialize any field config.
|
308
|
-
metadata[:config] = self.
|
317
|
+
metadata[:config] = self.field_config_for(f).presence
|
309
318
|
|
310
319
|
next [f, metadata.compact]
|
311
320
|
}.to_h
|
312
321
|
end
|
313
322
|
|
314
323
|
# Get a hash of metadata to be rendered in the `OPTIONS` response.
|
315
|
-
def
|
324
|
+
def options_metadata
|
316
325
|
return super.merge(
|
317
326
|
{
|
318
327
|
primary_key: self.get_model.primary_key,
|
@@ -398,9 +407,8 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
398
407
|
return self.class.get_fields(input_fields: self.class.fields)
|
399
408
|
end
|
400
409
|
|
401
|
-
|
402
|
-
|
403
|
-
return self.class.get_options_metadata
|
410
|
+
def options_metadata
|
411
|
+
return self.class.options_metadata
|
404
412
|
end
|
405
413
|
|
406
414
|
# Get a list of parameters allowed for the current action.
|
@@ -410,35 +418,34 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
410
418
|
@_get_allowed_parameters = self.allowed_parameters
|
411
419
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
412
420
|
|
413
|
-
#
|
421
|
+
# Assemble strong parameters.
|
414
422
|
variations = []
|
415
423
|
hash_variations = {}
|
416
|
-
alt_hash_variations = {}
|
417
424
|
reflections = self.class.get_model.reflections
|
418
425
|
@_get_allowed_parameters = self.get_fields.map { |f|
|
419
426
|
f = f.to_s
|
420
427
|
|
421
|
-
#
|
422
|
-
if reflections.key?("#{f}
|
423
|
-
hash_variations[f] = ACTIVESTORAGE_KEYS
|
428
|
+
# ActionText Integration:
|
429
|
+
if self.class.enable_action_text && reflections.key?("rich_test_#{f}")
|
424
430
|
next f
|
425
431
|
end
|
426
432
|
|
427
|
-
# ActiveStorage Integration: `
|
428
|
-
if reflections.key?("#{f}
|
429
|
-
hash_variations[f] =
|
430
|
-
|
431
|
-
next nil
|
433
|
+
# ActiveStorage Integration: `has_one_attached`
|
434
|
+
if self.class.enable_active_storage && reflections.key?("#{f}_attachment")
|
435
|
+
hash_variations[f] = ACTIVESTORAGE_KEYS
|
436
|
+
next f
|
432
437
|
end
|
433
438
|
|
434
|
-
#
|
435
|
-
if reflections.key?("
|
436
|
-
|
439
|
+
# ActiveStorage Integration: `has_many_attached`
|
440
|
+
if self.class.enable_active_storage && reflections.key?("#{f}_attachments")
|
441
|
+
hash_variations[f] = ACTIVESTORAGE_KEYS
|
442
|
+
next nil
|
437
443
|
end
|
438
444
|
|
439
445
|
# Return field if it's not an association.
|
440
446
|
next f unless ref = reflections[f]
|
441
447
|
|
448
|
+
# Add `_id`/`_ids` variations for associations.
|
442
449
|
if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
|
443
450
|
if id_field.ends_with?("_ids")
|
444
451
|
hash_variations[id_field] = []
|
@@ -447,34 +454,36 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
447
454
|
end
|
448
455
|
end
|
449
456
|
|
457
|
+
# Add `_attributes` variations for associations.
|
450
458
|
if self.permit_nested_attributes_assignment
|
451
459
|
hash_variations["#{f}_attributes"] = (
|
452
|
-
self.class.
|
460
|
+
self.class.field_config_for(f)[:sub_fields] + ["_destroy"]
|
453
461
|
)
|
454
462
|
end
|
455
463
|
|
456
|
-
# Associations are not allowed to be submitted in their bare form
|
464
|
+
# Associations are not allowed to be submitted in their bare form (if they are submitted that
|
465
|
+
# way, they will be translated to either ID assignment or nested attributes assignment).
|
457
466
|
next nil
|
458
467
|
}.compact
|
459
468
|
@_get_allowed_parameters += variations
|
460
469
|
@_get_allowed_parameters << hash_variations
|
461
|
-
|
470
|
+
|
462
471
|
return @_get_allowed_parameters
|
463
472
|
end
|
464
473
|
|
465
|
-
|
466
|
-
def get_serializer_class
|
474
|
+
def serializer_class
|
467
475
|
return super || RESTFramework::NativeSerializer
|
468
476
|
end
|
469
477
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
+
def apply_filters(data)
|
479
|
+
# TODO: Compatibility; remove in 1.0.
|
480
|
+
if filtered_data = self.try(:get_filtered_data, data)
|
481
|
+
return filtered_data
|
482
|
+
end
|
483
|
+
|
484
|
+
return self.filter_backends&.reduce(data) { |d, filter|
|
485
|
+
filter.new(controller: self).filter_data(d)
|
486
|
+
} || data
|
478
487
|
end
|
479
488
|
|
480
489
|
# Use strong parameters to filter the request body using the configured allowed parameters.
|
@@ -500,6 +509,47 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
500
509
|
end
|
501
510
|
end
|
502
511
|
|
512
|
+
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
513
|
+
#
|
514
|
+
# rubocop:disable Layout/LineLength
|
515
|
+
#
|
516
|
+
# Example base64 images (red, green, and blue squares):
|
517
|
+
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC
|
518
|
+
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNk+M9Qz0AEYBxVSF+FAAhKDveksOjmAAAAAElFTkSuQmCC
|
519
|
+
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC
|
520
|
+
#
|
521
|
+
# rubocop:enable Layout/LineLength
|
522
|
+
has_many_attached_scalar_data = {}
|
523
|
+
if self.class.enable_active_storage
|
524
|
+
self.class.get_model.attachment_reflections.keys.each do |k|
|
525
|
+
if data[k].is_a?(Array)
|
526
|
+
data[k] = data[k].map { |v|
|
527
|
+
if v.is_a?(String)
|
528
|
+
v = BASE64_TRANSLATE.call(k, v)
|
529
|
+
|
530
|
+
# Remember scalars because Rails strong params will remove it.
|
531
|
+
if v.is_a?(String)
|
532
|
+
has_many_attached_scalar_data[k] ||= []
|
533
|
+
has_many_attached_scalar_data[k] << v
|
534
|
+
end
|
535
|
+
elsif v.is_a?(Hash)
|
536
|
+
if v[:io].is_a?(String)
|
537
|
+
v[:io] = StringIO.new(Base64.decode64(v[:io]))
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
next v
|
542
|
+
}
|
543
|
+
elsif data[k].is_a?(Hash)
|
544
|
+
if data[k][:io].is_a?(String)
|
545
|
+
data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
|
546
|
+
end
|
547
|
+
elsif data[k].is_a?(String)
|
548
|
+
data[k] = BASE64_TRANSLATE.call(k, data[k])
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
503
553
|
# Filter the request body with strong params. If `bulk` is true, then we apply allowed
|
504
554
|
# parameters to the `_json` key of the request body.
|
505
555
|
body_params = if allowed_params == true
|
@@ -511,6 +561,15 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
511
561
|
ActionController::Parameters.new(data).permit(*allowed_params)
|
512
562
|
end
|
513
563
|
|
564
|
+
# ActiveStorage Integration: Workaround for Rails strong params not allowing you to permit an
|
565
|
+
# array containing a mix of scalars and hashes. This is needed for `has_many_attached`, because
|
566
|
+
# API consumers must be able to provide scalar `signed_id` values for existing attachments along
|
567
|
+
# with hashes for new attachments. It's worth mentioning that base64 scalars are converted to
|
568
|
+
# hashes that conform to the ActiveStorage API.
|
569
|
+
has_many_attached_scalar_data.each do |k, v|
|
570
|
+
body_params[k].unshift(*v)
|
571
|
+
end
|
572
|
+
|
514
573
|
# Filter primary key, if configured.
|
515
574
|
if self.filter_pk_from_request_body && bulk_mode != :update
|
516
575
|
body_params.delete(pk)
|
@@ -519,35 +578,6 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
519
578
|
# Filter fields in `exclude_body_fields`.
|
520
579
|
(self.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
521
580
|
|
522
|
-
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
523
|
-
#
|
524
|
-
# rubocop:disable Layout/LineLength
|
525
|
-
#
|
526
|
-
# Example base64 image:
|
527
|
-
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=
|
528
|
-
#
|
529
|
-
# rubocop:enable Layout/LineLength
|
530
|
-
self.class.get_model.attachment_reflections.keys.each do |k|
|
531
|
-
if body_params[k].is_a?(Array)
|
532
|
-
body_params[k] = body_params[k].map { |v|
|
533
|
-
if v.is_a?(String)
|
534
|
-
BASE64_TRANSLATE.call(k, v)
|
535
|
-
elsif v.is_a?(ActionController::Parameters)
|
536
|
-
if v[:io].is_a?(String)
|
537
|
-
v[:io] = StringIO.new(Base64.decode64(v[:io]))
|
538
|
-
end
|
539
|
-
v
|
540
|
-
end
|
541
|
-
}
|
542
|
-
elsif body_params[k].is_a?(ActionController::Parameters)
|
543
|
-
if body_params[k][:io].is_a?(String)
|
544
|
-
body_params[k][:io] = StringIO.new(Base64.decode64(body_params[k][:io]))
|
545
|
-
end
|
546
|
-
elsif body_params[k].is_a?(String)
|
547
|
-
body_params[k] = BASE64_TRANSLATE.call(k, body_params[k])
|
548
|
-
end
|
549
|
-
end
|
550
|
-
|
551
581
|
return body_params
|
552
582
|
end
|
553
583
|
alias_method :get_create_params, :get_body_params
|
@@ -567,13 +597,12 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
567
597
|
|
568
598
|
# Get the records this controller has access to *after* any filtering is applied.
|
569
599
|
def get_records
|
570
|
-
return @records ||= self.
|
600
|
+
return @records ||= self.apply_filters(self.get_recordset)
|
571
601
|
end
|
572
602
|
|
573
|
-
# Get a single record by primary key or another column, if allowed. The return value is
|
574
|
-
# exposed to the view as the `@record` instance variable.
|
603
|
+
# Get a single record by primary key or another column, if allowed. The return value is memoized
|
604
|
+
# and exposed to the view as the `@record` instance variable.
|
575
605
|
def get_record
|
576
|
-
# Cache the result.
|
577
606
|
return @record if @record
|
578
607
|
|
579
608
|
find_by_key = self.class.get_model.primary_key
|
@@ -598,7 +627,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
598
627
|
self.get_recordset
|
599
628
|
end
|
600
629
|
|
601
|
-
# Return the record. Route key is always `:id` by Rails convention.
|
630
|
+
# Return the record. Route key is always `:id` by Rails' convention.
|
602
631
|
if is_pk
|
603
632
|
return @record = collection.find(request.path_parameters[:id])
|
604
633
|
else
|
@@ -625,11 +654,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
625
654
|
# This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
|
626
655
|
# the serializer directly. This would fail for active model serializers, but maybe we don't
|
627
656
|
# care?
|
628
|
-
|
657
|
+
s = RESTFramework::Utils.wrap_ams(self.serializer_class)
|
629
658
|
serialized_records = records.map do |record|
|
630
|
-
|
631
|
-
{errors: record.errors.presence}.compact,
|
632
|
-
)
|
659
|
+
s.new(record, controller: self).serialize.merge!({errors: record.errors.presence}.compact)
|
633
660
|
end
|
634
661
|
|
635
662
|
return serialized_records
|
@@ -195,7 +195,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
195
195
|
attachment_reflections = @model.attachment_reflections
|
196
196
|
|
197
197
|
fields.each do |f|
|
198
|
-
field_config = @controller.class.
|
198
|
+
field_config = @controller.class.field_config_for(f)
|
199
199
|
next if field_config[:write_only]
|
200
200
|
|
201
201
|
if f.in?(column_names)
|
@@ -246,27 +246,38 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
246
246
|
includes[f] = sub_config
|
247
247
|
includes_map[f] = f.to_sym
|
248
248
|
end
|
249
|
-
elsif ref = reflections["rich_text_#{f}"]
|
249
|
+
elsif @controller.class.enable_action_text && ref = reflections["rich_text_#{f}"]
|
250
250
|
# ActionText Integration: Define rich text serializer method.
|
251
251
|
includes_map[f] = :"rich_text_#{f}"
|
252
252
|
serializer_methods[f] = f
|
253
253
|
self.define_singleton_method(f) do |record|
|
254
254
|
next record.send(f).to_s
|
255
255
|
end
|
256
|
-
elsif ref = attachment_reflections[f]
|
256
|
+
elsif @controller.class.enable_active_storage && ref = attachment_reflections[f]
|
257
257
|
# ActiveStorage Integration: Define attachment serializer method.
|
258
258
|
if ref.macro == :has_one_attached
|
259
259
|
serializer_methods[f] = f
|
260
260
|
includes_map[f] = {"#{f}_attachment": :blob}
|
261
261
|
self.define_singleton_method(f) do |record|
|
262
|
-
|
262
|
+
attached = record.send(f)
|
263
|
+
next attached.attachment ? {
|
264
|
+
filename: attached.filename,
|
265
|
+
signed_id: attached.signed_id,
|
266
|
+
url: attached.url,
|
267
|
+
} : nil
|
263
268
|
end
|
264
269
|
elsif ref.macro == :has_many_attached
|
265
270
|
serializer_methods[f] = f
|
266
271
|
includes_map[f] = {"#{f}_attachments": :blob}
|
267
272
|
self.define_singleton_method(f) do |record|
|
268
273
|
# Iterating the collection yields attachment objects.
|
269
|
-
next record.send(f).map
|
274
|
+
next record.send(f).map { |a|
|
275
|
+
{
|
276
|
+
filename: a.filename,
|
277
|
+
signed_id: a.signed_id,
|
278
|
+
url: a.url,
|
279
|
+
}
|
280
|
+
}
|
270
281
|
end
|
271
282
|
end
|
272
283
|
elsif @model.method_defined?(f)
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -28,7 +28,7 @@ module RESTFramework::Utils
|
|
28
28
|
[f, {}]
|
29
29
|
}.to_h if metadata[:fields].is_a?(Array)
|
30
30
|
metadata[:fields]&.each do |field, cfg|
|
31
|
-
cfg[:label] = controller.
|
31
|
+
cfg[:label] = controller.label_for(field) unless cfg[:label]
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -47,7 +47,7 @@ module RESTFramework::Utils
|
|
47
47
|
|
48
48
|
# Insert action label if it's not provided.
|
49
49
|
if controller
|
50
|
-
metadata[:label] ||= controller.
|
50
|
+
metadata[:label] ||= controller.label_for(k)
|
51
51
|
end
|
52
52
|
|
53
53
|
next [
|
@@ -153,16 +153,21 @@ module RESTFramework::Utils
|
|
153
153
|
end
|
154
154
|
|
155
155
|
# Parse fields hashes.
|
156
|
-
def self.parse_fields_hash(
|
157
|
-
parsed_fields =
|
158
|
-
model ? self.fields_for(
|
156
|
+
def self.parse_fields_hash(h, model, exclude_associations:, action_text:, active_storage:)
|
157
|
+
parsed_fields = h[:only] || (
|
158
|
+
model ? self.fields_for(
|
159
|
+
model,
|
160
|
+
exclude_associations: exclude_associations,
|
161
|
+
action_text: action_text,
|
162
|
+
active_storage: active_storage,
|
163
|
+
) : []
|
159
164
|
)
|
160
|
-
parsed_fields +=
|
161
|
-
parsed_fields -=
|
162
|
-
parsed_fields -=
|
165
|
+
parsed_fields += h[:include].map(&:to_s) if h[:include]
|
166
|
+
parsed_fields -= h[:exclude].map(&:to_s) if h[:exclude]
|
167
|
+
parsed_fields -= h[:except].map(&:to_s) if h[:except]
|
163
168
|
|
164
169
|
# Warn for any unknown keys.
|
165
|
-
(
|
170
|
+
(h.keys - [:only, :except, :include, :exclude]).each do |k|
|
166
171
|
Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
|
167
172
|
end
|
168
173
|
|
@@ -173,16 +178,24 @@ module RESTFramework::Utils
|
|
173
178
|
# Get the fields for a given model, including not just columns (which includes
|
174
179
|
# foreign keys), but also associations. Note that we always return an array of
|
175
180
|
# strings, not symbols.
|
176
|
-
def self.fields_for(model, exclude_associations:
|
181
|
+
def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
|
177
182
|
foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
|
178
183
|
base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
|
179
184
|
|
180
185
|
return base_fields if exclude_associations
|
181
186
|
|
182
|
-
#
|
183
|
-
|
187
|
+
# ActionText Integration: Determine the normalized field names for action text attributes.
|
188
|
+
atf = action_text ? model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
|
189
|
+
n.to_s.start_with?("rich_text_")
|
190
|
+
}.map { |n| n.to_s.delete_prefix("rich_text_") } : []
|
191
|
+
|
192
|
+
# ActiveStorage Integration: Determine the normalized field names for active storage attributes.
|
193
|
+
asf = active_storage ? model.attachment_reflections.keys : []
|
194
|
+
|
195
|
+
# Associations:
|
196
|
+
associations = model.reflections.map { |association, ref|
|
184
197
|
# Ignore associations for which we have custom integrations.
|
185
|
-
if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob
|
198
|
+
if ref.class_name.in?(%w(ActionText::RichText ActiveStorage::Attachment ActiveStorage::Blob))
|
186
199
|
next nil
|
187
200
|
end
|
188
201
|
|
@@ -193,11 +206,9 @@ module RESTFramework::Utils
|
|
193
206
|
end
|
194
207
|
|
195
208
|
next association
|
196
|
-
}.compact
|
197
|
-
|
198
|
-
|
199
|
-
n.to_s.delete_prefix("rich_text_")
|
200
|
-
} + model.attachment_reflections.keys
|
209
|
+
}.compact
|
210
|
+
|
211
|
+
return base_fields + associations + atf + asf
|
201
212
|
end
|
202
213
|
|
203
214
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
@@ -234,4 +245,13 @@ module RESTFramework::Utils
|
|
234
245
|
|
235
246
|
return nil
|
236
247
|
end
|
248
|
+
|
249
|
+
# Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
|
250
|
+
def self.wrap_ams(s)
|
251
|
+
if defined?(ActiveModel::Serializer) && (s < ActiveModel::Serializer)
|
252
|
+
return RESTFramework::ActiveModelSerializerAdapterFactory.for(s)
|
253
|
+
end
|
254
|
+
|
255
|
+
return s
|
256
|
+
end
|
237
257
|
end
|
data/lib/rest_framework.rb
CHANGED
@@ -31,57 +31,57 @@ module RESTFramework
|
|
31
31
|
EXTERNAL_ASSETS = {
|
32
32
|
# Bootstrap
|
33
33
|
"bootstrap.min.css" => {
|
34
|
-
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.
|
35
|
-
sri: "sha384-
|
34
|
+
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
|
35
|
+
sri: "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH",
|
36
36
|
},
|
37
37
|
"bootstrap.min.js" => {
|
38
|
-
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.
|
39
|
-
sri: "sha384-
|
38
|
+
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
|
39
|
+
sri: "sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz",
|
40
40
|
},
|
41
41
|
|
42
42
|
# Bootstrap Icons
|
43
43
|
"bootstrap-icons.min.css" => {
|
44
|
-
url: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.
|
44
|
+
url: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css",
|
45
45
|
inline_fonts: true,
|
46
46
|
},
|
47
47
|
|
48
48
|
# Highlight.js
|
49
49
|
"highlight.min.js" => {
|
50
|
-
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.
|
51
|
-
sri: "sha512-
|
50
|
+
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js",
|
51
|
+
sri: "sha512-6QBAC6Sxc4IF04SvIg0k78l5rP5YgVjmHX2NeArelbxM3JGj4imMqfNzEta3n+mi7iG3nupdLnl3QrbfjdXyTg==",
|
52
52
|
},
|
53
53
|
"highlight-json.min.js" => {
|
54
|
-
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.
|
55
|
-
sri: "sha512-
|
54
|
+
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/languages/json.min.js",
|
55
|
+
sri: "sha512-8JO7/pRnd1Ce8OBXWQg85e5wNPJdBaQdN8w4oDa+HelMXaLwCxTdbzdWHmJtWR9AmcI6dOln4FS5/KrzpxqqfQ==",
|
56
56
|
},
|
57
57
|
"highlight-xml.min.js" => {
|
58
|
-
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.
|
59
|
-
sri: "sha512
|
58
|
+
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/languages/xml.min.js",
|
59
|
+
sri: "sha512-/vq6wbS2Qkv8Hj4mP3Jd/m6MbnIrquzZiUt9tIluQfe332IQeFDrSIK7j2cjAyn6/9Ntb2WMPbo1CAxu26NViA==",
|
60
60
|
},
|
61
61
|
"highlight-a11y-dark.min.css" => {
|
62
|
-
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.
|
62
|
+
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-dark.min.css",
|
63
63
|
sri: "sha512-Vj6gPCk8EZlqnoveEyuGyYaWZ1+jyjMPg8g4shwyyNlRQl6d3L9At02ZHQr5K6s5duZl/+YKMnM3/8pDhoUphg==",
|
64
64
|
extra_tag_attrs: {class: "rrf-dark-mode"},
|
65
65
|
},
|
66
66
|
"highlight-a11y-light.min.css" => {
|
67
|
-
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.
|
67
|
+
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-light.min.css",
|
68
68
|
sri: "sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA==",
|
69
69
|
extra_tag_attrs: {class: "rrf-light-mode"},
|
70
70
|
},
|
71
71
|
|
72
72
|
# NeatJSON
|
73
73
|
"neatjson.min.js" => {
|
74
|
-
url: "https://cdn.jsdelivr.net/npm/neatjson@0.10.
|
74
|
+
url: "https://cdn.jsdelivr.net/npm/neatjson@0.10.6/javascript/neatjson.min.js",
|
75
75
|
exclude_from_docs: true,
|
76
76
|
},
|
77
77
|
|
78
78
|
# Trix
|
79
79
|
"trix.min.css" => {
|
80
|
-
url: "https://unpkg.com/trix@2.0.
|
80
|
+
url: "https://unpkg.com/trix@2.0.8/dist/trix.css",
|
81
81
|
exclude_from_docs: true,
|
82
82
|
},
|
83
83
|
"trix.min.js" => {
|
84
|
-
url: "https://unpkg.com/trix@2.0.
|
84
|
+
url: "https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js",
|
85
85
|
exclude_from_docs: true,
|
86
86
|
},
|
87
87
|
}.map { |name, cfg|
|