DRF-extensions

DRF-extensions is a collection of custom extensions for Django REST Framework. Source repository is available at https://github.com/chibisov/drf-extensions.

Viewsets

Extensions for viewsets.

DetailSerializerMixin

This mixin lets add custom serializer for detail view. Just add mixin and specify serializer_detail_class attribute:

from django.contrib.auth.models import User
from myapps.serializers import UserSerializer, UserDetailSerializer
from rest_framework_extensions.mixins import DetailSerializerMixin

class UserViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
    serializer_class = UserSerializer
    serializer_detail_class = UserDetailSerializer
    queryset = User.objects.all()

Sometimes you need to set custom QuerySet for detail view. For example, in detail view you want to show user groups and permissions for these groups. You can make it by specifying queryset_detail attribute:

from django.contrib.auth.models import User
from myapps.serializers import UserSerializer, UserDetailSerializer
from rest_framework_extensions.mixins import DetailSerializerMixin

class UserViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
    serializer_class = UserSerializer
    serializer_detail_class = UserDetailSerializer
    queryset = User.objects.all()
    queryset_detail = queryset.prefetch_related('groups__permissions')

If you use DetailSerializerMixin and don't specify serializer_detail_class attribute, then serializer_class will be used.

If you use DetailSerializerMixin and don't specify queryset_detail attribute, then queryset will be used.

PaginateByMaxMixin

New in DRF-extensions 0.2.2

This mixin allows to paginate results by max_paginate_by value. This approach is useful when clients want to take as much paginated data as possible, but don't want to bother about backend limitations.

from myapps.serializers import UserSerializer
from rest_framework_extensions.mixins import PaginateByMaxMixin

class UserViewSet(PaginateByMaxMixin,
                  viewsets.ReadOnlyModelViewSet):
    max_paginate_by = 100
    serializer_class = UserSerializer

And now you can send requests with ?page_size=max argument:

# Request
GET /users/?page_size=max HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    count: 1000,
    next: "https://localhost:8000/v1/users/?page=2&page_size=max",
    previous: null,
    results: [
        ...100 items...
    ]
}

This mixin could be used only with Django Rest Framework >= 2.3.8, because max_paginate_by was introduced in 2.3.8 version.

Cache/ETAG mixins

The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.

ReadOnlyCacheResponseAndETAGMixin and CacheResponseAndETAGMixin are no longer available to use.

See discussion in Issue #177

Routers

Extensions for routers.

You will need to use custom ExtendedDefaultRouter or ExtendedSimpleRouter for routing if you want to take advantages of described extensions. For example you have standard implementation:

from rest_framework.routers import DefaultRouter
router = DefaultRouter()

You should replace DefaultRouter with ExtendedDefaultRouter:

from rest_framework_extensions.routers import (
    ExtendedDefaultRouter as DefaultRouter
)
router = DefaultRouter()

Or SimpleRouter with ExtendedSimpleRouter:

from rest_framework_extensions.routers import (
    ExtendedSimpleRouter as SimpleRouter
)
router = SimpleRouter()

Pluggable router mixins

New in DRF-extensions 0.2.4

Every feature in extended routers has it's own mixin. That means that you can use the only features you need in your custom routers. ExtendedRouterMixin has all set of drf-extensions features. For example you can use it with third-party routes:

from rest_framework_extensions.routers import ExtendedRouterMixin
from third_party_app.routers import SomeRouter

class ExtendedSomeRouter(ExtendedRouterMixin, SomeRouter):
    pass

Nested routes

New in DRF-extensions 0.2.4

Nested routes allows you create nested resources with viewsets.

For example:

from rest_framework_extensions.routers import ExtendedSimpleRouter
from yourapp.views import (
    UserViewSet,
    GroupViewSet,
    PermissionViewSet,
)

router = ExtendedSimpleRouter()
(
    router.register(r'users', UserViewSet, basename='user')
          .register(r'groups',
                    GroupViewSet,
                    basename='users-group',
                    parents_query_lookups=['user_groups'])
          .register(r'permissions',
                    PermissionViewSet,
                    basename='users-groups-permission',
                    parents_query_lookups=['group__user', 'group'])
)
urlpatterns = router.urls

There is one requirement for viewsets which used in nested routers. They should add mixin NestedViewSetMixin. That mixin adds automatic filtering by parent lookups:

# yourapp.views
from rest_framework_extensions.mixins import NestedViewSetMixin

class UserViewSet(NestedViewSetMixin, ModelViewSet):
    model = UserModel

class GroupViewSet(NestedViewSetMixin, ModelViewSet):
    model = GroupModel

