rest_framework 0.9.2 → 0.9.3
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/rest_framework/_external.html.erb +13 -0
- data/app/views/rest_framework/_head.html.erb +2 -192
- data/{docs/assets/js/rest_framework.js → app/views/rest_framework/_shared.html} +69 -42
- data/docs/Gemfile +1 -0
- data/docs/Gemfile.lock +14 -14
- data/docs/_config.yml +4 -2
- data/docs/_guide/2_controllers.md +342 -0
- data/docs/_guide/3_serializers.md +1 -1
- data/docs/_guide/4_filtering_and_ordering.md +8 -8
- data/docs/_includes/external.html +9 -0
- data/docs/_includes/head.html +135 -15
- data/docs/_includes/shared.html +164 -0
- data/lib/rest_framework/controller_mixins/base.rb +23 -36
- data/lib/rest_framework/controller_mixins/models.rb +86 -75
- data/lib/rest_framework/controller_mixins.rb +1 -0
- data/lib/rest_framework/engine.rb +9 -0
- data/lib/rest_framework/filters/base.rb +9 -0
- data/lib/rest_framework/filters/model_ordering.rb +48 -0
- data/lib/rest_framework/filters/model_query.rb +51 -0
- data/lib/rest_framework/filters/model_search.rb +41 -0
- data/lib/rest_framework/filters/ransack.rb +25 -0
- data/lib/rest_framework/filters.rb +6 -150
- data/lib/rest_framework/paginators.rb +7 -11
- data/lib/rest_framework/serializers.rb +10 -10
- data/lib/rest_framework/utils.rb +15 -7
- data/lib/rest_framework.rb +93 -4
- data/vendor/assets/javascripts/rest_framework/bootstrap.js +7 -0
- data/vendor/assets/javascripts/rest_framework/highlight-json.js +7 -0
- data/vendor/assets/javascripts/rest_framework/highlight-xml.js +29 -0
- data/vendor/assets/javascripts/rest_framework/highlight.js +1202 -0
- data/vendor/assets/javascripts/rest_framework/neatjson.js +8 -0
- data/vendor/assets/javascripts/rest_framework/trix.js +6 -0
- data/vendor/assets/stylesheets/rest_framework/bootstrap-icons.css +13 -0
- data/vendor/assets/stylesheets/rest_framework/bootstrap.css +6 -0
- data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.css +7 -0
- data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.css +7 -0
- data/vendor/assets/stylesheets/rest_framework/trix.css +410 -0
- metadata +23 -5
- data/docs/_guide/2_controller_mixins.md +0 -293
- data/docs/assets/css/rest_framework.css +0 -159
@@ -0,0 +1,164 @@
|
|
1
|
+
<!--
|
2
|
+
AUTOGENERATED
|
3
|
+
Updates must be written to `shared.{css,js}` and synced with `rake maintain_assets`.
|
4
|
+
-->
|
5
|
+
<style>
|
6
|
+
:root {
|
7
|
+
--rrf-red: #900;
|
8
|
+
--rrf-red-hover: #5f0c0c;
|
9
|
+
--rrf-light-red: #db2525;
|
10
|
+
--rrf-light-red-hover: #b80404;
|
11
|
+
}
|
12
|
+
#rrfAccentBar {
|
13
|
+
background-color: var(--rrf-red);
|
14
|
+
height: .3em;
|
15
|
+
}
|
16
|
+
header nav { background-color: black; }
|
17
|
+
|
18
|
+
/* Header adjustments. */
|
19
|
+
h1 { font-size: 2rem; }
|
20
|
+
h2 { font-size: 1.7rem; }
|
21
|
+
h3 { font-size: 1.5rem; }
|
22
|
+
h4 { font-size: 1.3rem; }
|
23
|
+
h5 { font-size: 1.1rem; }
|
24
|
+
h6 { font-size: 1rem; }
|
25
|
+
h1, h2, h3, h4, h5, h6 {
|
26
|
+
color: var(--rrf-red);
|
27
|
+
}
|
28
|
+
html[data-bs-theme="dark"] h1,
|
29
|
+
html[data-bs-theme="dark"] h2,
|
30
|
+
html[data-bs-theme="dark"] h3,
|
31
|
+
html[data-bs-theme="dark"] h4,
|
32
|
+
html[data-bs-theme="dark"] h5,
|
33
|
+
html[data-bs-theme="dark"] h6 {
|
34
|
+
color: var(--rrf-light-red);
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Improve code and code blocks. */
|
38
|
+
pre code, .trix-content pre {
|
39
|
+
display: block;
|
40
|
+
overflow-x: auto;
|
41
|
+
padding: .5em !important;
|
42
|
+
}
|
43
|
+
code, .trix-content pre {
|
44
|
+
--bs-code-color: black;
|
45
|
+
background-color: #eee !important;
|
46
|
+
border: 1px solid #aaa;
|
47
|
+
border-radius: 3px;
|
48
|
+
padding: .1em .3em;
|
49
|
+
}
|
50
|
+
html[data-bs-theme="dark"] code, html[data-bs-theme="dark"] .trix-content pre {
|
51
|
+
--bs-code-color: white;
|
52
|
+
background-color: #2b2b2b !important;
|
53
|
+
}
|
54
|
+
|
55
|
+
/* Anchors */
|
56
|
+
a:not(.nav-link) {
|
57
|
+
text-decoration: none;
|
58
|
+
color: var(--rrf-red);
|
59
|
+
}
|
60
|
+
a:hover:not(.nav-link) {
|
61
|
+
text-decoration: underline;
|
62
|
+
color: var(--rrf-red-hover);
|
63
|
+
}
|
64
|
+
html[data-bs-theme="dark"] a:not(.nav-link) { color: var(--rrf-light-red); }
|
65
|
+
html[data-bs-theme="dark"] a:hover:not(.nav-link) { color: var(--rrf-light-red-hover); }
|
66
|
+
|
67
|
+
</style>
|
68
|
+
<script>
|
69
|
+
;(() => {
|
70
|
+
// Get the real mode from a selected mode. Anything other than "light" or "dark" is treated as
|
71
|
+
// "system" mode.
|
72
|
+
const rrfGetRealMode = (selectedMode) => {
|
73
|
+
if (selectedMode === "light" || selectedMode === "dark") {
|
74
|
+
return selectedMode
|
75
|
+
}
|
76
|
+
|
77
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
78
|
+
return "dark"
|
79
|
+
}
|
80
|
+
|
81
|
+
return "light"
|
82
|
+
}
|
83
|
+
|
84
|
+
// Set the mode, given a "selected" mode.
|
85
|
+
const rrfSetSelectedMode = (selectedMode) => {
|
86
|
+
// Anything except "light" or "dark" is casted to "system".
|
87
|
+
if (selectedMode !== "light" && selectedMode !== "dark") {
|
88
|
+
selectedMode = "system"
|
89
|
+
}
|
90
|
+
|
91
|
+
// Store selected mode in `localStorage`.
|
92
|
+
localStorage.setItem("rrfMode", selectedMode)
|
93
|
+
|
94
|
+
// Set the mode selector to the selected mode.
|
95
|
+
const modeComponent = document.getElementById("rrfModeComponent")
|
96
|
+
if (modeComponent) {
|
97
|
+
let labelHTML
|
98
|
+
modeComponent.querySelectorAll("button[data-rrf-mode-value]").forEach((el) => {
|
99
|
+
if (el.getAttribute("data-rrf-mode-value") === selectedMode) {
|
100
|
+
el.classList.add("active")
|
101
|
+
labelHTML = el.querySelector("i").outerHTML.replace("ms-2", "me-1")
|
102
|
+
} else {
|
103
|
+
el.classList.remove("active")
|
104
|
+
}
|
105
|
+
})
|
106
|
+
modeComponent.querySelector("button[data-bs-toggle]").innerHTML = labelHTML
|
107
|
+
}
|
108
|
+
|
109
|
+
// Get the real mode to use.
|
110
|
+
realMode = rrfGetRealMode(selectedMode)
|
111
|
+
|
112
|
+
// Set the `realMode` effects.
|
113
|
+
if (realMode === "light") {
|
114
|
+
document.querySelectorAll(".rrf-light-mode").forEach((el) => {
|
115
|
+
el.disabled = false
|
116
|
+
})
|
117
|
+
document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
|
118
|
+
el.disabled = true
|
119
|
+
})
|
120
|
+
document.querySelectorAll(".rrf-mode").forEach((el) => {
|
121
|
+
el.setAttribute("data-bs-theme", "light")
|
122
|
+
})
|
123
|
+
} else if (realMode === "dark") {
|
124
|
+
document.querySelectorAll(".rrf-light-mode").forEach((el) => {
|
125
|
+
el.disabled = true
|
126
|
+
})
|
127
|
+
document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
|
128
|
+
el.disabled = false
|
129
|
+
})
|
130
|
+
document.querySelectorAll(".rrf-mode").forEach((el) => {
|
131
|
+
el.setAttribute("data-bs-theme", "dark")
|
132
|
+
})
|
133
|
+
} else {
|
134
|
+
console.log(`RRF: Unknown mode: ${mode}`)
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
// Initialize dark/light mode before page fully loads to prevent flash.
|
139
|
+
rrfSetSelectedMode(localStorage.getItem("rrfMode"))
|
140
|
+
|
141
|
+
// Initialize dark/light mode after page load (mostly so mode component is updated).
|
142
|
+
document.addEventListener("DOMContentLoaded", (event) => {
|
143
|
+
rrfSetSelectedMode(localStorage.getItem("rrfMode"))
|
144
|
+
|
145
|
+
// Also set up mode selector.
|
146
|
+
document.querySelectorAll("#rrfModeComponent button[data-rrf-mode-value]").forEach((el) => {
|
147
|
+
el.addEventListener("click", (event) => {
|
148
|
+
rrfSetSelectedMode(event.target.getAttribute("data-rrf-mode-value"))
|
149
|
+
})
|
150
|
+
})
|
151
|
+
})
|
152
|
+
|
153
|
+
// Handle case where user changes system theme.
|
154
|
+
if (window.matchMedia) {
|
155
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
156
|
+
const selectedMode = localStorage.getItem("rrfMode")
|
157
|
+
if (selectedMode !== "light" && selectedMode !== "dark") {
|
158
|
+
rrfSetSelectedMode("system")
|
159
|
+
}
|
160
|
+
})
|
161
|
+
}
|
162
|
+
})()
|
163
|
+
|
164
|
+
</script>
|
@@ -6,28 +6,18 @@ require_relative "../utils"
|
|
6
6
|
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
|
7
7
|
# is defined.
|
8
8
|
module RESTFramework::BaseControllerMixin
|
9
|
-
|
10
|
-
filter_pk_from_request_body: true,
|
11
|
-
exclude_body_fields: %w[
|
12
|
-
created_at
|
13
|
-
created_by
|
14
|
-
created_by_id
|
15
|
-
updated_at
|
16
|
-
updated_by
|
17
|
-
updated_by_id
|
18
|
-
_method
|
19
|
-
utf8
|
20
|
-
authenticity_token
|
21
|
-
].freeze,
|
9
|
+
RRF_BASE_CONFIG = {
|
22
10
|
extra_actions: nil,
|
23
11
|
extra_member_actions: nil,
|
24
|
-
filter_backends: nil,
|
25
12
|
singleton_controller: nil,
|
26
13
|
|
27
14
|
# Options related to metadata and display.
|
28
15
|
title: nil,
|
29
16
|
description: nil,
|
30
17
|
inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
|
18
|
+
}
|
19
|
+
RRF_BASE_INSTANCE_CONFIG = {
|
20
|
+
filter_backends: nil,
|
31
21
|
|
32
22
|
# Options related to serialization.
|
33
23
|
rescue_unknown_format_with: :json,
|
@@ -42,10 +32,6 @@ module RESTFramework::BaseControllerMixin
|
|
42
32
|
page_size_query_param: "page_size",
|
43
33
|
max_page_size: nil,
|
44
34
|
|
45
|
-
# Options related to bulk actions and batch processing.
|
46
|
-
bulk_guard_query_param: nil,
|
47
|
-
enable_batch_processing: nil,
|
48
|
-
|
49
35
|
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
50
36
|
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
51
37
|
disable_adapters_by_default: true,
|
@@ -146,7 +132,7 @@ module RESTFramework::BaseControllerMixin
|
|
146
132
|
# :nocov:
|
147
133
|
def rrf_finalize
|
148
134
|
if RESTFramework.config.freeze_config
|
149
|
-
self::
|
135
|
+
(self::RRF_BASE_CONFIG.keys + self::RRF_BASE_INSTANCE_CONFIG.keys).each { |k|
|
150
136
|
v = self.send(k)
|
151
137
|
v.freeze if v.is_a?(Hash) || v.is_a?(Array)
|
152
138
|
}
|
@@ -161,14 +147,15 @@ module RESTFramework::BaseControllerMixin
|
|
161
147
|
base.extend(ClassMethods)
|
162
148
|
|
163
149
|
# Add class attributes (with defaults) unless they already exist.
|
164
|
-
|
150
|
+
RRF_BASE_CONFIG.each do |a, default|
|
165
151
|
next if base.respond_to?(a)
|
166
152
|
|
167
|
-
base.class_attribute(a)
|
153
|
+
base.class_attribute(a, default: default, instance_accessor: false)
|
154
|
+
end
|
155
|
+
RRF_BASE_INSTANCE_CONFIG.each do |a, default|
|
156
|
+
next if base.respond_to?(a)
|
168
157
|
|
169
|
-
|
170
|
-
# parameter on `class_attribute`.
|
171
|
-
base.send(:"#{a}=", default)
|
158
|
+
base.class_attribute(a, default: default)
|
172
159
|
end
|
173
160
|
|
174
161
|
# Alias `extra_actions` to `extra_collection_actions`.
|
@@ -219,7 +206,7 @@ module RESTFramework::BaseControllerMixin
|
|
219
206
|
|
220
207
|
# Get the configured serializer class.
|
221
208
|
def get_serializer_class
|
222
|
-
return nil unless serializer_class = self.
|
209
|
+
return nil unless serializer_class = self.serializer_class
|
223
210
|
|
224
211
|
# Support dynamically resolving serializer given a symbol or string.
|
225
212
|
serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
|
@@ -242,7 +229,7 @@ module RESTFramework::BaseControllerMixin
|
|
242
229
|
|
243
230
|
# Get filtering backends, defaulting to no backends.
|
244
231
|
def get_filter_backends
|
245
|
-
return self.
|
232
|
+
return self.filter_backends || []
|
246
233
|
end
|
247
234
|
|
248
235
|
# Filter an arbitrary data set over all configured filter backends.
|
@@ -299,7 +286,7 @@ module RESTFramework::BaseControllerMixin
|
|
299
286
|
end
|
300
287
|
|
301
288
|
# Do not use any adapters by default, if configured.
|
302
|
-
if self.
|
289
|
+
if self.disable_adapters_by_default && !kwargs.key?(:adapter)
|
303
290
|
kwargs[:adapter] = nil
|
304
291
|
end
|
305
292
|
|
@@ -309,27 +296,27 @@ module RESTFramework::BaseControllerMixin
|
|
309
296
|
begin
|
310
297
|
respond_to do |format|
|
311
298
|
if payload == ""
|
312
|
-
format.json { head(kwargs[:status] || :no_content) } if self.
|
313
|
-
format.xml { head(kwargs[:status] || :no_content) } if self.
|
299
|
+
format.json { head(kwargs[:status] || :no_content) } if self.serialize_to_json
|
300
|
+
format.xml { head(kwargs[:status] || :no_content) } if self.serialize_to_xml
|
314
301
|
else
|
315
302
|
format.json {
|
316
303
|
jkwargs = kwargs.merge(json_kwargs)
|
317
304
|
render(json: payload, layout: false, **jkwargs)
|
318
|
-
} if self.
|
305
|
+
} if self.serialize_to_json
|
319
306
|
format.xml {
|
320
307
|
xkwargs = kwargs.merge(xml_kwargs)
|
321
308
|
render(xml: payload, layout: false, **xkwargs)
|
322
|
-
} if self.
|
309
|
+
} if self.serialize_to_xml
|
323
310
|
# TODO: possibly support more formats here if supported?
|
324
311
|
end
|
325
312
|
format.html {
|
326
313
|
@payload = payload
|
327
314
|
if payload == ""
|
328
|
-
@json_payload = "" if self.
|
329
|
-
@xml_payload = "" if self.
|
315
|
+
@json_payload = "" if self.serialize_to_json
|
316
|
+
@xml_payload = "" if self.serialize_to_xml
|
330
317
|
else
|
331
|
-
@json_payload = payload.to_json if self.
|
332
|
-
@xml_payload = payload.to_xml if self.
|
318
|
+
@json_payload = payload.to_json if self.serialize_to_json
|
319
|
+
@xml_payload = payload.to_xml if self.serialize_to_xml
|
333
320
|
end
|
334
321
|
@title ||= self.class.get_title
|
335
322
|
@description ||= self.class.description
|
@@ -347,7 +334,7 @@ module RESTFramework::BaseControllerMixin
|
|
347
334
|
}
|
348
335
|
end
|
349
336
|
rescue ActionController::UnknownFormat
|
350
|
-
if !already_rescued_unknown_format && rescue_format = self.
|
337
|
+
if !already_rescued_unknown_format && rescue_format = self.rescue_unknown_format_with
|
351
338
|
request.format = rescue_format
|
352
339
|
already_rescued_unknown_format = true
|
353
340
|
retry
|
@@ -14,7 +14,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
14
14
|
}
|
15
15
|
include RESTFramework::BaseControllerMixin
|
16
16
|
|
17
|
-
|
17
|
+
RRF_BASE_MODEL_CONFIG = {
|
18
18
|
# Core attributes related to models.
|
19
19
|
model: nil,
|
20
20
|
recordset: nil,
|
@@ -22,20 +22,31 @@ module RESTFramework::BaseModelControllerMixin
|
|
22
22
|
# Attributes for configuring record fields.
|
23
23
|
fields: nil,
|
24
24
|
field_config: nil,
|
25
|
-
action_fields: nil,
|
26
25
|
|
27
26
|
# Options for what should be included/excluded from default fields.
|
28
27
|
exclude_associations: false,
|
29
28
|
include_active_storage: false,
|
30
29
|
include_action_text: false,
|
31
|
-
|
30
|
+
}
|
31
|
+
RRF_BASE_MODEL_INSTANCE_CONFIG = {
|
32
32
|
# Attributes for finding records.
|
33
33
|
find_by_fields: nil,
|
34
34
|
find_by_query_param: "find_by",
|
35
35
|
|
36
|
-
#
|
36
|
+
# Options for handling request body parameters.
|
37
37
|
allowed_parameters: nil,
|
38
|
-
|
38
|
+
filter_pk_from_request_body: true,
|
39
|
+
exclude_body_fields: %w[
|
40
|
+
created_at
|
41
|
+
created_by
|
42
|
+
created_by_id
|
43
|
+
updated_at
|
44
|
+
updated_by
|
45
|
+
updated_by_id
|
46
|
+
_method
|
47
|
+
utf8
|
48
|
+
authenticity_token
|
49
|
+
].freeze,
|
39
50
|
|
40
51
|
# Attributes for the default native serializer.
|
41
52
|
native_serializer_config: nil,
|
@@ -59,13 +70,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
59
70
|
# Options for association assignment.
|
60
71
|
permit_id_assignment: true,
|
61
72
|
permit_nested_attributes_assignment: true,
|
62
|
-
allow_all_nested_attributes: false,
|
63
73
|
|
64
74
|
# Option for `recordset.create` vs `Model.create` behavior.
|
65
75
|
create_from_recordset: true,
|
66
76
|
|
67
77
|
# Control if filtering is done before find.
|
68
78
|
filter_recordset_before_find: true,
|
79
|
+
|
80
|
+
# Options for `ransack` filtering.
|
81
|
+
ransack_options: nil,
|
82
|
+
ransack_query_param: "q",
|
83
|
+
ransack_distinct: true,
|
84
|
+
ransack_distinct_query_param: "distinct",
|
69
85
|
}
|
70
86
|
|
71
87
|
module ClassMethods
|
@@ -130,6 +146,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
130
146
|
columns = model.columns_hash
|
131
147
|
end
|
132
148
|
config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
|
149
|
+
config[:sub_fields] = config[:sub_fields].map(&:to_s)
|
133
150
|
|
134
151
|
# Serialize very basic metadata about sub-fields.
|
135
152
|
config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
|
@@ -228,21 +245,17 @@ module RESTFramework::BaseModelControllerMixin
|
|
228
245
|
if ref = reflections[f]
|
229
246
|
metadata[:kind] = "association"
|
230
247
|
|
231
|
-
# Determine if we render id/ids fields.
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
elsif ref.belongs_to?
|
236
|
-
metadata[:id_field] = "#{f}_id"
|
237
|
-
end
|
248
|
+
# Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this
|
249
|
+
# interface.
|
250
|
+
if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
|
251
|
+
metadata[:id_field] = id_field
|
238
252
|
end
|
239
253
|
|
240
254
|
# Determine if we render nested attributes options.
|
241
|
-
if self.permit_nested_attributes_assignment
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
end
|
255
|
+
if self.permit_nested_attributes_assignment && (
|
256
|
+
nested_opts = model.nested_attributes_options[f.to_sym].presence
|
257
|
+
)
|
258
|
+
metadata[:nested_attributes_options] = {field: "#{f}_attributes", **nested_opts}
|
246
259
|
end
|
247
260
|
|
248
261
|
begin
|
@@ -364,7 +377,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
364
377
|
# self.setup_channel
|
365
378
|
|
366
379
|
if RESTFramework.config.freeze_config
|
367
|
-
self::
|
380
|
+
(self::RRF_BASE_MODEL_CONFIG.keys + self::RRF_BASE_MODEL_INSTANCE_CONFIG.keys).each { |k|
|
368
381
|
v = self.send(k)
|
369
382
|
v.freeze if v.is_a?(Hash) || v.is_a?(Array)
|
370
383
|
}
|
@@ -381,31 +394,21 @@ module RESTFramework::BaseModelControllerMixin
|
|
381
394
|
base.extend(ClassMethods)
|
382
395
|
|
383
396
|
# Add class attributes (with defaults) unless they already exist.
|
384
|
-
|
397
|
+
RRF_BASE_MODEL_CONFIG.each do |a, default|
|
385
398
|
next if base.respond_to?(a)
|
386
399
|
|
387
|
-
base.class_attribute(a)
|
388
|
-
|
389
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
390
|
-
# parameter on `class_attribute`.
|
391
|
-
base.send(:"#{a}=", default)
|
400
|
+
base.class_attribute(a, default: default, instance_accessor: false)
|
392
401
|
end
|
393
|
-
|
394
|
-
|
395
|
-
def _get_specific_action_config(action_config_key, generic_config_key)
|
396
|
-
action_config = self.class.send(action_config_key)&.with_indifferent_access || {}
|
397
|
-
action = self.action_name&.to_sym
|
398
|
-
|
399
|
-
# Index action should use :list serializer if :index is not provided.
|
400
|
-
action = :list if action == :index && !action_config.key?(:index)
|
402
|
+
RRF_BASE_MODEL_INSTANCE_CONFIG.each do |a, default|
|
403
|
+
next if base.respond_to?(a)
|
401
404
|
|
402
|
-
|
405
|
+
base.class_attribute(a, default: default)
|
406
|
+
end
|
403
407
|
end
|
404
408
|
|
405
|
-
# Get a list of fields
|
409
|
+
# Get a list of fields for this controller.
|
406
410
|
def get_fields
|
407
|
-
|
408
|
-
return self.class.get_fields(input_fields: fields)
|
411
|
+
return self.class.get_fields(input_fields: self.class.fields)
|
409
412
|
end
|
410
413
|
|
411
414
|
# Pass fields to get dynamic metadata based on which fields are available.
|
@@ -413,19 +416,11 @@ module RESTFramework::BaseModelControllerMixin
|
|
413
416
|
return self.class.get_options_metadata
|
414
417
|
end
|
415
418
|
|
416
|
-
# Get a list of find_by fields for the current action.
|
417
|
-
def get_find_by_fields
|
418
|
-
return self.class.find_by_fields
|
419
|
-
end
|
420
|
-
|
421
419
|
# Get a list of parameters allowed for the current action.
|
422
420
|
def get_allowed_parameters
|
423
421
|
return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
|
424
422
|
|
425
|
-
@_get_allowed_parameters = self.
|
426
|
-
:allowed_action_parameters,
|
427
|
-
:allowed_parameters,
|
428
|
-
)
|
423
|
+
@_get_allowed_parameters = self.allowed_parameters
|
429
424
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
430
425
|
|
431
426
|
# For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
|
@@ -454,20 +449,16 @@ module RESTFramework::BaseModelControllerMixin
|
|
454
449
|
# Return field if it's not an association.
|
455
450
|
next f unless ref = reflections[f]
|
456
451
|
|
457
|
-
if self.
|
458
|
-
if
|
459
|
-
hash_variations[
|
460
|
-
|
461
|
-
variations <<
|
452
|
+
if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
|
453
|
+
if id_field.ends_with?("_ids")
|
454
|
+
hash_variations[id_field] = []
|
455
|
+
else
|
456
|
+
variations << id_field
|
462
457
|
end
|
463
458
|
end
|
464
459
|
|
465
|
-
if self.
|
466
|
-
|
467
|
-
hash_variations["#{f}_attributes"] = {}
|
468
|
-
else
|
469
|
-
hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
|
470
|
-
end
|
460
|
+
if self.permit_nested_attributes_assignment
|
461
|
+
hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
|
471
462
|
end
|
472
463
|
|
473
464
|
# Associations are not allowed to be submitted in their bare form.
|
@@ -483,37 +474,57 @@ module RESTFramework::BaseModelControllerMixin
|
|
483
474
|
return super || RESTFramework::NativeSerializer
|
484
475
|
end
|
485
476
|
|
486
|
-
# Get filtering backends, defaulting to using `
|
477
|
+
# Get filtering backends, defaulting to using `ModelQueryFilter`, `ModelOrderingFilter`, and
|
487
478
|
# `ModelSearchFilter`.
|
488
479
|
def get_filter_backends
|
489
|
-
return self.
|
490
|
-
RESTFramework::
|
480
|
+
return self.filter_backends || [
|
481
|
+
RESTFramework::ModelQueryFilter,
|
491
482
|
RESTFramework::ModelOrderingFilter,
|
492
483
|
RESTFramework::ModelSearchFilter,
|
493
484
|
]
|
494
485
|
end
|
495
486
|
|
496
487
|
# Use strong parameters to filter the request body using the configured allowed parameters.
|
497
|
-
def get_body_params(
|
498
|
-
data
|
488
|
+
def get_body_params(bulk_mode: nil)
|
489
|
+
data = self.request.request_parameters
|
499
490
|
pk = self.class.get_model&.primary_key
|
491
|
+
allowed_params = self.get_allowed_parameters
|
492
|
+
|
493
|
+
# Before we filter the data, dynamically dispatch association assignment to either the id/ids
|
494
|
+
# assignment ActiveRecord API or the nested assignment ActiveRecord API. Note that there is no
|
495
|
+
# need to check for `permit_id_assignment` or `permit_nested_attributes_assignment` here, since
|
496
|
+
# that is enforced by strong parameters generated by `get_allowed_parameters`.
|
497
|
+
self.class.get_model.reflections.each do |name, ref|
|
498
|
+
if payload = data[name]
|
499
|
+
if payload.is_a?(Hash) || (payload.is_a?(Array) && payload.all? { |x| x.is_a?(Hash) })
|
500
|
+
# Assume nested attributes assignment.
|
501
|
+
attributes_key = "#{name}_attributes"
|
502
|
+
data[attributes_key] = data.delete(name) unless data[attributes_key]
|
503
|
+
elsif id_field = RESTFramework::Utils.get_id_field(name, ref)
|
504
|
+
# Assume id/ids assignment.
|
505
|
+
data[id_field] = data.delete(name) unless data[id_field]
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
500
509
|
|
501
510
|
# Filter the request body with strong params. If `bulk` is true, then we apply allowed
|
502
511
|
# parameters to the `_json` key of the request body.
|
503
|
-
body_params = if
|
512
|
+
body_params = if allowed_params == true
|
513
|
+
ActionController::Parameters.new(data).permit!
|
514
|
+
elsif bulk_mode
|
504
515
|
pk = bulk_mode == :update ? [pk] : []
|
505
|
-
ActionController::Parameters.new(data).permit({_json:
|
516
|
+
ActionController::Parameters.new(data).permit({_json: allowed_params + pk})
|
506
517
|
else
|
507
|
-
ActionController::Parameters.new(data).permit(*
|
518
|
+
ActionController::Parameters.new(data).permit(*allowed_params)
|
508
519
|
end
|
509
520
|
|
510
521
|
# Filter primary key if configured.
|
511
|
-
if self.
|
522
|
+
if self.filter_pk_from_request_body && bulk_mode != :update
|
512
523
|
body_params.delete(pk)
|
513
524
|
end
|
514
525
|
|
515
526
|
# Filter fields in `exclude_body_fields`.
|
516
|
-
(self.
|
527
|
+
(self.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
517
528
|
|
518
529
|
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
519
530
|
#
|
@@ -595,9 +606,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
595
606
|
is_pk = true
|
596
607
|
|
597
608
|
# Find by another column if it's permitted.
|
598
|
-
if find_by_param = self.
|
609
|
+
if find_by_param = self.find_by_query_param.presence
|
599
610
|
if find_by = params[find_by_param].presence
|
600
|
-
find_by_fields = self.
|
611
|
+
find_by_fields = self.find_by_fields&.map(&:to_s)
|
601
612
|
|
602
613
|
if !find_by_fields || find_by.in?(find_by_fields)
|
603
614
|
is_pk = false unless find_by_key == find_by
|
@@ -621,7 +632,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
621
632
|
|
622
633
|
# Determine what collection to call `create` on.
|
623
634
|
def get_create_from
|
624
|
-
if self.
|
635
|
+
if self.create_from_recordset
|
625
636
|
# Create with any properties inherited from the recordset. We exclude any `select` clauses
|
626
637
|
# in case model callbacks need to call `count` on this collection, which typically raises a
|
627
638
|
# SQL `SyntaxError`.
|
@@ -660,13 +671,13 @@ module RESTFramework::ListModelMixin
|
|
660
671
|
records = self.get_records
|
661
672
|
|
662
673
|
# Handle pagination, if enabled.
|
663
|
-
if self.
|
674
|
+
if self.paginator_class
|
664
675
|
# If there is no `max_page_size`, `page_size_query_param` is not `nil`, and the page size is
|
665
676
|
# set to "0", then skip pagination.
|
666
|
-
unless !self.
|
667
|
-
self.
|
668
|
-
params[self.
|
669
|
-
paginator = self.
|
677
|
+
unless !self.max_page_size &&
|
678
|
+
self.page_size_query_param &&
|
679
|
+
params[self.page_size_query_param] == "0"
|
680
|
+
paginator = self.paginator_class.new(data: records, controller: self)
|
670
681
|
page = paginator.get_page
|
671
682
|
serialized_page = self.serialize(page)
|
672
683
|
return paginator.get_paginated_response(serialized_page)
|
@@ -1,2 +1,11 @@
|
|
1
1
|
class RESTFramework::Engine < Rails::Engine
|
2
|
+
initializer "rest_framework.assets" do
|
3
|
+
config.after_initialize do |app|
|
4
|
+
if RESTFramework.config.use_vendored_assets
|
5
|
+
app.config.assets.precompile += RESTFramework::EXTERNAL_ASSETS.keys.map do |name|
|
6
|
+
"rest_framework/#{name}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
2
11
|
end
|