DRF RetrieveUpdateDestroyAPIView with POST

Hi All,

I’m building an endpoint with Django Rest Framework which is used for CRUD operation on a user’s settings.

class UserSetting(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    user = models.OneToOneField(CustomUser, related_name="settings", on_delete=models.CASCADE)
    settings = JSONField(null=True, blank=True)

I was going to use a viewset, but since I know the user via request.user and my queryset will always be a get_object_or_404 I thought I’d just use generics.RetrieveUpdateDestroyAPIView as opposed to having to pass an identifier for a retrieve request, to user/settings/identifier like one would when using a viewset.

As an example, I would like the URL user/settings to:

  1. GET a single object that is the request.user's setting object
  2. POST that will create a UserSetting object for request.user
  3. PUT/PATCH that will update request.user's UserSetting object
  4. DELETE to delete request.user's UserSetting object

In order to get the user from the request, I have overriden save() in my serialiser to get the user from the request context:

    def save(self, **kwargs):
        user = None
        request = self.context.get("request")
        if request and hasattr(request, "user"):
            user = request.user
        kwargs["user"] = user
        return super().save(**kwargs)

And everything seemed to work just fine and dandy until I realised I can’t POST to my endpoint, only GET, PUT, PATCH, and DELETE.

I tried adding CreateModelMixin to my view, but that didn’t seem to help.

class UserSettingsViewset(CreateModelMixin, generics.RetrieveUpdateDestroyAPIView):

From what I have worked out so far is, that it looks like I’ll have to create a separate endpoint for POST requests that will create a UserSetting.

So, with the long story out of the way, does anybody know how to create a single endpoint that can do all of CRUD at a single URL?

I suppose in the worst case I could do something like use a Viewset and use the user.username as a lookup field, but that is just a guess at an idea whilst I write this.

Any thoughts how I can tackle my problem? Or if it is even a good idea to be tackling such a problem?

Cheers,

C

Looking at the source code for the generics and the mixins, it looks like you could just add a post method to your view class to handle that situation. You might then also want/need to add a custom “create” function to build your user model as needed.

(The CreateModelMixin appears to only handle the definition of the create function, it doesn’t provide the post function needed to handle that verb.)

Disclaimer: I’ve never done this, I’m just taking a WAG based upon what I’m reading from the docs and source code. If you haven’t looked at the source, I really suggest you do so - understanding the how these classes work makes it a whole lot easier to understand how to use and extend them.

Ken

1 Like

Hi Ken,

Thank you, that was wonderfully helpful and you’ve put me back on the straight and narrow. And yes, you’re spot on, I should dive into the source code a bit more. I’m still recovering from my last dive into the serialisers source, but that shouldn’t have stopped me from having a look this time.

Looking at the source for generics.CreateAPIView we see the following

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Following the mixin from the signature to CreateAPIView we see the following:

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

So I reasoned, much as you had implied, that since the CreateModelMixin had a create() method, then all I had to do was add the CreateModelMixin to my class, and add a POST method which called create() from CreateModelMixin.

‘My’ (Ken’s) solution

class UserSettingsView(CreateModelMixin, generics.RetrieveUpdateDestroyAPIView):
    serializer_class = UserSettingsSerializer
    permission_classes = [IsAuthenticated, IsOwner]

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

After going through that exercise, it became quite clear (within my own limitations) how the various *APIViews are built. Very clever, and very clear.

Again, thank you very much Ken! You’ve been tremendously helpful.

Cheers,

C