class PermissionViewSet(NestedViewSetMixin, ModelViewSet):
    model = PermissionModel

With such kind of router we have next resources:

Every resource is automatically filtered by parent lookups.

# Request
GET /users/1/groups/2/permissions/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8


[
  {
    id: 3,
    name: "read"
  },
  {
    id: 4,
    name: "update"
  },
  {
    id: 5,
    name: "delete"
  }
]

For request above permissions will be filtered by user with pk 1 and group with pk 2:

Permission.objects.filter(group__user=1, group=2)

Example with registering more then one nested resource in one depth:

permissions_routes = router.register(
    r'permissions',
    PermissionViewSet,
    basename='permission'
)
permissions_routes.register(
    r'groups',
    GroupViewSet,
    basename='permissions-group',
    parents_query_lookups=['permissions']
)
permissions_routes.register(
    r'users',
    UserViewSet,
    basename='permissions-user',
    parents_query_lookups=['groups__permissions']
)

With such kind of router we have next resources:

Nested router mixin

You can use rest_framework_extensions.routers.NestedRouterMixin for adding nesting feature into your routers:

from rest_framework_extensions.routers import NestedRouterMixin
from rest_framework.routers import SimpleRouter

class SimpleRouterWithNesting(NestedRouterMixin, SimpleRouter):
    pass

Usage with generic relations

If you want to use nested router for generic relation fields, you should explicitly filter QuerySet by content type.

For example if you have such kind of models:

class Task(models.Model):
    title = models.CharField(max_length=30)

class Book(models.Model):
    title = models.CharField(max_length=30)

class Comment(models.Model):
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey()
    text = models.CharField(max_length=30)

Lets create viewsets for that models:

class TaskViewSet(NestedViewSetMixin, ModelViewSet):
    model = TaskModel

class BookViewSet(NestedViewSetMixin, ModelViewSet):
    model = BookModel

class CommentViewSet(NestedViewSetMixin, ModelViewSet):
    queryset = CommentModel.objects.all()

And router like this:

router = ExtendedSimpleRouter()
# tasks route
(
    router.register(r'tasks', TaskViewSet)
          .register(r'comments',
                    CommentViewSet,
                    'tasks-comment',
                    parents_query_lookups=['object_id'])
)
# books route
(
    router.register(r'books', BookViewSet)
          .register(r'comments',
                    CommentViewSet,
                    'books-comment',
                    parents_query_lookups=['object_id'])
)

As you can see we've added to parents_query_lookups only one object_id value. But when you make requests to comments endpoint for both tasks and books routes there is no context for current content type.

# Request
GET /tasks/123/comments/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

[
    {
        id: 1,
        content_type: 1,
        object_id: 123,
        text: "Good task!"
    },
    {
        id: 2,
        content_type: 2,  // oops. Wrong content type (for book)
        object_id: 123,   // task and book has the same id
        text: "Good book!"
    },
]

For such kind of cases you should explicitly filter QuerySets of nested viewsets by content type:

from django.contrib.contenttypes.models import ContentType

class CommentViewSet(NestedViewSetMixin, ModelViewSet):
    queryset = CommentModel.objects.all()

class TaskCommentViewSet(CommentViewSet):
    def get_queryset(self):
        return super(TaskCommentViewSet, self).get_queryset().filter(
            content_type=ContentType.objects.get_for_model(TaskModel)
        )

class BookCommentViewSet(CommentViewSet):
    def get_queryset(self):
        return super(BookCommentViewSet, self).get_queryset().filter(
            content_type=ContentType.objects.get_for_model(BookModel)
        )

Lets use new viewsets in router:

router = ExtendedSimpleRouter()
# tasks route
(
    router.register(r'tasks', TaskViewSet)
          .register(r'comments',
                    TaskCommentViewSet,
                    'tasks-comment',
                    parents_query_lookups=['object_id'])
)
# books route
(
    router.register(r'books', BookViewSet)
          .register(r'comments',
                    BookCommentViewSet,
                    'books-comment',
                    parents_query_lookups=['object_id'])
)

Serializers

Extensions for serializers functionality.

PartialUpdateSerializerMixin

New in DRF-extensions 0.2.3

By default every saving of ModelSerializer saves the whole object. Even partial update just patches model instance. For example:

from myapps.models import City
from myapps.serializers import CitySerializer

moscow = City.objects.get(pk=10)
city_serializer = CitySerializer(
    instance=moscow,
    data={'country': 'USA'},
    partial=True
)
if city_serializer.is_valid():
    city_serializer.save()

# equivalent to
moscow.country = 'USA'
moscow.save()

SQL representation for previous example will be:

UPDATE city SET name='Moscow', country='USA' WHERE id=1;

