Deserializing SlugRelatedField on ForeignKey with Django Rest Framework - django

I am struggling to set a PrimaryKeyRelatedField using a SlugRelatedField.
My models.py looks like:
class Airport(models.Model):
name = models.CharField(max_length=200, blank=True)
iata = models.CharField(max_length=20, blank=True)
<other unrelated fields>
class Flight(models.Model):
flight_number = models.CharField(max_length=25, blank=False, unique=True)
origin = models.ForeignKey(Airport, related_name='origin', null=True)
destination = models.ForeignKey(Airport, related_name='destination', null=True)
scheduled_departure = models.DateTimeField(null=True)
scheduled_arrival = models.DateTimeField(null=True)
My view looks like:
class FlightList(APIView):
# List all flights, or create a new flight
queryset = Flight.objects.all()
serializer_class = FlightSerializer
def get(self, request, format=None):
flights = Flight.objects.all()
serializer = FlightSerializer(flights, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = FlightSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
print serializer.validated_data
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
If I set my serializers.py to:
class FlightSerializer(serializers.ModelSerializer):
class Meta:
model = Flight
fields = ('flight_number', 'origin', 'destination', 'scheduled_departure', 'scheduled_arrival')
From the shell I see the serializer renders to:
FlightSerializer():
flight_number = CharField(max_length=25, validators=[<UniqueValidator(queryset=Flight.objects.all())>])
origin = PrimaryKeyRelatedField(allow_null=True, queryset=Airport.objects.all(), required=False)
destination = PrimaryKeyRelatedField(allow_null=True, queryset=Airport.objects.all(), required=False)
scheduled_departure = DateTimeField(allow_null=True, required=False)
scheduled_arrival = DateTimeField(allow_null=True, required=False)
I am able to POST origin and destination pk's that match up with Airport entries in the database to create new flights. Great.
However, I actually need to post the airport code, (i.e.) LAX as the origin or destination, and have DRF figure out the appropriate instance.
I think the way to do this is using SlugRelatedField. Despite best efforts, I'm unable to set this up properly.
I am trying to set serializers.py like this:
class FlightSerializer(serializers.ModelSerializer):
origin = serializers.SlugRelatedField(
many=True,
read_only=True,
slug_field='iata',
)
class Meta:
model = Flight
fields = ('flight_number', 'origin', 'destination', 'scheduled_departure', 'scheduled_arrival')
When I pass up a valid airport code (iata) like KMS as the origin it seems to get ignored.
Is this what SlugRelatedField is supposed to be used for?
What am I doing wrong to allow my POST to send up a string, have DRF look at a field for matching db entries and store that as beautifully as it does when I simply pass up a valid PK?

Got some help from a colleague on this.
The problem was I was just misconfiguration of SlugRelatedField. To get at the airport codes, I needed to use a queryset and set the slug_field. That's all:
class FlightSerializer(serializers.ModelSerializer):
origin = serializers.SlugRelatedField(
slug_field='iata',
queryset=Airport.objects.all()
)
class Meta:
model = Flight
fields = ('flight_number', 'origin', 'destination', 'scheduled_departure', 'scheduled_arrival')

Related

unique_together does not prevent duplicate records

I have two models like this :
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
def __str__(self):
return self.user.username
class Meta:
unique_together = ('user', 'place_detail',)
this is the json i post to my apiview :
{
"lat": "29.621142463088336",
"lng": "52.520185499694527",
"name":"cafesama1",
"address":"streetn1",
"type":"cafe"
}
in views.py :
class PreferedLocationsOfUsers(APIView):
def post(self, request, format=None):
serializer = PreferLocationSerializer(data=request.data)
if serializer.is_valid():
location= Preference(**serializer.data)
location.save()
user_perefeces = PrefenceOfUser(user=request.user,place_detail=location)
user_perefeces.save()
return Response({'location saved'},status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
i want to prevent the user to save dublicated records in database but when location object is saved in PrefenceOfUser unique_together does not prevent dublicating. any idea why?
You do have a migration issue as per the comments. Get rid of the duplicates, re-run the migration until you get no errors and you're fine.
However, I would turn this around as a model design problem. You shouldn't need to resort to manual constraints for this.
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
users = models.ManyToManyField(get_user_model(), blank=True)
This above code will have exactly the same implications as yours, is cleaner and more Djangoesque.
You can still access what you call PrefenceOfUser through Preference.users.through. If you plan to add more attributes to the selection (ie. when did user add their preference), you can just do:
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
extra_field = models.WhateverField()
def __str__(self):
return self.user.username
and change the Preference.users to
models.ManyToManyField(get_user_model(), through=PrefenceOfUser)
which will still still ensure the uniqueness.

Django Serializer defining initial object

I can't seem to figure out how to pass in an initial value to a serializer. I have a multitenant django site and I am trying to now get APIs setup. The client field exists but needs to be hidden and read only. I thought this worked like a form and a view in traditional django. I would normally pass in the get_initial in the view. I tried that first but it doesn't work. I think I need to get the value directly in the serializer but I can't seem to get it to work.
model:
class Location(ClientAwareModel):
name = models.CharField(max_length=64, blank=True)
address = models.CharField(max_length=64)
address2 = models.CharField(max_length=64, blank=True)
city = models.CharField(max_length=64)
state = USStateField()
zip_code = USZipCodeField()
class Client(models.Model):
name = models.CharField(max_length=100)
subdomain_prefix = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class ClientAwareModel(models.Model):
client = models.ForeignKey(Client, on_delete=models.PROTECT)
class Meta:
abstract = True
def hostname_from_request(request):
# split on `:` to remove port
return request.get_host().split(':')[0].lower()
def client_from_request(request):
hostname = hostname_from_request(request)
subdomain_prefix = hostname.split('.')[0]
return Client.objects.filter(subdomain_prefix=subdomain_prefix).first()
serializer (you can see all my failed attempts commented out:
class LocationSerializer(serializers.ModelSerializer):
def get_client(self, obj):
# return client_from_request(self.request)
return client_from_request(self.context['request'])
# client = serializers.SerializerMethodField('get_client')
# client = serializers.SerializerMethodField()
# client = serializers.Field(source='get_client', read_only=True)
# client = serializers.ReadOnlyField(source='get_client')
# client = serializers.PrimaryKeyRelatedField(read_only=True, default='get_client')
client = serializers.PrimaryKeyRelatedField(read_only=True, source='get_client')
# client = serializers.HiddenField(default=get_client(self))
class Meta:
model = Location
fields = ['name', 'address', 'address2', 'city', 'state', 'zip_code', 'client']
viewset:
class LocationViewSet(viewsets.ModelViewSet):
queryset = Location.objects.all()
serializer_class = LocationSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
client = client_from_request(self.request)
return super().get_queryset().filter(client=client)
you can see the different ways I tried to pass in the value but no matter what I do I get
IntegrityError at /locations/
null value in column "client_id" violates not-null constraint
One easy way to pass the client object to serializer is to pass it in perform_create method, something like:
from rest_framework import serializers
class LocationViewSet(viewsets.ModelViewSet):
queryset = Location.objects.all()
serializer_class = LocationSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
client = client_from_request(self.request)
return super().get_queryset().filter(client=client)
def perform_create(self, serializer):
client = client_from_request(self.request)
if not client:
raise serializers.ValidationError("Client does not exist")
serializer.save(client=client)
And also remove the client field from your serializer:
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ['name', 'address', 'address2', 'city', 'state', 'zip_code']
In your viewset you are trying to get Location by filtering Client when your Location model does not have a Client FK.
Right here:
def get_queryset(self):
client = client_from_request(self.request)
return super().get_queryset().filter(client=client) <----
As #gregory mentioned in the comment above, adding Foreign Key would solve your problem, then you can just simply add it to your serializer.

Always getting {"detail":"Unsupported media type \"application/json\" in request."} error when I try to post data on postman

I am working on a project that requires me to upload an image. However when I am trying to upload one and posting I ma getting the above error. I have no clue what to do anymore.
I have already tried using FileUploadParser and creating class Base64ImageField too. Please Help.
models
class UserProfile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE, default=None, null=True)
avatar = models.ImageField(upload_to='', blank=True, null=True)
code = models.CharField(max_length=8, unique=True, default=unique_rand)
emailVerified = models.NullBooleanField(null=True, default=None)
facebookId = models.CharField( null=True,unique=True, default=None,max_length=255)
googleId = models.CharField(null=True,unique=True,default=None,max_length=255)
phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$')
mobile = models.CharField(validators=[phone_regex, MinLengthValidator(10)], max_length=10, null=True, default=None)
mobileVerified = models.NullBooleanField(null=True,default=None)
status = models.BooleanField(default=False)
serializers
class UserProfileSerializer(serializers.ModelSerializer):
user = UserSerializer()
avatar = Base64ImageField(required=False)
code = serializers.CharField(read_only=True)
serializers.FileField(use_url=False)
class Meta:
model = UserProfile
fields = '__all__'
extra_kwargs = {'user': {'required': False}}
def create(self, validated_data):
user_data = validated_data.pop('user')
user = User.objects.create(**user_data)
image = validated_data.pop('avatar')
upr=UserProfile.objects.create(user=user,image=image,**validated_data)
return upr
views
class UserCreate(generics.ListCreateAPIView):
serializer_class = UserProfileSerializer
user_serializer = UserSerializer
queryset = UserProfile.objects.all()
parser_classes = (FormParser,MultiPartParser)
def pre_save(self, request):
request.avatar = self.request.FILES.get('file')
def post(self, request):
print(request.data)
serializer= UserProfileSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Maybe try using "multipart form" instead of JSON as your POST payload type. For the value (I am using Insomnia), select the dropdown on the right and select "File" instead of "Text". Then upload a file.
That worked for me, not sure if it's the same problem. Hope that helps!
Here's a post that has the answer you're looking for.
Posting a File and Associated Data to a RESTful WebService preferably as JSON

Invalid data. Expected a dictionary, but got str error with serializer field in Django Rest Framework

I'm using Django 2.x and Django REST Framework.
I have two models like
class Contact(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.PROTECT)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100, blank=True, null=True)
modified = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
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)
the serializer.py the file has serializers defined as
class ContactSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contact
fields = ('id', 'first_name', 'last_name', 'created', 'modified')
class AmountGivenSerializer(serializers.ModelSerializer):
contact = ContactSerializer()
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'amount', 'given_date', 'created'
)
views.py
class AmountGivenViewSet(viewsets.ModelViewSet):
serializer_class = AmountGivenSerializer
def perform_create(self, serializer):
save_data = {}
contact_pk = self.request.data.get('contact', None)
if not contact_pk:
raise ValidationError({'contact': ['Contact is required']})
contact = Contact.objects.filter(
user=self.request.user,
pk=contact_pk
).first()
if not contact:
raise ValidationError({'contact': ['Contact does not exists']})
save_data['contact'] = contact
serializer.save(**save_data)
But when I add a new record to AmountGiven model and passing contact id in contact field
it is giving error as
{"contact":{"non_field_errors":["Invalid data. Expected a dictionary, but got str."]}}
When I remove contact = ContactSerializer() from AmountGivenSerializer, it works fine as expected but then in response as depth is set to 1, the contact data contains only model fields and not other property fields defined.
I'm not a big fan of this request parsing pattern. From what I understand, you want to be able to see all the contact's details when you retrieve an AmountGiven object and at the same time be able to create and update AmountGiven by just providing the contact id.
So you can change your AmountGiven serializer to have 2 fields for the contact model field. Like this:
class AmountGivenSerializer(serializers.ModelSerializer):
contact_detail = ContactSerializer(source='contact', read_only=True)
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
Note that the contact_detail field has a source attribute.
Now the default functionality for create and update should work out of the box (validation and everything).
And when you retrieve an AmountGiven object, you should get all the details for the contact in the contact_detail field.
Update
I missed that you need to check whether the Contact belongs to a user (however, I don't see a user field on your Contact model, maybe you missed posting it). You can simplify that check:
class AmountGivenViewSet(viewsets.ModelViewSet):
serializer_class = AmountGivenSerializer
def perform_create(self, serializer):
contact = serializer.validated_data.get('contact')
if contact.user != self.request.user:
raise ValidationError({'contact': ['Not a valid contact']})
serializer.save()
Override the __init__() method of AmountGivenSerializer as
class AmountGivenSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(AmountGivenSerializer, self).__init__(*args, **kwargs)
if 'view' in self.context and self.context['view'].action != 'create':
self.fields.update({"contact": ContactSerializer()})
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'amount', 'given_date', 'created'
)
Description
The issue was the DRF expects a dict like object from contact field since you are defined a nested serializer. So, I removed the nested relationship dynamically with the help of overriding the __init__() method
For those who got here but have relatively simple serializers, this error can also occur when the request data is malformed, in my case JSON encoded twice.
The serializer will decode the JSON, but as it is encoded twice request.data will still be a string. The error therefore makes sense as a "dictionnary" was expected, but we still have a "string".
You can check the output of the following to confirm whether this is the issue you are experiencing:
print(type(request.data)) # Should be <class 'dict'>

