Populating json from multiple django models - python

I have the following 2 Django models, Lessons and UserLessons.
Lessons would be created by an admin and UserLessons is basically when a user is in progress with the Lesson or completed the Lesson, linked by a foreign key. UserLesson doesn't necessarily contain a Lesson entry, till the user actually starts with that specific one.
As I'm building an API (with DRF), I need to list the full list of all Lessons - easy.
LessonList = Lesson.objects.all().values('id', 'title')
This returns
[
{
"id": 1,
"title": "Lesson 1"
},
{
"id": 2,
"title": "Lesson 2"
},
{
"id": 3,
"title": "Lesson 3"
}
]
However, I need to be able to merge it with UserLesson (eg UserLessonList = UserLesson.objects.filter(user=request.user).values('id', 'lesson__id', 'completed') which currently returns
[
{
"id": 2,
"lesson__id": 1,
"completed": true
},
{
"id": 3,
"lesson__id": 2,
"completed": true
}
]
Ideally, it should return all the lessons from the db, and the the completed values, defaulting to completed: false if that specific lesson doesn't exist in DB.
Any suggestions?
Edit:
Views
class LessonList(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request):
LessonList = Lesson.objects.all().values('id', 'title')
UserLessonList = UserLesson.objects.filter(user=request.user).values('id', 'lesson__id', 'completed')
return Response(LessonList)
Models
class Lesson(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=250, verbose_name=u'Title')
slug = models.SlugField(null=True, blank=True, help_text='eg, lesson-1-whats-up')
published = models.BooleanField(default=False)
def __str__(self):
return(self.title)
class UserLesson(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, null=True)
completed = models.BooleanField(default=False)
def __str__(self):
text = str(self.lesson.title)
return(text)

You should use ModelViewSet and serializers. Exactly ModelSerializer. Something like this:
class LessonSerializer(serializers.ModelSerializer):
completed = serializers.SerializerMethodField()
class Meta:
model = Lesson
fields = ['id', 'title', 'completed']
def get_completed(self, obj):
user = self.context.get('request').user
return UserLesson.objects.filter(user=user, lesson=obj, completed=True).exists()
class LessonViewSet(viewsets.ModelViewSet):
queryset = Lesson.objects.filter(published=True)
serializer_class = LessonSerializer
permission_classes = [IsAuthenticated]

Related

DRF one-to-one relation serializer with a pre-populated model

I have a couple models, one of which is already populated with data (book name/ chapter number/ paragraph number data), and I am implementing the feature for each user to be able to add a note per each unique book name/ chapter number/ paragraph number, which I could, but I have been stack for a couple of days trying to retrieve books with the related_name note of the current user if they have any. Here are my models:
Book model that is already populated with data.
from django.db import models
class Book(models.Model):
day = models.CharField(max_length=128)
book = models.CharField(max_length=128)
chapter = models.CharField(max_length=256)
paragraph = models.CharField(max_length=256)
text = models.TextField()
link = models.CharField(max_length=256)
def __str__(self):
return f'{self.book}_{self.chapter}.{self.paragraph} '
class Meta:
ordering = ['-id']
verbose_name = "Paragraph"
verbose_name_plural = "Paragraph"
Here is the Note model that should store the current user's note regarding a specific unique book name / chapter number / paragraph number:
from django.db import models
from django.conf import settings
from rest_framework.reverse import reverse
from paragraphs.models import Book
class Note(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='author', on_delete=models.CASCADE)
paragraph = models.ForeignKey(Book, related_name='note', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
text = models.TextField(default=None)
def __str__(self):
return f'Note on {self.paragraph}'
class Meta:
ordering = ['created']
def save(self, *args, **kwargs):
"""
"""
options = {'text': self.text} if self.text else {}
super(Note, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('note-detail', args=[self.id])
Here are my serializers:
Book serializer
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
Note serializer
from rest_framework import serializers
from .models import Note
from users.serializers import UserSerializer
from paragraphs.serializers import BookSerializer
class NoteSerializer(serializers.ModelSerializer):
owner = UserSerializer(many=False, read_only=True)
class Meta:
model = Note
fields = ['id', 'owner', 'paragraph', 'text', 'created']
def to_representation(self, instance):
self.fields['paragraph'] = BookSerializer(read_only=True)
return super(NoteSerializer, self).to_representation(instance)
def user(self):
request = self.context.get('request', None)
if request:
return request.user
return None
def create(self, validated_data):
note, _ = Note.objects.update_or_create(
owner=self.user(),
paragraph=validated_data.get('paragraph', None),
defaults={'text': validated_data.get('text', None)})
return note
The data I am getting:
{
"id": 25,
"day": "2",
"book": "Some book",
"chapter": "1",
"paragraph": "3",
"text": "This is an example text that the user would like to attach a note to",
"link": "https://somelink.com",
}
The data I am trying to get:
{
"id": 25,
"day": "2",
"book": "Some book",
"chapter": "1",
"paragraph": "3",
"text": "This is an example text that the user would like to attach a note to",
"link": "https://somelink.com",
"note": "note of current user or none"
}
Any help is appreciated
models.py:
class Book(models.Model):
day = models.CharField(max_length=128)
book = models.CharField(max_length=128)
chapter = models.CharField(max_length=256)
paragraph = models.CharField(max_length=256)
text = models.TextField()
link = models.CharField(max_length=256)
def __str__(self):
return f'{self.book}_{self.chapter}.{self.paragraph} '
class Meta:
ordering = ['-id']
class Note(models.Model):
owner = models.ForeignKey(to = User, related_name='author', on_delete=models.CASCADE)
book = models.ForeignKey(Book, related_name='note', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
text = models.TextField(default=None)
def __str__(self):
return '%s(%s)' %(self.owner,self.book)
serializers.py:
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = ['id', 'owner', 'book', 'text', 'created']
class BookSerializer(serializers.ModelSerializer):
note = serializers.StringRelatedField(many=True, read_only=True)
# note = NoteSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ['day','book','chapter','paragraph','text','link','note']
Output:
{
"day": "2",
"book": "some book",
"chapter": "1",
"paragraph": "example",
"text": "some textttttttttttttttttttttttttttttttttttttttttttttttt",
"link": "http://127.0.0.1:8000/admin/api/book/add/",
"note": [
"admin(some book_1.example )"
]
}
It's return all field:
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = ['id', 'owner', 'book', 'text', 'created']
class BookSerializer(serializers.ModelSerializer):
# note = serializers.StringRelatedField(many=True, read_only=True)
note = NoteSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ['day','book','chapter','paragraph','text','link','note']
Output:
{
"day": "2",
"book": "some book",
"chapter": "1",
"paragraph": "example",
"text": "some textttttttttttttttttttttttttttttttttttttttttttttttt",
"link": "http://127.0.0.1:8000/admin/api/book/add/",
"note": [
{
"id": 2,
"owner": 1,
"book": 1,
"text": "saaaaaaaaaaaaaaaaaaa",
"created": "2021-02-24T14:34:13.279750Z"
}
]
}
What you're actually trying to achieve is for the NoteSerializer to include fields from the Foreign-key related book model. Overriding the to_representation method of the serializer is clunky and not the way to go. See here a better approach.

Add extra field in response output in DRF 3.0

I have the following models
class Restaurant(models.Model):
name_of_the_restaurant = models.CharField(max_length=30, blank=True)
opening_time = models.TimeField(auto_now=False, auto_now_add=False)
closing_time = models.TimeField(auto_now=False, auto_now_add=False)
And
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
city = models.CharField(max_length=30, blank=True)
country = models.CharField(max_length=30, blank=True)
postal_code = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
favourite_restaurant = models.ManyToManyField(Restaurant,
blank=True,
related_name='favourite_restaurant',
related_query_name='favourite_restaurant')
I have defined a serializer for Restaurant model which is mainly :
class RestaurantSerializer(serializers.ModelSerializer):
class Meta:
model = Restaurant
fields = '__all__'
Now in my ViewSet logic I am doing the following :
class RestaurantListView(generics.ListAPIView):
serializer_class = RestaurantSerializer
def get_queryset(self):
queryset = {'Error': 'Please pass valid url parameters'}
city = self.request.query_params.get('city', None)
postal_code = self.request.query_params.get('postalcode', None)
country = self.request.query_params.get('country', None)
if city is not None or postal_code is not None:
queryset = Restaurant.objects.filter(
Q(city=city) | Q(pincode=postal_code))
if country and city is not None and postal_code is None:
queryset = Restaurant.objects.filter(country=country, city=city)
return queryset
def get(self, request, format=None):
restaurant_qs = self.get_queryset()
ids_list = [restaurant.id for restaurant in restaurant_qs]
favourite_restaurant = is_favourite_restaurant(ids_list, self.request.user)
serializer = RestaurantSerializer(restaurant_qs, many=True)
return Response(serializer.data)
where is_favourite_restaurant is a custom function function which returns queryset of FAVOURITE restaurant(s) of a user. Now in the output for this GET request I am getting result as :
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414"
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397"
}
]
Whereas the desired output I want is to append an extra field is_favourite:true to that restaurant which user has previously marked favourite. And hence the output should be
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414",
"is_favourite": true,
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397"
}
]
EDIT :
Definition of is_favourite_restaurant function :
def is_favourite_restaurant(restaurant_qs, user):
favourite_restaurant_qs = Profile.objects.get(user=user).favourite_restaurant.filter(
pk__in=restaurant_qs.values_list('id', flat=True))
return favourite_restaurant_qs
You can use SerializerMethodField. SerializerMethodField allows add extra field which is read only as you want.
class RestaurantSerializer(serializers.ModelSerializer):
is_favorite = serializers.SerializerMethodField()
class Meta:
model = Restaurant
fields = ('your', 'fields', 'is_favorite')
def get_is_like(self, obj):
return is_favourite_restaurant(obj.id, self.context['request'].user)
Normally, ListAPIView add context to serializer. As you use your create method, you should add manually.
serializer = RestaurantSerializer(restaurant_qs, many=True, context={'request': self.request})
Context allows access some data which is we send from the view.
As you did not shown your is_favourite_restaurant, i can't say that what should you do in that function. I guess you should change ids parameter from array to one id.
Your response looks like
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414",
"is_favourite": True,
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397",
"is_favourite": False,
}
]
def is_favourite_restaurant(restaurant_id, user):
favourite_restaurant_qs = Profile.objects.get(user=user).favourite_restaurant.filter(
pk=restaurant_id).exists()
return favourite_restaurant_qs