Django's save method has keyword argument update_fields. Only the fields named in that list will be updated:

moscow.country = 'USA'
moscow.save(update_fields=['country'])

SQL representation for example with update_fields usage will be:

UPDATE city SET country='USA' WHERE id=1;

To use update_fields for every partial update you should mixin PartialUpdateSerializerMixin to your serializer:

from rest_framework_extensions.serializers import (
    PartialUpdateSerializerMixin
)

class CitySerializer(PartialUpdateSerializerMixin,
                     serializers.ModelSerializer):
    class Meta:
        model = City

Fields

Set of serializer fields that extends default fields functionality.

ResourceUriField

Represents a hyperlinking uri that points to the detail view for that object.

from rest_framework_extensions.fields import ResourceUriField

class CitySerializer(serializers.ModelSerializer):
    resource_uri = ResourceUriField(view_name='city-detail')

    class Meta:
        model = City

Request example:

# Request
GET /cities/268/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
  id: 268,
  resource_uri: "http://localhost:8000/v1/cities/268/",
  name: "Serpuhov"
}

Permissions

Extensions for permissions.

Object permissions

New in DRF-extensions 0.2.2

Django Rest Framework allows you to use DjangoObjectPermissions out of the box. But it has one limitation - if user has no permissions for viewing resource he will get 404 as response code. In most cases it's good approach because it solves security issues by default. But what if you wanted to return 401 or 403? What if you wanted to say to user - "You need to be logged in for viewing current resource" or "You don't have permissions for viewing current resource"?

ExtenedDjangoObjectPermissions will help you to be more flexible. By default it behaves as standard DjangoObjectPermissions. For example, it is safe to replace DjangoObjectPermissions with extended permissions class:

from rest_framework_extensions.permissions import (
    ExtendedDjangoObjectPermissions as DjangoObjectPermissions
)

class CommentView(viewsets.ModelViewSet):
    permission_classes = (DjangoObjectPermissions,)

Now every request from unauthorized user will get 404 response:

# Request
GET /comments/1/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 404 NOT FOUND
Content-Type: application/json; charset=UTF-8

{"detail": "Not found"}

With ExtenedDjangoObjectPermissions you can disable hiding forbidden for read objects by changing hide_forbidden_for_read_objects attribute:

from rest_framework_extensions.permissions import (
    ExtendedDjangoObjectPermissions
)

class CommentViewObjectPermissions(ExtendedDjangoObjectPermissions):
    hide_forbidden_for_read_objects = False

class CommentView(viewsets.ModelViewSet):
    permission_classes = (CommentViewObjectPermissions,)

Now lets see request response for user that has no permissions for viewing CommentView object:

# Request
GET /comments/1/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 403 FORBIDDEN
Content-Type: application/json; charset=UTF-8

{u'detail': u'You do not have permission to perform this action.'}

ExtenedDjangoObjectPermissions could be used only with Django Rest Framework >= 2.3.8, because DjangoObjectPermissions was introduced in 2.3.8 version.

Caching

To cache something is to save the result of an expensive calculation so that you don't have to perform the calculation next time. Here's some pseudocode explaining how this would work for a dynamically generated api response:

given a URL, try finding that API response in the cache
if the response is in the cache:
    return the cached response
else:
    generate the response
    save the generated response in the cache (for next time)
    return the generated response

Cache response

DRF-extensions allows you to cache api responses with simple @cache_response decorator. There are two requirements for decorated method:

Usage example:

from rest_framework.response import Response
from rest_framework import views
from rest_framework_extensions.cache.decorators import (
    cache_response
)
from myapp.models import City

class CityView(views.APIView):
    @cache_response()
    def get(self, request, *args, **kwargs):
        cities = City.objects.all().values_list('name', flat=True)
        return Response(cities)

If you request view first time you'll get it from processed SQL query. (~60ms response time):

# Request
GET /cities/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

['Moscow', 'London', 'Paris']

Second request will hit the cache. No sql evaluation, no database query. (~30 ms response time):

# Request
GET /cities/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

['Moscow', 'London', 'Paris']

Reduction in response time depends on calculation complexity inside your API method. Sometimes it reduces from 1 second to 10ms, sometimes you win just 10ms.

New in DRF-extensions 0.4.0

The decorator will render and discard the original DRF response in favor of Django's HttpResponse. This allows the cache to retain a smaller memory footprint and eliminates the need to re-render responses on each request. Furthermore it eliminates the risk for users to unknowingly cache whole Serializers and QuerySets.

You can disable this behavior in your test suite by using dummy caching for the DRF-extensions cache (set via DEFAULT_USE_CACHE).

