I'm using Django 2.x and Django REST Framework
My models.py file contents
class ModeOfPayment(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField()
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()
mode_of_payment = models.ForeignKey(
ModeOfPayment,
on_delete=models.PROTECT,
blank=True,
default=None,
null=True
)
and serializers.py
class ModeOfPaymentSerializer(serializers.ModelSerializer):
class Meta:
model = ModeOfPayment
fields = ('id', 'title')
class AmountGivenSerializer(serializers.ModelSerializer):
mode_of_payment = ModeOfPaymentSerializer()
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'amount', 'mode_of_payment',
)
def update(self, instance, validated_data):
mode_of_payment = validated_data.pop('mode_of_payment')
instance.mode_of_payment_id = mode_of_payment.id
return instance
and views.py
class AmountGivenViewSet(viewsets.ModelViewSet):
serializer_class = AmountGivenSerializer
permission_classes = (IsAuthenticated, AdminAuthenticationPermission,)
filter_fields = ('contact__id',)
def get_queryset(self):
queryset = AmountGiven.objects.filter(
contact__user=self.request.user
)
return queryset
But when I post data using postman with PUT method to update the existing record
It still says
{
"mode_of_payment": [
"This field is required."
]
}
Edit 2: Response after Daniel answer
{
"id": "326218dc-66ab-4c01-95dc-ce85f226012d",
"contact": {
"id": "b1b87766-86c5-4029-aa7f-887f436d6a6e",
"first_name": "Prince",
"last_name": "Raj",
"user": 3
},
"amount": 3000,
"mode_of_payment": "0cd51796-a423-4b75-a0b5-80c03f7b1e65",
}
You've told AmountSerializer to accept a nested dict representing a ModeOfPayment instance, by setting the mode_of_payment field to ModeOfPaymentSerializer. But that's not what you're sending; you're sending the ID of the ModeOfPayment.
You should remove that line in AmountGivenSerializer.
Edit
I was wrong, you need to declare the field explicitly as a PrimaryKeyRelatedField:
class AmountGivenSerializer(serializers.ModelSerializer):
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
class Meta:
...
Now it will accept a UUID in the data.
Related
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',)
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)
I've got established a relationship between 2 models: Order and OrderLine. I've created serializers for both of them following the DRF documentation, yet when printing the serializer.data the nested objects don't show.
Here are my models:
class Order(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
session_id = models.CharField(max_length=256)
class OrderLine(models.Model):
order_id = models.ForeignKey(Order, on_delete=models.DO_NOTHING)
product_id = models.ForeignKey(Product, on_delete=models.DO_NOTHING)
price = models.DecimalField(decimal_places=2, max_digits=20)
quantity = models.IntegerField()
total = models.DecimalField(decimal_places=2, max_digits=20)
created_at = models.DateField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
These are the serializers:
from rest_framework import serializers
from .models import Order, OrderLine
class OrderLineSerializer(serializers.ModelSerializer):
"""
OrderLine serializer
"""
class Meta:
model = OrderLine
fields = ['product_id', 'price', 'quantity', 'total']
class OrderSerializer(serializers.ModelSerializer):
"""
Order serializer
"""
items = OrderLineSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ['session_id', 'subtotal', 'total', 'items']
read_only_fields = ['id']
and this is the view:
class OrderAPIViewSet(viewsets.ViewSet):
def create(self, request):
order = Order.objects.create(session_id=request.data['session_id'])
for item in request.data['items']:
product = Product.objects.get(pk=item['product_id'])
total = Decimal(item['price'] * item['quantity'])
OrderLine.objects.create(
order_id=order,
product_id=product,
price=Decimal(item['price']),
quantity=item['quantity'],
total=total
)
serializer = OrderSerializer(instance=order)
print("HERE")
print(serializer.data)
return Response(status=status.HTTP_200_OK)
From my REST client I'm posting the following object:
{
"session_id":uuid,
"items": [
{
"product_id": product.id,
"price": 5.80,
"quantity": 2,
}
]
}
but when the print statement in the view is executed this is what's being printed:
{
"session_id":"4def7bdb-dedb-46aa-9c70-1d9e4f522149",
"subtotal":"0.00",
"total":"0.00"
}
notice the the items subresource is not being included.
What am i missing?
Because you passed read_only=True parameter to OrderLineSerializer. When your serializer's builtin method to_internal_value() run, clear data not including in your fields list. For more details: read_only
I'm creating an API which list and save transactions. My Transactions Model has a FK to a Category model. My goal when creating a Transaction is to also create the Category if the category is new.
The first time I create the transaction it successfully creates the new category and transaction. The next time I attempt the create a transaction with the existing category, I get an error that the category already exists. In my Transaction serializer I added a create method that should be using get_or_create for the category. However I'm still getting an error on my unique fields. My expectation is that it would be returning the existing Category.
It seems like it's throwing the error before it gets to the create method in the Transaction serializer before it has a chance to use get_or_create.
Models:
class Category(models.Model):
name = models.CharField(max_length=128, null=False)
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
created_time = models.DateTimeField(auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('name', 'owner')
class Transaction(models.Model):
date = models.DateField()
payee = models.CharField(max_length=256)
category = models.ForeignKey(Category,
related_name='category',
on_delete=models.CASCADE)
amount = MoneyField(max_digits=19,
decimal_places=2,
default_currency='USD')
created_time = models.DateTimeField(db_index=True,
auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
Serializers:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
class TransactionSerializer(serializers.ModelSerializer):
balance = serializers.DecimalField(
decimal_places=2, max_digits=19, read_only=True)
category = CategorySerializer(many=False, read_only=False)
class Meta:
model = Transaction
fields = ('id', 'date', 'payee', 'category',
'amount', 'balance', 'created_time', 'modified_time',
'is_cleared', 'paid_or_deposited')
def create(self, validated_data):
category_data = validated_data.pop('category')
category, created = Category.objects.get_or_create(**category_data)
transaction = Transaction.objects.create(category=category,
**validated_data)
return transaction
POST:
{
"date": "2018-12-19",
"payee": "Test",
"category": {"owner": 1, "name": "TEST"},
"amount": "-134"
}
Error:
{
"category": {
"non_field_errors": [
"The fields name, owner must make a unique set."
]
}
}
You're right about not reaching your create() method.
This happens because ModelSerializer by default creates validators based on your model Meta.unique_together value: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
Simplest way to disable this type of validators is to override get_unique_together_validators for your serializer:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
def get_unique_together_validators(self):
return []
Another solution, which is cleaner is to override Meta.validations of your CategorySerializer*:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
validators = []
* be aware that this will disable serializer validators unique_for_date, unique_for_month and unique_for_year that come from model
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', )