rest_framework 0.8.14 → 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: '0528c2ec3d68b9a457b8c35bd00aa8ad7f19d0bb440fe0d75652b8bade7f7312'
4
- data.tar.gz: 364a40189c072a0064397e9410c8c8e93b5b9348f478dc8283de3b8786cd8009
3
+ metadata.gz: 0b8523aadeaa24412d0a2ea3d5a0f940864f69f13b4f97859c7b7a072b035a30
4
+ data.tar.gz: 3c7a734e85ef542221738030d711b912114712c7e0f0e28115bacfbf43fee032
5
5
  SHA512:
6
- metadata.gz: ab0cb394bd1528a03d8b1fd7b42d09c77c076ebabfdd8c8b54816187cad2984b1e22db29f26acf3408edb9f2afefc6261248b4833e8ba857265282107ff6af36
7
- data.tar.gz: 72d2ae348a3eea1a7a7c7964f08f7ef79286a75b6b3029241ba84e0ea36be1ec6211ce694fc3ce760aa08a2cdcca5474764bd505e92d44dc24b7723a541859f8
6
+ metadata.gz: 986584c39f0388eec578bf53f71989f62df6c5eea18001d90023790b2c3fa3781f3b21416215aa58f153265eaf6fdce964ca2dbaf035dd1247b052963ed6d82c
7
+ data.tar.gz: 2dc9b1a29e1c7fdd1a6e010d9198a9f761b6eaed1ca37cbaa60dcad8a76ccd8972a6a9e2a5e8b8496b24794d5fe59a0a2ad35ff25ba2f1cd69c463fce572744f
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.14
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,24 +98,24 @@ 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&.map(&:to_s) 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
109
116
  ) : []
117
+ elsif input_fields
118
+ input_fields = input_fields.map(&:to_s)
110
119
  end
111
120
 
112
121
  return input_fields
@@ -146,7 +155,9 @@ module RESTFramework::BaseModelControllerMixin
146
155
  end
147
156
 
148
157
  # Get metadata about the resource's fields.
149
- def get_fields_metadata
158
+ def fields_metadata
159
+ return @_fields_metadata if @_fields_metadata
160
+
150
161
  # Get metadata sources.
151
162
  model = self.get_model
152
163
  fields = self.get_fields.map(&:to_s)
@@ -154,8 +165,14 @@ module RESTFramework::BaseModelControllerMixin
154
165
  column_defaults = model.column_defaults
155
166
  reflections = model.reflections
156
167
  attributes = model._default_attributes
157
-
158
- 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|
159
176
  # Initialize metadata to make the order consistent.
160
177
  metadata = {
161
178
  type: nil,
@@ -171,6 +188,11 @@ module RESTFramework::BaseModelControllerMixin
171
188
  metadata[:primary_key] = true
172
189
  end
173
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
+
174
196
  # Determine `type`, `required`, `label`, and `kind` based on schema.
175
197
  if column = columns[f]
176
198
  metadata[:kind] = "column"
@@ -247,9 +269,22 @@ module RESTFramework::BaseModelControllerMixin
247
269
  }.compact
248
270
  end
249
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
+
250
285
  # Determine if this is just a method.
251
- if model.method_defined?(f)
252
- metadata[:kind] ||= "method"
286
+ if !metadata[:kind] && model.method_defined?(f)
287
+ metadata[:kind] = "method"
253
288
  end
254
289
 
255
290
  # Collect validator options into a hash on their type, while also updating `required` based
@@ -288,7 +323,8 @@ module RESTFramework::BaseModelControllerMixin
288
323
  def get_options_metadata
289
324
  return super.merge(
290
325
  {
291
- fields: self.get_fields_metadata,
326
+ primary_key: self.get_model.primary_key,
327
+ fields: self.fields_metadata,
292
328
  callbacks: self._process_action_callbacks.as_json,
293
329
  },
294
330
  )
@@ -375,9 +411,9 @@ module RESTFramework::BaseModelControllerMixin
375
411
  end
376
412
 
