Django Rest Framework
Note
To include the Django REST framework JSON:API
(djangorestframework-jsonapi
).
https://django-rest-framework-json-api.readthedocs.io/
Standards
Put the API views (ViewSet
etc) in an api.py
file
e.g. contact/api.py
.
URLs to include namespace="api"
e.g:
url(
regex=r"^api/0.1/", view=include((router.urls, "api"), namespace="api")
),
Usage
Requirements:
# requirements/base.txt
djangorestframework
Tip
Find the version number in Requirements
Tip
For the JSON API, see JSON API
In example/base.py
for an app, settings/base.py
for a project:
THIRD_PARTY_APPS = (
'rest_framework',
# http://www.django-rest-framework.org/api-guide/authentication#tokenauthentication
'rest_framework.authtoken',
# http://www.django-rest-framework.org/api-guide/authentication#tokenauthentication
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': True,
# not sure if this is required or not
# 'DATETIME_FORMAT': '%Y%m%dT%H%M%SZ',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAdminUser',
),
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
Add the following to urls.py
(perhaps in your project
folder):
from rest_framework.authtoken import views
url(regex=r'^token/$',
view=views.obtain_auth_token,
name='api.token.auth',
),
Note
You can change the regex
to another URL if you want…
Create a token for each of the users who will use the API:
from rest_framework.authtoken.models import Token
Token.objects.create(user=...)
Tip
To auto-generate a token for every user, check out TokenAuthentication
JSON API
Standard settings for the JSON API:
REST_FRAMEWORK = {
"PAGE_SIZE": 20,
"EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler",
"DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination",
"DEFAULT_PARSER_CLASSES": (
"rest_framework_json_api.parsers.JSONParser",
"rest_framework.parsers.FormParser",
"rest_framework.parsers.MultiPartParser",
),
"DEFAULT_RENDERER_CLASSES": (
"rest_framework_json_api.renderers.JSONRenderer",
"rest_framework_json_api.renderers.BrowsableAPIRenderer",
),
"DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata",
"DEFAULT_SCHEMA_CLASS": "rest_framework_json_api.schemas.openapi.AutoSchema",
"DEFAULT_FILTER_BACKENDS": (
"rest_framework_json_api.filters.QueryParameterValidationFilter",
"rest_framework_json_api.filters.OrderingFilter",
"rest_framework_json_api.django_filters.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
),
"SEARCH_PARAM": "filter[search]",
"TEST_REQUEST_RENDERER_CLASSES": (
"rest_framework_json_api.renderers.JSONRenderer",
),
"TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json",
}
JSON_API_FORMAT_FIELD_NAMES = "dasherize"
Ordering / Sort
Add ordering_fields
:
from api.api_utils import SoftDeleteViewSet
class SkillViewSet(SoftDeleteViewSet):
ordering_fields = ["name", "location__name"]
Sort using the sort
parameter e.g:
parameters = {"filter[parent]": parent.pk, "sort": sort}
url = url_with_querystring(reverse("api:skill-list"), **parameters)
Our standard settings for the JSON API, include the OrderingFilter
The OrderingFilter documentation for the REST Framework uses the
ordering
parameter, but the JSON API seems to usesort
(which is good)!
Pagination
To switch off pagination, add pagination_class = None
to the viewset.
resource_name
To change the resource_name
if the model name does not match what you
want to return e.g. we have a Category
model which can return a
Location
or Department
:
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Category
resource_name = "Location"
fields = "name"
Tip
Note the resource_name
on this serializer.
To use the LocationSerializer
, add included_serializers
to your
serializer (where location
matches one of your fields
):
included_serializers = {"location": LocationSerializer}
MethodNotAllowed
To prevent use of a method, add the following to your
viewsets.ModelViewSet
:
from rest_framework.exceptions import MethodNotAllowed
def create(self, request, *args, **kwargs):
raise MethodNotAllowed(
"POST", detail="Method 'POST' not allowed"
)
def perform_destroy(self, instance):
raise MethodNotAllowed(
"GET", detail="Method 'GET' not allowed"
)
To test:
from http import HTTPStatus
assert HTTPStatus.METHOD_NOT_ALLOWED == response.status_code
error_detail = response.data["detail"]
# or
# error_detail = response.data["errors"]
assert "Method 'POST' not allowed" == str(error_detail)
# or
# assert "Method "GET" not allowed" == str(error_detail)
Testing
JSON API - File Upload
Add the MultiPartRenderer
to settings:
# settings/base.py
REST_FRAMEWORK = {
"TEST_REQUEST_RENDERER_CLASSES": (
"rest_framework_json_api.renderers.JSONRenderer",
"rest_framework.renderers.MultiPartRenderer",
),
}
Add multipart
to the post
in the test code:
file_name = Path(
settings.BASE_DIR, settings.MEDIA_ROOT, "data", "1-2-3.doc"
)
with open(file_name, "rb") as f:
data = {"file": f}
response = api_client_auth(user).post(url, data, format="multipart")
Tip
The MultiPartRenderer
is used for post
requests which use the
multipart
parameter .
Sample
Test code using the api_client
fixture from our api app:
import pytest
from django.urls import reverse
from http import HTTPStatus
from api.tests.fixture import api_client
from login.tests.factories import UserFactory
@pytest.mark.django_db
def test_something(api_client):
response = api_client.get(reverse('docrecord.api.document'))
assert HTTPStatus.OK == response.status_code, response.data
URL
For this router:
router.register(r"tasks", ExampleWorkTaskViewSet, basename="task")
We can test as follows:
# create returns 'HTTPStatus.CREATED'
.post(reverse("api:task-list"))
# get (retrieve)
.get(reverse("api:task-detail", args=[str(uuid.uuid4())]))
# update for JSON - returns 'HTTPStatus.OK'
.patch(reverse("api:task-detail", args=[str(uuid.uuid4())]), data)
# update for REST API
.put(reverse("api:task-detail", args=[str(uuid.uuid4())]), data)
# delete - returns 'HTTPStatus.NO_CONTENT'
.delete(reverse("api:task-detail", args=[str(uuid.uuid4())]))
# list
.get(reverse("api:task-list"))