rest_framework 0.8.15 → 0.8.17

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.
@@ -4,11 +4,13 @@
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
6
  <!-- 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>
7
+ <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">
8
+ <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
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
9
10
 
10
11
  <!-- 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" />
12
+ <link rel="stylesheet" class="rrf-light-mode" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-light.min.css" integrity="sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
13
+ <link rel="stylesheet" class="rrf-dark-mode" 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
14
  <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
15
  <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
16
  <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,31 +18,105 @@
16
18
  <!-- NeatJSON -->
17
19
  <script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
18
20
 
21
+ <!-- Trix -->
22
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.0/dist/trix.css">
23
+ <script type="text/javascript" src="https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js"></script>
24
+
19
25
  <!-- Custom Style -->
20
26
  <style>
21
- /* Adjust headers to always take up their entire row, and tweak the sizing. */
22
- h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
27
+ /********************************
28
+ * START OF LIB/DOCS COMMON CSS *
29
+ ********************************/
30
+
31
+ :root {
32
+ --rrf-red: #900;
33
+ --rrf-red-hover: #5f0c0c;
34
+ --rrf-light-red: #db2525;
35
+ --rrf-light-red-hover: #b80404;
36
+ }
37
+ #rrfAccentBar {
38
+ background-color: var(--rrf-red);
39
+ height: .3em;
40
+ }
41
+ header nav { background-color: black; }
42
+
43
+ /* Header adjustments. */
23
44
  h1 { font-size: 2rem; }
24
45
  h2 { font-size: 1.7rem; }
25
46
  h3 { font-size: 1.5rem; }
26
47
  h4 { font-size: 1.3rem; }
27
48
  h5 { font-size: 1.1rem; }
28
49
  h6 { font-size: 1rem; }
50
+ h1, h2, h3, h4, h5, h6 {
51
+ color: var(--rrf-red);
52
+ }
53
+ html[data-bs-theme="dark"] h1,
54
+ html[data-bs-theme="dark"] h2,
55
+ html[data-bs-theme="dark"] h3,
56
+ html[data-bs-theme="dark"] h4,
57
+ html[data-bs-theme="dark"] h5,
58
+ html[data-bs-theme="dark"] h6 {
59
+ color: var(--rrf-light-red);
60
+ }
29
61
 
