rest_framework 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/views/rest_framework/_external.html.erb +13 -0
  4. data/app/views/rest_framework/_head.html.erb +2 -192
  5. data/{docs/assets/js/rest_framework.js → app/views/rest_framework/_shared.html} +69 -42
  6. data/docs/Gemfile +1 -0
  7. data/docs/Gemfile.lock +14 -14
  8. data/docs/_config.yml +4 -2
  9. data/docs/_guide/2_controllers.md +342 -0
  10. data/docs/_guide/3_serializers.md +1 -1
  11. data/docs/_guide/4_filtering_and_ordering.md +8 -8
  12. data/docs/_includes/external.html +9 -0
  13. data/docs/_includes/head.html +135 -15
  14. data/docs/_includes/shared.html +164 -0
  15. data/lib/rest_framework/controller_mixins/base.rb +23 -36
  16. data/lib/rest_framework/controller_mixins/models.rb +86 -75
  17. data/lib/rest_framework/controller_mixins.rb +1 -0
  18. data/lib/rest_framework/engine.rb +9 -0
  19. data/lib/rest_framework/filters/base.rb +9 -0
  20. data/lib/rest_framework/filters/model_ordering.rb +48 -0
  21. data/lib/rest_framework/filters/model_query.rb +51 -0
  22. data/lib/rest_framework/filters/model_search.rb +41 -0
  23. data/lib/rest_framework/filters/ransack.rb +25 -0
  24. data/lib/rest_framework/filters.rb +6 -150
  25. data/lib/rest_framework/paginators.rb +7 -11
  26. data/lib/rest_framework/serializers.rb +10 -10
  27. data/lib/rest_framework/utils.rb +15 -7
  28. data/lib/rest_framework.rb +93 -4
  29. data/vendor/assets/javascripts/rest_framework/bootstrap.js +7 -0
  30. data/vendor/assets/javascripts/rest_framework/highlight-json.js +7 -0
  31. data/vendor/assets/javascripts/rest_framework/highlight-xml.js +29 -0
  32. data/vendor/assets/javascripts/rest_framework/highlight.js +1202 -0
  33. data/vendor/assets/javascripts/rest_framework/neatjson.js +8 -0
  34. data/vendor/assets/javascripts/rest_framework/trix.js +6 -0
  35. data/vendor/assets/stylesheets/rest_framework/bootstrap-icons.css +13 -0
  36. data/vendor/assets/stylesheets/rest_framework/bootstrap.css +6 -0
  37. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.css +7 -0
  38. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.css +7 -0
  39. data/vendor/assets/stylesheets/rest_framework/trix.css +410 -0
  40. metadata +23 -5
  41. data/docs/_guide/2_controller_mixins.md +0 -293
  42. data/docs/assets/css/rest_framework.css +0 -159
