Django rest framework posting expects dictionary - python

I am trying to post to my API with foreign key relationships. It's throwing me back an error saying it's expecting a dictionary as opposed to int for character, character_opponent and stage. This is because the way my models are set up. They have foreign key relationships. The model in question looks like this:
import uuid
from django.db import models
from django.utils import timezone
from analysis.models import Analysis
from characters.models import Character
from stages.models import Stage
class Match(models.Model):
analysis = models.ForeignKey(Analysis, on_delete=models.CASCADE)
character = models.ForeignKey(Character, on_delete=models.CASCADE, related_name='character')
character_won = models.BooleanField()
character_opponent = models.ForeignKey(Character, on_delete=models.CASCADE, related_name='character_opponent')
character_opponent_won = models.BooleanField()
created_at = models.DateTimeField(editable=False)
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
updated_at = models.DateTimeField(editable=False)
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
def __str__(self):
return '%s vs. %s on %s' % (self.character, self.character_opponent, self.stage)
def save(self, *args, **kwargs):
''' On save, update timestamps '''
if not self.created_at:
self.created_at = timezone.now()
self.updated_at = timezone.now()
return super(Match, self).save(*args, **kwargs)
class Meta:
db_table = "matches"
And here is my serializer:
from rest_framework import serializers
from matches.models import Match
from characters.serializers import CharacterSerializer
from stages.serializers import StageSerializer
class MatchSerializer(serializers.ModelSerializer):
character = CharacterSerializer()
character_opponent = CharacterSerializer()
stage = StageSerializer()
class Meta:
model = Match
fields = ('id', 'analysis', 'character', 'character_won', 'character_opponent', 'character_opponent_won', 'stage')
Is there some option I am missing here to be able to post properly? Clearly I shouldn't have to pass the entire character object each time I want to post something, right? I should just be able to pass the primary key.

From your few comments I understood that you need nested serializer in GET method. What I suggest is, use two[or more] serializers for your API class.
Assuming you are using ModelViewSet API class is using,then you could override get_serializer_class() method as below,
from rest_framework.viewsets import ModelViewSet
class MatchAPI(ModelViewSet):
queryset = Match.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return MatchCreateSerializer
return MatchSerializer
And your MatchCreateSerializer will be like this,
class MatchCreateSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = Match
Thus, you only need to provide the PKs of analysis,character etc while creation of Match instance

It will come down to your CharacterSerializer and StageSerializer. If you want to input 1 format (using serialisers.PrimaryKeyRelatedField()), but output another (CharacterSerializer, StageSerializer), you might be best served using 2 serialisers and switching in your view.
In your view you can override get_serializer_class and check your request method, or in the case of a viewset you can check the method being invoked.

When you declare a serializer related field using another serializer, like this
character = CharacterSerializer()
you are telling django-rest-framework that you want a nested serializer. What you want is something like this instead
character = serializers.PrimaryKeyRelatedField()
or you can actually just leave the explicit field declaration out of the serializer (since this is the default), see the doc on serializer relations.

Related

Django form not populating with POST data

SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.

Catch-all field for unserialisable data of serializer

