rest_framework 0.8.15 → 0.8.16
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 +26 -22
- data/app/views/rest_framework/_head.html.erb +92 -22
- data/app/views/rest_framework/_html_form.html.erb +55 -3
- data/app/views/rest_framework/_raw_form.html.erb +31 -3
- data/lib/rest_framework/controller_mixins/base.rb +10 -3
- data/lib/rest_framework/controller_mixins/models.rb +120 -44
- data/lib/rest_framework/filters.rb +7 -9
- data/lib/rest_framework/serializers.rb +35 -12
- data/lib/rest_framework/utils.rb +11 -2
- data/lib/rest_framework/version.rb +4 -1
- data/lib/rest_framework.rb +2 -8
- metadata +5 -6
- data/app/views/rest_framework/_form_routes.html.erb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b8523aadeaa24412d0a2ea3d5a0f940864f69f13b4f97859c7b7a072b035a30
|
4
|
+
data.tar.gz: 3c7a734e85ef542221738030d711b912114712c7e0f0e28115bacfbf43fee032
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 986584c39f0388eec578bf53f71989f62df6c5eea18001d90023790b2c3fa3781f3b21416215aa58f153265eaf6fdce964ca2dbaf035dd1247b052963ed6d82c
|
7
|
+
data.tar.gz: 2dc9b1a29e1c7fdd1a6e010d9198a9f761b6eaed1ca37cbaa60dcad8a76ccd8972a6a9e2a5e8b8496b24794d5fe59a0a2ad35ff25ba2f1cd69c463fce572744f
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.8.
|
1
|
+
0.8.16
|
@@ -1,14 +1,14 @@
|
|
1
1
|
<!doctype html>
|
2
|
-
<html>
|
2
|
+
<html data-bs-theme="dark">
|
3
3
|
<head>
|
4
4
|
<title><%= @title %></title>
|
5
5
|
<%= render partial: 'rest_framework/head' %>
|
6
6
|
</head>
|
7
7
|
|
8
8
|
<body>
|
9
|
-
<div
|
10
|
-
<div class="w-100 m-0 p-0" style="height: .3em; background-color: #
|
11
|
-
<nav class="navbar navbar-dark
|
9
|
+
<div>
|
10
|
+
<div class="w-100 m-0 p-0" style="height: .3em; background-color: #900;"></div>
|
11
|
+
<nav class="navbar navbar-dark" style="background-color: #111">
|
12
12
|
<div class="container">
|
13
13
|
<span class="navbar-brand p-0">
|
14
14
|
<h1 title="RRF v<%= RESTFramework::VERSION %>" class="text-light font-weight-light m-0 p-0" style="font-size: 1em">
|
@@ -57,7 +57,7 @@
|
|
57
57
|
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "OPTIONS" } %>
|
58
58
|
<button type="button" class="btn btn-primary" onclick="rrfOptions(this)">OPTIONS</button>
|
59
59
|
<% end %>
|
60
|
-
<button type="button" class="btn btn-primary" onclick="
|
60
|
+
<button type="button" class="btn btn-primary" onclick="rrfGet(this)">GET</button>
|
61
61
|
</div>
|
62
62
|
<% if @description.present? %>
|
63
63
|
<br><br><p style="display: inline-block; margin-bottom: 0"><%= @description %></p>
|
@@ -68,12 +68,12 @@
|
|
68
68
|
<div class="row">
|
69
69
|
<div>
|
70
70
|
<pre style="white-space: normal">
|
71
|
-
<code
|
71
|
+
<code>
|
72
72
|
<strong><%= request.method %></strong> <%= request.path %><br>
|
73
73
|
</code>
|
74
74
|
</pre>
|
75
75
|
<pre style="white-space: normal">
|
76
|
-
<code
|
76
|
+
<code>
|
77
77
|
<strong>HTTP <%= response.status %> <%= response.message %></strong><br>
|
78
78
|
<strong>Content-Type:</strong> <%= response.content_type %>
|
79
79
|
</code>
|
@@ -101,25 +101,29 @@
|
|
101
101
|
</ul>
|
102
102
|
</div>
|
103
103
|
<div class="tab-content pt-2">
|
104
|
-
<div class="tab-pane fade show active" id="tab-json" role="
|
104
|
+
<div class="tab-pane fade show active" id="tab-json" role="tabpanel">
|
105
105
|
<% if @json_payload.present? %>
|
106
|
-
<div
|
107
|
-
|
108
|
-
JSON.
|
109
|
-
|
110
|
-
|
111
|
-
</div>
|
106
|
+
<div><pre class="rrf-copy"><code class="auto-hljs language-json"><%=
|
107
|
+
JSON.pretty_generate(
|
108
|
+
JSON.parse(@json_payload)
|
109
|
+
) unless @json_payload == ''
|
110
|
+
%></code></pre></div>
|
112
111
|
<% end %>
|
113
112
|
</div>
|
114
|
-
<div class="tab-pane fade" id="tab-xml" role="
|
113
|
+
<div class="tab-pane fade" id="tab-xml" role="tabpanel">
|
115
114
|
<% if @xml_payload.present? %>
|
116
|
-
<div><pre class="rrf-copy"><code class="language-xml"><%=
|
115
|
+
<div><pre class="rrf-copy"><code class="auto-hljs language-xml"><%=
|
116
|
+
CGI.unescapeHTML(@xml_payload)
|
117
|
+
%></code></pre></div>
|
117
118
|
<% end %>
|
118
119
|
</div>
|
119
120
|
</div>
|
120
121
|
</div>
|
121
122
|
<% end %>
|
122
123
|
<% if @route_groups.present? %>
|
124
|
+
<%
|
125
|
+
@is_model_controller = controller.class.included_modules.include?(RESTFramework::ModelControllerMixin)
|
126
|
+
%>
|
123
127
|
<div class="row">
|
124
128
|
<div>
|
125
129
|
<ul class="nav nav-tabs">
|
@@ -137,9 +141,9 @@
|
|
137
141
|
Raw Form
|
138
142
|
</a>
|
139
143
|
</li>
|
140
|
-
<% if
|
144
|
+
<% if @is_model_controller %>
|
141
145
|
<li class="nav-item">
|
142
|
-
<a class="nav-link" href="#tab-
|
146
|
+
<a class="nav-link" href="#tab-html-form" data-bs-toggle="tab" role="tab">
|
143
147
|
HTML Form
|
144
148
|
</a>
|
145
149
|
</li>
|
@@ -148,15 +152,15 @@
|
|
148
152
|
</ul>
|
149
153
|
</div>
|
150
154
|
<div class="tab-content pt-2">
|
151
|
-
<div class="tab-pane fade show active" id="tab-routes" role="
|
155
|
+
<div class="tab-pane fade show active" id="tab-routes" role="tabpanel">
|
152
156
|
<%= render partial: 'rest_framework/routes' %>
|
153
157
|
</div>
|
154
158
|
<% unless @_rrf_form_routes.empty? %>
|
155
|
-
<div class="tab-pane fade" id="tab-raw-form" role="
|
159
|
+
<div class="tab-pane fade" id="tab-raw-form" role="tabpanel">
|
156
160
|
<%= render partial: 'rest_framework/raw_form' %>
|
157
161
|
</div>
|
158
|
-
<% if
|
159
|
-
<div class="tab-pane fade" id="tab-
|
162
|
+
<% if @is_model_controller %>
|
163
|
+
<div class="tab-pane fade" id="tab-html-form" role="tabpanel">
|
160
164
|
<%= render partial: 'rest_framework/html_form' %>
|
161
165
|
</div>
|
162
166
|
<% end %>
|
@@ -3,12 +3,15 @@
|
|
3
3
|
<%= csrf_meta_tags %>
|
4
4
|
<%= csp_meta_tag rescue nil %>
|
5
5
|
|
6
|
+
<!-- ActiveStorage -->
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/activestorage@5.2.8-1/app/assets/javascripts/activestorage.min.js"></script>
|
8
|
+
|
6
9
|
<!-- Bootstrap -->
|
7
|
-
<link
|
8
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.
|
10
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js" integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
|
9
12
|
|
10
13
|
<!-- Highlight.js -->
|
11
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/
|
14
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css" integrity="sha512-Vj6gPCk8EZlqnoveEyuGyYaWZ1+jyjMPg8g4shwyyNlRQl6d3L9At02ZHQr5K6s5duZl/+YKMnM3/8pDhoUphg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
12
15
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js" integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
13
16
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js" integrity="sha512-0xYvyncS9OLE7GOpNBZFnwyh9+bq4HVgk4yVVYI678xRvE22ASicF1v6fZ1UiST+M6pn17MzFZdvVCI3jTHSyw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
14
17
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js" integrity="sha512-5zBcw+OKRkaNyvUEPlTSfYylVzgpi7KpncY36b0gRudfxIYIH0q0kl2j26uCUB3YBRM6ytQQEZSgRg+ZlBTmdA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
@@ -16,6 +19,10 @@
|
|
16
19
|
<!-- NeatJSON -->
|
17
20
|
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
18
21
|
|
22
|
+
<!-- Trix -->
|
23
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.0/dist/trix.css">
|
24
|
+
<script type="text/javascript" src="https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js"></script>
|
25
|
+
|
19
26
|
<!-- Custom Style -->
|
20
27
|
<style>
|
21
28
|
/* Adjust headers to always take up their entire row, and tweak the sizing. */
|
@@ -28,19 +35,34 @@ h5 { font-size: 1.1rem; }
|
|
28
35
|
h6 { font-size: 1rem; }
|
29
36
|
|
30
37
|
/* Make code and code blocks a little nicer looking. */
|
38
|
+
pre code {
|
39
|
+
display: block;
|
40
|
+
overflow-x: auto;
|
41
|
+
}
|
31
42
|
code {
|
32
43
|
padding: .5em !important;
|
33
|
-
background-color: #
|
44
|
+
background-color: #2b2b2b !important;
|
34
45
|
border: 1px solid #aaa;
|
35
46
|
border-radius: 3px;
|
36
47
|
}
|
37
48
|
|
38
|
-
/*
|
39
|
-
.
|
49
|
+
/* Reduce label font size. */
|
50
|
+
label.form-label {
|
51
|
+
font-size: .8em;
|
52
|
+
}
|
53
|
+
|
54
|
+
trix-editor:empty:not(:focus)::before {
|
55
|
+
pointer-events: none;
|
56
|
+
}
|
57
|
+
|
58
|
+
/* Make Trix buttons visible in the dark mode. */
|
59
|
+
trix-toolbar .trix-button-group {
|
40
60
|
background-color: #f8f8f8;
|
41
61
|
}
|
62
|
+
|
63
|
+
/* Make route group expansion obvious to the user. */
|
42
64
|
.rrf-routes .rrf-route-group-header:hover {
|
43
|
-
background-color: #
|
65
|
+
background-color: #333;
|
44
66
|
}
|
45
67
|
.rrf-routes .rrf-route-group-header td {
|
46
68
|
cursor: pointer;
|
@@ -73,29 +95,34 @@ code {
|
|
73
95
|
// What to do when document loads.
|
74
96
|
document.addEventListener("DOMContentLoaded", (event) => {
|
75
97
|
// Pretty-print JSON.
|
76
|
-
|
98
|
+
document.querySelectorAll(".language-json").forEach((el, index) => {
|
77
99
|
el.innerHTML = neatJSON(JSON.parse(el.innerText), {
|
78
100
|
wrap: 80,
|
79
101
|
afterComma: 1,
|
80
102
|
afterColon: 1,
|
81
|
-
})
|
82
|
-
|
103
|
+
}).replaceAll("&", "&")
|
104
|
+
.replaceAll("<", "<")
|
105
|
+
.replaceAll(">", ">")
|
106
|
+
.replaceAll('"', """)
|
107
|
+
.replaceAll("'", "'")
|
108
|
+
})
|
83
109
|
|
84
110
|
// Then highlight it.
|
85
|
-
hljs.
|
111
|
+
hljs.configure({cssSelector: "pre code.auto-hljs"})
|
112
|
+
hljs.highlightAll()
|
86
113
|
|
87
|
-
// Replace
|
88
|
-
|
114
|
+
// Replace text node links with anchor tag links.
|
115
|
+
document.querySelectorAll(".rrf-copy code").forEach((el, index) => {
|
89
116
|
el.innerHTML = rrfLinkify(el.innerHTML)
|
90
|
-
})
|
117
|
+
})
|
91
118
|
|
92
119
|
// Insert copy link and callback to copy contents of `<code>` element.
|
93
|
-
|
120
|
+
document.querySelectorAll("rrf-copy").forEach((el, index) => {
|
94
121
|
el.insertAdjacentHTML(
|
95
122
|
"afterbegin",
|
96
123
|
"<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
|
97
124
|
)
|
98
|
-
})
|
125
|
+
})
|
99
126
|
})
|
100
127
|
|
101
128
|
// Convert plain-text links to anchor tag links.
|
@@ -134,10 +161,10 @@ function rrfCopyToClipboard(element) {
|
|
134
161
|
return false
|
135
162
|
}
|
136
163
|
|
137
|
-
// Refresh the window.
|
138
|
-
function
|
164
|
+
// Refresh the window as a `GET` request.
|
165
|
+
function rrfGet(button) {
|
139
166
|
button.disabled = true
|
140
|
-
window.location.
|
167
|
+
window.location.replace(window.location.href)
|
141
168
|
}
|
142
169
|
|
143
170
|
// Call `DELETE` on the current path.
|
@@ -158,11 +185,41 @@ function rrfSubmitRawForm(button) {
|
|
158
185
|
|
159
186
|
// Grab the selected route/method, media type, and the body.
|
160
187
|
const [method, path] = document.getElementById("rawFormRoute").value.split(":")
|
161
|
-
const
|
162
|
-
|
188
|
+
const mediaType = document.getElementById("rawFormMediaType").value
|
189
|
+
let body = document.getElementById("rawFormContent").value
|
190
|
+
|
191
|
+
// If the media type is `multipart/form-data`, then we need to build a FormData object.
|
192
|
+
if (mediaType == "multipart/form-data") {
|
193
|
+
let formData = new FormData()
|
194
|
+
|
195
|
+
// Add body to `formData`.
|
196
|
+
const bodySearchParams = new URLSearchParams(body)
|
197
|
+
bodySearchParams.forEach((value, key) => {
|
198
|
+
formData.append(key, value)
|
199
|
+
})
|
200
|
+
|
201
|
+
// Add file(s) to `formData`.
|
202
|
+
const rawFilesForm = document.getElementById("rawFilesForm")
|
203
|
+
if (rawFilesForm) {
|
204
|
+
rawFilesForm.querySelectorAll("input[type=file]").forEach((el, index) => {
|
205
|
+
const files = el.files
|
206
|
+
for (let i = 0; i < files.length; i++) {
|
207
|
+
formData.append(el.name, files[i])
|
208
|
+
}
|
209
|
+
})
|
210
|
+
}
|
211
|
+
|
212
|
+
// Set body to be the form data.
|
213
|
+
body = formData
|
214
|
+
}
|
163
215
|
|
164
216
|
// Perform the API call.
|
165
|
-
rrfAPICall(path, method, {
|
217
|
+
rrfAPICall(path, method, {
|
218
|
+
body,
|
219
|
+
// If the media type is `multipart/form-data`, then we don't want to set the content type
|
220
|
+
// because it must be set by `fetch` to include boundary.
|
221
|
+
headers: mediaType == "multipart/form-data" ? {} : {"Content-Type": mediaType},
|
222
|
+
})
|
166
223
|
}
|
167
224
|
|
168
225
|
// Make an HTML API call and replace the document with the response.
|
@@ -174,4 +231,17 @@ function rrfAPICall(path, method, kwargs={}) {
|
|
174
231
|
.then((response) => response.text())
|
175
232
|
.then((body) => { rrfReplaceDocument(body) })
|
176
233
|
}
|
234
|
+
|
235
|
+
// Check if `rawFilesFormWrapper` should be displayed when media type is changed.
|
236
|
+
function rrfCheckRawFilesFormDisplay(el) {
|
237
|
+
const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
|
238
|
+
|
239
|
+
if (rawFilesFormWrapper) {
|
240
|
+
if (el.value === "multipart/form-data") {
|
241
|
+
rawFilesFormWrapper.style.display = "block"
|
242
|
+
} else {
|
243
|
+
rawFilesFormWrapper.style.display = "none"
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
177
247
|
</script>
|
@@ -1,7 +1,59 @@
|
|
1
1
|
<div style="max-width: 60em; margin: auto">
|
2
|
-
|
2
|
+
<div class="mb-2">
|
3
|
+
<label class="form-label w-100">Route
|
4
|
+
<select class="form-control form-control-sm" id="htmlFormRoute">
|
5
|
+
<% @_rrf_form_routes.each do |route| %>
|
6
|
+
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
7
|
+
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
8
|
+
<% end %>
|
9
|
+
</select>
|
10
|
+
</label>
|
11
|
+
</div>
|
3
12
|
|
4
|
-
<%=
|
5
|
-
|
13
|
+
<%= form_with(
|
14
|
+
model: @record,
|
15
|
+
url: "",
|
16
|
+
method: "PATCH",
|
17
|
+
id: "htmlForm",
|
18
|
+
scope: "",
|
19
|
+
) do |form| %>
|
20
|
+
<% controller.get_fields.map(&:to_s).each do |f| %>
|
21
|
+
<%
|
22
|
+
# Don't provide form fields for associations or primary keys.
|
23
|
+
metadata = controller.class.fields_metadata[f]
|
24
|
+
next if !metadata || metadata[:kind] == "association" || metadata[:read_only]
|
25
|
+
%>
|
26
|
+
<div class="mb-2">
|
27
|
+
<% if metadata[:kind] == "rich_text" %>
|
28
|
+
<label class="form-label w-100"><%= controller.class.get_label(f) %></label>
|
29
|
+
<%= form.rich_text_area f %>
|
30
|
+
<% elsif metadata[:kind] == "attachment" %>
|
31
|
+
<label class="form-label w-100"><%= controller.class.get_label(f) %>
|
32
|
+
<%= form.file_field f, multiple: metadata.dig(:attachment, :macro) == :has_many_attached %>
|
33
|
+
</label>
|
34
|
+
<% else %>
|
35
|
+
<label class="form-label w-100"><%= controller.class.get_label(f) %>
|
36
|
+
<%= form.text_field f, class: "form-control form-control-sm" %>
|
37
|
+
</label>
|
38
|
+
<% end %>
|
39
|
+
</div>
|
40
|
+
<% end %>
|
41
|
+
|
42
|
+
<%= form.submit "Submit", name: "", class: "btn btn-primary", style: "float: right" %>
|
6
43
|
<% end %>
|
44
|
+
|
45
|
+
<script>
|
46
|
+
// Update form anytime the route changes.
|
47
|
+
document.getElementById("htmlFormRoute").addEventListener("change", (event) => {
|
48
|
+
const [verb, path] = event.target.value.split(":")
|
49
|
+
const form = document.getElementById("htmlForm")
|
50
|
+
form.action = path
|
51
|
+
form.querySelector("input[name='_method']").value = verb
|
52
|
+
})
|
53
|
+
|
54
|
+
document.addEventListener("DOMContentLoaded", (event) => {
|
55
|
+
// Trigger the change event to update the form initially.
|
56
|
+
document.getElementById("htmlFormRoute").dispatchEvent(new Event("change"))
|
57
|
+
})
|
58
|
+
</script>
|
7
59
|
</div>
|
@@ -1,9 +1,18 @@
|
|
1
1
|
<div style="max-width: 60em; margin: auto">
|
2
|
-
|
2
|
+
<div class="mb-2">
|
3
|
+
<label class="form-label w-100">Route
|
4
|
+
<select class="form-control form-control-sm" id="rawFormRoute">
|
5
|
+
<% @_rrf_form_routes.each do |route| %>
|
6
|
+
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
7
|
+
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
8
|
+
<% end %>
|
9
|
+
</select>
|
10
|
+
</label>
|
11
|
+
</div>
|
3
12
|
|
4
13
|
<div class="mb-2">
|
5
14
|
<label class="form-label w-100">Media Type
|
6
|
-
<select class="form-control" id="rawFormMediaType">
|
15
|
+
<select class="form-control form-control-sm" id="rawFormMediaType" onchange="rrfCheckRawFilesFormDisplay(this)">
|
7
16
|
<% ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"].each do |t| %>
|
8
17
|
<option value="<%= t %>"><%= t %></option>
|
9
18
|
<% end %>
|
@@ -13,9 +22,28 @@
|
|
13
22
|
|
14
23
|
<div class="mb-2">
|
15
24
|
<label class="form-label w-100">Content
|
16
|
-
<textarea class="form-control" style="font-family: monospace" id="rawFormContent" rows="8" cols="60"></textarea>
|
25
|
+
<textarea class="form-control form-control-sm" style="font-family: monospace" id="rawFormContent" rows="8" cols="60"></textarea>
|
17
26
|
</label>
|
18
27
|
</div>
|
19
28
|
|
29
|
+
<% if @is_model_controller && model = controller.class.get_model %>
|
30
|
+
<% if attachment_reflections = model.attachment_reflections.presence %>
|
31
|
+
<div class="mb-2" style="display: none" id="rawFilesFormWrapper">
|
32
|
+
<%= form_with(
|
33
|
+
model: @record,
|
34
|
+
url: "",
|
35
|
+
id: "rawFilesForm",
|
36
|
+
scope: "",
|
37
|
+
) do |form| %>
|
38
|
+
<% attachment_reflections.each do |field, ref| %>
|
39
|
+
<label class="form-label w-100"><%= controller.class.get_label(field) %>
|
40
|
+
<%= form.file_field field, multiple: ref.macro == :has_many_attached %>
|
41
|
+
</label>
|
42
|
+
<% end %>
|
43
|
+
<% end %>
|
44
|
+
</div>
|
45
|
+
<% end %>
|
46
|
+
<% end %>
|
47
|
+
|
20
48
|
<button type="button" class="btn btn-primary" style="float: right" onclick="rrfSubmitRawForm(this)">Submit</button>
|
21
49
|
</div>
|
@@ -8,8 +8,16 @@ require_relative "../utils"
|
|
8
8
|
module RESTFramework::BaseControllerMixin
|
9
9
|
RRF_BASE_CONTROLLER_CONFIG = {
|
10
10
|
filter_pk_from_request_body: true,
|
11
|
-
exclude_body_fields: [
|
12
|
-
|
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
|
13
21
|
].freeze,
|
14
22
|
extra_actions: nil,
|
15
23
|
extra_member_actions: nil,
|
@@ -323,7 +331,6 @@ module RESTFramework::BaseControllerMixin
|
|
323
331
|
@json_payload = payload.to_json if self.class.serialize_to_json
|
324
332
|
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
325
333
|
end
|
326
|
-
@template_logo_text ||= "Rails REST Framework"
|
327
334
|
@title ||= self.class.get_title
|
328
335
|
@description ||= self.class.description
|
329
336
|
@route_props, @route_groups = RESTFramework::Utils.get_routes(
|
@@ -3,6 +3,15 @@ require_relative "../filters"
|
|
3
3
|
|
4
4
|
# This module provides the core functionality for controllers based on models.
|
5
5
|
module RESTFramework::BaseModelControllerMixin
|
6
|
+
BASE64_REGEX = /data:(.*);base64,(.*)/
|
7
|
+
BASE64_TRANSLATE = ->(field, value) {
|
8
|
+
_, content_type, payload = value.match(BASE64_REGEX).to_a
|
9
|
+
return {
|
10
|
+
io: StringIO.new(Base64.decode64(payload)),
|
11
|
+
content_type: content_type,
|
12
|
+
filename: "image_#{field}#{Rack::Mime::MIME_TYPES.invert[content_type]}",
|
13
|
+
}
|
14
|
+
}
|
6
15
|
include RESTFramework::BaseControllerMixin
|
7
16
|
|
8
17
|
RRF_BASE_MODEL_CONTROLLER_CONFIG = {
|
@@ -89,20 +98,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
89
98
|
return self.get_model.human_attribute_name(s, default: super)
|
90
99
|
end
|
91
100
|
|
92
|
-
# Get the available fields.
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
def get_fields(input_fields: nil, fallback: true)
|
97
|
-
input_fields ||= self.fields if fallback
|
101
|
+
# Get the available fields. Fallback to this controller's model columns, or an empty array. This
|
102
|
+
# should always return an array of strings.
|
103
|
+
def get_fields(input_fields: nil)
|
104
|
+
input_fields ||= self.fields
|
98
105
|
|
99
106
|
# If fields is a hash, then parse it.
|
100
107
|
if input_fields.is_a?(Hash)
|
101
108
|
return RESTFramework::Utils.parse_fields_hash(
|
102
109
|
input_fields, self.get_model, exclude_associations: self.exclude_associations
|
103
110
|
)
|
104
|
-
elsif !input_fields
|
105
|
-
# Otherwise, if fields is nil
|
111
|
+
elsif !input_fields
|
112
|
+
# Otherwise, if fields is nil, then fallback to columns.
|
106
113
|
model = self.get_model
|
107
114
|
return model ? RESTFramework::Utils.fields_for(
|
108
115
|
model, exclude_associations: self.exclude_associations
|
@@ -148,7 +155,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
148
155
|
end
|
149
156
|
|
150
157
|
# Get metadata about the resource's fields.
|
151
|
-
def
|
158
|
+
def fields_metadata
|
159
|
+
return @_fields_metadata if @_fields_metadata
|
160
|
+
|
152
161
|
# Get metadata sources.
|
153
162
|
model = self.get_model
|
154
163
|
fields = self.get_fields.map(&:to_s)
|
@@ -156,8 +165,14 @@ module RESTFramework::BaseModelControllerMixin
|
|
156
165
|
column_defaults = model.column_defaults
|
157
166
|
reflections = model.reflections
|
158
167
|
attributes = model._default_attributes
|
159
|
-
|
160
|
-
|
168
|
+
readonly_attributes = model.readonly_attributes
|
169
|
+
exclude_body_fields = self.exclude_body_fields.map(&:to_s)
|
170
|
+
rich_text_association_names = model.reflect_on_all_associations(:has_one)
|
171
|
+
.collect(&:name)
|
172
|
+
.select { |n| n.to_s.start_with?("rich_text_") }
|
173
|
+
attachment_reflections = model.attachment_reflections
|
174
|
+
|
175
|
+
return @_fields_metadata = fields.map { |f|
|
161
176
|
# Initialize metadata to make the order consistent.
|
162
177
|
metadata = {
|
163
178
|
type: nil,
|
@@ -173,6 +188,11 @@ module RESTFramework::BaseModelControllerMixin
|
|
173
188
|
metadata[:primary_key] = true
|
174
189
|
end
|
175
190
|
|
191
|
+
# Determine if the field is a read-only attribute.
|
192
|
+
if metadata[:primary_key] || f.in?(readonly_attributes) || f.in?(exclude_body_fields)
|
193
|
+
metadata[:read_only] = true
|
194
|
+
end
|
195
|
+
|
176
196
|
# Determine `type`, `required`, `label`, and `kind` based on schema.
|
177
197
|
if column = columns[f]
|
178
198
|
metadata[:kind] = "column"
|
@@ -249,9 +269,22 @@ module RESTFramework::BaseModelControllerMixin
|
|
249
269
|
}.compact
|
250
270
|
end
|
251
271
|
|
272
|
+
# Determine if this is an ActionText "rich text".
|
273
|
+
if :"rich_text_#{f}".in?(rich_text_association_names)
|
274
|
+
metadata[:kind] = "rich_text"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Determine if this is an ActiveStorage attachment.
|
278
|
+
if ref = attachment_reflections[f]
|
279
|
+
metadata[:kind] = "attachment"
|
280
|
+
metadata[:attachment] = {
|
281
|
+
macro: ref.macro,
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
252
285
|
# Determine if this is just a method.
|
253
|
-
if model.method_defined?(f)
|
254
|
-
metadata[:kind]
|
286
|
+
if !metadata[:kind] && model.method_defined?(f)
|
287
|
+
metadata[:kind] = "method"
|
255
288
|
end
|
256
289
|
|
257
290
|
# Collect validator options into a hash on their type, while also updating `required` based
|
@@ -290,7 +323,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
290
323
|
def get_options_metadata
|
291
324
|
return super.merge(
|
292
325
|
{
|
293
|
-
|
326
|
+
primary_key: self.get_model.primary_key,
|
327
|
+
fields: self.fields_metadata,
|
294
328
|
callbacks: self._process_action_callbacks.as_json,
|
295
329
|
},
|
296
330
|
)
|
@@ -377,9 +411,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
377
411
|
end
|
378
412
|
|
379
413
|
# Get a list of fields, taking into account the current action.
|
380
|
-
def get_fields
|
414
|
+
def get_fields
|
381
415
|
fields = self._get_specific_action_config(:action_fields, :fields)
|
382
|
-
return self.class.get_fields(input_fields: fields
|
416
|
+
return self.class.get_fields(input_fields: fields)
|
383
417
|
end
|
384
418
|
|
385
419
|
# Pass fields to get dynamic metadata based on which fields are available.
|
@@ -387,14 +421,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
387
421
|
return self.class.get_options_metadata
|
388
422
|
end
|
389
423
|
|
390
|
-
# Get a list of find_by fields for the current action.
|
391
|
-
# wants to find by virtual columns.
|
424
|
+
# Get a list of find_by fields for the current action.
|
392
425
|
def get_find_by_fields
|
393
|
-
return self.class.find_by_fields
|
426
|
+
return self.class.find_by_fields
|
394
427
|
end
|
395
428
|
|
396
|
-
# Get a list of parameters allowed for the current action.
|
397
|
-
# columns so arbitrary fields can be submitted if no fields are defined.
|
429
|
+
# Get a list of parameters allowed for the current action.
|
398
430
|
def get_allowed_parameters
|
399
431
|
return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
|
400
432
|
|
@@ -403,35 +435,54 @@ module RESTFramework::BaseModelControllerMixin
|
|
403
435
|
:allowed_parameters,
|
404
436
|
)
|
405
437
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
406
|
-
return @_get_allowed_parameters = nil unless fields = self.get_fields
|
407
438
|
|
408
439
|
# For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
|
409
|
-
|
410
|
-
|
411
|
-
|
440
|
+
variations = []
|
441
|
+
hash_variations = {}
|
442
|
+
reflections = self.class.get_model.reflections
|
443
|
+
@_get_allowed_parameters = self.get_fields.map { |f|
|
412
444
|
f = f.to_s
|
413
|
-
|
445
|
+
|
446
|
+
# ActiveStorage Integration: `has_one_attached`.
|
447
|
+
if reflections.key?("#{f}_attachment")
|
448
|
+
next f
|
449
|
+
end
|
450
|
+
|
451
|
+
# ActiveStorage Integration: `has_many_attached`.
|
452
|
+
if reflections.key?("#{f}_attachments")
|
453
|
+
hash_variations[f] = []
|
454
|
+
next nil
|
455
|
+
end
|
456
|
+
|
457
|
+
# ActionText Integration.
|
458
|
+
if reflections.key?("rich_test_#{f}")
|
459
|
+
next f
|
460
|
+
end
|
461
|
+
|
462
|
+
# Return field if it's not an association.
|
463
|
+
next f unless ref = reflections[f]
|
414
464
|
|
415
465
|
if self.class.permit_id_assignment
|
416
466
|
if ref.collection?
|
417
|
-
|
467
|
+
hash_variations["#{f.singularize}_ids"] = []
|
418
468
|
elsif ref.belongs_to?
|
419
|
-
|
469
|
+
variations << "#{f}_id"
|
420
470
|
end
|
421
471
|
end
|
422
472
|
|
423
473
|
if self.class.permit_nested_attributes_assignment
|
424
474
|
if self.class.allow_all_nested_attributes
|
425
|
-
|
475
|
+
hash_variations["#{f}_attributes"] = {}
|
426
476
|
else
|
427
|
-
|
477
|
+
hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
|
428
478
|
end
|
429
479
|
end
|
430
480
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
@_get_allowed_parameters
|
481
|
+
# Associations are not allowed to be submitted in their bare form.
|
482
|
+
next nil
|
483
|
+
}.compact
|
484
|
+
@_get_allowed_parameters += variations
|
485
|
+
@_get_allowed_parameters << hash_variations
|
435
486
|
return @_get_allowed_parameters
|
436
487
|
end
|
437
488
|
|
@@ -454,14 +505,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
454
505
|
def get_body_params(data: nil)
|
455
506
|
data ||= request.request_parameters
|
456
507
|
|
457
|
-
# Filter the request body
|
458
|
-
|
459
|
-
body_params = if allowed_parameters = self.get_allowed_parameters
|
460
|
-
data = ActionController::Parameters.new(data)
|
461
|
-
data.permit(*allowed_parameters)
|
462
|
-
else
|
463
|
-
data
|
464
|
-
end
|
508
|
+
# Filter the request body with strong params.
|
509
|
+
body_params = ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
|
465
510
|
|
466
511
|
# Filter primary key if configured.
|
467
512
|
if self.class.filter_pk_from_request_body
|
@@ -471,6 +516,27 @@ module RESTFramework::BaseModelControllerMixin
|
|
471
516
|
# Filter fields in `exclude_body_fields`.
|
472
517
|
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
473
518
|
|
519
|
+
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
520
|
+
#
|
521
|
+
# rubocop:disable Layout/LineLength
|
522
|
+
#
|
523
|
+
# Good example base64 image:
|
524
|
+
# 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=
|
525
|
+
#
|
526
|
+
# rubocop:enable Layout/LineLength
|
527
|
+
self.class.get_model.attachment_reflections.keys.each do |k|
|
528
|
+
next unless (body_params[k].is_a?(String) && body_params[k].match?(BASE64_REGEX)) ||
|
529
|
+
(body_params[k].is_a?(Array) && body_params[k].all? { |v|
|
530
|
+
v.is_a?(String) && v.match?(BASE64_REGEX)
|
531
|
+
})
|
532
|
+
|
533
|
+
if body_params[k].is_a?(Array)
|
534
|
+
body_params[k] = body_params[k].map { |v| BASE64_TRANSLATE.call(k, v) }
|
535
|
+
else
|
536
|
+
body_params[k] = BASE64_TRANSLATE.call(k, body_params[k])
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
474
540
|
return body_params
|
475
541
|
end
|
476
542
|
alias_method :get_create_params, :get_body_params
|
@@ -492,8 +558,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
492
558
|
|
493
559
|
# Get the recordset but with any associations included to avoid N+1 queries.
|
494
560
|
def get_recordset_with_includes
|
495
|
-
reflections = self.class.get_model.reflections
|
496
|
-
associations = self.get_fields
|
561
|
+
reflections = self.class.get_model.reflections
|
562
|
+
associations = self.get_fields.map { |f|
|
563
|
+
if reflections.key?(f)
|
564
|
+
f.to_sym
|
565
|
+
elsif reflections.key?("rich_text_#{f}")
|
566
|
+
:"rich_text_#{f}"
|
567
|
+
elsif reflections.key?("#{f}_attachment")
|
568
|
+
:"#{f}_attachment"
|
569
|
+
elsif reflections.key?("#{f}_attachments")
|
570
|
+
:"#{f}_attachments"
|
571
|
+
end
|
572
|
+
}.compact
|
497
573
|
|
498
574
|
if associations.any?
|
499
575
|
return self.get_recordset.includes(associations)
|
@@ -11,11 +11,10 @@ end
|
|
11
11
|
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
12
12
|
# controller class.
|
13
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
14
|
-
# Get a list of filterset fields for the current action.
|
15
|
-
# to try filtering by any query parameter because that could clash with other query parameters.
|
14
|
+
# Get a list of filterset fields for the current action.
|
16
15
|
def _get_fields
|
17
16
|
# Always return a list of strings; `@controller.get_fields` already does this.
|
18
|
-
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
17
|
+
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
19
18
|
end
|
20
19
|
|
21
20
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
@@ -64,8 +63,7 @@ end
|
|
64
63
|
|
65
64
|
# A filter backend which handles ordering of the recordset.
|
66
65
|
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
67
|
-
# Get a list of ordering fields for the current action.
|
68
|
-
# user wants to order by a virtual column.
|
66
|
+
# Get a list of ordering fields for the current action.
|
69
67
|
def _get_fields
|
70
68
|
return @controller.class.ordering_fields&.map(&:to_s) || @controller.get_fields
|
71
69
|
end
|
@@ -88,7 +86,8 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
88
86
|
column = field
|
89
87
|
direction = :asc
|
90
88
|
end
|
91
|
-
|
89
|
+
|
90
|
+
next if !column.in?(fields) && column.split(".").first.in?(fields)
|
92
91
|
|
93
92
|
ordering[column] = direction
|
94
93
|
end
|
@@ -113,15 +112,14 @@ end
|
|
113
112
|
|
114
113
|
# Multi-field text searching on models.
|
115
114
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
116
|
-
# Get a list of search fields for the current action.
|
117
|
-
# common string-like columns by default.
|
115
|
+
# Get a list of search fields for the current action.
|
118
116
|
def _get_fields
|
119
117
|
if search_fields = @controller.class.search_fields
|
120
118
|
return search_fields&.map(&:to_s)
|
121
119
|
end
|
122
120
|
|
123
121
|
columns = @controller.class.get_model.column_names
|
124
|
-
return @controller.get_fields
|
122
|
+
return @controller.get_fields.select { |f|
|
125
123
|
f.in?(RESTFramework.config.search_columns) && f.in?(columns)
|
126
124
|
}
|
127
125
|
end
|
@@ -219,13 +219,18 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
219
219
|
includes = {}
|
220
220
|
methods = []
|
221
221
|
serializer_methods = {}
|
222
|
+
|
223
|
+
column_names = @model.column_names
|
224
|
+
reflections = @model.reflections
|
225
|
+
attachment_reflections = @model.attachment_reflections
|
226
|
+
|
222
227
|
fields.each do |f|
|
223
228
|
field_config = @controller.class.get_field_config(f)
|
224
229
|
next if field_config[:write_only]
|
225
230
|
|
226
|
-
if f.in?(
|
231
|
+
if f.in?(column_names)
|
227
232
|
columns << f
|
228
|
-
elsif ref =
|
233
|
+
elsif ref = reflections[f]
|
229
234
|
sub_columns = []
|
230
235
|
sub_methods = []
|
231
236
|
field_config[:sub_fields].each do |sf|
|
@@ -242,9 +247,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
242
247
|
# If we need to limit the number of serialized association records, then dynamically add a
|
243
248
|
# serializer method to do so.
|
244
249
|
if limit = self._get_associations_limit
|
245
|
-
|
246
|
-
|
247
|
-
self.define_singleton_method(method_name) do |record|
|
250
|
+
serializer_methods[f] = f
|
251
|
+
self.define_singleton_method(f) do |record|
|
248
252
|
next record.send(f).limit(limit).as_json(**sub_config)
|
249
253
|
end
|
250
254
|
else
|
@@ -253,8 +257,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
253
257
|
|
254
258
|
# If we need to include the association count, then add it here.
|
255
259
|
if @controller.class.native_serializer_include_associations_count
|
256
|
-
method_name = "
|
257
|
-
serializer_methods[method_name] =
|
260
|
+
method_name = "#{f}.count"
|
261
|
+
serializer_methods[method_name] = method_name
|
258
262
|
self.define_singleton_method(method_name) do |record|
|
259
263
|
next record.send(f).count
|
260
264
|
end
|
@@ -262,6 +266,24 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
262
266
|
else
|
263
267
|
includes[f] = sub_config
|
264
268
|
end
|
269
|
+
elsif ref = reflections["rich_text_#{f}"]
|
270
|
+
# ActionText Integration: Define rich text serializer method.
|
271
|
+
serializer_methods[f] = f
|
272
|
+
self.define_singleton_method(f) do |record|
|
273
|
+
next record.send(f).to_s
|
274
|
+
end
|
275
|
+
elsif ref = attachment_reflections[f]
|
276
|
+
# ActiveStorage Integration: Define attachment serializer method.
|
277
|
+
serializer_methods[f] = f
|
278
|
+
if ref.macro == :has_one_attached
|
279
|
+
self.define_singleton_method(f) do |record|
|
280
|
+
next record.send(f).attachment&.url
|
281
|
+
end
|
282
|
+
else
|
283
|
+
self.define_singleton_method(f) do |record|
|
284
|
+
next record.send(f).map { |x| x.attachment&.url }
|
285
|
+
end
|
286
|
+
end
|
265
287
|
elsif @model.method_defined?(f)
|
266
288
|
methods << f
|
267
289
|
end
|
@@ -272,9 +294,10 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
272
294
|
}
|
273
295
|
end
|
274
296
|
|
275
|
-
# Get the raw serializer config
|
276
|
-
#
|
277
|
-
|
297
|
+
# Get the raw serializer config, prior to any adjustments from the request.
|
298
|
+
#
|
299
|
+
# Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
|
300
|
+
def get_raw_serializer_config
|
278
301
|
# Return a locally defined serializer config if one is defined.
|
279
302
|
if local_config = self.get_local_native_serializer_config
|
280
303
|
return local_config.deep_dup
|
@@ -286,7 +309,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
286
309
|
end
|
287
310
|
|
288
311
|
# If the config wasn't determined, build a serializer config from controller fields.
|
289
|
-
if @model && fields = @controller&.get_fields
|
312
|
+
if @model && fields = @controller&.get_fields
|
290
313
|
return self._get_controller_serializer_config(fields.deep_dup)
|
291
314
|
end
|
292
315
|
|
@@ -296,7 +319,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
296
319
|
|
297
320
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
298
321
|
def get_serializer_config
|
299
|
-
return filter_from_request(self.
|
322
|
+
return filter_from_request(self.get_raw_serializer_config)
|
300
323
|
end
|
301
324
|
|
302
325
|
# Serialize a single record and merge results of `serializer_methods`.
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -180,7 +180,12 @@ module RESTFramework::Utils
|
|
180
180
|
return model.column_names.reject { |c|
|
181
181
|
c.in?(foreign_keys)
|
182
182
|
} + model.reflections.map { |association, ref|
|
183
|
-
#
|
183
|
+
# Ignore associations for which we have custom integrations.
|
184
|
+
if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
|
185
|
+
next nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# Exclude user-specified associations.
|
184
189
|
if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
|
185
190
|
next nil
|
186
191
|
end
|
@@ -192,7 +197,11 @@ module RESTFramework::Utils
|
|
192
197
|
end
|
193
198
|
|
194
199
|
next association
|
195
|
-
}.compact
|
200
|
+
}.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
|
201
|
+
n.to_s.start_with?("rich_text_")
|
202
|
+
}.map { |n|
|
203
|
+
n.to_s.delete_prefix("rich_text_")
|
204
|
+
} + model.attachment_reflections.keys
|
196
205
|
end
|
197
206
|
|
198
207
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
@@ -28,7 +28,10 @@ module RESTFramework
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.stamp_version
|
31
|
-
|
31
|
+
# Only stamp the version if it's not unknown.
|
32
|
+
if RESTFramework::VERSION != "0.unknown"
|
33
|
+
File.write(VERSION_FILEPATH, RESTFramework::VERSION)
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
def self.unstamp_version
|
data/lib/rest_framework.rb
CHANGED
@@ -21,11 +21,7 @@ module RESTFramework
|
|
21
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
22
22
|
# APIs to be defined to behave differently.
|
23
23
|
class Config
|
24
|
-
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES =
|
25
|
-
ActionText::RichText
|
26
|
-
ActiveStorage::Attachment
|
27
|
-
ActiveStorage::Blob
|
28
|
-
).freeze
|
24
|
+
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = [].freeze
|
29
25
|
DEFAULT_LABEL_FIELDS = %w(name label login title email username url).freeze
|
30
26
|
DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w(description note).freeze
|
31
27
|
|
@@ -78,9 +74,7 @@ module RESTFramework
|
|
78
74
|
end
|
79
75
|
|
80
76
|
def self.features
|
81
|
-
return @features ||= {
|
82
|
-
html_forms: false,
|
83
|
-
}
|
77
|
+
return @features ||= {}
|
84
78
|
end
|
85
79
|
end
|
86
80
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregory N. Schmit
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -35,7 +35,6 @@ files:
|
|
35
35
|
- README.md
|
36
36
|
- VERSION
|
37
37
|
- app/views/layouts/rest_framework.html.erb
|
38
|
-
- app/views/rest_framework/_form_routes.html.erb
|
39
38
|
- app/views/rest_framework/_head.html.erb
|
40
39
|
- app/views/rest_framework/_html_form.html.erb
|
41
40
|
- app/views/rest_framework/_raw_form.html.erb
|
@@ -62,7 +61,7 @@ licenses:
|
|
62
61
|
metadata:
|
63
62
|
homepage_uri: https://rails-rest-framework.com
|
64
63
|
source_code_uri: https://github.com/gregschmit/rails-rest-framework
|
65
|
-
post_install_message:
|
64
|
+
post_install_message:
|
66
65
|
rdoc_options: []
|
67
66
|
require_paths:
|
68
67
|
- lib
|
@@ -79,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
78
|
version: '0'
|
80
79
|
requirements: []
|
81
80
|
rubygems_version: 3.2.33
|
82
|
-
signing_key:
|
81
|
+
signing_key:
|
83
82
|
specification_version: 4
|
84
83
|
summary: A framework for DRY RESTful APIs in Ruby on Rails.
|
85
84
|
test_files: []
|
@@ -1,10 +0,0 @@
|
|
1
|
-
<div class="mb-2">
|
2
|
-
<label class="form-label w-100">Route
|
3
|
-
<select class="form-control" id="rawFormRoute">
|
4
|
-
<% @_rrf_form_routes.each do |route| %>
|
5
|
-
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
6
|
-
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
7
|
-
<% end %>
|
8
|
-
</select>
|
9
|
-
</label>
|
10
|
-
</div>
|