377
413
  # Get a list of fields, taking into account the current action.
378
- def get_fields(fallback: false)
414
+ def get_fields
379
415
  fields = self._get_specific_action_config(:action_fields, :fields)
380
- return self.class.get_fields(input_fields: fields, fallback: fallback)
416
+ return self.class.get_fields(input_fields: fields)
381
417
  end
382
418
 
383
419
  # Pass fields to get dynamic metadata based on which fields are available.
@@ -385,14 +421,12 @@ module RESTFramework::BaseModelControllerMixin
385
421
  return self.class.get_options_metadata
386
422
  end
387
423
 
388
- # Get a list of find_by fields for the current action. Do not fallback to columns in case the user
389
- # wants to find by virtual columns.
424
+ # Get a list of find_by fields for the current action.
390
425
  def get_find_by_fields
391
- return self.class.find_by_fields || self.get_fields
426
+ return self.class.find_by_fields
392
427
  end
393
428
 
394
- # Get a list of parameters allowed for the current action. By default we do not fallback to
395
- # columns so arbitrary fields can be submitted if no fields are defined.
429
+ # Get a list of parameters allowed for the current action.
396
430
  def get_allowed_parameters
397
431
  return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
398
432
 
@@ -401,35 +435,54 @@ module RESTFramework::BaseModelControllerMixin
401
435
  :allowed_parameters,
402
436
  )
403
437
  return @_get_allowed_parameters if @_get_allowed_parameters
404
- return @_get_allowed_parameters = nil unless fields = self.get_fields
405
438
 
406
439
  # For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
407
- id_variations = []
408
- variations = {}
409
- @_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|
410
444
  f = f.to_s
411
- 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]
412
464
 
413
465
  if self.class.permit_id_assignment
414
466
  if ref.collection?
415
- variations["#{f.singularize}_ids"] = []
467
+ hash_variations["#{f.singularize}_ids"] = []
416
468
  elsif ref.belongs_to?
417
- id_variations << "#{f}_id"
469
+ variations << "#{f}_id"
418
470
  end
419
471
  end
420
472
 
421
473
  if self.class.permit_nested_attributes_assignment
422
474
  if self.class.allow_all_nested_attributes
423
- variations["#{f}_attributes"] = {}
475
+ hash_variations["#{f}_attributes"] = {}
424
476
  else
425
- variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
477
+ hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
426
478
  end
427
479
  end
428
480
 
429
- next f
430
- }.flatten
431
- @_get_allowed_parameters += id_variations
432
- @_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
433
486
  return @_get_allowed_parameters
434
487
  end
435
488
 
@@ -452,14 +505,8 @@ module RESTFramework::BaseModelControllerMixin
452
505
  def get_body_params(data: nil)
453
506
  data ||= request.request_parameters
454
507
 
455
- # Filter the request body and map to strings. Return all params if we cannot resolve a list of
456
- # allowed parameters or fields.
457
- body_params = if allowed_parameters = self.get_allowed_parameters
458
- data = ActionController::Parameters.new(data)
459
- data.permit(*allowed_parameters)
460
- else
461
- data
462
- end
508
+ # Filter the request body with strong params.
509
+ body_params = ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
463
510
 
464
511
  # Filter primary key if configured.
465
512
  if self.class.filter_pk_from_request_body
@@ -469,6 +516,27 @@ module RESTFramework::BaseModelControllerMixin
469
516
  # Filter fields in `exclude_body_fields`.
470
517
  (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
471
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
+
472
540
  return body_params
473
541
  end
474
542
  alias_method :get_create_params, :get_body_params
@@ -490,8 +558,18 @@ module RESTFramework::BaseModelControllerMixin
490
558
 
491
559
  # Get the recordset but with any associations included to avoid N+1 queries.
492
560
  def get_recordset_with_includes
493
- reflections = self.class.get_model.reflections.keys
494
- 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
495
573
 
496
574
  if associations.any?
497
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.14
4
+ version: 0.8.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory N. Schmit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-02 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
@@ -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>