I have a route where meta-data can be POSTed. If known fields are POSTed, I would like to store them in a structured manner in my DB, only storing unknown fields or fields that fail validation in a JSONField.
Let's assume my model to be:
# models.py
from django.db import models
class MetaData(models.Model):
shipping_address_zip_code = models.CharField(max_length=5, blank=True, null=True)
...
unparseable_info = models.JSONField(blank=True, null=True)
I would like to use the built-in serialisation logic to validate whether a zip_code is valid (5 letters or less). If it is, I would proceed normally and store it in the shipping_address_zip_code field. If it fails validation however, I would like to store it as a key-value-pair in the unparseable_info field and still return a success message to the client calling the route.
I have many more fields and am looking for a generic solution, but only including one field here probably helps in illustrating my problem.
As you are looking for a generic solution, there are a few points that you should consider:
Make sure not to place any model-level validations in your model as you want it to get saved irrespective of the validation status.
Only validate on the serializer-level with custom validation methods.
Make unparseable_info field read-only as it is something we don't want the user to send but receive.
Make use of the errors dictionary provided by the serializer as it gets populated with field-specific errors when we call is_valid.
This is how it might translate into code, inside models.py:
class MetaData(models.Model):
shipping_address_zip_code = models.CharField(blank=True, null=True)
...
unparseable_info = models.JSONField(blank=True, null=True)
then inside serializers.py:
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
model = MetaData
read_only_fields = ('unparseable_info', )
fields = '__all__'
# Write validators for all of your fields.
finally inside your views.py method, something like this (you can do this inside serializer's save method as well):
meta_data = MetaDataSerializer(data=request.data)
if not meta_data.is_valid():
meta_data.unparseable_info = meta_data.errors
meta_data.save()
# Return meta_data.data in JSONResponse.
You can use Django serializer that store fields that fail validation in JSONField.
Here is an example that worked for me:
from rest_framework import serializers
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
model = MetaData
fields = 'all'
def validate_shipping_address_zip_code(self, value):
if len(value) > 5:
raise serializers.ValidationError("Zip code must be 5 characters or less.")
return value
def create(self, validated_data):
unparseable_info = {}
for field, value in self.initial_data.items():
try:
validated_data[field] = self.fields[field].run_validation(value)
except serializers.ValidationError as e:
unparseable_info[field] = value
instance = MetaData.objects.create(**validated_data)
if unparseable_info:
instance.unparseable_info = unparseable_info
instance.save()
return instance
def validate_shipping_address_zip_code(self, value):
if value >= 5:
return value
else:
raise serializers.ValidationError("Message Here")
there's much more validators in serializer look into more detail
https://www.django-rest-framework.org/api-guide/serializers/

Partial updating a ManyToMany field, but keeping its get representation

I've been scratching my head about this problem for a couple of hours now. Basically, I have two models: User and Project:
class User(AbstractUser):
username = None
email = models.EmailField("Email Address", unique=True)
avatar = models.ImageField(upload_to="avatars", default="avatars/no_avatar.png")
first_name = models.CharField("First name", max_length=50)
last_name = models.CharField("Last name", max_length=50)
objects = UserManager()
USERNAME_FIELD = "email"
class Project(models.Model):
name = models.CharField("Name", max_length=8, unique=True)
status = models.CharField(
"Status",
max_length=1,
choices=[("O", "Open"), ("C", "Closed")],
default="O",
)
description = models.CharField("Description", max_length=3000, default="")
owner = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="project_owner"
)
participants = models.ManyToManyField(User, related_name="project_participants", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
I use standard ModelViewSets for both of them, nothing changed. Then there's my Project serializer:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
status = serializers.CharField(source="get_status_display", required=False)
owner = UserSerializer()
participants = UserSerializer(many=True)
I use UserSerializers here, because having them achieved first of my two goals:
I wanted to get the user data when getting the project from the API -> owner is a serialized User with all the fields, same for participants, but it's a list of users
I want to be able to partially update the Project, for example add a participant
So I searched through the docs and SO and I always found answers that answer one of those questions, but never both of them.
The thing with my second goal is: when I do the partial update (via PATCH, of course), I get the response that: "Invalid data. Expected a dictionary, but got int." when I pass a list of ints (user ids) for the participants. I thought: okay, maybe I have to pass the whole user data to change it. But then I realised: when I remove the UserSerializer from ProjectSerializer - passing just the list of ints in Postman works just fine. And that is a life saver, cuz who wants to create a request with a whole bunch of data, when I can just pass user ids.
But then of course when I remove the UserSerializer, when I call get project, I get participants: [1,2,3,4,...], not participants: [{"id": 1, "name": "John", ...}, ...}]. And I really want this behavior, because I don't want to make additional API calls just to get the users' data by their IDs.
So summing up my question is: Is there a way to leave those serializers in place but still be able to partially update my model without having to pass whole serialized data to the API (dicts instead of IDs)? Frankly, I don't care about the serializers, so maybe the question is this: Can I somehow make it possible to partially update my Products' related fields like owner or participants just by passing the related entities IDs while still maintaining an ability to get my projects with those fields expanded (serialized entities - dicts, instead of just IDs)?
#Edit:
My view:
from rest_framework import viewsets, permissions
from projects.models import Project
from projects.api.serializers import ProjectSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
lookup_field = "name"
def get_queryset(self):
if self.request.user.is_superuser:
return Project.objects.all()
else:
return Project.objects.filter(owner=self.request.user.id)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, participants=[self.request.user])
Answer:
To anyone reading this, I've solved this problem and I actually created a base class for all my viewsets that I want this behavior to be in:
from rest_framework.response import Response
class ReadWriteViewset:
write_serializer_class = None
read_serializer_class = None
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
write_serializer = self.write_serializer_class(
instance=instance,
data=request.data,
partial=partial,
)
write_serializer.is_valid(raise_exception=True)
self.perform_update(write_serializer)
read_serializer = self.read_serializer_class(instance)
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
Then you use it kinda like in here
I'm assuming that you are using a ModelViewSet. You could use different serializers for different methods.
class ProjectViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action in ['create', 'update']:
return WriteProjectSerializer # your serializer not using `UserSerializer` that works for updating
return ProjectSerializer # your default serializer with all data
Edit for using different serializers in same method:
# you can override `update` and use a different serializer in the response. The rest of the code is basically the default behavior
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
write_serializer = WriteProjectSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_update(serializer)
read_serializer = ProjectSerializer(instance)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
A good way to see the default code for all these methods is using Classy DRF. You can see all methods that come with using ModelViewSet and use that code with some changes. Here I'm using the default code for update but changing for a new serializer for the response.

