Queryset in one to many relation - python

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.

Related

How to override related field in django?

Let's say we have such models.
class Product(models.Model):
name = models.CharField(max_length=100)
# ...
main_photo = models.ImageField(upload_to='photos/')
class ProductPhoto(models.Model):
product = models.ForeignKey(Product, related_name='photos', on_delete=models.CASCADE)
photo = models.ImageField(upload_to='photos/')
def __str__(self):
return self.photo.url
I have two views:
ProductsView. It provides list of products with general information about each one, including name, ..., main_photo only.
ProductDetailsView. It provides more detailed info, including all photos.
class ProductsView(ListAPIView):
serializer_class = ProductSerializer
class ProductDetailsView(RetrieveAPIView):
serializer_class = ProductDetailsSerializer
serializers:
class ProductSerializer(ModelSerializer):
class Meta:
model = Product
fields = ('id', 'name', 'main_photo')
class ProductDetailsSerializer(ModelSerializer):
photos = StringRelatedField(many=True)
class Meta:
model = Product
fields = ('id', 'name', 'main_photo', 'photos')
I want detailed view to provide all photos in flat array photos, like this [main_photo, ...rest_photos].
In other words,
In response to detailed view instead of this:
{
"id": 1,
"name": "name",
"main_photo": "/media/photos/main_photo.jpg",
"photos": [
"/media/photos/photo1.jpg",
"/media/photos/photo2.jpg",
"/media/photos/photo3.jpg"
],
}
I want to get this:
{
"id": 1,
"name": "name",
"photos": [
"/media/photos/main_photo.jpg",
"/media/photos/photo1.jpg",
"/media/photos/photo2.jpg",
"/media/photos/photo3.jpg"
],
}
How can I do this with django rest framework? On which level should this logic be implemented? Model, View, Serializer?
I think it should be somewhere here, but not quite sure how should it look.
class ProductDetailsView(RetrieveAPIView):
serializer_class = ProductDetailsSerializer
def get_queryset(self):
query_set = Product.objects.all()
# ...
return query_set
For url of the photos add a __str__ method in ProductPhoto which will return only url of the photo
class ProductPhoto(models.Model):
...
def __str__(self):
return self.photo.url
and change ProductDetailsSerializer like this
class ProductDetailsSerializer(ModelSerializer):
photo_list = serializers.SerializerMethodField()
def get_photo_list(self, obj):
db_photos = obj.photos.all()
result = []
if obj.main_photo:
result.append(obj.main_photo.url)
for p in db_photos:
result.append(p.photo.url)
return result
class Meta:
model = Product
fields = ('id', 'name', 'photo_list')
For more relation related documentation for DRF check this

Python Django Rest Framework: The `.create()` method does not support writable nested fields by default

