How to add aggregate values in Django Rest Framework ModelViewSets - python

In my DRF app I have the following model, serializer and view.
models.py
class Log(models.Model):
plant = models.ForeignKey(Plant, on_delete=models.CASCADE)
date_time = models.DateTimeField()
water_consumption = models.PositiveSmallIntegerField()
elec_consumption = models.PositiveSmallIntegerField()
serializers.py
class ConsumptionSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = ("water_consumption", "elec_consumption")
views.py
class ConsumptionViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated, ]
serializer_class = ConsumptionSerializer
def get_queryset(self):
# Get params from url
start_date = self.request.query_params.get('start_date')
end_date = self.request.query_params.get('end_date')
# Make sure params are not null
if start_date is not None and end_date is not None:
queryset = queryset.filter(date_time__range=[start_date, end_date])
return queryset
else:
raise ValidationError({"ERROR": ["No params found in url"]})
This works and it outputs something like this in JSON:
[
{
"water_consumption": 1,
"electrical_consumption": 1
},
{
"water_consumption": 1,
"electrical_consumption": 1
},
{
"water_consumption": 1,
"electrical_consumption": 1
},
]
What I would like to achieve is to receive some aggregated data alongside those data, like this:
{
"total_water_consumption": 3,
"total_elec_consumption": 3,
"detailed_logs": [{
"water_consumption": 1,
"electrical_consumption": 1
},
{
"water_consumption": 1,
"electrical_consumption": 1
},
{
"water_consumption": 1,
"electrical_consumption": 1
}]
}
How should I customise the queryset to have the total values added?
Thank you in advance.

You can do it with SerializerMethodField in your serializer file
Docs in : https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
And you can access request in serializer as context:
from django.db.models import Sum
class ConsumptionSerializer(serializers.ModelSerializer):
total_water_consumption = serializers.SerializerMethodField()
total_elec_consumption = serializers.SerializerMethodField()
class Meta:
model = Log
fields = ("water_consumption", "elec_consumption")
def get_total_water_consumption(self, obj):
request = self.context['request']
query_params = request.query_params.get(...)
return Log.objects.filter(...).aggregate(Sum('water_consumption')).get('water_consumption__sum')
def get_total_elec_consumption(self, obj):
request = self.context['request']
query_params = request.query_params.get(...)
return Log.objects.filter(...).aggregate(Sum('elec_consumption')).get('elec_consumption__sum')

Related

Django - filtering related objects

I have a puzzle.
These are my models:
class StatusGroup(models.Model):
name = models.TextField()
def __str__(self):
return self.name
class StatusDetail(models.Model):
action = models.CharField(choices=[("CORRECT", "CORRECT"),
("INCORRECT", "INCORRECT")],
max_length=64)
status_group = models.ForeignKey(to=StatusGroup,
on_delete=models.CASCADE,
related_name="status_details")
def __str__(self):
return f"Detail: {self.action}"
serializers:
class StatusDetailSerializer(serializers.ModelSerializer):
class Meta:
model= models.StatusDetail
fields = "__all__"
class StatusGroupSerializer(serializers.ModelSerializer):
status_details = StatusDetailSerializer(many=True)
class Meta:
model = models.StatusGroup
fields = [
"pk",
"status_details",
"name"
]
And a view:
class Status(viewsets.ModelViewSet):
queryset = models.StatusGroup.objects.all()
serializer_class = serializers.StatusGroupSerializer
authentication_classes = []
permission_classes = [permissions.AllowAny]
filter_backends = (DjangoFilterBackend,)
filterset_fields = ['status_details__action']
When I hit localhost:8000/api/status?status_details__action=INCORRECT
I get:
[
{
"pk": 2,
"status_details": [
{
"id": 3,
"action": "CORRECT",
"status_group": 2
},
{
"id": 4,
"action": "INCORRECT",
"status_group": 2
}
],
"name": "Mixed"
}
]
Whereas I would like to have:
[
{
"pk": 2,
"status_details": [
{
"id": 4,
"action": "INCORRECT",
"status_group": 2
}
],
"name": "Mixed"
}
]
How do I force Django to filter the related objects? I can get the result I want in SQL console, but Django adds, all the related objects that belong to the StatusGroup.
I have a misconception, but I don't know what that is.
try this way (using query set):
from django.db.models import Prefetch
queryset = StatusGroup.objects.prefetch_related(
Prefetch('status_details', queryset=StatusDetail.objects.filter(action='what_you_want'), to_attr='action'))
and in your serializer class:
class StatusGroupSerializer(serializers.ModelSerializer):
status_details = StatusDetailSerializer(source='action', many=True, read_only=True)
First you can create a FilterSet class like this:
from django_filters import rest_framework as filters
class StatusGroupFilter(filters.FilterSet):
action = filters.CharFilter(
field_name="status_details__action",
lookup_expr="iexact"
)
class Meta:
model = StatusGroup
fields = ["action"]
Then in your views:
class Status(viewsets.ModelViewSet):
queryset = models.StatusGroup.objects.all()
serializer_class = serializers.StatusGroupSerializer
authentication_classes = []
permission_classes = [permissions.AllowAny]
filter_backends = (DjangoFilterBackend,)
# Set the filterset class here
filterset_class = StatusGroupFilter
Then in the URL you can call localhost:8000/api/status?action=INCORRECT

