Collection of various tricks for Django REST framework.
- Django 2.2, 3.0, 3.1, 3.2, 4.0 and 4.1.
- Python 3.7, 3.8, 3.9, 3.10 and 3.11.
- djangorestframework: Initially written with 3.6.3, but nowadays tested with >=3.10,<3.14. May (still) work on earlier- or (even) support later- versions, although not guaranteed.
Install latest stable version from PyPI:
pip install django-rest-framework-tricks
or latest development version from GitHub:
pip install https://github.com/barseghyanartur/django-rest-framework-tricks/archive/master.tar.gz
Add
rest_frameworkandrest_framework_trickstoINSTALLED_APPS:INSTALLED_APPS = ( # ... # REST framework 'rest_framework', # REST framework tricks (this package) 'rest_framework_tricks', # ... )
Documentation is available on Read the Docs.
- Nested serializers: Nested (writable) serializers for non-relational fields.
- Ordering filter: Developer friendly names for ordering options (for instance, for related field names).
- File field with restrictions: Restrict the file field (in size).
Nested serializers for non-relational fields.
Our imaginary Book model consists of the following (non-relational) Django
model fields:
title:CharFielddescription:TextFieldsummary:TextFieldpublication_date:DateTimeFieldstate:CharField(with choices)isbn:CharFieldprice:DecimalFieldpages:IntegerFieldstock_count:IntegerField
In our REST API, we want to split the Book serializer into parts using nested serializers to have the following structure:
{
"id": "",
"title": "",
"description": "",
"summary": "",
"publishing_information": {
"publication_date": "",
"isbn": "",
"pages": ""
},
"stock_information": {
"stock_count": "",
"price": "",
"state": ""
}
}The only variation from standard implementation here is that we declare two
NestedProxyField fields on the Book model level for to be used in
BookSerializer serializer.
Note, that the change does not cause model change (no migrations or whatsoever).
from django.db import models
from rest_framework_tricks.models.fields import NestedProxyFieldBOOK_PUBLISHING_STATUS_PUBLISHED = 'published'
BOOK_PUBLISHING_STATUS_NOT_PUBLISHED = 'not_published'
BOOK_PUBLISHING_STATUS_IN_PROGRESS = 'in_progress'
BOOK_PUBLISHING_STATUS_CHOICES = (
(BOOK_PUBLISHING_STATUS_PUBLISHED, "Published"),
(BOOK_PUBLISHING_STATUS_NOT_PUBLISHED, "Not published"),
(BOOK_PUBLISHING_STATUS_IN_PROGRESS, "In progress"),
)
BOOK_PUBLISHING_STATUS_DEFAULT = BOOK_PUBLISHING_STATUS_PUBLISHED
class Book(models.Model):
"""Book."""
title = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
summary = models.TextField(null=True, blank=True)
publication_date = models.DateField()
state = models.CharField(max_length=100,
choices=BOOK_PUBLISHING_STATUS_CHOICES,
default=BOOK_PUBLISHING_STATUS_DEFAULT)
isbn = models.CharField(max_length=100, unique=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
pages = models.PositiveIntegerField(default=200)
stock_count = models.PositiveIntegerField(default=30)
# List the fields for `PublishingInformationSerializer` nested
# serializer. This does not cause a model change.
publishing_information = NestedProxyField(
'publication_date',
'isbn',
'pages',
)
# List the fields for `StockInformationSerializer` nested serializer.
# This does not cause a model change.
stock_information = NestedProxyField(
'stock_count',
'price',
'state',
)
class Meta:
"""Meta options."""
ordering = ["isbn"]
def __str__(self):
return self.titleAt first, we add nested_proxy_field property to the Meta class
definitions of PublishingInformationSerializer and
StockInformationSerializer nested serializers.
Then we define our (main) BookSerializer class, which is going to be
used as a serializer_class of the BookViewSet. We inherit the
BookSerializer from
rest_framework_tricks.serializers.HyperlinkedModelSerializer
instead of the one of the Django REST framework. There's also a
rest_framework_tricks.serializers.ModelSerializer available.
from rest_framework import serializers
from rest_framework_tricks.serializers import (
HyperlinkedModelSerializer,
)
from .models import BookNote
If you get validation errors about null-values, add allow_null=True
next to the required=False for serializer field definitions.
Nested serializer
class PublishingInformationSerializer(serializers.ModelSerializer):
"""Publishing information serializer."""
publication_date = serializers.DateField(required=False)
isbn = serializers.CharField(required=False)
pages = serializers.IntegerField(required=False)
class Meta:
"""Meta options."""
model = Book
fields = (
'publication_date',
'isbn',
'pages',
)
# Note, that this should be set to True to identify that
# this serializer is going to be used as `NestedProxyField`.
nested_proxy_field = TrueNested serializer
class StockInformationSerializer(serializers.ModelSerializer):
"""Stock information serializer."""
class Meta:
"""Meta options."""
model = Book
fields = (
'stock_count',
'price',
'state',
)
# Note, that this should be set to True to identify that
# this serializer is going to be used as `NestedProxyField`.
nested_proxy_field = TrueMain serializer to be used in the ViewSet
# Note, that we are importing the ``HyperlinkedModelSerializer`` from
# the `rest_framework_tricks.serializers`. Names of the serializers
# should match the names of model properties set with ``NestedProxyField``
# fields.
class BookSerializer(HyperlinkedModelSerializer):
"""Book serializer."""
publishing_information = PublishingInformationSerializer(required=False)
stock_information = StockInformationSerializer(required=False)
class Meta:
"""Meta options."""
model = Book
fields = (
'url',
'id',
'title',
'description',
'summary',
'publishing_information',
'stock_information',
)Absolutely no variations from standard implementation here.
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from .models import Book
from .serializers import BookSerializerclass BookViewSet(ModelViewSet):
"""Book ViewSet."""
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [AllowAny]OPTIONS /books/api/books/
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"name": "Book List",
"description": "Book ViewSet.",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"title": {
"type": "string",
"required": true,
"read_only": false,
"label": "Title",
"max_length": 100
},
"description": {
"type": "string",
"required": false,
"read_only": false,
"label": "Description"
},
"summary": {
"type": "string",
"required": false,
"read_only": false,
"label": "Summary"
},
"publishing_information": {
"type": "nested object",
"required": false,
"read_only": false,
"label": "Publishing information",
"children": {
"publication_date": {
"type": "date",
"required": false,
"read_only": false,
"label": "Publication date"
},
"isbn": {
"type": "string",
"required": false,
"read_only": false,
"label": "Isbn"
},
"pages": {
"type": "integer",
"required": false,
"read_only": false,
"label": "Pages"
}
}
},
"stock_information": {
"type": "nested object",
"required": false,
"read_only": false,
"label": "Stock information",
"children": {
"stock_count": {
"type": "integer",
"required": false,
"read_only": false,
"label": "Stock count"
},
"price": {
"type": "decimal",
"required": true,
"read_only": false,
"label": "Price"
},
"state": {
"type": "choice",
"required": false,
"read_only": false,
"label": "State",
"choices": [
{
"value": "published",
"display_name": "Published"
},
{
"value": "not_published",
"display_name": "Not published"
},
{
"value": "in_progress",
"display_name": "In progress"
}
]
}
}
}
}
}
}Unlimited nesting depth is supported.
Our imaginary Author model could consist of the following (non-relational)
Django model fields:
salutation:CharFieldname:CharFieldemail:EmailFieldbirth_date:DateFieldbiography:TextFieldphone_number:CharFieldwebsite:URLFieldcompany:CharFieldcompany_phone_number:CharFieldcompany_email:EmailFieldcompany_website:URLField
In our REST API, we could split the Author serializer into parts using nested serializers to have the following structure:
{
"id": "",
"salutation": "",
"name": "",
"birth_date": "",
"biography": "",
"contact_information": {
"personal_contact_information": {
"email": "",
"phone_number": "",
"website": ""
},
"business_contact_information": {
"company": "",
"company_email": "",
"company_phone_number": "",
"company_website": ""
}
}
}Our model would have to be defined as follows (see Advanced usage examples
for complete model definition):
class Author(models.Model):
"""Author."""
# ...
# List the fields for `PersonalContactInformationSerializer` nested
# serializer. This does not cause a model change.
personal_contact_information = NestedProxyField(
'email',
'phone_number',
'website',
)
# List the fields for `BusinessContactInformationSerializer` nested
# serializer. This does not cause a model change.
business_contact_information = NestedProxyField(
'company',
'company_email',
'company_phone_number',
'company_website',
)
# List the fields for `ContactInformationSerializer` nested
# serializer. This does not cause a model change.
contact_information = NestedProxyField(
'personal_contact_information',
'business_contact_information',
)
# ...See the Advanced usage examples for complete example.
Developer friendly names for ordering options (for instance, for related field names) for making better APIs.
Absolutely no variations from standard implementation here.
from django.db import modelsclass Profile(models.Model):
"""Profile."""
user = models.ForeignKey('auth.User')
biography = models.TextField()
hobbies = models.TextField()Absolutely no variations from standard implementation here.
from rest_framework import serializers
from .models import Profileclass ProfileSerializer(serializers.ModelSerializer):
"""Profile serializer."""
username = serializers.CharField(source='user.username', read_only=True)
full_name = serializers.SerializerMethodField()
email = serializers.CharField(source='user.email', read_only=True)
class Meta(object):
model = Profile
fields = (
'id',
'username',
'full_name',
'email',
'biography',
'hobbies',
)
def get_full_name(self, obj):
return obj.user.get_full_name()The only variation from standard implementation here is that we
use rest_frameworks_tricks.filters.OrderingFilter instead
of rest_framework.filters.OrderingFilter.
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework_tricks.filters import OrderingFilter
from .models import Profile
from .serializers import ProfileSerializerclass ProfileViewSet(ModelViewSet):
"""Profile ViewSet."""
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
permission_classes = [AllowAny]
filter_backends = (OrderingFilter,)
ordering_fields = {
'id': 'id',
'username': 'user__username',
'email': 'user__email',
'full_name': ['user__first_name', 'user__last_name']
}
ordering = ('id',)Note, that our ordering options are now equal to the field names in the serializer (JSON response). API becomes easier to use/understand that way.
GET /api/profile/?ordering=email
GET /api/profile/?ordering=-username
GET /api/profile/?ordering=full_name
GET /api/profile/?ordering=-full_name
Absolutely no variations from standard implementation here.
from django.db import modelsclass Profile(models.Model):
"""Upload."""
username = models.CharField(max_length=255)
resume = models.FileField()from rest_framework import serializers
from rest_framework_tricks.fields import ConstrainedFileField
from .models import Uploadclass ProfileSerializer(serializers.ModelSerializer):
"""Profile serializer."""
username = serializers.CharField()
# Restrict resume to 5Mb
resume = ConstrainedFileField(max_upload_size=5_242_880)
class Meta(object):
model = Profile
fields = (
'id',
'username',
'resume',
)In order to be able to quickly evaluate the django-rest-framework-tricks,
a demo app (with a quick installer) has been created (works on Ubuntu/Debian,
may work on other Linux systems as well, although not guaranteed). Follow the
instructions below to have the demo running within a minute.
Grab and run the latest rest_framework_tricks_demo_installer.sh demo
installer:
wget -O - https://raw.github.com/barseghyanartur/django-rest-framework-tricks/master/examples/rest_framework_tricks_demo_installer.sh | bashOpen your browser and test the app.
http://127.0.0.1:8001/books/api/
Project is covered with tests.
To test with all supported Python/Django versions type:
toxTo test against specific environment, type:
tox -e py39-django32To test just your working environment type:
pytest -vvvTo run a single test in your working environment type:
pytest -vvv src/rest_framework_tricks/tests/test_nested_proxy_field.pypip install -r examples/requirements/test.txtKeep the following hierarchy.
=====
title
=====
header
======
sub-header
----------
sub-sub-header
~~~~~~~~~~~~~~
sub-sub-sub-header
^^^^^^^^^^^^^^^^^^
sub-sub-sub-sub-header
++++++++++++++++++++++
sub-sub-sub-sub-sub-header
**************************
GPL-2.0-only OR LGPL-2.1-or-later
For any security issues contact me at the e-mail given in the Author section.
For overall issues, go to GitHub.
Artur Barseghyan <artur.barseghyan@gmail.com>