I am using django rest framework to create an api endpoint. I am using the default user model django offers. I need to create a post which uses the user as a foreign key. A user called "author" in the post can have multiple posts.
This is an example of a post json.
[
{
"author": {
"id": 1,
"username": "sorin"
},
"title": "First Post",
"description": "Hello World!",
"created_at": "2020-08-05T14:20:51.981163Z",
"updated_at": "2020-08-05T14:20:51.981163Z"
}
]
This is the model.
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
This is the serializer.
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
class PostSerializer(serializers.HyperlinkedModelSerializer):
author = UserSerializer()
class Meta:
model = Post
fields = ('author', 'title', 'description', 'created_at', 'updated_at')
I am getting the error "The .create() method does not support writable nested fields by default." when trying to make a post request using a "username", "title" and "description".
Any help to how to solve this?
I like hooking in the create function of the serializer for these kind of use cases.
Make sure your UserSerializer is set to read_only=True.
class PostSerializer(serializers.HyperlinkedModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Post
fields = ('author', 'title', 'description', 'created_at', 'updated_at')
def create(self, validated_data):
request = self.context['request']
author_data = request.data.get('author')
if author is None or not isinstance(author.get('id'), int):
raise ValidationError({'author': ['This field is invalid.']})
author_instance = get_object_or_404(User, id=author.get('id'))
return Post.objects.create(author=author_instance, **validated_data)

Django serializer output as part of JSON object

Context
I have 2 models: Customer & DeviceGroup.
Currently I have an API endpoint /api/v1/device-groups/?customer_uuid=<customer_uuid> which returns the DeviceGroups that are related to the given Customer like this:
[
{
"group_uuid": "c61361ac-0826-41bb-825a-8aa8e014ae0c",
"device_group_name": "Default",
"color": "0a2f45",
"is_default": true
},
{
"group_uuid": "1a86e8e4-b41b-4f33-aefb-ce984ef96144",
"device_group_name": "Testgroup",
"color": "123456",
"is_default": false
}
]
Goal
I want the array of DeviceGroups be part of an object like this:
"device_groups":
[
{
"group_uuid": "c61361ac-0826-41bb-825a-8aa8e014ae0c",
"device_group_name": "Default",
"color": "0a2f45",
"is_default": true
},
{
"group_uuid": "1a86e8e4-b41b-4f33-aefb-ce984ef96144",
"device_group_name": "Testgroup",
"color": "123456",
"is_default": false
}
]
Models
# models.py
class Customer(models.Model):
customer_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True)
customer_name = models.CharField(max_length=128, unique=True)
class DeviceGroup(models.Model):
group_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True)
customer_uuid = models.ForeignKey(Customer, on_delete=models.DO_NOTHING)
device_group_name = models.CharField(max_length=20)
color = models.CharField(max_length=10)
is_default = models.BooleanField(default=False)
Serializer
# serializers.py
class DeviceGroupSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceGroup
fields = ('group_uuid', 'device_group_name', 'color', 'is_default')
View
# views.py
class DeviceGroupCustomerViewSet(viewsets.ModelViewSet):
serializer_class = DeviceGroupSerializer
def get_queryset(self):
return DeviceGroup.objects.filter(customer_uuid=self.request.GET['customer_uuid'])
I tried creating a new serializer but it did not solve my problem:
class TestSerializer(serializers.ModelSerializer):
device_groups = DeviceGroupSerializer(many=True, read_only=True)
class Meta:
model = DeviceGroup
fields = ('device_groups', 'group_uuid', 'device_group_name', 'color', 'is_default')
What do I need to change in order to get my desired output?
you can update your views like
def list(self, request):
queryset = DeviceGroup.objects.filter(customer_uuid=self.request.GET['customer_uuid'])
serializer = UserSerializer(queryset, many=True)
return Response({'device_groups': serializer.data})
this will get the desired output..
Just modify your new serializer named TestSerializer in the following way.
class TestSerializer(serializers.Serializer):
device_groups = serializers.SerializerMethodField(read_only=True)
def get_device_groups(self, model):
return DeviceGroupSerializer(model).data
The response will be a paginated response. If you want to disable it just mention pagination_class as None in your ModelViewset class.
To achieve this fairly easily without loosing the pagination, I would do this:
from rest_framework.pagination import PageNumberPagination
class DeviceGroupPagination(PageNumberPagination):
def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('device_groups', data)
]))
Then in the views
class DeviceGroupCustomerViewSet(viewsets.ModelViewSet):
serializer_class = DeviceGroupSerializer
pagination_class = DeviceGroupPagination
...
So now, in place of results, you will have device_groups

Django REST Framework One to Many related field data insert and filtering

I have two model, Person and Appointment. A Person have many Appointment.
models.py
class Person(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField()
def __str__(self):
return self.name
class Appointment(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='appointments')
date = models.DateField(unique=True)
class Meta:
ordering = 'date',
def __str__(self):
return str(self.date)
and my serializers.py file is:
from rest_framework import serializers
from appointment.models import Person, Appointment
class AppointmentSerializer(serializers.ModelSerializer):
class Meta:
model = Appointment
fields = ('date',)
class PersonSerializer(serializers.ModelSerializer):
appointments = AppointmentSerializer(many=True)
class Meta:
model = Person
fields = ('name', 'email', 'appointments',)
def create(self, validated_data):
appointments_data = validated_data.pop('appointments')
person = Person.objects.create(**validated_data)
for appointment_data in appointments_data:
Appointment.objects.create(person=person, **appointment_data)
return person
and finally my viewsets is:
class PersonViewSet(viewsets.ModelViewSet):
serializer_class = PersonSerializer
queryset = Person.objects.all()
filter_backends = [DjangoFilterBackend]
filter_fields = ['name', 'appointments']
This return persons_list as my expectation:
[
{
"name": "Arif Hasan",
"email": "arif#gmail.com",
"appointments": [
{
"date": "2016-10-10"
},
{
"date": "2016-10-17"
},
{
"date": "2016-11-07"
}
]
},
{
"name": "Atanu Shome",
"email": "atanu#gmail.com",
"appointments": [
{
"date": "2016-11-13"
}
]
}
]
But i can't create new appointment here.
Can't filter person by date range.
You should use tuples for specifying filter_backends, filter_fields.
Also you want to filter by date field so you have to specify appointments__date in your filter_fields insted of just appointments.
You should have your viewset defined as follows:
class PersonViewSet(viewsets.ModelViewSet):
serializer_class = PersonSerializer
queryset = Person.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ('name', 'appointments__date',)
Check : Django filtering

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