rest_framework 0.8.15 → 0.8.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"