Timeout

You can specify cache timeout in seconds, providing first argument:

class CityView(views.APIView):
    @cache_response(60 * 15)
    def get(self, request, *args, **kwargs):
        ...

In the above example, the result of the get() view will be cached for 15 minutes.

If you don't specify timeout argument then value from REST_FRAMEWORK_EXTENSIONS settings will be used. By default it's None, which means "cache forever". You can change this default in settings:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15
}

Usage of the specific cache

New in DRF-extensions 0.2.3

@cache_response can also take an optional keyword argument, cache, which directs the decorator to use a specific cache (from your CACHES setting) when caching results. By default, the default cache will be used, but you can specify any cache you want:

class CityView(views.APIView):
    @cache_response(60 * 15, cache='special_cache')
    def get(self, request, *args, **kwargs):
        ...

You can specify what cache to use by default in settings:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_USE_CACHE': 'special_cache'
}

Cache key

By default every cached data from @cache_response decorator stored by key, which calculated with DefaultKeyConstructor.

You can change cache key by providing key_func argument, which must be callable:

def calculate_cache_key(view_instance, view_method,
                        request, args, kwargs):
    return '.'.join([
        len(args),
        len(kwargs)
    ])

class CityView(views.APIView):
    @cache_response(60 * 15, key_func=calculate_cache_key)
    def get(self, request, *args, **kwargs):
        ...

You can implement view method and use it for cache key calculation by specifying key_func argument as string:

class CityView(views.APIView):
    @cache_response(60 * 15, key_func='calculate_cache_key')
    def get(self, request, *args, **kwargs):
        ...

    def calculate_cache_key(self, view_instance, view_method,
                            request, args, kwargs):
        return '.'.join([
            len(args),
            len(kwargs)
        ])

Key calculation function will be called with next parameters:

Default key function

If @cache_response decorator used without key argument then default key function will be used. You can change this function in settings:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_CACHE_KEY_FUNC':
      'rest_framework_extensions.utils.default_cache_key_func'
}

default_cache_key_func uses DefaultKeyConstructor as a base for key calculation.

Caching errors

New in DRF-extensions 0.2.7

By default every response is cached, even failed. For example:

class CityView(views.APIView):
    @cache_response()
    def get(self, request, *args, **kwargs):
        raise Exception("500 error comes from here")

First request to CityView.get will fail with 500 status code error and next requests to this endpoint will return 500 error from cache.

You can change this behaviour by turning off caching error responses:

class CityView(views.APIView):
    @cache_response(cache_errors=False)
    def get(self, request, *args, **kwargs):
        raise Exception("500 error comes from here")

You can change default behaviour by changing DEFAULT_CACHE_ERRORS setting:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_CACHE_ERRORS': False
}

CacheResponseMixin

It is common to cache standard viewset retrieve and list methods. That is why CacheResponseMixin exists. Just mix it into viewset implementation and those methods will use functions, defined in REST_FRAMEWORK_EXTENSIONS settings:

By default those functions are using DefaultKeyConstructor and extends it:

You can change those settings for custom cache key generation:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_OBJECT_CACHE_KEY_FUNC':
      'rest_framework_extensions.utils.default_object_cache_key_func',
    'DEFAULT_LIST_CACHE_KEY_FUNC':
      'rest_framework_extensions.utils.default_list_cache_key_func',
    'DEFAULT_CACHE_RESPONSE_TIMEOUT': None,
}

Mixin example usage:

from myapps.serializers import UserSerializer
from rest_framework_extensions.cache.mixins import CacheResponseMixin

class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer

You can change cache key function by providing object_cache_key_func or list_cache_key_func methods in view class:

class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer

    def object_cache_key_func(self, **kwargs):
        return 'some key for object'

    def list_cache_key_func(self, **kwargs):
        return 'some key for list'

Of course you can use custom key constructor:

from yourapp.key_constructors import (
    CustomObjectKeyConstructor,
    CustomListKeyConstructor,
)

class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer
    object_cache_key_func = CustomObjectKeyConstructor()
    list_cache_key_func = CustomListKeyConstructor()

New in DRF-extensions development

You can change cache timeout by providing object_cache_timeout or list_cache_timeout properties in view class:

class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer
    object_cache_timeout = 3600 # one hours (in seconds) 
    list_cache_timeout = 60 # one minute (in seconds)

If you want to cache only retrieve method then you could use rest_framework_extensions.cache.mixins.RetrieveCacheResponseMixin.

If you want to cache only list method then you could use rest_framework_extensions.cache.mixins.ListCacheResponseMixin.

Key constructors

