Allow related field in post request in DRF - django

I have created model with many to many relationship and I have join table when I keep additional variable for it:
class BorderStatus(models.Model):
STATUS_CHOICES = [("OP", "OPEN"), ("SEMI", "CAUTION"), ("CLOSED", "CLOSED")]
origin_country = models.ForeignKey(OriginCountry, on_delete=models.CASCADE, default="0")
destination = models.ForeignKey(Country, on_delete=models.CASCADE, default="0")
status = models.CharField(max_length=6, choices=STATUS_CHOICES, default="CLOSED")
extra = 1
class Meta:
unique_together = [("destination", "origin_country")]
verbose_name_plural = "Border Statuses"
def __str__(self):
return (
f"{self.origin_country.origin_country.name} -> {self.destination.name}"
f" ({self.status})"
)
Other models:
# Create your models here.
class Country(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='Country')
class Meta:
verbose_name_plural = "Countries"
def __str__(self):
return self.name
class OriginCountry(models.Model):
origin_country = models.ForeignKey(
Country, related_name="origins", on_delete=models.CASCADE
)
destinations = models.ManyToManyField(
Country, related_name="destinations", through="BorderStatus"
)
class Meta:
verbose_name_plural = "Origin Countries"
def __str__(self):
return self.origin_country.name
Here is my serializer for the endpoint:
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""Create serializer for editing single connection based on origin and destination name- to change status"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
And my endpoint:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
The problem Im having is that I cant create any new combination for the BorderStatus model in this serializer via post request.
If I remove the lines:
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
Then the form will work, but then I wont have the string representation of those variables, instead I get IDs.
Is there any way to allow request to accept origin_country and destination while being related fields?
EDIT:
To clarify how OriginCountry works, it is has a nested field:
[{ "id": 1
"origin_country": "Canada",
"dest_country": [
{
"id": 1,
"name": "France",
"status": "CLOSED"
},
{
"id": 2,
"name": "Canada",
"status": "OP"
}
]
},
]

