Problem: Not able to group my JSON output by date
SOLUTION IN BOTTOM OF THIS POST
I am serializing a model and getting this output:
[
{
"date": "2020-11-24",
"name": "Chest",
"workout": {
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 80
}
},
{
"date": "2020-11-24",
"name": "Chest",
"workout": {
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 85
}
},
{
"date": "2020-11-24",
"name": "Chest",
"workout": {
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 90
}
},
I want to get it like the JSON below and group it by date.
[
{
"date": "2020-11-24",
"workout": {
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 80
},
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 85
},
"name": "Chest",
"exercise": 1,
"repetitions": 10,
"weight": 90
},
}
]
I have one model:
class WorkoutLog(models.Model):
date = models.DateField()
name = models.CharField(max_length=50) #When save() name = Workout.name
exercise = models.ForeignKey('Exercise', related_name='log', on_delete=models.CASCADE)
repetitions = models.IntegerField()
weight = models.IntegerField()
Trying to serialize and group the JSON by date:
class WorkoutLogSerializer(serializers.ModelSerializer):
class Meta:
model = WorkoutLog
fields = ['date', 'workout']
workout = serializers.SerializerMethodField('get_workout')
def get_workout(self, obj):
return {
'name': obj.name,
'exercise': obj.exercise_id,
'repetitions': obj.repetitions,
'weight': obj.weight,
}
The code lets me custom the field layout, but not really grouping it by date. Do you have any suggestions on how to structure it?
Many thanks for all help!
in case its needed, here is my view.py
def workout_log(request):
if request.method == 'GET':
workout_log = WorkoutLog.objects.all()
serializer = WorkoutLogSerializer(workout_log, many=True)
return JsonResponse(serializer.data, safe=False)
SOLUTION BY Mahmoud Adel:
views.py
def workout_log(request):
if request.method == 'GET':
workout_log = WorkoutLog.objects.order_by('date').values('date').distinct()
serializer = WorkoutLogSerializer(workout_log, many=True)
return JsonResponse(serializer.data, safe=False)
serializers.py
class WorkoutFieldSerializer(serializers.Serializer):
name = serializers.CharField()
#exercise = serializers.IntegerField()
repetitions = serializers.IntegerField()
weight = serializers.IntegerField()
class WorkoutLogSerializer(serializers.ModelSerializer):
class Meta:
model = WorkoutLog
fields = ['date', 'workout']
workout = serializers.SerializerMethodField('get_workout')
def get_workout(self, obj):
workouts = WorkoutLog.objects.filter(date=obj['date'])
workout_serializer = WorkoutFieldSerializer(workouts, many=True)
return workout_serializer.data
You can do something like this
Let's start first with your view, I will tweak the queryset like that
def workout_log(request):
if request.method == 'GET':
workout_log = WorkoutLog.objects.order_by('date').distinct('date').only('date')
serializer = WorkoutLogSerializer(workout_log, many=True)
return JsonResponse(serializer.data, safe=False)
Then on your WorkoutLogSerializer
class WorkoutLogSerializer(serializers.ModelSerializer):
class Meta:
model = WorkoutLog
fields = ['date', 'workout']
workout = serializers.SerializerMethodField('get_workout')
def get_workout(self, obj):
workouts = WorkoutLog.objects.filter(date=obj.date)
workout_serializer = WorkoutSerializer(workouts, many=True)
return workout_serializer.data
And finally, create WorkoutSerializer
class WorkoutSerializer(serializers.Serializers):
name = serializers.CharField()
exercise = serializers.IntegerField()
repetitions = serializers.IntegerField()
weight = serializers.IntegerField()
The previous way will first be got to the DB to get the distinct dates then on WorkoutLogSerializer will use each date to select the corresponding objects that have it, then we serialize those objects.
We get the intended result by doing so, note that this will result in 2 DB hits, there may be another way that will do it in one DB hit, I will update my answer if I have figured it out
NOTE: I have written this to explain the flow and the logic I didn't run it, although it should work I may forget something that will show an error for you, feel free to try it.
UPDATE: check this answer comments if you are using SQLite.
Related
I have a qs qs_payments that returns the following:
[
{
"id": 19,
"month": "2021-05-01",
"amount": 3745,
"offer": 38
},
{
"id": 67,
"month": "2021-03-01",
"amount": 4235,
"offer": 39
},
{
"id": 68,
"month": "2021-04-01",
"amount": 4270,
"offer": 39
},
{
"id": 69,
"month": "2021-05-01",
"amount": 4305,
"offer": 39
}
]
I now try to re-group the data so that each month contains all items of that particular month (in the given example there would be two nested items for May/2021-05-21)
I tried the following using itertools but this returns Payment object is not subscriptable. Context is that I want to render one component with its line items in React.js.
class PayrollRun(APIView):
...
payment_runs = [{'subject': k, 'Results': list(g)} for k, g in itertools.groupby(qs_payments, key=itemgetter('month'))]
print(payment_runs)
serializer = PaymentSerializer(qs_payments, many=True)
return Response(serializer.data)
The desired outcome is s.th. like (if that makes sense?)
[
{
"2021-05-01": [{
"id": 69,
"amount": 4305,
"offer": 39
},
{
"id": 19,
"amount": 3745,
"offer": 38
}],
},
"2021-04-01": [{
...
}],
]
Update to try to even simpler approach with a related field to Period
# models.py
class Period(models.Model):
period = models.DateField()
class Payment(models.Model):
offer = models.ForeignKey(Offer, on_delete=models.CASCADE)
month = models.ForeignKey(Period, on_delete=models.CASCADE)
amount = models.PositiveIntegerField()
# serializers.py
class PaymentSerializer(serializers.ModelSerializer):
class Meta:
model = Payment
fields = '__all__'
class PeriodSerializer(serializers.ModelSerializer):
payments = serializers.SerializerMethodField()
class Meta:
model = Period
fields = '__all__'
# views.py
def get(self, request):
# Loop payment table for company related payments, create dict of dict for each month
# Get todays date
today = datetime.date(datetime.now())
# Get the related company according to the logged in user
company = Company.objects.get(userprofile__user=request.user)
# Get a queryset of payments that relate to the company and are not older than XX days
qs_payments = Payment.objects.filter(offer__company=company)
print(qs_payments)
serializer = PeriodSerializer(qs_payments, many=True)
return Response(serializer.data)
What about something like this?
models.py
class Period(...):
date = models.DateField(...)
Then in your serializers:
serializers.py
class PaymentSerializer(...):
...
class PeriodSerializer(...):
payments = serializers.SerializerMethodField()
def get_payments(self, obj):
payments = Payment.objects.filter(
month = obj.date
)
return PaymentSerializer(payments, many=True).data
class Meta:
fields = ['payments', ...]
model = Month
EDIT:
You can simplify this further by adding a foreign key relation to a Month on your Payment model, then you don't need to write the get_payments method, you can just serialize the Payments on each Month directly.
This would look like:
models.py
class Period(...):
date = models.DateField(...)
class Payment(...):
offer = models.ForeignKey(Offer, on_delete=models.CASCADE)
month = models.ForeignKey(Period, on_delete=models.CASCADE, related_name='payments') # <-- this
amount = models.PositiveIntegerField()
serializers.py
class PaymentSerializer(...):
...
class PeriodSerializer(...):
payments = PaymentSerializer(read_only=True, many=True)
class Meta:
fields = ['payments', ...]
model = Month
I am trying to achieve is to get all comments based on photo_id, and count the total replies at the same time.
models.py:
class PhotoComment(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE)
photo = models.ForeignKey(UserPhoto,on_delete=models.CASCADE)
comment_text = models.TextField(max_length = 500)
comment_reply = models.ForeignKey('self',related_name='replies',blank=True,null=True,on_delete = models.CASCADE)
...
def __str__(self):
return '%s(photo_%s)' %(self.user,self.photo)
views.py:
class PhotoCommentView(APIView):
def patch(self,request,formate=None):
photo_id = request.data.get('photo_id')
all_comment = list(PhotoComment.objects.filter(photo__id = photo_id,comment_reply = None).values('id','user__username','comment_text','user__profile__profile_pic','vote_upvote').annotate(total_reply = Count('comment_reply')))
return Response({"Data" : all_comment},status=status.HTTP_200_OK)
Real result is like this:
{
"Data": [
{
"id": 12,
"user__username": "pradip",
"comment_text": "this is awesome pic bro....",
"user__profile__profile_pic": "profile_pic/gyroscope_HUq0uJ0.png",
"vote_upvote": 2,
"total_reply": 0⬅⬅⬅⬅
},
...
]
}
What I want for result is like this(Here comment id 12 contain total 3 replies.):
{
"Data": [
{
"id": 12,
"user__username": "pradip",
"comment_text": "this is awesome pic bro....",
"user__profile__profile_pic": "profile_pic/gyroscope_HUq0uJ0.png",
"vote_upvote": 2,
"total_reply": 3⬅⬅⬅⬅
},
....
]
}
I have no idea with this problem I need some help.
If you know any other easy or recommended solution to achieve this please tell me.
Thank you.
As stated by others, properties are refering to a specific object instance. Querysets are used to do things on a range of objects.
Therefore to replicate for a queryset the property you defined for an object instance, you need to setup an annotation:
from django.db.models import Count
all_comment = list(PhotoComment.objects.annotate(total=Count('comment_reply')
).filter(photo__id = request.data.get('photo_id')
).values('id','comment_text','total'))
Hope the general principle explained here will enable you to get to where you want to go
I use SerializerMethodField() in serializer to count comment reply.
serializers.py:
class ShowCommentSerializer(serializers.ModelSerializer):
total_reply = SerializerMethodField()
user_name = serializers.CharField(source='user.username')
profile_pic = serializers.CharField(source='user.profile.profile_pic')
class Meta:
model = PhotoComment
fields = ('id','user_name','profile_pic','comment_text','total_reply','created_date')
read_only_fields = fields
def get_total_reply(self, obj):
total = obj.replies.count()
return total
views.py:
class PhotoCommentView(APIView):
def patch(self,request,formate=None):
photo_id = request.data.get('photo_id')
comment_inst = PhotoComment.objects.filter(photo = photo_id,comment_reply=None)
serializer = ShowCommentSerializer(comment_inst,many=True)
return Response({"Data" : serializer.data},status=status.HTTP_200_OK)
Output:
{
"Data": [
{
"id": 29,
"user_name": "admin",
"profile_pic": "profile_pic/Airbus.png",
"comment_text": "good bro....",
"total_reply": 3,
"created_date": "2021-03-10T02:25:43.940127+05:30"
},
]
}
I can successfully add a new object(s) into notification_days but what is the most idiomatic way to handle removing any object(s)?
models.py
DAYS_OF_WEEK = (
(0, 'Mon'),
(1, 'Tue'),
(2, 'Wed'),
(3, 'Thu'),
(4, 'Fri'),
(5, 'Sat'),
(6, 'Sun'),
)
class WeekDay(models.Model):
day = models.IntegerField(choices=DAYS_OF_WEEK)
def __str__(self):
return self.get_day_display()
class Company(models.Model):
name = models.CharField(...)
notification_days = models.ManyToManyField(WeekDay)
serializers.py
class WeekDaySerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
day_display = serializers.SerializerMethodField()
class Meta:
model = WeekDay
fields = ['id', 'day', 'day_display']
def get_day_display(self, obj):
return obj.get_day_display()
class CompanySettingsSerializer(serializers.ModelSerializer):
notification_days = WeekDaySerializer(many=True)
class Meta:
model = Company
fields = [
'name',
'notification_days'
]
def update(self, instance, validated_data):
notification_days = validated_data.get('notification_days')
for day in notification_days:
day_id = day.get('id', None)
if item_id:
if not instance.notification_days.filter(pk=day_id).exists():
week_day = WeekDay.objects.get(pk=day_id)
instance.notification_days.add(week_day)
return instance
api.py
class CompanySettingsAPIView(RetrieveUpdateAPIView):
authentication_classes = (SessionAuthentication, )
permission_classes = (IsCompanyAdmin, )
serializer_class = CompanySettingsSerializer
def get_object(self):
return Company.objects.get(pk=self.kwargs.get('pk'))
Sample GET response:
{
"name": "Django",
"notification_days": [
{
"id": 1,
"day": 0,
"day_display": "Mon"
},
{
"id": 2,
"day": 1,
"day_display": "Tue"
}
]
}
When I send PUT request with the following body, new week day is successfuly added:
{
"name": "Django",
"notification_days": [
{
"id": 1,
"day": 0,
"day_display": "Mon"
},
{
"id": 2,
"day": 1,
"day_display": "Tue"
},
{
"id": 4,
"day": 3
}
]
}
You need only ids on the request.
And use .set() method to update the relations.
PUT Request:
{
"name": "Django",
"notification_days_ids": [
1,
2,
4
]
}
Serializer:
class CompanySettingsSerializer(serializers.ModelSerializer):
notification_days = WeekDaySerializer(many=True, read_only=True)
notification_days_ids = serializers.PrimaryKeyRelatedField(
many=True,
write_only=True,
source='notification_days', # just to make it looks a little bit better
queryset=WeekDay.objects.all()
)
class Meta:
model = Company
fields = [
'name',
'notification_days',
'notification_days_ids',
]
def update(self, instance, validated_data):
if 'notification_days' in validated_data: # to handle PATCH request
instance.notification_days.set(validated_data['notification_days'])
return instance
First clear the old notification days
instance.notification_days.clear()
and add the new ones from put request
instance.notification_days.add()
It helped me
I am trying to get data in a particular format but i'm not able to get the desired output.
My Model:
class Category(models.Model):
name = models.CharField(max_length=40)
class Expense(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="category_name")
description = models.CharField(max_length=200)
total_amount = models.IntegerField()
class Expense_Details(models.Model):
expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name="payment")
user = models.IntegerField()
amount = models.FloatField()
type = models.CharField(max_length=100) ---->type is owe or lend
When I request /api/expenses/:
Expected Output
{
“total_expenses”: 10,
“Expenses”:
[{
“id”: 1,
“category”: 1,
“created_by”: 1, ------> user logged id
“description”: “lunch”,
“total_amount”: “105”,
“owe”: [{
“user_id”: 1,
“amount”: 10
},
{
“user_id”: 2,
“amount”: 95
}],
“lend”: [{
“user_id”: 3,
“amount”: 10
},
{
“user_id”: 4,
“amount”: 95
}],
}, ...
]
}
My output:
{
"results": [
{
"id": 1,
"category": 1,
"description": "lunch at the burj al arab",
"total_amount": 105,
"payment": [
{
"user": 1,
"amount": -10
},
{
"user": 2,
"amount": -95
},
{
"user": 3,
"amount": 10
},
{
"user": 4,
"amount": 95
}
]
}
]
}
My Serializer:
class ExpenseDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = Expense_Details
fields = ['user', 'amount']
class ExpenseSerializer(serializers.ModelSerializer):
payment = serializers.SerializerMethodField()
def get_payment(self, obj):
return ExpenseDetailsSerializer(obj.payment.all(), many=True).data
class Meta:
model = Expense
fields = ['id', 'category', 'description', 'total_amount', 'payment',]
What Query should I use to get Output in the above format? How will my serializer look like? How can I separate own and lend? Also I have stored own and lend with + and - sign to differentiate between them.
Use a ListField for the same.
Documentation: https://www.django-rest-framework.org/api-guide/fields/#listfield
Also refer How to serialize an 'object list' in Django REST Framework
Here you can try something like:
class ExpenseSerializer(serializers.Serializer):
payment = serializers.ListField(child=ExpenseDetailsSerializer())
def get_payment(self, obj):
return ExpenseDetailsSerializer(obj.payment.all(), many=True).data
class Meta:
model = Expense
fields = ['id', 'category', 'description', 'total_amount', 'payment',]
I have two serializers like this.
class CourseSerializer(serializers.ModelSerializer):
pk = serializers.IntegerField(read_only=True)
name = serializers.CharField(required=True, max_length=30, allow_blank=False)
chapters = serializers.SerializerMethodField()
def get_chapters(self, obj):
queryset = Chapter.objects.all().filter(course=obj.pk)
serializer_class = ChapterSerializer(queryset, many=True)
return serializer_class.data
class ChapterSerializer(serializers.ModelSerializer):
pk = serializers.IntegerField(read_only=True)
name = serializers.CharField(required=True, max_length=30, allow_blank=False)
course = serializers.StringRelatedField()
cards_count = serializers.SerializerMethodField()
cards_learned_count = serializers.SerializerMethodField()
cards_still_to_go = serializers.SerializerMethodField()
cards_not_learned = serializers.SerializerMethodField()
def get_cards_learned_count(self, obj):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
queryset = Card.objects.all().filter(chapter=obj.pk)
card_count = 0
for q in queryset:
card_detail = UserCardDetail.objects.all().filter(card=q, user=user, learned=True)
card_detail.count()
card_count += card_detail.count()
return card_count
Now when I get the course serializer instead of getting the proper values in the chapter field, I get zeroes, but when I call the chapter serializer I get the right values.
I noticed that the MethodField works with obj - however shouldn't that be passed already?
Is there something extra I have to pass to the chapters field?
Edit: Output and Expected Output
OUTPUT
{
"name": "Example 1",
"pk": 32,
"locked": false,
"chapters": [
{
"pk": 15,
"name": "Transformatori",
"course": "Tesit",
"cards_count": 5,
"cards_learned_count": 0,
"cards_still_to_go": 0,
"cards_not_learned": 0,
"favorite_cards": 0
},
],
},
expected output
{
"name": "Example 1",
"pk": 32,
"locked": false,
"chapters": [
{
"pk": 15,
"name": "Transformatori",
"course": "Tesit",
"cards_count": 5,
"cards_learned_count": 3,
"cards_still_to_go": 0,
"cards_not_learned": 2,
"favorite_cards": 1
},
],
},