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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/rest_framework/_external.html.erb +13 -0
- data/app/views/rest_framework/_head.html.erb +2 -192
- data/{docs/assets/js/rest_framework.js → app/views/rest_framework/_shared.html} +69 -42
- data/docs/Gemfile +1 -0
- data/docs/Gemfile.lock +14 -14
- data/docs/_config.yml +4 -2
- data/docs/_guide/2_controllers.md +342 -0
- data/docs/_guide/3_serializers.md +1 -1
- data/docs/_guide/4_filtering_and_ordering.md +8 -8
- data/docs/_includes/external.html +9 -0
- data/docs/_includes/head.html +135 -15
- data/docs/_includes/shared.html +164 -0
- data/lib/rest_framework/controller_mixins/base.rb +23 -36
- data/lib/rest_framework/controller_mixins/models.rb +86 -75
- data/lib/rest_framework/controller_mixins.rb +1 -0
- data/lib/rest_framework/engine.rb +9 -0
- data/lib/rest_framework/filters/base.rb +9 -0
- data/lib/rest_framework/filters/model_ordering.rb +48 -0
- data/lib/rest_framework/filters/model_query.rb +51 -0
- data/lib/rest_framework/filters/model_search.rb +41 -0
- data/lib/rest_framework/filters/ransack.rb +25 -0
- data/lib/rest_framework/filters.rb +6 -150
- data/lib/rest_framework/paginators.rb +7 -11
- data/lib/rest_framework/serializers.rb +10 -10
- data/lib/rest_framework/utils.rb +15 -7
- data/lib/rest_framework.rb +93 -4
- data/vendor/assets/javascripts/rest_framework/bootstrap.js +7 -0
- data/vendor/assets/javascripts/rest_framework/highlight-json.js +7 -0
- data/vendor/assets/javascripts/rest_framework/highlight-xml.js +29 -0
- data/vendor/assets/javascripts/rest_framework/highlight.js +1202 -0
- data/vendor/assets/javascripts/rest_framework/neatjson.js +8 -0
- data/vendor/assets/javascripts/rest_framework/trix.js +6 -0
- data/vendor/assets/stylesheets/rest_framework/bootstrap-icons.css +13 -0
- data/vendor/assets/stylesheets/rest_framework/bootstrap.css +6 -0
- data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.css +7 -0
- data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.css +7 -0
- data/vendor/assets/stylesheets/rest_framework/trix.css +410 -0
- metadata +23 -5
- data/docs/_guide/2_controller_mixins.md +0 -293
- 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::
|
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
|
12
|
-
you can either adjust the `filter_backends` controller attribute or you can
|
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
|
-
##
|
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`
|
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, `
|
20
|
+
If you include `ModelControllerMixin` into your controller, `ModelQueryFilter` is included in the filter
|
21
21
|
backends by default.
|
22
22
|
|
23
|
-
##
|
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
|
-
##
|
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">
|
data/docs/_includes/head.html
CHANGED
@@ -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=
|
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
|
-
|
9
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
<!--
|
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 || [];
|