As you could see from previous section cache key calculation might seem fairly simple operation. But let's see next example. We make ordinary HTTP request to cities resource:

# Request
GET /cities/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

['Moscow', 'London', 'Paris']

By the moment all goes fine - response returned and cached. Let's make the same request requiring XML response:

# Request
GET /cities/ HTTP/1.1
Accept: application/xml

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

['Moscow', 'London', 'Paris']

What is that? Oh, we forgot about format negotiations. We can add format to key bits:

def calculate_cache_key(view_instance, view_method,
                        request, args, kwargs):
    return '.'.join([
        len(args),
        len(kwargs),
        request.accepted_renderer.format  # here it is
    ])

# Request
GET /cities/ HTTP/1.1
Accept: application/xml

# Response
HTTP/1.1 200 OK
Content-Type: application/xml; charset=UTF-8

<?xml version="1.0" encoding="utf-8"?>
<root>
    <list-item>Moscow</list-item>
    <list-item>London</list-item>
    <list-item>Paris</list-item>
</root>

That's cool now - we have different responses for different formats with different cache keys. But there are many cases, where key should be different for different requests:

Of course we can use custom calculate_cache_key methods and reuse them for different API methods, but we can't reuse just parts of them. For example, one method depends on user id and language, but another only on user id. How to be more DRYish? Let's see some magic:

from rest_framework_extensions.key_constructor.constructors import (
    KeyConstructor
)
from rest_framework_extensions.key_constructor import bits
from your_app.utils import get_city_by_ip

class CityGetKeyConstructor(KeyConstructor):
    unique_method_id = bits.UniqueMethodIdKeyBit()
    format = bits.FormatKeyBit()
    language = bits.LanguageKeyBit()

class CityHeadKeyConstructor(CityGetKeyConstructor):
    user = bits.UserKeyBit()
    request_meta = bits.RequestMetaKeyBit(params=['REMOTE_ADDR'])

class CityView(views.APIView):
    @cache_response(key_func=CityGetKeyConstructor())
    def get(self, request, *args, **kwargs):
        cities = City.objects.all().values_list('name', flat=True)
        return Response(cities)

    @cache_response(key_func=CityHeadKeyConstructor())
    def head(self, request, *args, **kwargs):
        city = ''
        user = self.request.user
        if user.is_authenticated and user.city:
            city = Response(user.city.name)
        if not city:
            city = get_city_by_ip(request.META['REMOTE_ADDR'])
        return Response(city)

Firstly, let's revise CityView.get method cache key calculation. It constructs from 3 bits:

The second method head has the same unique_method_id, format and language bits, buts extends with 2 more:

All default key bits are listed in this section.

Default key constructor

DefaultKeyConstructor is located in rest_framework_extensions.key_constructor.constructors module and constructs a key from unique method id, request format and request language. It has the following implementation:

class DefaultKeyConstructor(KeyConstructor):
    unique_method_id = bits.UniqueMethodIdKeyBit()
    format = bits.FormatKeyBit()
    language = bits.LanguageKeyBit()

How key constructor works

Key constructor class works in the same manner as the standard django forms and key bits used like form fields. Lets go through key construction steps for DefaultKeyConstructor.

Firstly, constructor starts iteration over every key bit:

Then constructor gets data from every key bit calling method get_data:

Every key bit get_data method is called with next arguments:

After this it combines every key bit data to one dict, which keys are a key bits names in constructor, and values are returned data:

{
    'unique_method_id': u'your_app.views.SometView.get',
    'format': u'json',
    'language': u'en'
}

Then constructor dumps resulting dict to json:

'{"unique_method_id": "your_app.views.SometView.get", "language": "en", "format": "json"}'

And finally compresses json with md5 and returns hash value:

'b04f8f03c89df824e0ecd25230a90f0e0ebe184cf8c0114342e9471dd2275baa'

Custom key bit

We are going to create a simple key bit which could be used in real applications with next properties:

The task is - cache every read request and invalidate all cache data after write to any model, which used in API. This approach let us don't think about granular cache invalidation - just flush it after any model instance change/creation/deletion.

Lets create models:

# models.py
from django.db import models

class Group(models.Model):
    title = models.CharField()

class Profile(models.Model):
    name = models.CharField()
    group = models.ForeignKey(Group)

Define serializers:

# serializers.py
from yourapp.models import Group, Profile
from rest_framework import serializers

class GroupSerializer(serializers.ModelSerializer):
    class Meta:
        model = Group

class ProfileSerializer(serializers.ModelSerializer):
    group = GroupSerializer()

    class Meta:
        model = Profile

Create views:

# views.py
from yourapp.serializers import GroupSerializer, ProfileSerializer
from yourapp.models import Group, Profile

class GroupViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = GroupSerializer
    queryset = Group.objects.all()

class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProfileSerializer
    queryset = Profile.objects.all()

And finally register views in router:

# urls.py
from yourapp.views import GroupViewSet,ProfileViewSet

router = DefaultRouter()
router.register(r'groups', GroupViewSet)
router.register(r'profiles', ProfileViewSet)
urlpatterns = router.urls

At the moment we have API, but it's not cached. Lets cache it and create our custom key bit:

# views.py
import datetime
from django.core.cache import cache
from django.utils.encoding import force_str
from yourapp.serializers import GroupSerializer, ProfileSerializer
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.key_constructor.constructors import (
    DefaultKeyConstructor
)
from rest_framework_extensions.key_constructor.bits import (
    KeyBitBase,
    RetrieveSqlQueryKeyBit,
    ListSqlQueryKeyBit,
    PaginationKeyBit
)

class UpdatedAtKeyBit(KeyBitBase):
    def get_data(self, **kwargs):
        key = 'api_updated_at_timestamp'
        value = cache.get(key, None)
        if not value:
            value = datetime.datetime.utcnow()
            cache.set(key, value=value)
        return force_str(value)

class CustomObjectKeyConstructor(DefaultKeyConstructor):
    retrieve_sql = RetrieveSqlQueryKeyBit()
    updated_at = UpdatedAtKeyBit()

class CustomListKeyConstructor(DefaultKeyConstructor):
    list_sql = ListSqlQueryKeyBit()
    pagination = PaginationKeyBit()
    updated_at = UpdatedAtKeyBit()

class GroupViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = GroupSerializer

    @cache_response(key_func=CustomObjectKeyConstructor())
    def retrieve(self, *args, **kwargs):
        return super(GroupViewSet, self).retrieve(*args, **kwargs)

    @cache_response(key_func=CustomListKeyConstructor())
    def list(self, *args, **kwargs):
        return super(GroupViewSet, self).list(*args, **kwargs)

class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProfileSerializer

    @cache_response(key_func=CustomObjectKeyConstructor())
    def retrieve(self, *args, **kwargs):
        return super(ProfileViewSet, self).retrieve(*args, **kwargs)

    @cache_response(key_func=CustomListKeyConstructor())
    def list(self, *args, **kwargs):
        return super(ProfileViewSet, self).list(*args, **kwargs)

As you can see UpdatedAtKeyBit just adds to key information when API models has been update last time. If there is no information about it then new datetime will be used for key bit data.

Lets write cache invalidation. We just connect models to standard signals and change value in cache by key api_updated_at_timestamp:

# models.py
import datetime
from django.db import models
from django.db.models.signals import post_save, post_delete

def change_api_updated_at(sender=None, instance=None, *args, **kwargs):
    cache.set('api_updated_at_timestamp', datetime.datetime.utcnow())

class Group(models.Model):
    title = models.CharField()

class Profile(models.Model):
    name = models.CharField()
    group = models.ForeignKey(Group)

for model in [Group, Profile]:
    post_save.connect(receiver=change_api_updated_at, sender=model)
    post_delete.connect(receiver=change_api_updated_at, sender=model)

And that's it. When any model changes then value in cache by key api_updated_at_timestamp will be changed too. After this every key constructor, that used UpdatedAtKeyBit, will construct new keys and @cache_response decorator will cache data in new places.

Key constructor params

New in DRF-extensions 0.2.3

You can change params attribute for specific key bit by providing params dict for key constructor initialization function. For example, here is custom key constructor, which inherits from DefaultKeyConstructor and adds geoip key bit:

class CityKeyConstructor(DefaultKeyConstructor):
    geoip = bits.RequestMetaKeyBit(params=['GEOIP_CITY'])

If you wanted to use GEOIP_COUNTRY, you could create new key constructor:

class CountryKeyConstructor(DefaultKeyConstructor):
    geoip = bits.RequestMetaKeyBit(params=['GEOIP_COUNTRY'])

But there is another way. You can send params in key constructor initialization method. This is the dict attribute, where keys are bit names and values are bit params attribute value (look at CountryView):

class CityKeyConstructor(DefaultKeyConstructor):
    geoip = bits.RequestMetaKeyBit(params=['GEOIP_CITY'])

class CityView(views.APIView):
    @cache_response(key_func=CityKeyConstructor())
    def get(self, request, *args, **kwargs):
        ...

class CountryView(views.APIView):
    @cache_response(key_func=CityKeyConstructor(
        params={'geoip': ['GEOIP_COUNTRY']}
    ))
    def get(self, request, *args, **kwargs):
        ...

