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 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>