30
- /* Make code and code blocks a little nicer looking. */
31
- code {
62
+ /* Improve code and code blocks. */
63
+ pre code {
64
+ display: block;
65
+ overflow-x: auto;
66
+ }
67
+ code, .trix-content pre {
32
68
  padding: .5em !important;
33
- background-color: #f3f3f3 !important;
69
+ background-color: #eee !important;
34
70
  border: 1px solid #aaa;
35
71
  border-radius: 3px;
36
72
  }
73
+ p code {
74
+ padding: .1em .3em !important;
75
+ }
76
+ html[data-bs-theme="dark"] code, html[data-bs-theme="dark"] .trix-content pre {
77
+ background-color: #2b2b2b !important;
78
+ }
37
79
 
38
- /* Make route group expansion obvious to the user. */
39
- .rrf-routes .rrf-route-group-header {
40
- background-color: #f8f8f8;
80
+ /* Anchors */
81
+ a:not(.nav-link) {
82
+ text-decoration: none;
83
+ color: var(--rrf-red);
84
+ }
85
+ a:hover:not(.nav-link) {
86
+ text-decoration: underline;
87
+ color: var(--rrf-red-hover);
88
+ }
89
+ html[data-bs-theme="dark"] a:not(.nav-link) { color: var(--rrf-light-red); }
90
+ html[data-bs-theme="dark"] a:hover:not(.nav-link) { color: var(--rrf-light-red-hover); }
91
+
92
+ /******************************
93
+ * END OF LIB/DOCS COMMON CSS *
94
+ ******************************/
95
+
96
+ /* Header adjustments. */
97
+ h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
98
+
99
+ /* Reduce label font size. */
100
+ label.form-label {
101
+ font-size: .8em;
102
+ }
103
+
104
+ /* Make Trix buttons visible even in dark mode. */
105
+ trix-toolbar .trix-button-group {
106
+ background-color: #eee;
107
+ }
108
+
109
+ /* Make Trix dialog URL input visible in dark mode. */
110
+ input.trix-input--dialog {
111
+ color: black;
41
112
  }
113
+
114
+ /* Make route group expansion obvious to the user. */
42
115
  .rrf-routes .rrf-route-group-header:hover {
43
- background-color: #f0f0f0;
116
+ background-color: #ddd;
117
+ }
118
+ html[data-bs-theme="dark"] .rrf-routes .rrf-route-group-header:hover {
119
+ background-color: #333;
44
120
  }
45
121
  .rrf-routes .rrf-route-group-header td {
46
122
  cursor: pointer;
@@ -70,32 +146,134 @@ code {
70
146
 
71
147
  <!-- Custom JavaScript -->
72
148
  <script>
73
- // What to do when document loads.
149
+ /*******************************
150
+ * START OF LIB/DOCS COMMON JS *
151
+ *******************************/
152
+
153
+ ;(() => {
154
+ // Get the real mode from a selected mode. Anything other than "light" or "dark" is treated as
155
+ // "system" mode.
156
+ const rrfGetRealMode = (selectedMode) => {
157
+ if (selectedMode === "light" || selectedMode === "dark") {
158
+ return selectedMode
159
+ }
160
+
161
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
162
+ return "dark"
163
+ }
164
+
165
+ return "light"
166
+ }
167
+
168
+ // Set the mode, given a "selected" mode.
169
+ const rrfSetSelectedMode = (selectedMode) => {
170
+ const modeComponent = document.getElementById("rrfModeComponent")
171
+
172
+ // Anything except "light" or "dark" is casted to "system".
173
+ if (selectedMode !== "light" && selectedMode !== "dark") {
174
+ selectedMode = "system"
175
+ }
176
+
177
+ // Store selected mode in `localStorage`.
178
+ localStorage.setItem("rrfMode", selectedMode)
179
+
180
+ // Set the mode selector to the selected mode.
181
+ let labelHTML
182
+ modeComponent.querySelectorAll("button[data-rrf-mode-value]").forEach((el) => {
183
+ if (el.getAttribute("data-rrf-mode-value") === selectedMode) {
184
+ el.classList.add("active")
185
+ labelHTML = el.querySelector("i").outerHTML.replace("ms-2", "me-1")
186
+ } else {
187
+ el.classList.remove("active")
188
+ }
189
+ })
190
+ modeComponent.querySelector("button[data-bs-toggle]").innerHTML = labelHTML
191
+
192
+ // Get the real mode to use.
193
+ realMode = rrfGetRealMode(selectedMode)
194
+
195
+ // Set the `realMode` effects.
196
+ if (realMode === "light") {
197
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
198
+ el.disabled = false
199
+ })
200
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
201
+ el.disabled = true
202
+ })
203
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
204
+ el.setAttribute("data-bs-theme", "light")
205
+ })
206
+ } else if (realMode === "dark") {
207
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
208
+ el.disabled = true
209
+ })
210
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
211
+ el.disabled = false
212
+ })
213
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
214
+ el.setAttribute("data-bs-theme", "dark")
215
+ })
216
+ } else {
217
+ console.log(`RRF: Unknown mode: ${mode}`)
218
+ }
219
+ }
220
+
221
+ // Initialize dark/light mode.
222
+ document.addEventListener("DOMContentLoaded", (event) => {
223
+ const selectedMode = localStorage.getItem("rrfMode")
224
+ rrfSetSelectedMode(selectedMode)
225
+ document.querySelectorAll("#rrfModeComponent button[data-rrf-mode-value]").forEach((el) => {
226
+ el.addEventListener("click", (event) => {
227
+ rrfSetSelectedMode(event.target.getAttribute("data-rrf-mode-value"))
228
+ })
229
+ })
230
+ })
231
+
232
+ // Handle case where user changes system theme.
233
+ if (window.matchMedia) {
234
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
235
+ const selectedMode = localStorage.getItem("rrfMode")
236
+ if (selectedMode !== "light" && selectedMode !== "dark") {
237
+ rrfSetSelectedMode("system")
238
+ }
239
+ })
240
+ }
241
+ })()
242
+
243
+ /*****************************
244
+ * END OF LIB/DOCS COMMON JS *
245
+ *****************************/
246
+
74
247
  document.addEventListener("DOMContentLoaded", (event) => {
75
248
  // Pretty-print JSON.
76
- [...document.getElementsByClassName("language-json")].forEach((el, index) => {
249
+ document.querySelectorAll(".language-json").forEach((el, index) => {
77
250
  el.innerHTML = neatJSON(JSON.parse(el.innerText), {
78
251
  wrap: 80,
79
252
  afterComma: 1,
80
253
  afterColon: 1,
81
- })
82
- });
254
+ }).replaceAll("&", "&amp;")
255
+ .replaceAll("<", "&lt;")
256
+ .replaceAll(">", "&gt;")
257
+ .replaceAll('"', "&quot;")
258
+ .replaceAll("'", "&#039;")
259
+ })
83
260
 
