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)

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"))