rest_framework 0.9.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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|
|