Skip to content

Instantly share code, notes, and snippets.

@tomchristie
Created November 3, 2014 11:55
Show Gist options
  • Save tomchristie/a2ace4577eff2c603b1b to your computer and use it in GitHub Desktop.
Save tomchristie/a2ace4577eff2c603b1b to your computer and use it in GitHub Desktop.
PUT-as-create mixin class for Django REST framework.
class AllowPUTAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.save()
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
@cancan101
Copy link

PATCH is also allowed to create. From http://tools.ietf.org/html/rfc5789:

If the Request-URI does not
point to an existing resource, the server MAY create a new resource,
depending on the patch document type (whether it can logically modify
a null resource) and permissions, etc.

@Natim
Copy link

Natim commented Mar 28, 2018

from django.http import Http404

from rest_framework.response import Response
from rest_framework.request import clone_request

@feliesp
Copy link

feliesp commented Apr 23, 2018

from rest_framework import status

@mightyroser
Copy link

mightyroser commented May 14, 2018

If you want to support overridable hooks perform_create and/or perform_update similar to what are available in CreateModelMixin and UpdateModelMixin ( documented in http://www.django-rest-framework.org/api-guide/generic-views/ ) you could modify the update function above as follows:

def update(self, request, *args, **kwargs):
    partial = kwargs.pop('partial', False)
    instance = self.get_object_or_none()
    serializer = self.get_serializer(instance, data=request.data, partial=partial)
    serializer.is_valid(raise_exception=True)

    if instance is None:
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        lookup_value = self.kwargs[lookup_url_kwarg]
        extra_kwargs = {self.lookup_field: lookup_value}
        self.perform_create(serializer, **extra_kwargs)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    self.perform_udpate(serializer)
    return Response(serializer.data)

def perform_create(self, serializer, **kwargs):
    serializer.save(**kwargs)
    
def perform_udpate(self, serializer):
    serializer.save()

@manuel14
Copy link

The mixin works fine, but how can i make optional the pk field of my object? is it possible?

@PetrDlouhy
Copy link

I have problem with this fixture - I am getting duplicate errors if the request is called more than once quickly. Does anyone had the same problem? I tried @transaction.atomic, but without success.

@aryaniyaps
Copy link

aryaniyaps commented Mar 29, 2021

with support for perform_create and perform_update hooks (without changing signature)
and multiple-field mixins.

class PutAsCreateMixin(object):
    """
    The following mixin class may be used in order to support
    PUT-as-create behavior for incoming requests.
    """

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object_or_none()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if instance is None:
            self.perform_create(serializer)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        self.perform_update(serializer)
        return Response(serializer.data)

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

    def perform_create(self, serializer):
        if not hasattr(self, 'lookup_fields'):
            lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
            lookup_value = self.kwargs[lookup_url_kwarg]
            extra_kwargs = {self.lookup_field: lookup_value}
        else:
            # set kwargs for additional fields
            extra_kwargs = {
                field: self.kwargs[field]
                for field in self.lookup_fields if self.kwargs[field]
            }
        serializer.save(**extra_kwargs)
    
    def perform_update(self, serializer):
        serializer.save()

    def get_object_or_none(self):
        try:
            return self.get_object()
        except Http404:
            if self.request.method == 'PUT':
                # For PUT-as-create operation, we need to ensure that we have
                # relevant permissions, as if this was a POST request. This
                # will either raise a PermissionDenied exception, or simply
                # return None.
                self.check_permissions(clone_request(self.request, 'POST'))
            else:
                # PATCH requests where the object does not exist should still
                # return a 404 response.
                raise

this way, additional attributes can also be added during creation.

@libcthorne
Copy link

Are there any plans to have something like this included in DRF by default?

@tomchristie
Copy link
Author

Nope.

@SydneyUni-Jim
Copy link

This seems to work for me with Django 4.2 and Python 3.11. Although I haven't tested it with permissions.

from django.http
import rest_framework.mixins

class AllowPUTAsCreateMixin(rest_framework.mixins.CreateModelMixin, rest_framework.mixins.UpdateModelMixin):

    def put(self, request, *args, **kwargs):
        try:
            return self.update(request, *args, **kwargs)
        except Http404:
            pass
        return self.create(request, *args, **kwargs)

With this I can create a view that allows retrieving via GET and upsert via PUT.

import rest_framework.generics
import rest_framework.serializers

class Serializer(rest_framework.serializers.ModelSerializer):
    ...  # TODO: As normal

class RetrieveCreatePatch(AllowPUTAsCreateMixin, rest_framework.generics.RetrieveAPIView):
    queryset = MyModel.objects.all()
    serializer_class = Serializer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment