Cannot use serializer when ManyToManyField is empty - django

I am utilizing PrimaryKeyRelatedField to retrieve and write M2M data.
My models.py:
class Task(MP_Node):
...
linked_messages = models.ManyToManyField('workdesk.Message', blank=True, related_name='supported_tasks')
(MP_Node is an abstraction of models.Model from django-treebeard).
My serializers.py:
class TaskSerializer(serializers.ModelSerializer):
...
linked_messages = serializers.PrimaryKeyRelatedField(many=True, required=False, allow_null=True, queryset=Message.objects.all())
class Meta:
model = Task
fields = [..., 'linked_messages']
My api.py:
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
def create(self, request):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(raise_exception=True):
print(serializer.data)
With other fields, if the field is set to null=True in the models, or required=False on the serializer, I don't need to include them in the data to instantiate the serializer. However, these fields do not seem to work this way, instead returning KeyError: 'linked_messages' when serializer.data is called.
As a workaround I tried adding setting allow_null, as indicated by the docs, and then manually feed it a null value:
request.data['linked_messages'] = None
but this returns as 404:
"linked_messages":["This field may not be null."]
If I set it to a blank string:
"resources":["Expected a list of items but got type \"str\"."]
If I set it to an empty list, serializer.data again gives me an error:
`TypeError: unhashable type: 'list'`
It seems to have me any way I turn. What am I not understanding about this field?

Use default argument -
linked_messages = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Message.objects.all(),
default=[]
)
# print(serializer.data)
# {'linked_messages': []}

Related

Django DRF rename field in serializer but unique_together validation fails

In the serializer used to create a model I want to rename my model field to (field_name)_id so it's clearer for API consumers that this field is an ID field. The model also has a unique_together constraint on some fields. However when validation runs in the serializer, it fails with a KeyError that the field does not exist:
...rest_framework/utils/serializer_helpers.py", line 148, in __getitem__
return self.fields[key]
KeyError: 'question'
Is there a simple way to get this to work? Minimal example code below.
Model
class MyModel(Model):
question = ForeignKey('uppley.Question', null=False, on_delete=PROTECT)
user = ForeignKey('catalystlab.User', null=False, on_delete=PROTECT)
class Meta:
unique_together = ('question', 'user',)
Serializer
class MyCreateSerializer(ModelSerializer):
question_id = PrimaryKeyRelatedField(
write_only=True,
source='question',
queryset=Question.objects.all(),
)
user_id = PrimaryKeyRelatedField(
write_only=True,
source='user',
queryset=User.objects.all(),
)
class Meta:
model = MyModel
fields = ('question_id', 'user_id',)
test.py - test for demonstration purposes
question = QuestionFactory()
user = UserFactory()
data = {
'question_id': question.id,
'user_id': user.id,
}
serializer = MyCreateSerializer(data=data, write_only=True)
is_valid = serializer.is_valid(raise_exception=True) #KeyError exception raised here.
Previously with DRF 3.10.3 this all worked fine, however with 3.11.0 this now throws a KeyError as mentioned above.
What I have tried
Removing the source field on PrimaryKeyRelatedField for user_id and question_id in the Serializer actually results in bypassing the unique_together validation in DRF and the KeyError is avoided. However the validated data is not mapped back to the original field names (user and question). In this case we have to manually change the keys back to their original names before we can create an instance of the Model from the validated data.
Is there a better way to do this?
You can make a custom serializer like :-
class MyCreateSerializer(serializers.Serializer):
question_id = serializers.PrimaryKeyRelatedField(
write_only=True,
queryset=Question.objects.all(),
)
user_id = PrimaryKeyRelatedField(
write_only=True,
queryset=User.objects.all(),
)
and make custom create function in it for creating object. like :-
def create(self, validated_data):
try:
question = validated_data.get('question_id')
user = validated_data.get('user_id')
instance = MyModel.objects.create(question=question, user=user)
except TypeError:
raise TypeError("Something went wrong while creating objects")
return instance

Problem with POST from fronside. Method does not support writable dotted-source fields by default

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.

Django DRF add request.user to modelserializer

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")

Nested serializer with optional field raising KeyError

I have two serializers, first one (a bit simplified):
class FilterSerializer(serializers.Serializer):
brand = serializers.PrimaryKeyRelatedField(
queryset=org_models.Brand.objects, many=False,
error_messages={'does_not_exist': org_consts.BRAND_NOT_EXIST}
)
country = serializers.PrimaryKeyRelatedField(
queryset=org_models.Country.objects, many=False,
error_messages={'does_not_exist': org_consts.COUNTRY_NOT_EXIST}
)
level = serializers.PrimaryKeyRelatedField(
queryset=org_models.Level.objects, many=False, required=False,
error_messages={'does_not_exist': org_consts.DISTRICT_NOT_EXIST}
)
class Meta:
fields = ('brand', 'country', 'level')
and second:
class PeopleReportSerializer(serializers.Serializer):
filters = FilterSerializer(many=True)
start_date = serializers.DateField(required=False)
end_date = serializers.DateField(required=False)
class Meta:
fields = ('filters', 'start_date', 'end_date')
def validate(self, data):
"""Check if end_date occurs after start_date.
"""
if data.get('start_date') and data.get('end_date'):
if data['start_date'] > data['end_date']:
raise serializers.ValidationError(constants.INVALID_START_DATE)
return data
I've removed some fields for clarity.
So basically all I want from this serializer is validation if objects with posted ID exist in the database. On GET I would like it to return some data depending on request.user but that's another case.
So now in my view I'm doing something like this:
class PeopleReportView(ReportsPermissionMixin, views.APIView):
serializer_class = PeopleReportSerializer
task = staticmethod(people_report) # celery task, creates reports
def get(self, request):
# TODO: depending on request.user return initial data such as:
# country / brand / district and so on
return Response()
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True):
task = self.task.apply_async(kwargs=serializer.data)
return Response({'task_id': task.id})
my problem is that when I post data without level field (which is NOT a required field) I get KeyError: 'level'. FilterSerializer works fine, but when it's nested I'm getting this error. Apparently I was wrong, my test wasn't testing data property, only is_valid method.
Try using the allow_null flag instead:
level = serializers.PrimaryKeyRelatedField(
queryset=org_models.Level.objects, many=False, allow_null=True,
error_messages={'does_not_exist': org_consts.DISTRICT_NOT_EXIST}
)
instead of:
level = serializers.PrimaryKeyRelatedField(
queryset=org_models.Level.objects, many=False, required=False,
error_messages={'does_not_exist': org_consts.DISTRICT_NOT_EXIST}
)
See here for more information
Even though PrimaryKeyRelatedField is a Field it doesn't work the same way when initialized with required=False. Just like Remi wrote the only way that is supported in current version of DRF (3.5.X) for read/write fields is to allow_null on these fields (besides using some strange workarounds).
API has to send an empty field, and in object representation i.e. Serializer.data this fields will have a None value. Since in my case I mustn't had None values I modified Serialzier.to_representation method like so:
def to_representation(self, instance):
"""Excludes fields with None value from representation
"""
ret = super().to_representation(instance)
return {field: value for (field, value) in ret.items()
if value is not None}

Django Rest Framework return nested object using PrimaryKeyRelatedField

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.