How to serialize Inherited models in Django REST Framework

I'm working on a Django Rest Framework project, in which I have created the following models as:
from django.db import models
# Base Models...
choices = (
('Single', 'Single'),
('Multiple', 'Multiple'),
)
class UserAccountModel(models.Model):
deployment_name = models.CharField(max_length=150, blank=True)
credentials = models.FileField(upload_to='media/credentials/', name='credentials'),
project_name = models.CharField(max_length=150, blank=True)
project_id = models.CharField(max_length=100, blank=False, name='project_id')
cluster_name = models.CharField(max_length=150, blank=False)
zone_region = models.CharField(max_length=150, blank=False)
services = models.CharField(max_length=100, choices=choices)
def __str__(self):
return self.deployment_name
class AwdModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awdSource/', name='awd_source')
routing = models.TextField(name='routing', null=True)
def __str__(self):
return self.deployment_name
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
# def clean(self):
# if self.services == 'Multiple' and self.routing is None:
# raise ValidationError('You must have to provide routing for multiple services deployment.')
class AwodModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awodSource/', name='awod_source')
routing = models.TextField({'type': 'textarea'}, name='routing')
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
I need to serialize these models, Here's how I have implemented serializers for these models:
from rest_framework import serializers
from .models import UserAccountModel, AwdModel, AwodModel
class UserAccountSerializer(serializers.ModelSerializer):
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
class AWDSerializer(serializers.ModelSerializer):
class Meta(UserAccountSerializer.Meta):
model = AwdModel
fields = UserAccountSerializer.Meta.fields + ('awd_source', 'routing',)
class AWODSerializer(serializers.ModelSerializer):
class Meta:
model = AwodModel
fields = '__all__'
But, when I try to access, AWDSerialzer it return an error as:
AttributeError at /api/v1/deployments/
Got AttributeError when attempting to get a value for field project_id on serializer AWDSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
Original exception text was: 'QuerySet' object has no attribute 'project_id'.
Update: Here's my APIView code:
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments).data
return Response(data)
class DeploymentDetail(APIView):
def get(self, request, *args, **kwargs):
deployment = get_object_or_404(AwdModel, pk=kwargs['pk'])
data = AWDSerializer(deployment).data
return Response(data)
Help me, please!
Thanks in advance!
AttributeError at /api/v1/deployments/ Got AttributeError when
attempting to get a value for field project_id on serializer
AWDSerializer. The serializer field might be named incorrectly and not
match any attribute or key on the QuerySet instance. Original
exception text was: 'QuerySet' object has no attribute 'project_id'.
This is an attribute error, when attempting to get the value from field project_id .
Get rid of the name attribute in the project_id field.
Edit The APIView code
To serialize a queryset or list of objects instead of a single object
instance, you should pass the many=True flag when instantiating the
serializer. You can then pass a queryset or list of objects to be
serialized. [Serializing multiple objects]
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments, many=True).data
return Response(data)
I hope this will help.
The code that you posted appears to be valid and correct. The issue however is unrelated. The exception text 'QuerySet' object has no attribute 'project_id' Refers to an issue that likely originates from your restframework app's views.py file. The exception states that you are attempting to access the attribute 'project_id' from a QuerySet.
A QuerySet is a (lazy loaded) set of models and not a single model. Even if the query set had only one element you'd still be required to access that element before accessing it's attributes.
Because you haven't shared your views.py file I can't say for sure where the issue is however here is an incorrect use case example: MyModel.objects.all().project_id. Here we can see that I am attempting to access the attribute project_id from a query set. A correct use case would be MyModel.objects.all()[0].project_id. However this assumes that the query set is not empty.
Practically, most DjangoRestFramework views inherit from rest_framework.views.APIView which subclasses django's View Class. I would suggest checking the query_set within that class is being used correctly.
Feel free to share your implementation here for further comment.
[EDIT] - After views.py coded was added.
You are attempting to serializer an entire query set with the instantiation of a serializer data = AWDSerializer(deployments).data this is causing the attribute error.
I would recommend the generics.ListAPIView class and the use of the class attributes query_set and serializer_class. These are simple to implement. You can then invoke the APIViews default get method. Here is an example for your DeploymentsList view
from rest_framework import generics
class DeploymentsList(generics.ListAPIView):
serializer_class = AWDSerializer
queryset = AwdModel.objects.all()
def get(self, request, *args, **kwargs):
MAX_OBJECTS = int(20)
self.queryset = self.queryset[:MAX_OBJECTS]
return super(DeploymentsList, self).get(request, *args, **kwargs)
[EDIT] - FileField Serialization
In order to serialize the UserAccount.credentials file field so that we serializer the path, we can use the serializers.SerializerMethodField. I.e Your UserAccountSerializer becomes:
class UserAccountSerializer(serializers.ModelSerializer):
credentials = serializers.SerializerMethodField()
def get_credentials(self, user_account):
return user_account.credentials.path
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
When you inherit from a model class which is not defined as abstract in it’s own meta class, then Django creates a one-to-one relation between the subclass and its parent. Which actually creates two tables in the database; one for the base class and one for the subclass.
I haven’t tried your code, nor used Django 2, but would check using a relational field between the two serializer.