How to retrieve a many-to-many field with backward relationships lookup in Django REST Framework serializer?

Please correct my title if it's not correct. My problem is I want to retrieve FinishType's name from Product. I have tried 2 ways to achieve this: first attempt and second attempt.
My simplifed related models in models.py:
class Product(models.Model):
product_id = models.CharField(max_length=6)
color = models.ForeignKey(ColorParent, on_delete=models.SET_NULL, null=True)
collection = models.ForeignKey(ProductCollection, on_delete=models.SET_NULL, null=True)
#property
def get_distributors(self):
return Distributor.objects.filter(distributor__products=self).count()
def __str__(self):
return self.product_id
class FinishType(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class ProductFinishType(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
market = models.ForeignKey(Market, on_delete=models.CASCADE)
finish_types = models.ManyToManyField(FinishType)
def __str__(self):
return '%s - %s' % (self.product, self.market)
class ProductAlias(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
market = models.ForeignKey(Market, on_delete=models.CASCADE)
name = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return '%s - %s' % (self.product, self.name)
My serializers.py:
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
finishing = serializers.SerializerMethodField('get_finish_types')
distributor = serializers.ReadOnlyField(source='get_distributors')
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
#staticmethod
def get_finish_types(obj):
return [pft.name for pft in obj.productfinishtype_set.all().select_related('finish_types')] # first attempt
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
First attempt works for name field which fetches ProductAlias's name but gives me this error:
FieldError at /api/product_grids/
Invalid field name(s) given in select_related: 'finish_types'. Choices are: product, market
My get_finish_types() on second attempt:
#staticmethod
def get_finish_types(obj):
product_finish_types = obj.productfinishtype_set.all()
response = ProductFinishTypeSerializer(product_finish_types, many=True, source='finish_types').data
return response
It gives me the whole object datas:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
{
"id": 1,
"product": 1,
"market": 1,
"finish_types": [
1,
3,
5
]
}
],
"distributor": 5
},
My desired output is something like:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
"Polished",
"Carved",
"Melted"
],
"distributor": 5
},
Create a serializer for FinishType,
class FinishTypeSerializer(serializers.ModelSerializer):
class Meta:
model = FinishType
fields = ('name',)
and wire-up it in ProductGridSerializer using SerializerMethodField
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
distributor = serializers.ReadOnlyField(source='get_distributors')
finishing = serializers.SerializerMethodField()
def get_finishing(self, product):
qs = FinishType.objects.filter(productfinishtype__product=product)
return FinishTypeSerializer(qs, many=True).data
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
Inspired by #Arakkal Abu's queryset, I tried it using my first attempt.
FinishTypeSerializer added in serializers.py:
class FinishTypeSerializer(serializers.ModelSerializer):
class Meta:
model = FinishType
fields = ('name',)
ProductGridSerializer in serializers.py:
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
finishing = serializers.SerializerMethodField(source='get_finishing')
distributor = serializers.ReadOnlyField(source='get_distributors')
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
#staticmethod
def get_finishing(product):
return [pft.name for pft in FinishType.objects.filter(productfinishtype__product=product)]
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
The JSON output is:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
"Polished",
"Honed",
"Carved"
],
"distributor": 5
},