Related Object serializer Django restframework

My question is somewhat related to this one with some differences. I have a model similar to this one:
class Project(models.Model):
project_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
created_by_id = models.ForeignKey('auth.User', related_name='project', on_delete=models.SET_NULL, blank=True, null=True)
created_by = models.CharField(max_length=255, default="unknown")
created = models.DateTimeField(auto_now_add=True)
With the following serializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.ReadOnlyField(source='created_by_id.username')
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created')
And corresponding view:
class projectsView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(created_by_id=self.request.user)
This code behaves like I want but forces information redundancy and does not leverage the underlying relationnal database. I tried to use the info from the linked question to achieve a "write user id on database but return username on "get"" in a flat json without success:
Removing the "created_by" field in the model. Replacing the serializer with:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Which would NOT 100% give me what I want, i.e. replace the user id with the username in a flat json but return something like: {'project_id': <uuid>, 'created_by': <user json object>, 'created': <data>}. But still I get a {'created_by_id': ['This field is required.']} 400 error.
Question: How can I write a user id to a database object from the request.user information to refer to an actual user id but return a simple username in the GET request on the projectsView endpoint without explicitly storing the username in the Model? Or more generally speaking, how can I serialize database objects (Django models) into customer json response by using default serialization DRF features and default DRF views mixins?
Alternate formulation of the question: How can I store an ID reference to another DB record in my model (that can be accessed without it being supplied by the payload) but deserialize a derived information from that object reference at the serializer level such as one specific field of the referenced object?
I would recommend you to use Two different serializers for Get and POST operations. Change your serializers.py as
class ProjectGetSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by=validated_data['created_by_id'].username)
class Meta:
model = Project
fields = '__all__'
Also, I reccomend ModelViewSet for API class if you are looking for CRUD operations. Hence the view will be like this,
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return ProjectCreateSerializer
return ProjectGetSerializer
So, the payload to create Project is,
{
}
One thing you should remember, while you trying to create Project user must logged-in
UPDATE - 1
serializer.py
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by_id=self.context['request'].user)
views.py
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectCreateSerializer
The error is in the write_only field options. The required parameter default value is set to True while the intent is to not make it required if we take a look at the model. Here in the view, I use the perform_create as post processing to save on the Model DB representation. Since required default value is True at the creation level, the first .save() to the DB fails. Since this is purely internal logic, the required is not necessary. So simply adding the required=False option on the PrimaryKeyRelatedField does the job:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Enforcing the required=True at the Model level as well would require to override the .save function of the serializer if I insist on playing with the logic purely at the serializer level for deserialization. There might be a way to get the user ref within the serializer as well to keep the views implementation even more 'default'... This can be done by using the default value from Jerin:
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by',
write_only=True,
required=False,
default=serializers.CurrentUserDefault())
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Now to flaten the json with username only, you need to use a slug field instead of the UserSerializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.SlugRelatedField(
queryset=User.objects.all(), slug_field="username")
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
And then only the username field value of the User Model will show at the create_by json tag on the get payload.
UPDATE - 1
After some more tweaking here is the final version I came up with:
class ProjectSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), write_only=True, required=False, default=serializers.CurrentUserDefault())
created_by = serializers.SerializerMethodField('creator')
def creator(self, obj):
return obj.created_by_id.username
class Meta:
model = Project
fields = ('project_id', 'created_by_id', 'created_by', 'created')