Metadata-Version: 2.1
Name: django-rest-framework-tricks
Version: 0.2.14
Summary: Collection of various tricks for Django REST framework.
Home-page: https://github.com/barseghyanartur/django-rest-framework-tricks/
Author: Artur Barseghyan
Author-email: artur.barseghyan@gmail.com
License: GPL-2.0-only OR LGPL-2.1-or-later
Project-URL: Bug Tracker, https://github.com/barseghyanartur/django-rest-framework-tricks/
Project-URL: Documentation, https://django-rest-framework-tricks.readthedocs.io/
Project-URL: Source Code, https://github.com/barseghyanartur/django-rest-framework-tricks/
Project-URL: Changelog, https://django-rest-framework-tricks.readthedocs.io/en/latest/changelog.html
Keywords: django,django rest framework,tricks
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Environment :: Web Environment
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)
Classifier: Framework :: Django
Classifier: Framework :: Django :: 2.2
Classifier: Framework :: Django :: 3.0
Classifier: Framework :: Django :: 3.1
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.0
Classifier: Framework :: Django :: 4.1
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 4 - Beta
License-File: LICENSE_GPL2.0.txt
License-File: LICENSE_LGPL_2.1.txt

============================
django-rest-framework-tricks
============================
Collection of various tricks for
`Django REST framework <https://pypi.python.org/pypi/djangorestframework>`_.

.. 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
<http://django-rest-framework-tricks.readthedocs.io/>`_.

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
<https://github.com/barseghyanartur/django-rest-framework-tricks/blob/master/ADVANCED_USAGE_EXAMPLES.rst#nested-serializers>`_
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 issues contact me at the e-mail given in the `Author`_ section.

Author
======

Artur Barseghyan <artur.barseghyan@gmail.com>