Queryset in one to many relation

I am trying to get a Json of elements with their related elements
I had two tables, Service and Room. One service have many rooms. I would like to get the service where have room_id = x.
Models
class Service(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = True
db_table = 'Service'
class Room(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
service = models.ForeignKey(Service, models.DO_NOTHING, blank=True,
null=True)
class Meta:
managed = True
db_table = 'Room'
Serializer
class ServiceSerializer(serializers.ModelSerializer):
room_set = RoomSerializer(many=True, read_only=True)
class Meta:
model = Service
fields = ('name','room_set')
class RoomSerializer(serializers.ModelSerializer):
class Meta:
model = Room
fields = '__all__'
View
queryset = Service.objects.filter(room__id=1)
serializer = ServiceSerializer(queryset, many=True)
return JsonResponse(serializer.data, safe=False)
I expect a Json like this:
{
"name": "Hotel1",
"room_set": [
{
"id": 1,
"name": "Room1"
},
But I get this:
{
"name": "Hotel1",
"room_set": [
{
"id": 1,
"name": "Room1",
},
{
"id": 2,
"name": "Room2",
},
{
"id": 3,
"name": "Room3",
}
}
Is it possible to get a json like the one I'm expecting?
You can patch the set by adding a custom Prefetch object [Django-doc] with a filtered queryset, like:
from django.db.models import Prefetch
queryset = Service.objects.filter(
room__id=1
).prefetch_related(
Prefetch('room_set', queryset=Room.objects.filter(id=1), to_attr='room_set1')
)
serializer = ServiceSerializer(queryset, many=True)
return JsonResponse(serializer.data, safe=False)
and let the Serializer parse the new related manager:
class ServiceSerializer(serializers.ModelSerializer):
room_set = RoomSerializer(many=True, read_only=True, source='room_set1')
class Meta:
model = Service
fields = ('name','room_set1')
class RoomSerializer(serializers.ModelSerializer):
class Meta:
model = Room
fields = '__all__'
You can pass the room id via the serializer context and filter accordingly inside a SerializerMethodField()
class ServiceSerializer(serializers.ModelSerializer):
rooms = serializers.SerializerMethodField()
class Meta:
model = Service
fields = ('name','rooms')
get_rooms(self,service):
room_id = self.get_context('room')
if room_id:
queryset = service.rooms_set.filter(id=room_id)
return RoomSerializer(queryset,many=True).data
return RoomSerializer(service.rooms_set.all(),many=True).data
serializer = ServiceSerializer(queryset, many=True,context={'room':1})
return JsonResponse(serializer.data, safe=False)
This's how to do it via the serializer and it's highly customizable , Willem Van Onsem's answer is brief enough , but it also requires two queries the same as of mine.

Django REST Framework nested serializer FK creation

I'm in trouble creating a bunch of related models using DRF nested serializers.
They are failing validation on the foreign key.
Models
class Employee(models.Model):
user = models.OneToOneField(User) # Django user
...
class Task(models.Model):
author = models.ForeignKey(Employee, related_name='tasks')
title = models.CharField(max_length=64)
...
class EmployeeTarget(models.Model):
employee = models.ForeignKey(Employee, null=False)
task = models.ForeignKey(Task, null=False, related_name='employee_targets')
...
Objective
Basically I have the Employees already created, and I want to create a Task and related EmployeeTarget in a single request, getting the request user as the author. JSON request example:
{
"title": "Lorem Ipsum",
"employee_targets": [
{ "employee": 10 },
{ "employee": 11 }]
}
/* or */
{
"title": "Lorem Ipsum",
"employee_targets": [10,11]
}
Serializers
class EmployeeSerializer(serializers.ModelSerializer):
name = serializers.CharField(source="user.get_full_name", read_only=True)
email = serializers.CharField(source="user.email", read_only=True)
class Meta:
model = Employee
class EmployeeTargetSerializer(serializers.ModelSerializer):
employee = EmployeeSerializer()
class Meta:
model = EmployeeTarget
class TaskSerializer(base.ModelSerializer):
employee_targets = EmployeeTargetSerializer(many=True, required=False)
class Meta:
model = Task
def create(self, validated_data):
employee_target_data = validated_data.pop('employee_targets')
task = Task.objects.create(**validated_data)
EmployeeTarget.objects.create(task=task, **employee_target_data)
return task
ViewSet
class TaskViewSet(ModelViewSet):
serializer_class = TaskSerializer
def get_queryset(self):
request_employee = self.request.user.employee
return Task.objects.filter(Q(author=request_employee) |
Q(employee_targets__employee=request_employee))
def perform_create(self, serializer):
serializer.save(author=self.request.user.employee)
Result
I'm getting 400 BAD REQUEST with the following error:
{
"employee_targets": [
{
"employee": {
"non_field_errors": ["Invalid data. Expected a dictionary, but got int."]
},
"task": ["This field is required."]
}
],
"author": ["This field is required."]
}
The employee error was expected, but I haven't figured out how to create them using only the ID.
The bigger problem here is the employee_targets failing validation at the task FK, before the enclosing TaskSerializer specify them at create method.
Can you try with this:
class EmployeeSerializer(serializers.ModelSerializer):
name = serializers.CharField()
email = serializers.CharField()
class Meta:
depth = 2
model = Employee

Categories

Resources