Getting null value in column "userId_id" of relation "pnrDB_run when sending a POST to django backend

Still very new to Django. I'm trying to send a post request to my backend from insomnia right now and I'm getting the error Getting null value when I'm passing in the ID value.
model:
class Run(models.Model):
gameId = models.ForeignKey(Game,on_delete=models.CASCADE, related_name='runs')
userId = models.ForeignKey(User,on_delete=models.CASCADE, related_name='game')
name = models.CharField(max_length=30)
isComplete = models.BooleanField(default=False)
deaths = models.IntegerField()
badges = models.IntegerField()
def __str__(self):
return self.name
Here is my seralizer:
class RunSerialzer(serializers.HyperlinkedModelSerializer):
gameId = GameSerialzer(
read_only = True
)
userId = UserSerialzer(
read_only = True
)
class Meta:
model = Run
fields=('id','name','isComplete','deaths','badges','userId','gameId')
My view:
class RunList(generics.ListCreateAPIView):
queryset = Run.objects.all()
serializer_class = RunSerialzer
And my request I'm making:
{
"gameId":1,
"userId": 1,
"name":"testrun2",
"deaths":0,
"badges": 0,
"isComplete": false
}
and the second one I tried
{
"gameId": {
"id": 1,
"name": "Vintage White",
"photo": "https://archives.bulbagarden.net/media/upload/thumb/0/08/White_EN_boxart.png/250px-White_EN_boxart.png"
},
"userId": {
"id": 1,
"name": "TestUser"
},
"name":"testrun2",
"isComplete": false,
"deaths": 0,
"badges": 0
}
I feel like I need to create either a new Serailizer to get the instance I want in my Game model with the pk im passing in the request
since you are using read_only=True on your serializer for two of your fields you need to pass gameId and userId manually to save method of serializer.
in your view(RunList) you should overwrite perform_create method like following:
def perform_create(self, serializer):
gameId = self.request.data.get('gameId')
userId = self.request.data.get('userId')
return serializer.save(gameId=gameId, userId=userId)

How can I serialize one to many models in Django Rest?