You can try to override perform_create method of the viewset to make the necessary adjustments on-the-fly when new entry is posted:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
def perform_create(self, serializer):
origin_country, _ = models.Country.get_or_create(name=self.request.data.get('origin_country')
destination, _ = models.Country.get_or_create(name=self.request.data.get('destination')
return serializer.save(origin_country=origin_country, destination=destination)
Maybe you will also need to adjust your serializer to have:
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ['name']
class BorderStatusEditorSerializer(serializers.ModelSerializer):
origin_country = CountrySerializer()
destination = CountrySerializer()
...

Yes, I will try to give this combination.
You get this error because of Incorrect Type exception. Django checks data type validation on the serializer. For example here your dest_country returns a list of dicts but in your model it is a primary key (pk)
That's why on post django says : pk value expected, list received
But you can solve this error by using two different serializers (one to post another by default)
1. Create two different serializers
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""The First serialiser by default"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
class BorderStatusEditorCreateSerializer(serializers.ModelSerializer):
"""The Second serialiser for create"""
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
2.Add get_serializer_class method to for Viewset
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
serializer_classes = {
'create': BorderStatusEditorCreateSerializer, # serializer used on post
}
default_serializer_class = BorderStatusEditorSerializer # Your default serializer
def get_serializer_class(self):
return self.serializer_classes.get(self.action, self.default_serializer_class)

Related

How to post manytomany field value in Postman for API

I have a field which is ManyToMany. I would like to enter the value in POSTMAN for API post operation. But everytime It says: "This field is required." even though I provided the value.
Models:
class Day(models.Model):
day_name = models.CharField(
_("Day Name"), max_length=255, null=True, blank=True)
def __str__(self):
return self.day_name
class TutorProfile(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availablility = models.ManyToManyField(
Day,blank=True)
Serializer:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
Viewsets:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
With the following models.py (notice that your current Day.__str__ can raise an exception if day_name does not exist):
class Day(models.Model):
day_name = models.CharField(_("Day Name"), max_length=255, blank=True, null=True)
def __str__(self):
return self.day_name if self.day_name else "Unnamed"
class TutorProfile(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availability = models.ManyToManyField(Day, blank=True)
You do not need to explicitly add tutor_availability nor user as serializer fields:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = "__all__"
class TutorProfileSerializer(serializers.ModelSerializer):
# Omitting `image_url` as not reflected in `models.py`
# image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = TutorProfile
fields = "__all__"
With this viewset:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorProfileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
Then, after creating days with IDs 1 and 2 in admin, by sending the tutor_availability field as you are doing it, it should work. Request:
{
"user": 1,
"tutor_availability": [1, 2]
}
Response:
{
"id": 1,
"user": 1,
"tutor_availability": [
1,
2
]
}
Notice as well that I've changed availablility to availability and that it may be unsafe to allow authenticated users to pass the user field in the request, you may want to infer that from the user who makes the request.
In your TutorProfileSerializer you are using the DaySerializer for tutor_availablility field so when you do a post request your post action will wait for a list of dict, what you you need to do in first is to delete this line : from your TutorProfileSerializer and it will works.
tutor_availablility = DaySerializer(many=True)
If you still have the problem then you need to verify the validate method of the TutorProfileSerializer.
And if it works but you want a list of dict(of Day object) for GET request, you need to override the get_serializer_class() of your ViewSet and create two serializers one for post request and a second for get request:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
def get_serializer_class(self):
if self.action.method == 'GET':
return TutorGETProfileSerializer
return super(TutorprofileViewSet, self).get_serializer_class()
and the 2 serializers:
class TutorGETProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
class Meta:
model = TutorProfile
fields = '__all__'
read_only_fields = ('user',)

DRF update the nested foreign key of an object

I am trying to wrap my head around this for too long already :(
I need the following output for my frontend (specially the ID & name field in combination):
{
"serial": "e3461fb0",
"shipment": {
"id": 1,
"name": "via rotterdam"
}
}
model:
class Shipment(models.Model):
name = models.CharField("name", max_length = 128)
date = models.DateField()
class Product(models.Model):
serial = models.CharField("serial", max_length = 31, unique = True)
shipment = models.ForeignKey(Shipment, on_delete = models.CASCADE, blank = True, null = True)
serializer:
class ShipmentSerializer(serializers.ModelSerializer):
class Meta:
model = Shipment
fields = ["id", "name",]
class ProductSerializer(serializers.ModelSerializer):
shipment = ShipmentSerializer()
def update(self, instance, validated_data):
print("TEST:", instance, validated_data)
return super().update(instance, validated_data)
class Meta:
model = Product
fields = ["serial", "shipment",]
lookup_field = "serial"
read_only_fields = ["serial",]
ViewSet:
class ProductViewSet(ModelViewSet):
serializer_class = ProductSerializer
lookup_field = "serial"
http_method_names = ["get", "patch", "put"]
def get_queryset(self):
return Product.objects.all()
my problem here is the following: Lets say a product ist linked to a shipment and what I want now is to update that product by linking it to another shipment by using the id. But even in the HTML view of DRF I only get to see the name attribute of shipment. How can I only show/use the id here? I know I could modify the __str__ method of the model to return a string representation and split it later in the frontend, but I would like to avoid that if possible.
I was thinking of something like this:
Id field is what DRF make itself, and it's read-only by default
If you want to update that, you should override this field. Something like:
class ShipmentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
name = serializers.CharField(read_only=True)
class Meta:
model = Shipment
fields = ["id", "name",]

I have passed __all__ in fields of meta class and still i am getting back only one field. suggest a better way to fix it I want all the field there

I have included the serializer as well as the views.py, I am new so it might be possible that i made a very silly mistake here, which I am not able to figure out, please review this code and make me understand how to resolve this issue.
serializer.py
class ServiceSerializer(serializers.RelatedField):
def to_representation(self, value):
return value.name
class Meta:
model = Service
fields = ('name')
class SaloonSerializer(serializers.Serializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)
Here in the field of SaloonSerializers I have tried multiple things like only name field but still if get just one output which i have attache at the end of this post.
views.py
#api_view(['GET'])
def saloon_list(request):
if request.method=="GET":
saloons = Saloon.objects.all()
serializer = SaloonSerializer(saloons, many=True)
return Response(serializer.data)
models.py
class Service(models.Model):
name = models.CharField(max_length=200, blank=False)
price = models.IntegerField(blank=False)
time = models.IntegerField(blank=False)
def __str__(self):
return self.name
class Saloon(models.Model):
name = models.CharField(max_length = 200, blank=False)
services = models.ManyToManyField(Service)
def __str__(self):
return self.name
output:
[
{
"services": [
"Cutting",
"Shaving",
"Eye Brow",
"Waxing",
"Facial massage"
]
},
{
"services": [
"Cutting",
"Shaving",
"Facial massage"
]
}
]
You need to work with a ModelSerializer: a simple serializer does not care about a Meta, and will only serialize items for which you have specified a serializer field yourself.
You thus should rewrite this to:
# ModelSerializer ↓
class SaloonSerializer(serializers.ModelSerializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)

How to get fields from Serializer into ListView for filtering

the model in question:
class CustomerPrices(models.Model):
url = models.OneToOneField('CustomerProductUrls', models.DO_NOTHING, db_column="url", primary_key=True)
written = models.DateTimeField()
reseller = models.CharField(max_length=250)
price = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'customer_prices'
unique_together = (('url', 'written', 'reseller'),)
the serializer (and one related serializer) in question:
class CustomerProductUrlsSerializer(serializers.ModelSerializer):
ean = CustomerProductsSerializer()
url = serializers.CharField(max_length=255, required=False)
class Meta:
model = CustomerProductUrls
fields = '__all__'
class CustomerPricesSerializer(serializers.ModelSerializer):
written = serializers.DateTimeField(format='%Y-%m-%d %H:%M:00', required=False)
reseller = serializers.CharField(max_length=250, required=False)
url = CustomerProductUrlsSerializer()
name = serializers.SerializerMethodField('get_name')
ean = serializers.SerializerMethodField('get_ean')
url = serializers.SerializerMethodField('get_url')
price = serializers.FloatField()
class Meta:
model = CustomerPrices
fields = '__all__'
def get_name(self, obj):
return obj.url.ean.name
def get_ean(self, obj):
return obj.url.ean.ean
def get_url(self, obj):
return obj.url.url
and the ListAPIView for the CustomerPrices class:
class CustomerPricesListView(generics.ListAPIView):
serializer_class = CustomerPricesSerializer
filter_backends = (DjangoFilterBackend, OrderingFilter)
fields = ('written', 'url', 'price')
filter_fields = fields
search_fields = fields
def get_queryset(self):
"""
This view should return a list of all the products where price >= 70
"""
return CustomerPrices.objects.filter(price__gte=70)
Inside my CustomerPricesSerializer I've got a field named ean as well as name the values for these two come through the related CustomerProductUrlsSerializer (and corresponding model). The code is working so far, I get a response looking like this:
"results": [
{
"url": "https://www.example.com/p/12345678",
"written": "2020-04-05 12:00:00",
"reseller": "ABC123",
"name": "Product 12345678",
"ean": "1234567890123",
"price": 98.3
}, ...
]
I'm using the DjangoFilterBackend and I would like to filter on the ean as well as the name, which is available in my response. But as soon as I add ean to my fields tupel inside the Serializer I get the Meta.fields contains fields that are not defined on this FilterSet: ean. I do understand that my queryset is returning the fields from CustomerPrices Model inside my ListAPIView, but how do I get get the ean as well as the name to be a part of the queryset and therefore a part of the available fields
The field ean does not belong to the CustomerPrices model but somewhere else. The DRF expects the filter tuples are directly related to the model (here CustomerPrices) and hence the error.
To resolve this issue, you have to provide the actual relationship to the ean field.
class CustomerPricesListView(generics.ListAPIView):
fields = ('written', 'url', 'price', 'url__ean__ean', 'url__ean__name')
filter_fields = fields
# rest of the code

How can I make a writable ManyToManyField with a Through Model in Django Rest Framework?

I have a Product class that has "source products"; I use a many-to-many field with a through model to represent it (the database tables already exist, and that's the only way I found to configure the models):
class Product(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
product_name = models.TextField()
source_products = models.ManyToManyField('self', symmetrical=False, related_name='derived_products', through='Link', through_fields=('product', 'source'),)
class Meta:
db_table = 'product'
managed = False
class Link(models.Model):
product = models.ForeignKey('Product', models.CASCADE, db_column='uuid', related_name='source_links')
source = models.ForeignKey('Product', models.CASCADE, db_column='source_uuid', related_name='+')
class Meta:
db_table = 'link'
unique_together = (('product', 'source'),)
managed = False
My serializer is dead simple:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )
A GET request returns:
{
"product_name": "dummy.fra",
"source_products": [
"17b021e7-3d6b-4d29-a80b-895d62710080"
],
"uuid": "48c5a344-877e-4e3f-9a4b-2daa136b68fe"
}
The since ManyToManyFields with a Through Model are read-only, how do I go about to create a product including the link between products? I'd like to send a POST request that follows the same format as the GET response (i.e., a source_products field that lists UUIDs of existing products).
You can use PrimaryKeyRelatedField but you have to write custom create method.
class ProductSerializer(serializers.ModelSerializer):
source_products = serializers.PrimaryKeyRelatedField(many=True, queryset=Products.objects.all())
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )
def create(self, validated_data):
source_products = validated_data.pop('source_products', [])
product = Product.objects.create(**validated_data)
for source in source_products:
product.source_products.add(source)
return product
Your data will be like this
{
"product_name": "dummy.fra",
"source_products": [
"17b021e7-3d6b-4d29-a80b-895d62710080"
],
"uuid": "48c5a344-877e-4e3f-9a4b-2daa136b68fe"
}
The serializer has to override create() and access the unvalidated GET parameters (self.context['request'].data) directly:
class ProductSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# extract sources data
unvalidated_data = self.context['request'].data
sources_data = unvalidated_data.get('source_products', [])
# save objects
instance = Product.objects.create(**validated_data)
for uuid in sources_data:
source = Product.objects.get(pk=uuid)
instance.source_links.create(source=source)
return instance
class Meta:
model = Product
fields = ('uuid', 'product_name', 'source_products', )