Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/barseghyanartur/django-rest-framework-tricks

Collection of various tricks for Django REST framework.
https://github.com/barseghyanartur/django-rest-framework-tricks

django django-rest-framework django-rest-framework-addon django-rest-framework-file-field django-rest-framework-filters djangorestframework nested-serializers

Last synced: 17 days ago
JSON representation

Collection of various tricks for Django REST framework.

Awesome Lists containing this project

README

        

============================
django-rest-framework-tricks
============================
Collection of various tricks for
`Django REST framework `_.

.. image:: https://img.shields.io/pypi/v/django-rest-framework-tricks.svg
:target: https://pypi.python.org/pypi/django-rest-framework-tricks
:alt: PyPI Version

.. image:: https://img.shields.io/pypi/pyversions/django-rest-framework-tricks.svg
:target: https://pypi.python.org/pypi/django-rest-framework-tricks/
:alt: Supported Python versions

.. image:: https://img.shields.io/pypi/djversions/django-rest-framework-tricks.svg
:target: https://pypi.python.org/pypi/django-rest-framework-tricks/
:alt: Supported Django versions

.. image:: https://github.com/barseghyanartur/django-rest-framework-tricks/workflows/test/badge.svg
:target: https://github.com/barseghyanartur/django-rest-framework-tricks/actions
:alt: Build Status

.. image:: https://readthedocs.org/projects/django-rest-framework-tricks/badge/?version=latest
:target: http://django-rest-framework-tricks.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status

.. image:: https://img.shields.io/badge/license-GPL--2.0--only%20OR%20LGPL--2.1--or--later-blue.svg
:target: https://github.com/barseghyanartur/django-rest-framework-tricks/#License
:alt: GPL-2.0-only OR LGPL-2.1-or-later

.. image:: https://coveralls.io/repos/github/barseghyanartur/django-rest-framework-tricks/badge.svg?branch=master
:target: https://coveralls.io/github/barseghyanartur/django-rest-framework-tricks?branch=master
:alt: Coverage

Prerequisites
=============

- 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.

Dependencies
============

- 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.

Installation
============

(1) Install latest stable version from PyPI:

.. code-block:: sh

pip install django-rest-framework-tricks

or latest development version from GitHub:

.. code-block:: sh

pip install https://github.com/barseghyanartur/django-rest-framework-tricks/archive/master.tar.gz

(2) Add ``rest_framework`` and ``rest_framework_tricks`` to ``INSTALLED_APPS``:

.. code-block:: python

INSTALLED_APPS = (
# ...
# REST framework
'rest_framework',

# REST framework tricks (this package)
'rest_framework_tricks',

# ...
)

Documentation
=============

Documentation is available on `Read the Docs
`_.

Main features and highlights
============================

