Django model nested serializer - django

I have the model structure like below
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct)
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='allowed_product')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
The query:
Product.objects.filter(condition__allow=1, condition__check=1)
I want format something like below
Base Product and inside that list of products based on allow and check filter
[
{
"name": "BaseProduct 1",
"products": [
{
"name": "TV",
}, {}, ....
]
},
........
]

Try it, if you use django rest framework
from rest_framework import serializers
from rest_framework.fields import empty
from django.utils.functional import cached_property
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('name')
class BaseProductSerializer(serializers.ModelSerializer):
products = serializers.SerializerMethodField()
class Meta:
model = BaseProduct
fields = ('name', 'products')
def __init__(self, instance=None, data=empty, **kwargs):
self._condition_allow = kwargs.pop('condition_allow', 1)
super(BaseProductSerializer, self).__init__(instance=None, data=empty, **kwargs)
#cached_property
def _request_data(self):
request = self.context.get('request')
# if POST
# return request.data if request else {}
# if GET params
return request.query_params if request else {}
#cached_property
def _condition(self):
return self._request_data.get('CONDITION_PARAM_NAME')
def get_products(self, obj):
qs = obj.product_set.filter(condition__allow=self._condition_allow, condition__check=1)
serializer = ProductSerializer(qs, many=True)
# ^^^^^
return serializer.data
in view
serialiser(qs, condition_allow=5)

Change your models to have related_name for the foreignkeys to have reverse relationship:
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct, related_name='products')
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='conditions')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
So now you can use it in your serializers:
class BaseProductSerializer:
class Meta:
model = BaseProduct
fields = ('name', 'products',)
class ProductSerializer:
class Meta:
model = Product
fields = ('conditions',)
class ConditionSerializer:
class Meta:
model = Condition
fields = '__all__'
Finally in your views, change this:
Product.objects.filter(condition__allow=1, condition__check=1)
into this:
BaseProduct.objects.filter(products__conditions__allow=1, products__conditions__allow=1)
And hopefully, this should generate JSON data in the format that you asked for.

Related

Django Rest Framework how to display related models as one json file

I have a few different models with a fact table of scenarios and 6 dimension tables that relate to it below as examples:
class fScenario(models.Model):
#Variables
scenarioId = models.IntegerField(primary_key=True)
description = models.CharField(max_length=100)
def __str__(self):
return str(self.scenarioId)
def get_absolute_url(self):
return reverse('scenario-detail', args=[str(self.scenarioId)])
class Meta:
ordering = ['scenarioId']
class dADA(models.Model):
#Variables
scenarioId = models.ForeignKey(fScenario, on_delete=models.CASCADE)
dateTimeId = models.DateTimeField('ADA Time/Date')
latitutde = models.FloatField(default=0)
longitude = models.FloatField(default=0)
instanceType = models.CharField(max_length=50, default='ADA')
def __str__(self):
return f'{self.scenarioId}, {self.instanceType}'
class Meta:
ordering = ['dateTimeId']
serializers.py
class fScenarioSerializer(serializers.ModelSerializer):
class Meta:
model = fScenario
fields = ['scenarioId', 'description']
class dADASerializer(serializers.ModelSerializer):
scenarioId = fScenarioSerializer(read_only=True)
class Meta:
model = dADA
fields = ['scenarioId', 'dateTimeId', 'latitutde', 'longitude', 'instanceType']
urls.py
router = routers.DefaultRouter()
router.register(r'fScenario', views.fScenarioViewSet)
router.register(r'dADA', views.dADAViewSet)
urlpatterns = [
path('', include(router.urls)),
views.py
class fScenarioViewSet(viewsets.ModelViewSet):
queryset = fScenario.objects.all()
serializer_class = fScenarioSerializer
class dADAViewSet(viewsets.ModelViewSet):
queryset = dADA.objects.all()
serializer_class = dADASerializer
I'm using rest framework view sets and I currently can see in my API separate fScenario and dADA views but cant figure out how to link the dADA to the fScenario view on one page.
You can use SerializerMethodField and Django many-to-one backward relation to retrieve related objects:
serializers.py
class fScenarioSerializer(serializers.ModelSerializer):
dADA_list = serializers.SerializerMethodField()
class Meta:
model = fScenario
fields = ['scenarioId', 'description', 'dADA_list']
def get_dADA_list(self, obj):
return obj.dada_set.all().values()

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

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)

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

Serializing joined tables in serializers rest framework

So i am trying to serialize multiple joined tables with django serializers. I cant find a way to do this. The query being executed is raw sql.
The models are as below
class UserDetail(models.Model):
user = models.OneToOneField(User, on_delete = models.CASCADE)
mobile_number = models.IntegerField()
national_id = models.CharField(max_length = 30)
date_created = models.DateTimeField(auto_now_add = True)
address = models.CharField(max_length = 250)
merchant_name = models.CharField(null = True, max_length = 30)
class Account(models.Model):
user = models.OneToOneField(User, on_delete = models.CASCADE)
account_number = models.BigIntegerField()
balance = models.FloatField()
account_type = models.ForeignKey(AccountType, on_delete = models.CASCADE)
The json for the expected result should as below
{
"userdetail": {
"mobile_number":""
},
"account": {
"account_number":""
},
"user": {
"first_name": "",
"last_name": "",
"email":""
}
}
The raw sql query is as below
queryset = Account.objects.raw('''SELECT auth_user.first_name,
auth_user.id,
auth_user.last_name,
auth_user.email,
authentication_userdetail.mobile_number,
authentication_account.account_number
FROM
public.auth_user,
public.authentication_account,
public.authentication_userdetail
WHERE
auth_user.id = authentication_userdetail.user_id
AND
auth_user.id = authentication_account.user_id
''')
If there is an alternative way to do this without using raw sql i would greatly appreciate it as im not a fan of executing raw sql queries with django ORM
Tried working with this solution but i cant seem to understand the way the queryset was serialized
Cross-table serialization Django REST Framework
Edited
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = UserDetail
fields = ('mobile_number',)
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ('account_number',)
class AccountInfoSerializer(serializers.ModelSerializer):
user_detail = UserDetailSerializer()
account = AccountSerializer()
user = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('user_detail', 'account', 'user')
def get_user(self, obj):
return {
'first_name': 'obj.first_name',
'last_name': 'obj.last_name',
'email': 'obj.email',
}
Code for the view
serializer_class = AccountInfoSerializer
def get_queryset(self, *args, ** kwargs):
user_id = self.request.query_params.get('user_id', None)
queryset = None
if user_id is not '':
queryset = UserDetail.objects.raw()
return queryset
you can try such solution:
from rest_framework import serializers
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = UserDetail
fields = ('mobile_number',)
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ('account_number',)
class UserSerializer(serializers.ModelSerializer):
userdetail = UserDetailSerializer()
account = AccountSerializer()
user = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('userdetail', 'account', 'user')
def get_user(self, obj):
return {
'first_name': 'obj.first_name',
'last_name': 'obj.last_name',
'email': 'obj.email',
}