How to get string representation of PrimaryKeyRelatedField in JSON

I'm using Django REST Framework and I'm fairly newbie to this thing.
I want to have string representation for the manytomanyfield and foreignkey fields in my JSON output rather than the value.
models.py
class Movie(models.Model):
"""Movie objects"""
name = models.CharField(max_length=128)
directorName = models.ForeignKey(Director)
genre = models.ManyToManyField(Genre)
serializers.py
class MovieSerializer(serializers.ModelSerializer):
"""
Serialiazing all the Movies.
"""
genre = serializers.PrimaryKeyRelatedField(many=True, queryset=Genre.objects.all())
directorName = serializers.PrimaryKeyRelatedField(queryset=Director.objects.all())
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Movie
fields = ('popularity',"directorName",'genre','imdbScore','name','owner')
JSON output
{"popularity":"90.0","directorName":1,"genre":[1,2,3],"imdbScore":"8.9","name":"Titanic"}
instead of directorName's and genre's display_name, I'm getting only the values.
Please suggest me how to correct this.
EDIT
[SOLVED]
You need to override the to_representation() method of PrimaryKeyRelatedField as it returns the pk.
To do that, you need to override the to_representation() method of PrimaryKeyRelatedField as it returns the pk.
You can create a MyPrimaryKeyRelatedField which inherits from PrimaryKeyRelatedField and then override its to_representation() method.
Instead of value.pk which PrimaryKeyRelatedField returned, return the string representation now. I have used six.text_type() instead of str() to handle both the Python 2(unicode) and Python 3(str) versions.
from django.utils import six
from rest_framework import serializers
class MyPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
return six.text_type(value) # returns the string(Python3)/ unicode(Python2) representation now instead of pk
Your serializers.py would then look like:
class MovieSerializer(serializers.ModelSerializer):
"""
Serialiazing all the Movies.
"""
genre = MyPrimaryKeyRelatedField(many=True, queryset=Genre.objects.all())
directorName = MyPrimaryKeyRelatedField(queryset=Director.objects.all())
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Movie
fields = ('popularity',"directorName",'genre','imdbScore','name','owner')
The simplest is probably to use StringRelatedField
class MovieSerializer(serializers.ModelSerializer):
directorName = serializers.StringRelatedField(many=True)
class Director(Model):
# [...]
def __unicode__(self):
return self.directorName
However, that does not work when you need different representations of the Director model. In that case you need to go with a custom serializer (see answer from Rahul Gupta).

Categories

Resources