7

I am using django rest framework, and I have an object being created via a modelviewset, and a modelserializer. This view is only accessible by authenticated users, and the object should set its 'uploaded_by' field, to be that user.

I've read the docs, and come to the conclusion that this should work

viewset:

class FooViewset(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAdminUser]
    queryset = Foo.objects.all()
    serializer_class = FooSerializer

    def get_serializer_context(self):
        return {"request": self.request}

serializer:

class FooSerializer(serializers.ModelSerializer):
    uploaded_by = serializers.PrimaryKeyRelatedField(
        read_only=True, default=serializers.CurrentUserDefault()
    )

    class Meta:
        model = Foo
        fields = "__all__"

However, this results in the following error:

django.db.utils.IntegrityError: NOT NULL constraint failed: bar_foo.uploaded_by_id

Which suggests that "uploaded_by" is not being filled by the serializer.

Based on my understanding of the docs, this should have added the field to the validated data from the serializer, as part of the create method.

Clearly I've misunderstood something!

Alex
  • 2,270
  • 3
  • 33
  • 65

3 Answers3

13

The problem lies in the read_only attribute on your uploaded_by field:

Read-only fields are included in the API output, but should not be included in the input during create or update operations. Any 'read_only' fields that are incorrectly included in the serializer input will be ignored.

Set this to True to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.

Source

Basically it's used for showing representation of an object, but is excluded in any update and create-process.

Instead, you can override the create function to store the desired user by manually assigning it.

class FooSerializer(serializers.ModelSerializer):

    uploaded_by = serializers.PrimaryKeyRelatedField(read_only=True)

    def create(self, validated_data):
        foo = Foo.objects.create(
            uploaded_by=self.context['request'].user,
            **validated_data
        )
        return foo
Johan
  • 3,577
  • 1
  • 14
  • 28
  • Ah, that makes sense, but it doesn't work without read_only: AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`. – Alex Apr 06 '19 at 14:15
  • The HiddenField seems to have worked in terms of creation, but the serializer doesn't show the uploaded_by field when showing already-existing objects – Alex Apr 06 '19 at 14:22
  • Well, essentially a serializer is used to serialize the input data, and if you want to populate certain fields you could do so in the perform_create function. I've updated my answer and removed the `HiddenField` part as I didn't get that you wanted to present the `uploaded_by` field as well – Johan Apr 06 '19 at 14:26
  • The docs say that perform_create should be in the viewset, but I've tried it in both, and it's never called – Alex Apr 06 '19 at 14:35
  • I see, have you tried to add `source='user.id'` (or just `source='user'`) as an [attribute](https://www.django-rest-framework.org/api-guide/fields/#source) to the original `PrimaryKeyRelatedField `? Without using any of my proposed solutions in my current answer. – Johan Apr 06 '19 at 14:45
  • I'm not manually defining the fields - just fields = "__all__" on the modelserializer – Alex Apr 06 '19 at 14:46
  • No, I mean adding `source` as an attribute to `uploaded_by = serializers.PrimaryKeyRelatedField(...)`. I've added an optional solution that I've used in one of my own projects as well. – Johan Apr 06 '19 at 14:56
  • that doesn't work either. It looks like the create override will work, but I'm confused as to why the perform_create doesn't work, since the docs seem to recommend it – Alex Apr 06 '19 at 15:00
  • It appears that the `perform_create` function is used for the [`CreateModelMixin`](https://github.com/encode/django-rest-framework/blob/0e10d32fb122619a7977909536b642d09603192a/rest_framework/mixins.py#L14), which I guess you'll need to use in the view instead. However, I've (once again) updated my answer to hopefully do the desired behaviour. – Johan Apr 06 '19 at 15:09
2

DRF tutorial recommend to override perform_create method in this case and then edit serializer so, that it reflect to new field

from rest_framework import generics, serializers
from .models import Post


class PostSerializer(serializers.HyperlinkedModelSerializer):
    author = serializers.ReadOnlyField(source='author.username')

    class Meta:
        model = models.Post
        fields = ['title', 'content', 'author']


class ListPost(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def perform_create(self, serializer):
        return serializer.save(author=self.request.user)
Ilya Davydov
  • 521
  • 8
  • 13
0

Cleaner way:

class PostCreateAPIView(CreateAPIView, GenericAPIView):
    queryset = Post.objects.all()
    serializer_class = PostCreationSerializer

    def perform_create(self, serializer):
        return serializer.save(author=self.request.user)

class PostCreationSerializer(serializers.ModelSerializer):
    author = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Post
        fields = ("content", "author")