So I have two models Invoice and Items. Invoices can have many Items. Here's how they are defined:
class Invoice(models.Model):
customer_name = models.CharField(max_length = 200, verbose_name='Customer Name')
customer_phone = models.IntegerField(null = True, blank = True, verbose_name='Customer Phone')
customer_address = models.TextField(null = True, blank = True, verbose_name='Customer Address')
invoice_id = models.UUIDField(primary_key = True, unique = True, default=uuid.uuid4, verbose_name='Invoice ID')
invoice_date = models.DateField(auto_now_add=True, verbose_name='Invoice Date')
def __str__(self):
return self.customer_name + ' - ' + str(self.invoice_id)
class Meta:
verbose_name = 'Invoice'
verbose_name_plural = 'Invoices'
class Items(models.Model):
invoice = models.ForeignKey(Invoice, on_delete = models.CASCADE, related_name='invoice')
item_name = models.CharField(max_length = 200, null = False)
item_quantity = models.IntegerField()
item_price = models.IntegerField()
item_id = models.AutoField(primary_key=True)
def __str__(self):
return self.item_name
class Meta:
verbose_name = 'Items'
verbose_name_plural = 'Items'
I want to implement two API endpoints, one to get the list of invoices, and another to get a specific invoice with its items.
For example, /api/ will return all invoices
[
{
"customer_name": "John Doe",
"customer_phone": 111666,
"customer_address": "Palo Alto, California",
"invoice_id": "a8aeb5a8-5498-40fd-9b4f-bb09a7057c71",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Cassian Green",
"customer_phone": 111000,
"customer_address": "2112 Illinois Avenue",
"invoice_id": "7d7b7878-3ffc-4dd2-a60a-fa207c147860",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Chelsea Davis",
"customer_phone": 666777,
"customer_address": "2260 Coplin Avenue",
"invoice_id": "3dda2054-49d7-49dc-9eba-ddc0fdacfd3b",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Derek Elliott",
"customer_phone": 111100,
"customer_address": "3236 Sundown Lane",
"invoice_id": "da112c0d-aff4-43d3-a465-910cc1483fc5",
"invoice_date": "2022-05-04"
}
]
and now if I request a specific invoice with its items, like /api/a8aeb5a8-5498-40fd-9b4f-bb09a7057c71 should return the following response
{
"invoice_id": "a8aeb5a8-5498-40fd-9b4f-bb09a7057c71",
"items": [
{
"item_name": "PC Cabinet",
"item_quantity": 1,
"item_price": 200
},
{
"item_name": "Motherboard",
"item_quantity": 1,
"item_price": 1000
},
{
"item_name": "PSU",
"item_quantity": 1,
"item_price": 300
}
]
}
Here are the serializers:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Items
fields = [
'item_name',
'item_quantity',
'item_price',
]
class InvoiceSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = [
'customer_name',
'customer_phone',
'customer_address',
'invoice_id',
'invoice_date',
'items'
]
And the views:
#api_view(['GET',])
def InvoiceList(request):
if request.method == 'GET':
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer(queryset, many=True)
return Response(serializer_class.data)
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
queryset = list(Items.objects.filter(invoice = pk))
serializer_class = ItemSerializer(instance=queryset, many=True)
return Response(serializer_class.data)
The first API endpoint is working as expected but the I need help with implementing the second one. How can I get that nested response?
Thanks.
Your related_name of invoice in Items model is wrong, you should set it as "items" because you are going to call that from Invoice model like that.
Also if you want to have different response from that second API you should change it to use other serializer than InvoiceList:
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
queryset = list(Items.objects.filter(invoice = pk))
serializer_class = InvoiceDetailSerializer(instance=queryset, many=True)
return Response(serializer_class.data)
I changed name of serializer to InvoiceDetailSerializer so you need to create new serializer:
class InvoiceDetailSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = [
'invoice_id',
'items'
]
you can try something like this. (not tested)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Items
fields = ['item_name', 'item_quantity', 'item_price',]
class InvoiceDetailsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = ['id', 'items']
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
invoice = Invoice.objects.get(id=pk)
serializer_class = InvoiceDetailsSerializer(instance=invoice)
return Response(serializer_class.data)

django drf about many-to-many RelatedField custom field