@@ -0,0 +1,342 @@
1
+ ---
2
+ layout: default
3
+ title: Controllers
4
+ position: 2
5
+ slug: controllers
6
+ ---
7
+ # Controllers
8
+
9
+ This is the core of the REST Framework. Generally speaking, projects already have an existing
10
+ controller inheritance hierarchy, so we want developers to be able to maintain that project
11
+ structure while leveraging the power of the REST Framework. Also, different controllers which
12
+ inherit from the same parent often need different REST Framework functionality and behavior. For
13
+ these reasons, REST Framework provides the controller functionality as modules that you mix into
14
+ your controllers.
15
+
16
+ Here is a graph of the controller mixins (all exist within the `RESTFramework` namespace), with
17
+ supplementary mixins shown as well:
18
+
19
+ ```shell
20
+ BaseControllerMixin
21
+ BaseModelControllerMixin (includes BaseControllerMixin)
22
+ ModelControllerMixin (includes BaseModelControllerMixin)
23
+ ├── ListModelMixin
24
+ ├── ShowModelMixin
25
+ ├── CreateModelMixin
26
+ ├── UpdateModelMixin
27
+ └── DestroyModelMixin
28
+ ReadOnlyModelControllerMixin (includes BaseModelControllerMixin)
29
+ ├── ListModelMixin
30
+ └── ShowModelMixin
31
+ BulkModelControllerMixin (includes ModelControllerMixin)
32
+ ├── BulkCreateModelMixin
33
+ ├── BulkUpdateModelMixin
34
+ └── BulkDestroyModelMixin
35
+ ```
36
+
37
+ All API controllers should include at least one of these top-level controller mixins, and can
38
+ include any of the supplementary mixins to add additional functionality. For example, if you want to
39
+ permit create but not update or destroy, then you could do this:
40
+
41
+ ```ruby
42
+ class Api::MoviesController < ApiController
43
+ include RESTFramework::BaseModelControllerMixin
44
+ include RESTFramework::CreateModelMixin
45
+ end
46
+ ```
47
+
48
+ ## BaseControllerMixin
49
+
50
+ To transform a controller into the simplest possible RESTful controller, you can include
51
+ `BaseControllerMixin`, which provides a simple `root` action so it can be used at the API root.
52
+
53
+ ```ruby
54
+ class ApiController < ApplicationController
55
+ include RESTFramework::BaseControllerMixin
56
+ end
57
+ ```
58
+
59
+ ### Response Rendering
60
+
61
+ A fundamental feature that REST Framework provides is the ability to render a browsable API. This
62
+ allows developers to discover and interact with the API's functionality, while also providing faster
63
+ and more lightweight JSON/XML formats for consumption by the systems these developers create.
64
+
65
+ The `api_response` method is how this is accomplished. The first argument is the data to be
66
+ rendered (often an array or hash), and keyword arguments can be provided to customize the response
67
+ (e.g., setting the HTTP status code). Using this method instead of the classic Rails helpers, such
68
+ as `render`, will automatically provide the browsable API as well as JSON/XML rendering.
69
+
70
+ Here is a simple example of rendering a hash with a `message` key:
71
+
72
+ ```ruby
73
+ class ApiController < ApplicationController
74
+ include RESTFramework::BaseControllerMixin
75
+ self.extra_actions = {test: :get}
76
+
77
+ def test
78
+ api_response({message: "Test successful!"})
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### Routing Extra Actions
84
+
85
+ The `extra_actions` property defines extra actions on the controller to be routed. It is a hash of
86
+ `endpoint -> method(s)` (where `method(s)` can be a method symbol or an array of method symbols).
87
+
88
+ ```ruby
89
+ class ApiController < ApplicationController
90
+ include RESTFramework::BaseControllerMixin
91
+ self.extra_actions = {test: :get}
92
+
93
+ def test
94
+ api_response({message: "Test successful!"})
95
+ end
96
+ end
97
+ ```
98
+
99
+ Or with multiple methods:
100
+
101
+ ```ruby
102
+ class ApiController < ApplicationController
103
+ include RESTFramework::BaseControllerMixin
104
+ self.extra_actions = {test: [:get, :post]}
105
+
106
+ def test
107
+ api_response({message: "Test successful!"})
108
+ end
109
+ end
110
+ ```
111
+
112
+ If your action conflicts with a builtin method, then you can also override the path:
113
+
114
+ ```ruby
115
+ class ApiController < ApplicationController
116
+ include RESTFramework::BaseControllerMixin
117
+
118
+ # This will route `test_action` to `/test`, in case there is already a `test` method that cannot
119
+ # be overridden.
120
+ self.extra_actions = {test_action: {path: :test, methods: :get}}
121
+
122
+ def test_action
123
+ api_response({message: "Test successful!"})
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## ModelControllerMixin
129
+
130
+ `ModelControllerMixin` assists with providing the standard model CRUD (create, read, update,
131
+ destroy) for your controller. This is the most commonly used mixin since it provides default
132
+ behavior for models which matches Rails' resourceful routing.
133
+
134
+ ```ruby
135
+ class Api::MoviesController < ApiController
136
+ include RESTFramework::ModelControllerMixin
137
+ end
138
+ ```
139
+
140
+ By default, all columns and associations are included in the controller's `fields`, which can be
141
+ helpful when developing an administrative API. For most APIs, however, `fields` should be explicitly
142
+ defined. See [Specifying the Fields](#specifying-the-fields) for more information.
143
+
144
+ ### Defining the Model
145
+
146
+ The `model` property allows you to define the model if it is not derivable from the controller name.
147
+
148
+ ```ruby
149
+ class Api::CoolMoviesController < ApiController
150
+ include RESTFramework::ModelControllerMixin
151
+
152
+ self.model = Movie
153
+ end
154
+ ```
155
+
156
+ ### Specifying the Recordset
157
+
158
+ The `recordset` property allows you to define the set of records this API should be limited to. If
159
+ you need to change the recordset based on properties of the request, then you can override the
160
+ `get_recordset` method.
161
+
162
+ ```ruby
163
+ class Api::CoolMoviesController < ApiController
164
+ include RESTFramework::ModelControllerMixin
165
+
166
+ self.recordset = Movie.where(cool: true).order({id: :asc})
167
+ end
168
+ ```
169
+
170
+ If you override `get_recordset`, you should ensure you also set the `model` property if it's not
171
+ derivable from the controller name.
172
+
173
+ ```ruby
174
+ class Api::CoolMoviesController < ApiController
175
+ include RESTFramework::ModelControllerMixin
176
+
177
+ self.model = Movie
178
+
179
+ def get_recordset
180
+ return Movie.where(cool: true).order({id: :asc})
181
+ end
182
+ end
183
+ ```
184
+
185
+ ### Specifying Extra Member Actions
186
+
187
+ While `extra_actions` (and the synonym `extra_collection_actions`) allows you to define additional
188
+ actions on the controller, `extra_member_actions` allows you to define additional member actions on
189
+ the controller, which require a record ID to be provided as a path param.
190
+
191
+ ```ruby
192
+ class Api::MoviesController < ApiController
193
+ include RESTFramework::ModelControllerMixin
194
+
195
+ self.extra_member_actions = {disable: :patch}
196
+
197
+ def disable
198
+ # REST Framework will rescue ActiveRecord::RecordNotFound.
199
+ @record = self.get_record
200
+
201
+ # REST Framework will rescue ActiveRecord::RecordInvalid or ActiveRecord::RecordNotSaved.
202
+ @record.update!(enabled: false)
203
+
204
+ return api_response(@record)
205
+ end
206
+ end
207
+ ```
208
+
209
+ ### Specifying the Fields
210
+
211
+ The `fields` property defines the fields available for things like serialization and allowed
212
+ parameters in body or query params. If `fields` is not set, then it will default to all columns and
213
+ associations, which is helpful when developing an administrative API. For most APIs, however,
214
+ `fields` should be explicitly defined.
215
+
216
+ While you can also define per-request fields by overriding `get_fields`, you should also define a
217
+ set of fields on the controller which is used for things like the `OPTIONS` metadata.
218
+
219
+ ```ruby
220
+ class Api::MoviesController < ApiController
221
+ include RESTFramework::ModelControllerMixin
222
+
223
+ self.fields = [:id, :name]
224
+ end
225
+ ```
226
+
227
+ You can also mutate the default set of fields by removing existing columns/associations, and even
228
+ adding methods. The framework will automatically detect if the given field is a column, association,
229
+ or method, and will handle it appropriately.
230
+
231
+ ```ruby
232
+ class Api::MoviesController < ApiController
233
+ include RESTFramework::ModelControllerMixin
234
+
235
+ # This will include all columns, all associations except `owners`, and the `is_featured` method.
236
+ self.fields = {
237
+ exclude: [:owners],
238
+ include: [:is_featured],
239
+ }
240
+ end
241
+ ```
242
+
243
+ ### Specifying the Serializer Configuration
244
+
245
+ For most cases, the default serializer configuration is sufficient, and can be modified by adjusting
246
+ the `fields` property on the controller. However, there are cases where you may want to define a
247
+ serializer configuation, such as when you want to customize nested associations, or if you want to
248
+ remove certain fields (like methods) when serializing multiple records.
249
+
250
+ The property `native_serializer_config` defines the serializer configuration if you are using the
251
+ default serializer. You can also specify serializers for singular/plural data.
252
+
253
+ ```ruby
254
+ class Api::MoviesController < ApiController
255
+ include RESTFramework::ModelControllerMixin
256
+
257
+ self.native_serializer_config = {
258
+ only: [:id, :name],
259
+ methods: [:active, :some_expensive_computed_property],
260
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
261
+ }
262
+
263
+ # Or you could configure a default and a plural serializer:
264
+ self.native_serializer_config = {
265
+ only: [:id, :name],
266
+ methods: [:active, :some_expensive_computed_property],
267
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
268
+ }
269
+ self.native_serializer_plural_config = {
270
+ only: [:id, :name],
271
+ methods: [:active],
272
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
273
+ }
274
+
275
+ # Or you could configure a default and a singular serializer:
276
+ self.native_serializer_config = {
277
+ only: [:id, :name],
278
+ methods: [:active],
279
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
280
+ }
281
+ self.native_serializer_singular_config = {
282
+ only: [:id, :name],
283
+ methods: [:active, :some_expensive_computed_property],
284
+ include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
285
+ }
286
+ end
287
+ ```
288
+
289
+ ### Specifying Allowed Parameters
290
+
291
+ The `allowed_parameters` property defines the allowed parameters for create/update actions. The
292
+ framework uses strong parameters to execute this filtering.
293
+
294
+ ```ruby
295
+ class Api::MoviesController < ApiController
296
+ include RESTFramework::ModelControllerMixin
297
+
298
+ self.allowed_parameters = [:name]
299
+ end
300
+ ```
301
+
302
+ If you want different allowed parameters for create/update actions, or if you need stronger control
303
+ over what request body parameters get passed to create/update, then you can also override the
304
+ `get_create_params` or `get_update_params` methods.
305
+
306
+ ### Controlling Create Behavior
307
+
308
+ The `create_from_recordset` attribute (`true` by default) is a boolean to control the behavior in
309
+ the `create` action. If it is disabled, records will not be created from the filtered recordset, but
310
+ rather will be created directly from the model interface.
311
+
312
+ For example, if this is your controller:
313
+
314
+ ```ruby
315
+ class Api::CoolMoviesController < ApiController
316
+ include RESTFramework::ModelControllerMixin
317
+
318
+ # Notice that `cool` is read-only with the following two configurations:
319
+ self.fields = [:id, :name]
320
+ self.native_serializer_config = {only: [:id, :name, :cool]}
321
+
322
+ # New records created from this controller will have `cool` set to `true`.
323
+ def get_recordset
324
+ return Movie.where(cool: true)
325
+ end
326
+ end
327
+ ```
328
+
329
+ Then if you hit the `create` action with the payload `{name: "Superman"}`, it will also set `cool`
330
+ to `true` on the new record, because that property is inherited from the recordset.
331
+
332
+ ## ReadOnlyModelControllerMixin
333
+
334
+ `ReadOnlyModelControllerMixin` only enables list/show actions.
335
+
336
+ ```ruby
337
+ class Api::ReadOnlyMoviesController < ApiController
338
+ include RESTFramework::ReadOnlyModelControllerMixin
339
+
340
+ self.model = Movie
341
+ end
342
+ ```
@@ -11,7 +11,7 @@ which can then be converted to JSON or XML.
11
11
 
12
12
  ## NativeSerializer
13
13
 
14
- This serializer uses Rails' native `ActiveModel::Serializers.serializable_hash` method to convert
14
+ This serializer uses Rails' native `ActiveModel::Serialization.serializable_hash` method to convert
15
15
  records/recordsets to Ruby primitives (`Array` and `Hash`).
16
16
 
17
17
  This is the default serializer, you can configure it using the controller class attributes
@@ -8,19 +8,19 @@ slug: filtering-and-ordering
8
8
 
9
9
  While you can control the recordset that the API exposes, sometimes you want the user to control the
10
10
  records they want to see, or the order of those records. Both filtering and ordering are
11
- accomplished through what we call filters. To control the filter backends that a controller uses,
12
- you can either adjust the `filter_backends` controller attribute or you can override the
13
- `get_filter_backends()` method.
11
+ accomplished through a generic mechanism called "filters". To control the filter backends that a
12
+ controller uses, you can either adjust the `filter_backends` controller attribute or you can
13
+ override the `get_filter_backends()` method.
14
14
 
15
- ## `ModelFilter`
15
+ ## ModelQueryFilter
16
16
 
17
17
  This filter provides basic user-controllable filtering of the recordset using query params. For
18
- example, a request to `/api/movies?cool=true` could return movies where `cool` is `true`.
18
+ example, a request to `/api/movies?cool=true` would return movies where `cool` is `true`.
19
19
 
20
- If you include `ModelControllerMixin` into your controller, `ModelFilter` is included in the filter
20
+ If you include `ModelControllerMixin` into your controller, `ModelQueryFilter` is included in the filter
21
21
  backends by default.
22
22
 
23
- ## `ModelOrderingFilter`
23
+ ## ModelOrderingFilter
24
24
 
25
25
  This filter provides basic user-controllable ordering of the recordset using query params. For
26
26
  example, a request to `/api/movies?ordering=name` could order the movies by `name` rather than `id`.
@@ -32,7 +32,7 @@ filter backends by default. You can use `ordering_fields` to controller which fi
32
32
  be ordered by. To adjust the parameter that the user passes, adjust `ordering_query_param`; the
33
33
  default is `"ordering"`.
34
34
 
35
- ## `ModelSearchFilter`
35
+ ## ModelSearchFilter
36
36
 
37
37
  This filter provides basic user-controllable searching of the recordset using the `search` query
38
38
  parameter (adjustable with the `search_query_param`). For example, a request to
@@ -0,0 +1,9 @@
1
+ <!-- AUTOGENERATED -->
2
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
3
+ <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" referrerpolicy="no-referrer" defer="defer"></script>
4
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.min.css" crossorigin="anonymous">
5
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js" integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==" crossorigin="anonymous" referrerpolicy="no-referrer" defer="defer"></script>
6
+ <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" defer="defer"></script>
7
+ <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" defer="defer"></script>
8
+ <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" class="rrf-dark-mode">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-light.min.css" integrity="sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA==" crossorigin="anonymous" class="rrf-light-mode">
@@ -1,29 +1,149 @@
1
1
  <head>
2
2
  <meta charset="utf-8">
3
3
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
4
- <link rel='icon' type='image/x-icon' href='/assets/images/favicon.ico' />
4
+ <link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico" />
5
5
 
6
6
  <title>{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
7
7
 
8
- <!-- Bootstrap -->
9
- <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">
10
- <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>
11
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
8
+ {% include external.html %}
9
+ {% include shared.html %}
12
10
 
13
- <!-- Highlight.js -->
14
- <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" />
15
- <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" />
16
- <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>
11
+ <style>
12
+ /* Header adjustments. */
13
+ h1, h2, h3, h4, h5, h6 {
14
+ width: 100%;
15
+ font-weight: normal;
16
+ margin-top: 1.8rem;
17
+ margin-bottom: 1.2rem;
18
+ }
19
+ h1 a:not(:hover),
20
+ h2 a:not(:hover),
21
+ h3 a:not(:hover),
22
+ h4 a:not(:hover),
23
+ h5 a:not(:hover),
24
+ h6 a:not(:hover) {
25
+ color: #ddd;
26
+ }
27
+ html[data-bs-theme="dark"] h1 a:not(:hover),
28
+ html[data-bs-theme="dark"] h2 a:not(:hover),
29
+ html[data-bs-theme="dark"] h3 a:not(:hover),
30
+ html[data-bs-theme="dark"] h4 a:not(:hover),
31
+ html[data-bs-theme="dark"] h5 a:not(:hover),
32
+ html[data-bs-theme="dark"] h6 a:not(:hover) {
33
+ color: #444;
34
+ }
35
+ h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover {
36
+ text-decoration: none !important;
37
+ }
38
+
39
+ /* Navbar */
40
+ .navbar .navbar-toggler {
41
+ margin: .2em 0;
42
+ padding: .2em .3em;
43
+ border: none;
44
+ }
45
+ .navbar .navbar-toggler .navbar-toggler-icon {
46
+ height: 1.1em;
47
+ width: 1.1em;
48
+ }
49
+ .navbar .navbar-nav .nav-item .nav-link {
50
+ padding: .45em .6em;
51
+ }
52
+ .navbar .navbar-nav .nav-item .nav-link:hover {
53
+ background-color: #262a2f;
54
+ }
55
+ .navbar .dropdown-menu a.dropdown-item {
56
+ font-size: .9em;
57
+ padding: .2em .8em;
58
+ }
59
+
60
+ /* Headers table. */
61
+ .headers-table {
62
+ padding: .5em 1em;
63
+ background-color: #eee;
64
+ border: 1px solid #aaa;
65
+ border-radius: .3em;
66
+ font-size: .9em;
67
+ }
68
+ html[data-bs-theme="dark"] .headers-table {
69
+ background-color: #2b2b2b;
70
+ }
71
+ .headers-table:empty { display: none; }
72
+ .headers-table ul {
73
+ list-style-type: none;
74
+ margin: 0;
75
+ padding-left: 0;
76
+ padding-right: .6em;
77
+ }
78
+ .headers-table ul li { margin: .3em 0; }
79
+ .headers-table ul ul { padding-left: .8em; padding-right: 0; }
80
+ .headers-table > ul > li {
81
+ font-weight: bold;
82
+ }
83
+
84
+ /* Style the github and mode component. */
85
+ #rrfGithubAndModeWrapper {
86
+ height: 2.4em;
87
+ }
88
+ #rrfGithubComponent {
89
+ display: inline-block;
90
+ position: relative;
91
+ top: .6em;
92
+ }
93
+ #rrfModeComponent .dropdown-toggle {
94
+ float: right;
95
+ }
96
+ #rrfModeComponent .dropdown-menu {
97
+ position: absolute;
98
+ right: 0;
99
+ left: auto;
100
+ top: 100%;
101
+ }
102
+ </style>
103
+
104
+ <script>
105
+ document.addEventListener("DOMContentLoaded", () => {
106
+ // Initialize `Highlight.js`.
107
+ hljs.configure({ ignoreUnescapedHTML: true })
108
+ hljs.highlightAll()
109
+
110
+ // Setup the floating table of contents.
111
+ let table = "<ul>"
112
+ let hlevel = 2
113
+ let hprevlevel = 2
114
+ document.querySelectorAll("h2, h3, h4").forEach((header) => {
115
+ hlevel = parseInt(header.tagName[1])
116
+
117
+ if (hlevel > hprevlevel) {
118
+ table += "<ul>"
119
+ } else if (hlevel < hprevlevel) {
120
+ Array(hprevlevel - hlevel)
121
+ .fill(0)
122
+ .forEach(function () {
123
+ table += "</ul>"
124
+ })
125
+ }
126
+ table += `<li><a href="${
127
+ header.querySelectorAll("a")[0].href
128
+ }">${header.childNodes[0].nodeValue.trim()}</a></li>`
129
+ hprevlevel = hlevel
130
+ })
131
+ if (hlevel > hprevlevel) {
132
+ table += "</ul>"
133
+ }
134
+ table += "</ul>"
135
+ if (table != "<ul></ul>") {
136
+ document.getElementById("headersTable").innerHTML = table
137
+ }
138
+ })
139
+ </script>
140
+
141
+ <!-- Extra Highlight.js languages not shared with the library. -->
17
142
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/shell.min.js" integrity="sha512-X2JngetHwVsp0j3n6lo8HGdXQKLpz2hwFfQkG996OfanpFaQJFgjKJlmzsdefWsHTQIwY539tD09JF48kCPMXw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
18
143
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/erb.min.js" integrity="sha512-flbEiCcectGeyRXyuMZW5jlAGIQ1/qrTZS6DsZDTqObM0JG/isYHvUyehOyt14ssmY85gZRYra+IJR9+azRuqw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
19
144
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/ruby.min.js" integrity="sha512-xRUQANk9Iw3wtAp0cBOa1Ghr7yIFrMiJiEujrMGf04qOau23exxj4R7DLUeLGfLiDbVSK0FyN8v2ns4m/6iNmQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
20
- <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>
21
- <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>
22
-
23
- <link rel="stylesheet" href="{{ "/assets/css/rest_framework.css" }}">
24
- <script src="{{ "/assets/js/rest_framework.js" }}"></script>
25
145
 
26
- <!-- Global site tag (gtag.js) - Google Analytics -->
146
+ <!-- Google Analytics: Global Site Tag -->
27
147
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-P2KRPNXQMT"></script>
28
148
  <script>
29
149
  window.dataLayer = window.dataLayer || [];