If there is no item provided for key bit then default key bit params value will be used.

Constructor's bits list

You can dynamically change key constructor's bits list in initialization method by altering bits attribute:

class CustomKeyConstructor(DefaultKeyConstructor):
    def __init__(self, *args, **kwargs):
        super(CustomKeyConstructor, self).__init__(*args, **kwargs)
        self.bits['geoip'] = bits.RequestMetaKeyBit(
            params=['GEOIP_CITY']
        )

Default key bits

Out of the box DRF-extensions has some basic key bits. They are all located in rest_framework_extensions.key_constructor.bits module.

FormatKeyBit

Retrieves format info from request. Usage example:

class MyKeyConstructor(KeyConstructor):
    format = FormatKeyBit()

LanguageKeyBit

Retrieves active language for request. Usage example:

class MyKeyConstructor(KeyConstructor):
    language = LanguageKeyBit()

UserKeyBit

Retrieves user id from request. If it is anonymous then returnes "anonymous" string. Usage example:

class MyKeyConstructor(KeyConstructor):
    user = UserKeyBit()

RequestMetaKeyBit

Retrieves data from request.META dict. Usage example:

class MyKeyConstructor(KeyConstructor):
    ip_address_and_user_agent = bits.RequestMetaKeyBit(
        ['REMOTE_ADDR', 'HTTP_USER_AGENT']
    )

You can use * for retrieving all meta data to key bit:

New in DRF-extensions 0.2.7

class MyKeyConstructor(KeyConstructor):
    all_request_meta = bits.RequestMetaKeyBit('*')

HeadersKeyBit

Same as RequestMetaKeyBit retrieves data from request.META dict. The difference is that HeadersKeyBit allows to use normal header names:

class MyKeyConstructor(KeyConstructor):
    user_agent_and_geobase_id = bits.HeadersKeyBit(
        ['user-agent', 'x-geobase-id']
    )
    # will process request.META['HTTP_USER_AGENT'] and
    #              request.META['HTTP_X_GEOBASE_ID']

You can use * for retrieving all headers to key bit:

New in DRF-extensions 0.2.7

class MyKeyConstructor(KeyConstructor):
    all_headers = bits.HeadersKeyBit('*')

ArgsKeyBit

New in DRF-extensions 0.2.7

Retrieves data from the view's positional arguments. A list of position indices can be passed to indicate which arguments to use. For retrieving all arguments you can use * which is also the default value:

class MyKeyConstructor(KeyConstructor):
    args = bits.ArgsKeyBit()  # will use all positional arguments

class MyKeyConstructor(KeyConstructor):
    args = bits.ArgsKeyBit('*')  # same as above

class MyKeyConstructor(KeyConstructor):
    args = bits.ArgsKeyBit([0, 2])

KwargsKeyBit

New in DRF-extensions 0.2.7

Retrieves data from the views's keyword arguments. A list of keyword argument names can be passed to indicate which kwargs to use. For retrieving all kwargs you can use * which is also the default value:

class MyKeyConstructor(KeyConstructor):
    kwargs = bits.KwargsKeyBit()  # will use all keyword arguments

class MyKeyConstructor(KeyConstructor):
    kwargs = bits.KwargsKeyBit('*')  # same as above

class MyKeyConstructor(KeyConstructor):
    kwargs = bits.KwargsKeyBit(['user_id', 'city'])

QueryParamsKeyBit

Retrieves data from request.GET dict. Usage example:

class MyKeyConstructor(KeyConstructor):
    part_and_callback = bits.QueryParamsKeyBit(
        ['part', 'callback']
    )

You can use * for retrieving all query params to key bit which is also the default value:

New in DRF-extensions 0.2.7

class MyKeyConstructor(KeyConstructor):
    all_query_params = bits.QueryParamsKeyBit('*')  # all qs parameters

class MyKeyConstructor(KeyConstructor):
    all_query_params = bits.QueryParamsKeyBit()  # same as above

PaginationKeyBit

Inherits from QueryParamsKeyBit and returns data from used pagination params.

class MyKeyConstructor(KeyConstructor):
    pagination = bits.PaginationKeyBit()

ListSqlQueryKeyBit

Retrieves sql query for view.filter_queryset(view.get_queryset()) filtering.

class MyKeyConstructor(KeyConstructor):
    list_sql_query = bits.ListSqlQueryKeyBit()

RetrieveSqlQueryKeyBit

Retrieves sql query for retrieving exact object.

class MyKeyConstructor(KeyConstructor):
    retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()

UniqueViewIdKeyBit

Combines data about view module and view class name.