I use the latest version of The Django REST Framework,
and The table in model is many-to-many related
My current model code looks like this:
model.py
class LvsDeploy(models.Model):
MASTER = 'MASTER'
BACKUP = 'BACKUP'
ROLE_CHOICES = (
(MASTER, 'MASTER'),
(BACKUP, 'BACKUP')
)
id = models.AutoField(primary_key=True)
cluster_name = models.CharField(max_length=30)
dip = models.CharField(max_length=15, verbose_name='DR IP')
role = models.CharField(max_length=10, choices=ROLE_CHOICES, verbose_name="role")
class LoadBalance(models.Model):
id = models.AutoField(primary_key=True)
lb_name = models.CharField(max_length=30, verbose_name='load balance')
cluster = models.ManyToManyField('LvsDeploy',related_name='cluster')
vip = models.CharField(max_length=50, verbose_name='VIP')
port = models.IntegerField(verbose_name='port')
def __str__(self):
return self.lb_name
My serializer code looks like this:
serializers.py
class LvsDeployReadOnly(serializers.ModelSerializer):
class Meta:
model = LvsDeploy
fields = '__all__'
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.RelatedField(source='cluster.name',read_only=True)
cluster = LvsDeployReadOnly(many=True)
##rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
##extra_fields = ['rs','cluster','cluster_name']
extra_fields = ['rs','cluster','cluster_name']
My views code:
class BalanceList(views.APIView):
def get(self, request):
queryset = LoadBalance.objects.all()
serializer = LoadBalanceSerializer(queryset, many=True)
print(serializer.data)
return Response(serializer.data)
and request actual output:
[
{
"id": 2,
"cluster_name": null,
"cluster": [
{
"id": 1,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"id": 2,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
But the cluster_name filed value is same in the dictionary of lists .
I want the output to look like this:
[
{
"id": 2,
"cluster_name": "lvs_sz01",
"cluster": [
{
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
How should I change it? Can you help me ?
I solved it myself with the following method:
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.SerializerMethodField()
cluster = LvsDeployReadOnly(many=True, read_only=True)
rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
extra_fields = ['rs','cluster','cluster_name']
def get_cluster_name(self,obj):
print(type(obj.lb_name))
lvs_cluster = obj.cluster.all()
cluster_name = [ i.cluster_name for i in lvs_cluster ][0]
print(cluster_name)
return cluster_name
Thanks!

Why my django server throws an error of invalid datatype?

Whenever i called my booking function with a post method in that it throws me an error of 'Invalid Data Type it expected dict but its an int'. So when i debug that i found a problem in my serializers so Please help me to resolve the error
model.py
class Booking(models.Model):
details = models.ForeignKey(PersonalDetails, on_delete=models.CASCADE)
quantity = models.IntegerField(null=False)
total_amount = models.IntegerField()
show = models.ForeignKey(ShowTime, on_delete=models.CASCADE)
def __str__(self) -> str:
return self.total_amount
views.py
class BookingTicketsQuery(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
booking_id = request.query_params.get('booking_no', None)
if booking_id is not None:
queryset = Booking.objects.get(id=booking_id)
else:
return Response("invalid", status=status.HTTP_409_CONFLICT)
seriliazer = BookingSerializer(queryset)
return Response(seriliazer.data)
def post(self, request, format=None):
recieve_data = JSONParser().parse(request)
showtime = ShowTime.objects.get(id=recieve_data['show'])
print(showtime)
if recieve_data['quantity'] > (showtime.total_seats - showtime.booked_seats):
return Response("Error: No seats available", status=status.HTTP_409_CONFLICT)
recieve_data['total_amount'] = showtime.total_price * \
recieve_data['quantity']
showtime.booked_seats += recieve_data['quantity']
showtime.save()
serializer = BookingSerializer(data=recieve_data)
if serializer.is_valid():
booking_obj = serializer.save()
return Response(booking_obj.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class ShowMovieSerializer(serializers.ModelSerializer):
cinema = CinemaSerializer()
movie = MovieSerializer()
class Meta:
model = ShowTime
fields = ('id', 'show_start_time', 'show_end_time',
'total_seats', 'booked_seats', 'cinema', 'movie')
class BookingSerializer(serializers.ModelSerializer):
show = ShowMovieSerializer()
details = PersonalDetailsSerializer()
class Meta:
model = Booking
fields = ('id', 'total_amount', 'quantity', 'details', 'show')
traceback
Bad Request: /api/v1/bookings/
[19/Jul/2021 12:07:20] "POST /api/v1/bookings/ HTTP/1.1" 400 168
{
"details": {
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
},
"show": {
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
}
}
my post data:
{
"details": 1,
"quantity":1,
"show":1
}
You added a ShowMovieSerializer() and PersonalDetailsSerializer(). Therefore drf expects data that represents both models (nested data). What you have to do instead of posting { "details": 1, "quantity":1, "show":1 }-data you have to adjusted your data that represents your model. You are posting data with a pk of your Movie and Show.
For instance:
{
"id":....,
"total_amount": ....,
....
"movie": {
"title": ...., # fields are only for explanation
"length": ....
},
"show": {
"title": ....,
"actor": ....
}
}
If you only want to establish a relationship between you ShowTime and Booking you have to remove your serializers.
dealing with nested data

Categories