- `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).

Usage examples
==============

Nested serializers
------------------

Nested serializers for non-relational fields.

Our imaginary ``Book`` model consists of the following (non-relational) Django
model fields:

- ``title``: ``CharField``
- ``description``: ``TextField``
- ``summary``: ``TextField``
- ``publication_date``: ``DateTimeField``
- ``state``: ``CharField`` (with choices)
- ``isbn``: ``CharField``
- ``price``: ``DecimalField``
- ``pages``: ``IntegerField``
- ``stock_count``: ``IntegerField``

In our REST API, we want to split the Book serializer into parts using nested
serializers to have the following structure:

.. code-block:: javascript

{
"id": "",
"title": "",
"description": "",
"summary": "",
"publishing_information": {
"publication_date": "",
"isbn": "",
"pages": ""
},
"stock_information": {
"stock_count": "",
"price": "",
"state": ""
}
}

Sample model
~~~~~~~~~~~~

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

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from django.db import models

from rest_framework_tricks.models.fields import NestedProxyField

Model definition
^^^^^^^^^^^^^^^^

.. code-block:: python

BOOK_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.title

Sample serializers
~~~~~~~~~~~~~~~~~~

At 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.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from rest_framework import serializers
from rest_framework_tricks.serializers import (
HyperlinkedModelSerializer,
)

from .models import Book

Defining the serializers
^^^^^^^^^^^^^^^^^^^^^^^^

.. note::

If you get validation errors about null-values, add ``allow_null=True``
next to the ``required=False`` for serializer field definitions.

**Nested serializer**

.. code-block:: python

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 = True

**Nested serializer**

.. code-block:: python

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 = True

**Main serializer to be used in the ViewSet**

.. code-block:: python

# 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',
)

Sample ViewSet
~~~~~~~~~~~~~~

Absolutely no variations from standard implementation here.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny

from .models import Book
from .serializers import BookSerializer

ViewSet definition
^^^^^^^^^^^^^^^^^^

.. code-block:: python

class BookViewSet(ModelViewSet):
"""Book ViewSet."""

queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [AllowAny]

Sample OPTIONS call
^^^^^^^^^^^^^^^^^^^

.. code-block:: text

OPTIONS /books/api/books/
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

.. code-block:: javascript

{
"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
~~~~~~~~~~~~~~~~~~~~~~~

Unlimited nesting depth is supported.

Our imaginary ``Author`` model could consist of the following (non-relational)
Django model fields:

- ``salutation``: ``CharField``
- ``name``: ``CharField``
- ``email``: ``EmailField``
- ``birth_date``: ``DateField``
- ``biography``: ``TextField``
- ``phone_number``: ``CharField``
- ``website``: ``URLField``
- ``company``: ``CharField``
- ``company_phone_number``: ``CharField``
- ``company_email``: ``EmailField``
- ``company_website``: ``URLField``

In our REST API, we could split the Author serializer into parts using
nested serializers to have the following structure:

.. code-block:: javascript

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

.. code-block:: python

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.

Ordering filter
---------------
Developer friendly names for ordering options (for instance, for related field
names) for making better APIs.

Sample model
~~~~~~~~~~~~

Absolutely no variations from standard implementation here.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from django.db import models

Model definition
^^^^^^^^^^^^^^^^

.. code-block:: python

class Profile(models.Model):
"""Profile."""

user = models.ForeignKey('auth.User')
biography = models.TextField()
hobbies = models.TextField()

Sample serializer
~~~~~~~~~~~~~~~~~

Absolutely no variations from standard implementation here.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from rest_framework import serializers

from .models import Profile

Defining the serializers
^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python

class 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()

Sample ViewSet
~~~~~~~~~~~~~~

The only variation from standard implementation here is that we
use ``rest_frameworks_tricks.filters.OrderingFilter`` instead
of ``rest_framework.filters.OrderingFilter``.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

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 ProfileSerializer

ViewSet definition
^^^^^^^^^^^^^^^^^^

.. code-block:: python

class 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',)

Sample GET calls
^^^^^^^^^^^^^^^^

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.

.. code-block:: text

GET /api/profile/?ordering=email
GET /api/profile/?ordering=-username
GET /api/profile/?ordering=full_name
GET /api/profile/?ordering=-full_name

File field with restrictions
----------------------------

Sample model
~~~~~~~~~~~~

Absolutely no variations from standard implementation here.

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from django.db import models

Model definition
^^^^^^^^^^^^^^^^

.. code-block:: python

class Profile(models.Model):
"""Upload."""

username = models.CharField(max_length=255)
resume = models.FileField()

Sample serializer
~~~~~~~~~~~~~~~~~

Required imports
^^^^^^^^^^^^^^^^

.. code-block:: python

from rest_framework import serializers
from rest_framework_tricks.fields import ConstrainedFileField

from .models import Upload

Defining the serializers
^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python

class 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',
)

Demo
====
Run demo locally
----------------
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:

.. code-block:: sh

wget -O - https://raw.github.com/barseghyanartur/django-rest-framework-tricks/master/examples/rest_framework_tricks_demo_installer.sh | bash

Open your browser and test the app.

.. code-block:: text

http://127.0.0.1:8001/books/api/

Testing
=======

Project is covered with tests.

To test with all supported Python/Django versions type:

.. code-block:: sh

tox

To test against specific environment, type:

.. code-block:: sh

tox -e py39-django32

To test just your working environment type:

.. code-block:: sh

pytest -vvv

To run a single test in your working environment type:

.. code-block:: sh

pytest -vvv src/rest_framework_tricks/tests/test_nested_proxy_field.py

.. code-block:: sh

pip install -r examples/requirements/test.txt

Writing documentation
=====================

Keep the following hierarchy.

.. code-block:: text

=====
title
=====

header
======

sub-header
----------

sub-sub-header
~~~~~~~~~~~~~~

sub-sub-sub-header
^^^^^^^^^^^^^^^^^^

sub-sub-sub-sub-header
++++++++++++++++++++++

sub-sub-sub-sub-sub-header
**************************

License
=======

GPL-2.0-only OR LGPL-2.1-or-later

Support
=======

For any security issues contact me at the e-mail given in the `Author`_ section.

For overall issues, go to `GitHub `_.

Author
======

Artur Barseghyan