rest_framework 0.8.15 → 0.8.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +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>
|