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!
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
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)
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")
Related
I have a serializer that is meant to act as a template for other ModelSerializers.
class CountryBasedModelSerializer(ModelSerializer):
def __init__(self, data, context):
assert 'country' in self.Meta.fields
class Meta:
model = Country
fields = ()
I want to use it with this, which is the actual serializer which will be called.
class CountryBasedProjectSerializer(CountryBasedModelSerializer):
class Meta:
model = Project
fields = ('id', 'country', 'name')
I want to use it with this inherited viewset:
class CountryBasedViewset(viewsets.ModelViewSet):
queryset = None
serializer_class = CountryBasedModelSerializer
def get_queryset(self):
return self.queryset.filter(country_pk=self.request.data["country"])
And this is the actual viewset that will be called:
class CountryProjectBasedViewset(CountryBasedViewset):
queryset = Project.objects.all()
Is there anything that I am clearly doing incorrectly?
Just define the serializer_class for CounteryProjectBasedViewSet as below
class CountryProjectBasedViewset(CountryBasedViewset):
queryset = Project.objects.all()
serializer_class = CountryBasedProjectSerializer
I create some dotted source field in my serializer. I did it cause have to display name value of foreign key not pk value. But when I trying to POST from frontend djang throws this : AssertionError at /api/my-api/
The .create() method does not support writable dotted-source fields by default.
Write an explicit .create() method for serializer MySerializer, or set read_only=True on dotted-source serializer fields.
So, when I set read_only = True my POST from frontend to request null for every field from dotted-source serializer fields.
This is my serializer:
class FcaWorksSerializer(serializers.ModelSerializer):
fell_form = serializers.CharField(source="fell_form.name" )
#...
main_type = serializers.CharField(source="main_type.name")
class Meta:
model = FcaWorks
fields = ('id_fca','wkod', 'main_type','fell_form','fell_type','kind',\
'sortiment','vol_drew','use_type','fca_res','ed_izm','vol_les','act_name',\
'obj_type','use_area','indicator','comment','date_report')
How I can to solve this problem?
Override the __init__() method of the serializer to adjust the serializer condition
class FcaWorksSerializer(serializers.ModelSerializer):
fell_form = serializers.CharField()
# ...
main_type = serializers.CharField()
class Meta:
model = FcaWorks
fields = ('id_fca', 'wkod', 'main_type', 'fell_form', 'fell_type', 'kind',
'sortiment', 'vol_drew', 'use_type', 'fca_res', 'ed_izm', 'vol_les', 'act_name',
'obj_type', 'use_area', 'indicator', 'comment', 'date_report')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['request'].method == 'GET':
self.fields['fell_form'].source = "fell_form.name"
self.fields['main_type'].source = "main_type.name"
def create(self, validated_data):
# here you will get the data
fell_form = validated_data['fell_form']
main_type = validated_data['main_type']
From the docs, there are multiple ways to deal with ForeignKey relations. You don't have to make your own create method if the Foreignkey relations are not "many-to-many".
In your case you can use one of the following:
SlugRelatedField
PrimaryKeyRelatedField
class FcaWorksSerializer(serializers.ModelSerializer):
fell_form = serializers.SlugRelatedField(slug_field="name", queryset = ***"""The queryset that fell_form came from"""*** )
#...
main_type = serializers.SlugRelatedField(slug_field="name", queryset = ***"""The queryset main_type came from"""***)
class Meta:
model = FcaWorks
fields = ('id_fca','wkod', 'main_type','fell_form','fell_type','kind',\
'sortiment','vol_drew','use_type','fca_res','ed_izm','vol_les','act_name',\
'obj_type','use_area','indicator','comment','date_report')
Then PrimaryKeyRelated Field usage:
class FcaWorksSerializer(serializers.ModelSerializer):
fell_form = serializers.PrimaryKeyRelatedField(source ="fell_form.name", queryset = ***"""The queryset that fell_form came from"""*** )
#...
main_type = serializers.PrimaryKeyRelatedField(source="main_type.name", queryset = ***"""The queryset main_type came from"""***)
This has worked for me when I had the same problem, however like previously stated for "Many-to-Many" field you have to explicitly write the create and update methods.
I use below solution to check is the user viewed the post or not.
Best way to make "viewed" attribute for messages inside user group?
and in django-rest-framework, i create a ListApiView to get all posts:
class PostListView(ListAPIView):
serializer_class = PostSerializer
permission_classes = (IsAuthenticated, )
pagination_class = PostListPagination
def get_queryset(self):
return Post.objects.filter(state='published').order_by('-created')
and the serializers:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields= '__all__'
now i want a boolean field named "viewed" for each post in PostListView to show that is the authenticated user viewed this post or not.
something like this:
class PostSerializer(serializers.ModelSerializer):
viewed = serializers.BooleanField(read_only=True)
class Meta:
model = Post
fields= '__all__'
def check_is_viewed(current_user, post_instance):
# if user viewed this post:
viewed.value = True
# else:
viewed.value = False
You could use MethodField.
class PostSerializer(serializers.ModelSerializer):
viewed = serializers.SerializerMethodField()
class Meta:
model = Post
fields= '__all__'
def get_viewed(self, obj):
return obj.viewers.exist()
I am trying to figure out the best way to add annotated fields, such as any aggregated (calculated) fields to DRF (Model)Serializers. My use case is simply a situation where an endpoint returns fields that are NOT stored in a database but calculated from a database.
Let's look at the following example:
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key = True, max_length = 255)
class IceCreamTruck(models.Model):
company = models.ForeignKey('IceCreamCompany', related_name='trucks')
capacity = models.IntegerField()
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamCompany
desired JSON output:
[
{
"name": "Pete's Ice Cream",
"total_trucks": 20,
"total_capacity": 4000
},
...
]
I have a couple solutions that work, but each have some issues.
Option 1: add getters to model and use SerializerMethodFields
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key=True, max_length=255)
def get_total_trucks(self):
return self.trucks.count()
def get_total_capacity(self):
return self.trucks.aggregate(Sum('capacity'))['capacity__sum']
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
def get_total_trucks(self, obj):
return obj.get_total_trucks
def get_total_capacity(self, obj):
return obj.get_total_capacity
total_trucks = SerializerMethodField()
total_capacity = SerializerMethodField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
The above code can perhaps be refactored a bit, but it won't change the fact that this option will perform 2 extra SQL queries per IceCreamCompany which is not very efficient.
Option 2: annotate in ViewSet.get_queryset
models.py as originally described.
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks = Count('trucks'),
total_capacity = Sum('trucks__capacity')
)
This will get the aggregated fields in a single SQL query but I'm not sure how I would add them to the Serializer as DRF doesn't magically know that I've annotated these fields in the QuerySet. If I add total_trucks and total_capacity to the serializer, it will throw an error about these fields not being present on the Model.
Option 2 can be made work without a serializer by using a View but if the model contains a lot of fields, and only some are required to be in the JSON, it would be a somewhat ugly hack to build the endpoint without a serializer.
Possible solution:
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity')
)
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
By using Serializer fields I got a small example to work. The fields must be declared as the serializer's class attributes so DRF won't throw an error about them not existing in the IceCreamCompany model.
I made a slight simplification of elnygreen's answer by annotating the queryset when I defined it. Then I don't need to override get_queryset().
# views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity'))
serializer_class = IceCreamCompanySerializer
# serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
As elnygreen said, the fields must be declared as the serializer's class attributes to avoid an error about them not existing in the IceCreamCompany model.
You can hack the ModelSerializer constructor to modify the queryset it's passed by a view or viewset.
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField(readonly=True)
total_capacity = serializers.IntegerField(readonly=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], QuerySet):
queryset = cls._build_queryset(args[0])
args = (queryset, ) + args[1:]
return super().__new__(cls, *args, **kwargs)
#classmethod
def _build_queryset(cls, queryset):
# modify the queryset here
return queryset.annotate(
total_trucks=...,
total_capacity=...,
)
There is no significance in the name _build_queryset (it's not overriding anything), it just allows us to keep the bloat out of the constructor.
I am using DRF to expose some API endpoints.
# models.py
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
assigned_to = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, many=True)
class Meta:
model = Project
fields = ('id', 'title', 'created_by', 'assigned_to')
# view.py
class ProjectList(generics.ListCreateAPIView):
mode = Project
serializer_class = ProjectSerializer
filter_fields = ('title',)
def post(self, request, format=None):
# get a list of user.id of assigned_to users
assigned_to = [x.get('id') for x in request.DATA.get('assigned_to')]
# create a new project serilaizer
serializer = ProjectSerializer(data={
"title": request.DATA.get('title'),
"created_by": request.user.pk,
"assigned_to": assigned_to,
})
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This all works fine, and I can POST a list of ids for the assigned to field. However, to make this function I had to use PrimaryKeyRelatedField instead of RelatedField. This means that when I do a GET then I only receive the primary keys of the user in the assigned_to field. Is there some way to maintain the current behavior for POST but return the serialized User details for the assigned_to field?
I recently solved this with a subclassed PrimaryKeyRelatedField() which uses the id for input to set the value, but returns a nested value using serializers. Now this may not be 100% what was requested here. The POST, PUT, and PATCH responses will also include the nested representation whereas the question does specify that POST behave exactly as it does with a PrimaryKeyRelatedField.
https://gist.github.com/jmichalicek/f841110a9aa6dbb6f781
class PrimaryKeyInObjectOutRelatedField(PrimaryKeyRelatedField):
"""
Django Rest Framework RelatedField which takes the primary key as input to allow setting relations,
but takes an optional `output_serializer_class` parameter, which if specified, will be used to
serialize the data in responses.
Usage:
class MyModelSerializer(serializers.ModelSerializer):
related_model = PrimaryKeyInObjectOutRelatedField(
queryset=MyOtherModel.objects.all(), output_serializer_class=MyOtherModelSerializer)
class Meta:
model = MyModel
fields = ('related_model', 'id', 'foo', 'bar')
"""
def __init__(self, **kwargs):
self._output_serializer_class = kwargs.pop('output_serializer_class', None)
super(PrimaryKeyInObjectOutRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return not bool(self._output_serializer_class)
def to_representation(self, obj):
if self._output_serializer_class:
data = self._output_serializer_class(obj).data
else:
data = super(PrimaryKeyInObjectOutRelatedField, self).to_representation(obj)
return data
You'll need to use a different serializer for POST and GET in that case.
Take a look into overriding the get_serializer_class() method on the view, and switching the serializer that's returned depending on self.request.method.