84
261
  // Then highlight it.
85
- hljs.highlightAll();
262
+ hljs.configure({cssSelector: "pre code.auto-hljs"})
263
+ hljs.highlightAll()
86
264
 
87
- // Replace all text nodes with anchor tag links.
88
- [...document.querySelectorAll(".rrf-copy code")].forEach((el, index) => {
265
+ // Replace text node links with anchor tag links.
266
+ document.querySelectorAll(".rrf-copy code").forEach((el, index) => {
89
267
  el.innerHTML = rrfLinkify(el.innerHTML)
90
- });
268
+ })
91
269
 
92
270
  // Insert copy link and callback to copy contents of `<code>` element.
93
- [...document.querySelectorAll("rrf-copy")].forEach((el, index) => {
271
+ document.querySelectorAll("rrf-copy").forEach((el, index) => {
94
272
  el.insertAdjacentHTML(
95
273
  "afterbegin",
96
274
  "<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
97
275
  )
98
- });
276
+ })
99
277
  })
100
278
 
101
279
  // Convert plain-text links to anchor tag links.
@@ -134,10 +312,10 @@ function rrfCopyToClipboard(element) {
134
312
  return false
135
313
  }
136
314
 
137
- // Refresh the window.
138
- function rrfRefresh(button) {
315
+ // Refresh the window as a `GET` request.
316
+ function rrfGet(button) {
139
317
  button.disabled = true
140
- window.location.reload()
318
+ window.location.replace(window.location.href)
141
319
  }
142
320
 
143
321
  // Call `DELETE` on the current path.
@@ -158,11 +336,41 @@ function rrfSubmitRawForm(button) {
158
336
 
159
337
  // Grab the selected route/method, media type, and the body.
160
338
  const [method, path] = document.getElementById("rawFormRoute").value.split(":")
161
- const media_type = document.getElementById("rawFormMediaType").value
162
- const body = document.getElementById("rawFormContent").value
339
+ const mediaType = document.getElementById("rawFormMediaType").value
340
+ let body = document.getElementById("rawFormContent").value
341
+
342
+ // If the media type is `multipart/form-data`, then we need to build a FormData object.
343
+ if (mediaType == "multipart/form-data") {
344
+ let formData = new FormData()
345
+
346
+ // Add body to `formData`.
347
+ const bodySearchParams = new URLSearchParams(body)
348
+ bodySearchParams.forEach((value, key) => {
349
+ formData.append(key, value)
350
+ })
351
+
352
+ // Add file(s) to `formData`.
353
+ const rawFilesForm = document.getElementById("rawFilesForm")
354
+ if (rawFilesForm) {
355
+ rawFilesForm.querySelectorAll("input[type=file]").forEach((el, index) => {
356
+ const files = el.files
357
+ for (let i = 0; i < files.length; i++) {
358
+ formData.append(el.name, files[i])
359
+ }
360
+ })
361
+ }
362
+
363
+ // Set body to be the form data.
364
+ body = formData
365
+ }
163
366
 
164
367
  // Perform the API call.
165
- rrfAPICall(path, method, {body, headers: {"Content-Type": media_type}})
368
+ rrfAPICall(path, method, {
369
+ body,
370
+ // If the media type is `multipart/form-data`, then we don't want to set the content type
371
+ // because it must be set by `fetch` to include boundary.
372
+ headers: mediaType == "multipart/form-data" ? {} : {"Content-Type": mediaType},
373
+ })
166
374
  }
167
375
 
168
376
  // Make an HTML API call and replace the document with the response.
@@ -174,4 +382,17 @@ function rrfAPICall(path, method, kwargs={}) {
174
382
  .then((response) => response.text())
175
383
  .then((body) => { rrfReplaceDocument(body) })
176
384
  }
385
+
386
+ // Check if `rawFilesFormWrapper` should be displayed when media type is changed.
387
+ function rrfCheckRawFilesFormDisplay(el) {
388
+ const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
389
+
390
+ if (rawFilesFormWrapper) {
391
+ if (el.value === "multipart/form-data") {
392
+ rawFilesFormWrapper.style.display = "block"
393
+ } else {
394
+ rawFilesFormWrapper.style.display = "none"
395
+ }
396
+ }
397
+ }
177
398
  </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>
data/docs/CNAME ADDED
@@ -0,0 +1 @@
1
+ rails-rest-framework.com
data/docs/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "github-pages"
4
+ gem "yard"