Django nested serializer allow_null=True - django

I have a nested serializer and I want to activate the allow_null to true, but it doesn't work.
TOP object have a nested Down object, the related_name must be present in the TOP object but with a null value. If the down object is not null all down object fields are required.
Example request with all fields in down object (this one works fine) :
{
"title": "Titre new rgfdgfdgthtrh",
"downs": {
"type": "Type example",
"is_external": true,
},
}
Example that i tryed to do : request when down object is null (this one doesn't work)
{
"title": "Titre new ",
"downs": {},
}
I have tryed with "downs": None or Null without success.
My views :
# My Views.py
class Top(models.Model):
class Meta:
verbose_name = _('Top')
verbose_name_plural = _('Tops')
top_guid = models.UUIDField(
primary_key=True,
unique=True,
default=uuid.uuid4,
editable=False)
title = models.CharField(
help_text=_('Title'),
verbose_name=_('title'),
max_length=100,
blank=False
)
class Down(models.Model):
top = models.OneToOneField(
Top,
on_delete=models.CASCADE,
help_text=_('Top'),
verbose_name="top",
related_name="downs"
)
type = models.CharField(
help_text=_('Type'),
verbose_name=_('type'),
max_length=30,
blank=False
)
is_external = models.BooleanField(
help_text=_('external (default = false)'),
verbose_name=_('external'),
blank=False,
default=False
)
and my serializers
# My serializers.py
class DownSerializer(serializers.ModelSerializer):
class Meta:
model = Down
fields = '__all__'
class TopSerializer(serializers.ModelSerializer):
downs = DownSerializer(many=False, required=False, allow_null=True)
class Meta:
model = Top
fields = ('top_guid', 'title', 'downs',)
def create(self, validated_data):
"""
Create and return a new `Topic` instance.
"""
downs_data = validated_data.pop('downs')
top = Top.objects.create(**validated_data)
Down.objects.create(top=top, **downs_data)
return top
def update(self, instance, validated_data):
"""
Update and return an existing `Topic` instance.
"""
# get bim_snippet data and bim_snippet object
downs_data = validated_data.pop('downs')
downs = instance.downs
# update top data and save top object
instance.title = validated_data.get('title', instance.title)
instance.top_type = validated_data.get('top_type', instance.top_type)
instance.save()
# update down data and save down object
downs.snippet_type = downs_data.get('type', downs.snippet_type)
downs.is_external = downs_data.get('is_external', downs.is_external)
downs.save()
return instance
Thank's a lot.

I think that if you add arguments like allow_null=True or read_only=False in your serializer class, you need to recreate your sqlite3 database. read_only was not working, but just after recreate the db it works fine. (makemigrations and migrate seem's to be not enought)

Related

Allow related field in post request in DRF

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)

django-rest-framework access field inside serializer

So I have a model like this
class DataSheet(BaseModel):
"""
Represents a single dataSheet.
dataSheets have their own model at the core. Model data is added to
the dataSheets in the form of separate records.
"""
class Meta:
verbose_name = 'datasheet'
verbose_name_plural = 'datasheets'
ordering = ['position', 'cluster']
required_db_features = {
'supports_deferrable_unique_constraints',
}
constraints = [
models.UniqueConstraint(
fields=['position', 'cluster'],
name='deferrable_unique_datasheet_position',
deferrable=models.Deferrable.DEFERRED
)
]
def __str__(self):
return self.name
objects = managers.DataSheetsManager()
positions = managers.PositionalManager()
position = models.PositiveSmallIntegerField(db_index=True, editable=True)
name = models.CharField(max_length=100, validators=[MinLengthValidator(2)], db_index=True)
description = models.CharField(max_length=1024, null=True, blank=True, db_index=True)
owner = models.ForeignKey('api_backend.Member', on_delete=models.CASCADE, db_index=True, editable=False)
fields = models.ManyToManyField('api_backend.Field')
overwrites = models.ManyToManyField('api_backend.RoleOverwrite')
parent = models.ForeignKey('api_backend.Category', on_delete=models.CASCADE, null=True, blank=True)
cluster = models.ForeignKey('api_backend.Cluster', on_delete=models.CASCADE, editable=False)
REQUIRED_FIELDS = [name, owner, cluster]
and a serializer like this
class DataSheetSerializer(serializers.ModelSerializer):
"""
A serialized DataSheet Object.
Datasheets have their own:
- array of fields
- array of role-overwrites
"""
def get_fields(self):
fields = super(DataSheetSerializer, self).get_fields()
fields['parent'].queryset = self.cluster.categories.all()
return fields
class Meta:
model = DataSheet
read_only_fields = ['position']
fields = '__all__'
# need to make sure that the parent category of the datasheet
# belongs to the datasheet's cluster only.
fields = partial.PartialFieldSerializer(many=True, read_only=True)
overwrites = partial.PartialOverWriteSerializer(many=True, read_only=True)
the thing is, I want to access the serializer model's cluster field inside of the get_fields method. However, I couldn't do the same. Can someone help me?
I've seen other answers involving initial_data, but that doesn't work here.
fields['parent'].queryset = self.cluster.categories.all()
cluster is an unresolved reference here.
self in get_fields is DataSheetSerializer instance not DataSheet model instance. hence it should not have cluster property. you can not access model DataSheet instance in get_fields as it gets fields from class DataSheet not from its instance. you can validate the field like
class DataSheetSerializer(serializers.ModelSerializer):
# ... other code
def validate(self, data):
parent = data.get('parent')
# check if parent is valid i.e in queryset
# if yes return data
# else raise serializers.validationError

IntegrityError: null value in column "invoiceOwner_id" violates not-null constraint

In my django app, i'm having difficulties whenever i want to add a new object that uses the table paymentInvoice.
The error i'm getting from my api looks like this
IntegrityError at /api/clients/invoice/
null value in column "invoiceOwner_id" violates not-null constraint
DETAIL: Failing row contains (10, INV-0006, Lix, 2020-08-04, 1, Pending, 3000, null).
NB: I haven't created the field invoiceOwner_id, postgres automatically added it or rather is using it as a representation for my invoiceOwner field
class Purchaser(models.Model):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=20, unique=True)
email = models.EmailField(max_length=255, unique=True, blank=True)
image = models.ImageField(default='default.png', upload_to='customer_photos/%Y/%m/%d/')
data_added = models.DateField(default=datetime.date.today)
def __str__(self):
return self.name
class paymentInvoice(models.Model):
invoiceNo = models.CharField(max_length=50, unique=True, default=increment_invoice_number)
invoiceOwner = models.ForeignKey(Purchaser, on_delete=models.CASCADE, related_name="invoice_detail")
product = models.CharField(max_length=50, blank=True)
date = models.DateField(default=datetime.date.today)
quantity = models.PositiveSmallIntegerField(blank=True, default=1)
payment_made = models.IntegerField(default=0)
def __str__(self):
return self.invoiceOwner.name
serilizers file
class paymentInvoiceSerializer(serializers.ModelSerializer):
invoiceOwner = serializers.SerializerMethodField()
class Meta:
model = paymentInvoice
fields = '__all__'
def get_invoiceOwner(self, instance):
return instance.invoiceOwner.name
views file
class paymentInvoiceListCreateView(ListCreateAPIView):
serializer_class = paymentInvoiceSerializer
queryset = paymentInvoice.objects.all().order_by('-date')
GET result from api call.
{
"id": 1,
"invoiceOwner": "Martin",
"invoiceNo": "INV-0001",
"product": "",
"date": "2020-08-04",
"quantity": 1,
"payment_made": 0
}
Tried passing below as POST but got the main error
{
"invoiceOwner": "Becky",
"product": "Lix",
"quantity": 1,
"payment_made": 3000
}
"invoiceOwner" in your serializers.py is a SerializerMethodField which is readonly
that's why you get an error, you have to define the create method yourself
As I said in comment: You need to explicitly override the create method in your serializer since your model has foreign key invoiceOwner, just to create that instance first as a Purchaser instance.
You can try the code below:
class paymentInvoiceSerializer(serializers.ModelSerializer):
invoiceOwner = serializers.SerializerMethodField()
class Meta:
model = paymentInvoice
fields = '__all__'
def get_invoiceOwner(self, instance):
return instance.invoiceOwner.name
def create(self, validated_data):
purchaser_name = validated_data.get("invoiceOwner")
purchaser = Purchaser(name=purchaser_name,
# you need to have phone, email, since these fields are unique,
# they can't remain null
)
purchaser.save()
return paymentInvoice.objects.create(invoiceOwner = purchaser, **validated_data)

Django REST Framework update foreign key field on PUT

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.

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', )