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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36036d097655858aedddc79945d798ae9e8c8dec48ee1e8c59e06e98b9bdf694
4
- data.tar.gz: fc9ced23a919bd6c0528f1b91999e89ca959ce433c0ef99938a46abc46574ca9
3
+ metadata.gz: 0b8523aadeaa24412d0a2ea3d5a0f940864f69f13b4f97859c7b7a072b035a30
4
+ data.tar.gz: 3c7a734e85ef542221738030d711b912114712c7e0f0e28115bacfbf43fee032
5
5
  SHA512:
6
- metadata.gz: 3dea10ba8f4d5cc9cc4fe186b1101823eb7268fe3543ce4dfd850d33d477cb3fe7ba413138ab0b3ade2490e4a3b5bb1bc8880d8ad41296ada615e0e732618ea5
7
- data.tar.gz: 3071fc01e29b932141f70301dae07de686c691416863906b09815d89f7cc6c50e1c4351582a94a5ec1fd67fecdd028808bf2f4efd3e57113790cc9f9f2baf8fc
6
+ metadata.gz: 986584c39f0388eec578bf53f71989f62df6c5eea18001d90023790b2c3fa3781f3b21416215aa58f153265eaf6fdce964ca2dbaf035dd1247b052963ed6d82c
7
+ data.tar.gz: 2dc9b1a29e1c7fdd1a6e010d9198a9f761b6eaed1ca37cbaa60dcad8a76ccd8972a6a9e2a5e8b8496b24794d5fe59a0a2ad35ff25ba2f1cd69c463fce572744f
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.15
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 class="bg-dark">
10
- <div class="w-100 m-0 p-0" style="height: .3em; background-color: #a00;"></div>
11
- <nav class="navbar navbar-dark bg-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="rrfRefresh(this)">GET</button>
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 class="language-plaintext">
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 class="language-plaintext">
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="tab">
104
+ <div class="tab-pane fade show active" id="tab-json" role="tabpanel">
105
105
  <% if @json_payload.present? %>
106
- <div>
107
- <pre class="rrf-copy"><code class="language-json"><%=
108
- JSON.pretty_generate(
109
- JSON.parse(@json_payload)
110
- ) unless @json_payload == '' %></code></pre>
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="tab">
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"><%= @xml_payload %></code></pre></div>
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 RESTFramework.features[:html_forms] %>
144
+ <% if @is_model_controller %>
141
145
  <li class="nav-item">
142
- <a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="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="tab">
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="tab">
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 RESTFramework.features[:html_forms] %>
159
- <div class="tab-pane fade" id="tab-raw-form" role="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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
8
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
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/vs.min.css" integrity="sha512-AVoZ71dJLtHRlsgWwujPT1hk2zxtFWsPlpTPCc/1g0WgpbmlzkqlDFduAvnOV4JJWKUquPc1ZyMc5eq4fRnKOQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
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: #f3f3f3 !important;
44
+ background-color: #2b2b2b !important;
34
45
  border: 1px solid #aaa;
35
46
  border-radius: 3px;
36
47
  }
37
48
 
38
- /* Make route group expansion obvious to the user. */
39
- .rrf-routes .rrf-route-group-header {
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: #f0f0f0;
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
- [...document.getElementsByClassName("language-json")].forEach((el, index) => {
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("&", "&amp;")
104
+ .replaceAll("<", "&lt;")
105
+ .replaceAll(">", "&gt;")
106
+ .replaceAll('"', "&quot;")
107
+ .replaceAll("'", "&#039;")
108
+ })
83
109
 
84
110
  // Then highlight it.
85
- hljs.highlightAll();
111
+ hljs.configure({cssSelector: "pre code.auto-hljs"})
112
+ hljs.highlightAll()
86
113
 
87
- // Replace all text nodes with anchor tag links.
88
- [...document.querySelectorAll(".rrf-copy code")].forEach((el, index) => {
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
- [...document.querySelectorAll("rrf-copy")].forEach((el, index) => {
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 rrfRefresh(button) {
164
+ // Refresh the window as a `GET` request.
165
+ function rrfGet(button) {
139
166
  button.disabled = true
140
- window.location.reload()
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 media_type = document.getElementById("rawFormMediaType").value
162
- const body = document.getElementById("rawFormContent").value
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, {body, headers: {"Content-Type": media_type}})
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
- <%= render partial: "rest_framework/form_routes" %>
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
- <%= form_for @ do |f| %>
5
- <%= f.file_field :picture %>
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
- <%= render partial: "rest_framework/form_routes" %>
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
- :created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
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. Returning `nil` indicates that anything should be accepted. If
93
- # `fallback` is true, then we should fallback to this controller's model columns, or an empty
94
- # array. This should always return an array of strings, no symbols, and possibly `nil` (only if
95
- # `fallback` is false).
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 && fallback
105
- # Otherwise, if fields is nil and fallback is true, then fallback to columns.
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 get_fields_metadata
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
- return fields.map { |f|
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] ||= "method"
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
- fields: self.get_fields_metadata,
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(fallback: false)
414
+ def get_fields
381
415
  fields = self._get_specific_action_config(:action_fields, :fields)
382
- return self.class.get_fields(input_fields: fields, fallback: fallback)
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. Do not fallback to columns in case the user
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 || self.get_fields
426
+ return self.class.find_by_fields
394
427
  end
395
428
 
396
- # Get a list of parameters allowed for the current action. By default we do not fallback to
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
- id_variations = []
410
- variations = {}
411
- @_get_allowed_parameters = fields.map { |f|
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
- next f unless ref = self.class.get_model.reflections[f]
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
- variations["#{f.singularize}_ids"] = []
467
+ hash_variations["#{f.singularize}_ids"] = []
418
468
  elsif ref.belongs_to?
419
- id_variations << "#{f}_id"
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
- variations["#{f}_attributes"] = {}
475
+ hash_variations["#{f}_attributes"] = {}
426
476
  else
427
- variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
477
+ hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
428
478
  end
429
479
  end
430
480
 
431
- next f
432
- }.flatten
433
- @_get_allowed_parameters += id_variations
434
- @_get_allowed_parameters << variations
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 and map to strings. Return all params if we cannot resolve a list of
458
- # allowed parameters or fields.
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.keys
496
- associations = self.get_fields(fallback: true).select { |f| f.in?(reflections) }
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. Fallback to columns because we don't want
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(fallback: true)
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. Do not fallback to columns in case the
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
- next unless !fields || column.in?(fields)
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. Fallback to columns but only grab a few
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(fallback: true).select { |f|
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?(@model.column_names)
231
+ if f.in?(column_names)
227
232
  columns << f
228
- elsif ref = @model.reflections[f]
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
- method_name = "__rrf_limit_method_#{f}"
246
- serializer_methods[method_name] = f
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 = "__rrf_count_method_#{f}"
257
- serializer_methods[method_name] = "#{f}.count"
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. Use `deep_dup` on any class mutables (array, hash, etc) to avoid
276
- # mutating class state.
277
- def _get_raw_serializer_config
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(fallback: true)
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._get_raw_serializer_config)
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`.
@@ -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
- # Exclude certain associations (by default, active storage and action text associations).
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
- File.write(VERSION_FILEPATH, RESTFramework::VERSION)
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
@@ -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 = %w(
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.15
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-03-03 00:00:00.000000000 Z
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>