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}
Related
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': []}
I created a custom create field in the tournament serializer to create and update nested field.
I have been trying to make that work for a long time, but I can't get rid of errors.
When I try to post data on the tournament update it returns this error:
NOT NULL constraint failed: api_tournament.organizer_id
Here, api is the name of the app.
models.py
class tournament(models.Model):
tournament_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=60)
organizer = models.ForeignKey(
client, null=False, blank=False, on_delete=models.CASCADE)
logo = models.ImageField(null=True, blank=False)
game = models.CharField(max_length=20, choices=GAMES)
fees = models.IntegerField()
def __str__(self):
return self.title
views.py
# tournament
#api_view(['GET'])
def tournamentList(request):
tournaments = tournament.objects.all()
serializer = tournamentSerializer(tournaments, many=True)
return Response(serializer.data)
#api_view(['GET'])
def tournamentDetail(request, tournamentId):
singleTournament = tournament.objects.get(tournament_id=tournamentId)
serializer = tournamentSerializer(singleTournament, many=False)
return Response(serializer.data)
#api_view(['POST'])
def tournamentCreate(request):
serializer = tournamentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
#api_view(['POST'])
def tournamentUpdate(request, tournamentId):
singleTournament = tournament.objects.get(tournament_id=tournamentId)
serializer = tournamentSerializer(
instance=singleTournament, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
#api_view(['DELETE'])
def tournamentDelete(request, tournamentId):
singleTournament = tournament.objects.get(tournament_id=tournamentId)
singleTournament.delete()
return Response("Deleted Item")
serializers.py
class tournamentSerializer(serializers.ModelSerializer):
organizer = clientSerializer(many=False, read_only=False)
class Meta:
model = tournament
fields = "__all__"
def create(self, validated_data):
organizer_data = validated_data.pop('organizer')
new_tournament = tournament.objects.create(**validated_data)
client.objects.create(organizer_data)
return new_tournament
"I created cusotm create field in the tournament serializer to create and update nested field, trying to make that work for a long time now but can't get rid of errors."
This error has nothing to do with create/update nested fields. But if you really go down that road:
When you initialize a serializer with an instance and call save(), it's not going to call the create method. It'll call the serializer update method. Here is the source code for proof.
if self.instance is not None:
self.instance = self.update(self.instance, validated_data)
assert self.instance is not None, (
'`update()` did not return an object instance.'
)
else:
self.instance = self.create(validated_data)
assert self.instance is not None, (
'`create()` did not return an object instance.'
)
return self.instance
But the serializer update method does not seem to be in the code you provided. It'll default to the ModelSerializer's update, which would raise an error about nested writes. Since that's not what happened, the bug must be earlier. This probably happened inside serializer.is_valid().
"When I try to post data on the tournament update it gives the error as
NOT NULL constraint failed: api_tournament.organizer_id
"
The error says that a constraint preventing null values has been violated. From the looks of it, the request.data might not have an "organizer_id" or "organizer" key. If you want to update a tournament without posting all the data during an update, either give the serializer field argument required=False or initialize the serializer with the argument partial=True
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
I'm using Django 2.x and Django REST Framework.
I have a model with contact as a foreign key
class AmountGiven(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
given_date = models.DateField(default=timezone.now)
created = models.DateTimeField(auto_now=True)
and serializer like
class AmountGivenSerializer(serializers.ModelSerializer):
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
contact field is required while creating a new record. But I do not want contact to be modified once it is created.
But when I send only amount with PUT method it says
{
"contact": [
"This field is required."
]
}
And when I use PATCH method, it works fine but if passing some other value for contact, it is updating contact as well.
I want to make contact field not-required while updating existing record. And even if it is passed, use the earlier one instead of setting the new data.
Trial 2
I tried overriding the contact field in the request to the previously stored value so that in case if changed contact is passed or no contact is passed, it will save earlier one.
So, in the viewset add the function
def update(self, request, *args, **kwargs):
obj = self.get_object()
request.data['contact'] = obj.contact_id
return super().update(request, *args, **kwargs)
But this is giving error as
This QueryDict instance is immutable
Use __init__ method of serializer to make it read when object is being updated:
class AmountGivenSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
"""If object is being updated don't allow contact to be changed."""
super().__init__(*args, **kwargs)
if self.instance is not None:
self.fields.get('parent').read_only = True
# self.fields.pop('parent') # or remove the field
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
Using self.context['view'].action is not recommended as it will not work when using the serializer out of DRF, eg. in normal Django views. It's best to use self.instance as it will work in every situation.
If your viewset is a ModelViewSet, you can overwrite the perform_update hook (because ModelViewSet inherits from GenericAPIView (take a look at "Save and deletion hooks"). You can access the old contact using the serializer's instance field:
class MyViewSet(viewsets.ModelViewSet):
# ... other stuff
def perform_update(self, serializer):
serializer.save(contact=serializer.instance.contact)
So you will have to provide a contact, but no matter which contact you provide, it will always use the old saved contact when updating.
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.