Skip to content

Commit 4eef958

Browse files
n2ygksliverc
authored andcommitted
autogenerate API documentation (django-json-api#479)
1 parent 16f3c97 commit 4eef958

18 files changed

+268
-199
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ any parts of the framework not mentioned in the documentation should generally b
2626
### Changed
2727

2828
* Replaced binary `drf_example` sqlite3 db with a [fixture](example/fixtures/drf_example.json). See [getting started](docs/getting-started.md#running-the-example-app).
29+
* Replaced unmaintained [API doc](docs/api.md) with [auto-generated API reference](docs/api.rst).
2930

3031
### Fixed
3132

docs/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
_build
2+
apidoc

docs/api.md

-60
This file was deleted.

docs/api.rst

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
API Reference
2+
=============
3+
4+
This API reference is autogenerated from the Python docstrings -- which need to be improved!
5+
6+
.. toctree::
7+
:maxdepth: 4
8+
9+
apidoc/rest_framework_json_api

docs/conf.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,18 @@
1717
import sys
1818
import os
1919
import shlex
20+
import django
2021

2122
# If extensions (or modules to document with autodoc) are in another directory,
2223
# add these directories to sys.path here. If the directory is relative to the
2324
# documentation root, use os.path.abspath to make it absolute, like shown here.
2425
sys.path.insert(0, os.path.abspath('..'))
26+
os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
27+
django.setup()
28+
29+
# Auto-generate API documentation.
30+
from sphinx.apidoc import main
31+
main(['sphinx-apidoc', '-e', '-T', '-M', '-f', '-o', 'apidoc', '../rest_framework_json_api'])
2532

2633
# -- General configuration ------------------------------------------------
2734

@@ -31,7 +38,9 @@
3138
# Add any Sphinx extension module names here, as strings. They can be
3239
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
3340
# ones.
34-
extensions = []
41+
extensions = ['sphinx.ext.autodoc']
42+
autodoc_member_order = 'bysource'
43+
autodoc_inherit_docstrings = False
3544

3645
# Add any paths that contain templates here, relative to this directory.
3746
templates_path = ['_templates']
@@ -62,9 +71,10 @@
6271
# built documents.
6372
#
6473
# The short X.Y version.
65-
version = '2.0'
74+
from rest_framework_json_api import VERSION
75+
version = VERSION
6676
# The full version, including alpha/beta/rc tags.
67-
release = '2.0.0-alpha.1'
77+
release = VERSION
6878

6979
# The language for content autogenerated by Sphinx. Refer to documentation
7080
# for a list of supported languages.

rest_framework_json_api/django_filters/backends.py

+30-16
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,32 @@ class DjangoFilterBackend(DjangoFilterBackend):
2020
chaining. It also returns a 400 error for invalid filters.
2121
2222
Filters can be:
23-
- A resource field equality test:
24-
`?filter[qty]=123`
25-
- Apply other [field lookup](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups) # noqa: E501
23+
24+
- A resource field
25+
equality test:
26+
27+
``?filter[qty]=123``
28+
29+
- Apply other
30+
https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups
2631
operators:
27-
`?filter[name.icontains]=bar` or `?filter[name.isnull]=true...`
28-
- Membership in a list of values:
29-
`?filter[name.in]=abc,123,zzz (name in ['abc','123','zzz'])`
30-
- Filters can be combined for intersection (AND):
31-
`?filter[qty]=123&filter[name.in]=abc,123,zzz&filter[...]`
32-
- A related resource path can be used:
33-
`?filter[inventory.item.partNum]=123456 (where `inventory.item` is the relationship path)`
32+
33+
``?filter[name.icontains]=bar`` or ``?filter[name.isnull]=true...``
34+
35+
- Membership in
36+
a list of values:
37+
38+
``?filter[name.in]=abc,123,zzz`` (name in ['abc','123','zzz'])
39+
40+
- Filters can be combined
41+
for intersection (AND):
42+
43+
``?filter[qty]=123&filter[name.in]=abc,123,zzz&filter[...]``
44+
45+
- A related resource path
46+
can be used:
47+
48+
``?filter[inventory.item.partNum]=123456`` (where `inventory.item` is the relationship path)
3449
3550
If you are also using rest_framework.filters.SearchFilter you'll want to customize
3651
the name of the query parameter for searching to make sure it doesn't conflict
@@ -65,12 +80,11 @@ def _validate_filter(self, keys, filterset_class):
6580

6681
def get_filterset(self, request, queryset, view):
6782
"""
68-
Sometimes there's no filterset_class defined yet the client still
83+
Sometimes there's no `filterset_class` defined yet the client still
6984
requests a filter. Make sure they see an error too. This means
70-
we have to get_filterset_kwargs() even if there's no filterset_class.
71-
72-
TODO: .base_filters vs. .filters attr (not always present)
85+
we have to `get_filterset_kwargs()` even if there's no `filterset_class`.
7386
"""
87+
# TODO: .base_filters vs. .filters attr (not always present)
7488
filterset_class = self.get_filterset_class(view, queryset)
7589
kwargs = self.get_filterset_kwargs(request, queryset, view)
7690
self._validate_filter(kwargs.pop('filter_keys'), filterset_class)
@@ -112,8 +126,8 @@ def get_filterset_kwargs(self, request, queryset, view):
112126

113127
def filter_queryset(self, request, queryset, view):
114128
"""
115-
Backwards compatibility to 1.1 (required for Python 2.7)
116-
In 1.1 filter_queryset does not call get_filterset or get_filterset_kwargs.
129+
This is backwards compatibility to django-filter 1.1 (required for Python 2.7).
130+
In 1.1 `filter_queryset` does not call `get_filterset` or `get_filterset_kwargs`.
117131
"""
118132
# TODO: remove when Python 2.7 support is deprecated
119133
if VERSION >= (2, 0, 0):

rest_framework_json_api/filters.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import re
2+
3+
from rest_framework.exceptions import ValidationError
4+
from rest_framework.filters import BaseFilterBackend, OrderingFilter
5+
6+
from rest_framework_json_api.utils import format_value
7+
8+
9+
class OrderingFilter(OrderingFilter):
10+
"""
11+
A backend filter that implements http://jsonapi.org/format/#fetching-sorting and
12+
raises a 400 error if any sort field is invalid.
13+
14+
If you prefer *not* to report 400 errors for invalid sort fields, just use
15+
:py:class:`rest_framework.filters.OrderingFilter` with
16+
:py:attr:`~rest_framework.filters.OrderingFilter.ordering_param` = "sort"
17+
18+
Also applies DJA format_value() to convert (e.g. camelcase) to underscore.
19+
(See JSON_API_FORMAT_FIELD_NAMES in docs/usage.md)
20+
"""
21+
#: override :py:attr:`rest_framework.filters.OrderingFilter.ordering_param`
22+
#: with JSON:API-compliant query parameter name.
23+
ordering_param = 'sort'
24+
25+
def remove_invalid_fields(self, queryset, fields, view, request):
26+
"""
27+
Extend :py:meth:`rest_framework.filters.OrderingFilter.remove_invalid_fields` to
28+
validate that all provided sort fields exist (as contrasted with the super's behavior
29+
which is to silently remove invalid fields).
30+
31+
:raises ValidationError: if a sort field is invalid.
32+
"""
33+
valid_fields = [
34+
item[0] for item in self.get_valid_fields(queryset, view,
35+
{'request': request})
36+
]
37+
bad_terms = [
38+
term for term in fields
39+
if format_value(term.replace(".", "__").lstrip('-'), "underscore") not in valid_fields
40+
]
41+
if bad_terms:
42+
raise ValidationError('invalid sort parameter{}: {}'.format(
43+
('s' if len(bad_terms) > 1 else ''), ','.join(bad_terms)))
44+
# this looks like it duplicates code above, but we want the ValidationError to report
45+
# the actual parameter supplied while we want the fields passed to the super() to
46+
# be correctly rewritten.
47+
# The leading `-` has to be stripped to prevent format_value from turning it into `_`.
48+
underscore_fields = []
49+
for item in fields:
50+
item_rewritten = item.replace(".", "__")
51+
if item_rewritten.startswith('-'):
52+
underscore_fields.append(
53+
'-' + format_value(item_rewritten.lstrip('-'), "underscore"))
54+
else:
55+
underscore_fields.append(format_value(item_rewritten, "underscore"))
56+
57+
return super(OrderingFilter, self).remove_invalid_fields(
58+
queryset, underscore_fields, view, request)
59+
60+
61+
class QueryParameterValidationFilter(BaseFilterBackend):
62+
"""
63+
A backend filter that performs strict validation of query parameters for
64+
JSON:API spec conformance and raises a 400 error if non-conforming usage is
65+
found.
66+
67+
If you want to add some additional non-standard query parameters,
68+
override :py:attr:`query_regex` adding the new parameters. Make sure to comply with
69+
the rules at http://jsonapi.org/format/#query-parameters.
70+
"""
71+
#: compiled regex that matches the allowed http://jsonapi.org/format/#query-parameters:
72+
#: `sort` and `include` stand alone; `filter`, `fields`, and `page` have []'s
73+
query_regex = re.compile(r'^(sort|include)$|^(filter|fields|page)(\[[\w\.\-]+\])?$')
74+
75+
def validate_query_params(self, request):
76+
"""
77+
Validate that query params are in the list of valid query keywords in
78+
:py:attr:`query_regex`
79+
80+
:raises ValidationError: if not.
81+
"""
82+
# TODO: For jsonapi error object conformance, must set jsonapi errors "parameter" for
83+
# the ValidationError. This requires extending DRF/DJA Exceptions.
84+
for qp in request.query_params.keys():
85+
if not self.query_regex.match(qp):
86+
raise ValidationError('invalid query parameter: {}'.format(qp))
87+
if len(request.query_params.getlist(qp)) > 1:
88+
raise ValidationError(
89+
'repeated query parameter not allowed: {}'.format(qp))
90+
91+
def filter_queryset(self, request, queryset, view):
92+
"""
93+
Overrides :py:meth:`BaseFilterBackend.filter_queryset` by first validating the
94+
query params with :py:meth:`validate_query_params`
95+
"""
96+
self.validate_query_params(request)
97+
return queryset

rest_framework_json_api/filters/__init__.py

-2
This file was deleted.

rest_framework_json_api/filters/queryvalidation.py

-37
This file was deleted.

rest_framework_json_api/filters/sort.py

-44
This file was deleted.

0 commit comments

Comments
 (0)