class MyKeyConstructor(KeyConstructor):
    unique_view_id = bits.UniqueViewIdKeyBit()

UniqueMethodIdKeyBit

Combines data about view module, view class name and view method name.

class MyKeyConstructor(KeyConstructor):
    unique_view_id = bits.UniqueMethodIdKeyBit()

ListModelKeyBit

New in DRF-extensions 0.3.2

Computes the semantic fingerprint of a list of objects returned by view.filter_queryset(view.get_queryset()) using a flat representation of all objects' values.

class MyKeyConstructor(KeyConstructor):
    list_model_values = bits.ListModelKeyBit()

RetrieveModelKeyBit

New in DRF-extensions 0.3.2

Computes the semantic fingerprint of a particular objects returned by view.get_object().

class MyKeyConstructor(KeyConstructor):
    retrieve_model_values = bits.RetrieveModelKeyBit()

Conditional requests

The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.

See discussion in Issue #177

Bulk operations

New in DRF-extensions 0.2.4

Bulk operations allows you to perform operations over set of objects with one request. There is third-party package django-rest-framework-bulk with support for all CRUD methods, but it iterates over every instance in bulk operation, serializes it and only after that executes operation.

It plays nice with create or update operations, but becomes unacceptable with partial update and delete methods over the QuerySet. Such kind of QuerySet could contain thousands of objects and should be performed as database query over the set at once.

Please note - DRF-extensions bulk operations applies over QuerySet, not over instances. It means that:

Safety

Bulk operations are very dangerous in case of making stupid mistakes. For example you wanted to delete user instance with DELETE request from your client application.

# Request
DELETE /users/1/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8

That was example of successful deletion. But there is the common situation when client could not get instance id and sends request to endpoint without it:

# Request
DELETE /users/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8

If you used bulk destroy mixin for /users/ endpoint, then all your user objects would be deleted.

To protect from such confusions DRF-extensions asks you to send X-BULK-OPERATION header for every bulk operation request. With this protection previous example would not delete any user instances:

# Request
DELETE /users/ HTTP/1.1
Accept: application/json

# Response
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json; charset=UTF-8

{
  "detail": "Header 'X-BULK-OPERATION' should be provided for bulk operation."
}

With X-BULK-OPERATION header it works as expected - deletes all user instances:

# Request
DELETE /users/ HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true

# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8

You can change bulk operation header name in settings:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_BULK_OPERATION_HEADER_NAME': 'X-CUSTOM-BULK-OPERATION'
}

To turn off protection you can set DEFAULT_BULK_OPERATION_HEADER_NAME as None.

Bulk destroy

This mixin allows you to delete many instances with one DELETE request.

from rest_framework_extensions.bulk_operations.mixins import ListDestroyModelMixin

class UserViewSet(ListDestroyModelMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer

Bulk destroy example - delete all users which emails ends with gmail.com:

# Request
DELETE /users/?email__endswith=gmail.com HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true

# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8

Bulk update

This mixin allows you to update many instances with one PATCH request. Note, that this mixin works only with partial update.

from rest_framework_extensions.mixins import ListUpdateModelMixin

class UserViewSet(ListUpdateModelMixin, viewsets.ModelViewSet):
    serializer_class = UserSerializer

Bulk partial update example - set email_provider of every user as google, if it's email ends with gmail.com:

# Request
PATCH /users/?email__endswith=gmail.com HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true

{"email_provider": "google"}

# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8

Settings

DRF-extensions follows Django Rest Framework approach in settings implementation.

In Django Rest Framework you specify custom settings by changing REST_FRAMEWORK variable in settings file:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.YAMLRenderer',
    ),
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.YAMLParser',
    )
}

In DRF-extensions there is a magic variable too called REST_FRAMEWORK_EXTENSIONS:

REST_FRAMEWORK_EXTENSIONS = {
    'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15
}

Accessing settings

If you need to access the values of DRF-extensions API settings in your project, you should use the extensions_api_settings object. For example:

from rest_framework_extensions.settings import extensions_api_settings

print extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT

Release notes

You can read about versioning, deprecation policy and upgrading from Django REST framework documentation.

0.7.0

0.6.0

Jan 27, 2020

0.5.0

May 10, 2019

0.4.0

Sep 5, 2018

0.3.2

Jan 4, 2017

0.3.1

Sep 29, 2016

0.2.8

Sep 21, 2015

0.2.7

Feb 2, 2015

0.2.6

Sep 9, 2014

0.2.5

July 9, 2014

0.2.4

July 7, 2014

0.2.3

Apr. 25, 2014

0.2.2

Mar. 23, 2014

0.2.1

Feb. 1, 2014

0.2

Nov. 5, 2013