Nested ManytoMany Serialization in Django Rest Framework - django

In my app I have a nested many to many relation like the following:
Models.py
class ReturnKitsProducts(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
class ReturnKits(models.Model):
kit = models.ForeignKey(Kit, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
items = models.ManyToManyField(ReturnKitsProducts)
class Return(models.Model):
transaction_date = models.DateTimeField(default=datetime.now)
transaction_no = models.IntegerField(default=0, blank=True, null=True)
flow = models.ForeignKey(Flow, on_delete=models.CASCADE)
kits = models.ManyToManyField(ReturnKits)
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
In this ReturnKitsProducts is connected to ReturnKits as M2M and ReturnKits is connected to Return as M2M. I have handles only single level of M2M serialization for updatation and creation like this:
Serializers.py
class ReturnKitsSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKits
fields = "__all__"
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
items_objects = validated_data.pop('kits', None)
prdcts = []
for item in items_objects:
i = ReturnKits.objects.create(**item)
prdcts.append(i)
instance = Return.objects.create(**validated_data)
print("prdcts", prdcts)
instance.items.set(prdcts)
return instance
But I am not sure how to do serialization in the above mentioned scenario. Please Help!!

You can try something like this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKitsProducts
fields = "__all__"
class ReturnKitsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True) # same as
class Meta:
model = ReturnKits
fields = "__all__"
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
items_objects = validated_data.pop('kits', None)
instance = Return.objects.create(**validated_data)
for item in item_objects:
return_products = item.pop('items')
i = ReturnKits.objects.create(**item)
for data in return_products:
return_product = ReturnKitsProducts.objects.create(**data)
i.items.add(return_product)
instance.items.add(i)
return instance
What I did was pulling out the data from the validated_data dictionary and create instances as necessary.

While the other answer works (for create and get), it is always better to use serializers to save data when possible. In future you may need some kind of custom validation or you may need to override default create method, for nested serializer.
If you don't do following, you will miss out all functions serializer class provides and may need to write your all code on your own in create function of your main serializer, which will be dirty.
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnKitsProducts
fields = "__all__"
class ReturnKitsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True) # same as
class Meta:
model = ReturnKits
fields = "__all__"
def validate_items(self, value):
serializer = ItemSerializer(data=value, many=True)
if serializer.is_valid():
return serializer # this will be finally received and used in create function of main serializer
else:
raise serializers.ValidationError(serializer.errors)
def create(self, validated_data):
items = validated_data.pop('items')
r = ReturnKits.objects.create(**validated_data)
for item in items:
r.items.add(item.save()) # save each serializer (already validated) and append saved object
return r
class ReturnSerializer(serializers.ModelSerializer):
kits = ReturnKitsSerializer(many=True)
def validate_kits(self, value):
serializer = ReturnKitsSerializer(data=value, many=True)
if serializer.is_valid():
return serializer
else:
raise serializers.ValidationError(serializer.errors)
class Meta:
model = Return
fields = "__all__"
def create(self, validated_data):
# IMP: At this point all the data (including nested) will have been validated, so no error will throw when saving the data
kits = validated_data.pop('kits', None)
instance = Return.objects.create(**validated_data)
for kit in kits:
i = kits.save() # save each serializer (already validated) and get a object
instance.kits.add(i)
return instance
This code will work for create and get. If you want to update all your data at once, you should develop a approach.
You can try approach I use:
Updating data with its nested data
If the dictionary contains id field, the corresponding nested data will be updated.
If the dictionary does not contain id, field , new nested data will be created.
If the dictionary contains only id as key, the nested data will be deleted.
There is no need to provide nested data which do not need to be updated or deleted.
https://github.com/SagarKAdhikari/drf-nested-relations : A library where you can validate and save/update nested data to any depth, though it works for generic relation and foreign keys only for now( since only that was required in my project). You can try understanding the code and implment, if your nested relations are much deeper than at current and you have too many of them.

Related

Got AttributeError when attempting to get a value for field `members` on serializer `HealthQuotationSerializer`

Trying out serialising parent and child model.Here are my models:
class HealthQuotation(models.Model):
quotation_no = models.CharField(max_length=50)
insuredpersons = models.IntegerField()
mobile_no = models.CharField(max_length=10)
def __str__(self):
return self.quotation_no
class HealthQuotationMember(models.Model):
premium = models.FloatField(null=True)
suminsured = models.FloatField()
quotation = models.ForeignKey(HealthQuotation,on_delete=models.CASCADE)
def __str__(self):
return str(self.quotation)
Here are my serializers:
class HealthQuotationMemberSerializer(serializers.ModelSerializer):
class Meta:
model = HealthQuotationMember
fields= "__all__"
class HealthQuotationSerializer(serializers.ModelSerializer):
members = HealthQuotationMemberSerializer(many=True)
class Meta:
model = HealthQuotation
fields = ['id','members']
On Serialising parent model with parent serializer, Django throws error "Got AttributeError when attempting to get a value for field members on serializer HealthQuotationSerializer. The serializer field might be named incorrectly and not match any attribute or key on the HealthQuotation instance. Original exception text was: 'HealthQuotation' object has no attribute".
Because you don't have members field in your model... Try to change your serializer as following and see if it works:
class HealthQuotationSerializer(serializers.ModelSerializer):
quotation = HealthQuotationMemberSerializer()
class Meta:
model = HealthQuotation
fields = ['id','quotation']
Note that I've removed many=True because there will be always one object per this data (ForeignKey). when you have more than one object such as Many2Many you should use many=True.
You have "HealthQuotation" as a parent and "HealthQuotationMember" as a child.
Now, you have decided to retrieve data from parent "HealthQuotation"
and its associated children which will come from "HealthQuotationMember", right?
To achieve that you can use Django SerializerMethodField():
Your serializers.py should look like:
class HealthQuotationMemberSerializer(serializers.ModelSerializer):
class Meta:
model = HealthQuotationMember
fields= '__all__'
class HealthQuotationSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField() # I am using SerializerMethodField()
class Meta:
model = HealthQuotation
fields = '__all__'
def get_members(self, quotation):
q = HealthQuotationMember.objects.filter(quotation = quotation)
serializer = HealthQuotationMemberSerializer(q, many=True)
return serializer.data
Your views.py
class GetHealthQuotationList(ListAPIView):
serializer_class = HealthQuotationSerializer
queryset = HealthQuotation.objects.all()
Your url.py should be:
path('get-health-quotation-list', GetHealthQuotationList.as_view()),
NOTE: In case you plan to retrieve data from child table and find its associated parent, then your serializer should be good to go without many=True argument.

List Field serializer giving 'ManyRelatedManager' object is not iterable error with M2M field

My models.py looks like this:
class IP(models.Model):
name = models.CharField(max_length=30, unique=True)
address = models.CharField(max_length=50, unique=True)
class IPGroup(models.Model):
name = models.CharField(max_length=50, unique=True)
ips = models.ManyToManyField('IP', through='IPGroupToIP')
class IPGroupToIP(BaseModel):
ip_group = models.ForeignKey('IPGroup', on_delete=models.CASCADE)
ip = models.ForeignKey('IP', on_delete=models.CASCADE)
Now, in order to create an IPGroup, I have the following serializer:
class IPGroupCreateSerializer(serializers.ModelSerializer):
ips = serializers.ListField()
class Meta:
model = IPGroup
fields = ['name', 'ips']
#transaction.atomic()
def create(self, validated_data):
ips_data = validated_data.pop('ips', None)
ip_group = IPGroup.objects.create(name=validated_data['name'])
if ips_data:
for ip in ips_data:
ip_obj, created = IP.objects.get_or_create(name=ip['name'], address=ip['address'])
IPGroupToIP.objects.create(ip_group_id=ip_group.id, ip_id=ip_obj.id)
return ip_group
My views are a simple class based view as follows:
class IPGroupCreateView(generics.CreateAPIView):
queryset = IPGroup.objects.get_queryset()
serializer_class = IPGroupCreateSerializer
My JSON payload looks like this:
{
"ips": [{"name":"example.com", "address":"10.1.1.9"}],
"name": "ipgroup1"
}
This how ever gives me an error stating:
TypeError at /api/v1/ip-group/
'ManyRelatedManager' object is not iterable
The strange thing is that when i check the DB, the IPGroup is created along with the M2M ips. So, the code is working as expected but the view is somehow returning a 500 server error instead of a 201 created. What am i doing wrong ?
Due to some complications, ListField() will become handy only while writing to the DB ( I'm not sure why this behavior).
In your context, adding write_only=True in ListField solve the exception. Hence the IPGroupCreateSerializer will be as
class IPGroupCreateSerializer(serializers.ModelSerializer):
ips = serializers.ListField(write_only=True)
I personally reccomend to use Nested Serializers to handle this situation. So,define a new IPSerializer class and use it instead of serializers.ListField()
class IPSerializer(serializers.ModelSerializer):
class Meta:
model = IP
fields = ('name', 'address')
class IPGroupCreateSerializer(serializers.ModelSerializer):
ips = IPSerializer(many=True)
class Meta:
model = IPGroup
fields = ['name', 'ips']
I had a serializer that contains a list field(named communities) and this list field's child parameter was PrimaryKeyRelatedField.
What I did to tackle this problem is as below.
I defined a method in the model of instance called get_communities.
pop the list field in the to_representation method on serializer.
call super.
update returned value with update method because it is a dictionary.
and return the updated ret.
def to_representation(self, instance):
if isinstance(instance, OrderedDict):
return super(SerializerClassName, self).to_representation(instance)
self.fields.pop('communities')
ret = super(SerializerClassName, self).to_representation(instance)
ret.update({'communities': instance.get_communities()})
return ret
This can be bypassed locally for a specific field, problem in a 'to_representation'
class MyListFileField(serializers.ListField):
def to_representation(self, data):
return [
self.child.to_representation(item.your_field) for item in data.all()
]
class MySerializer(serializers.ModelSerializer):
field= MyListFileField(child=serializers.FileField())

Django Rest framework Serialize many to many field

I am trying to serialise a json payload that has a field with an array, the .is_valid() check is returning true but I am getting KeyError: 'passengers' when I try to do this serializer.data['passengers'] but the other fields work fine (such as booking_number and status).
This is the response.data I am passing to the seralizer:
{'booking_number': 2839, 'passengers': [{'first_name': 'Jack', 'surname': 'Smith', 'email': 'smith#mail.com', 'phone_number': '1234'}], 'status': 'ON_HOLD'}
My seralizers:
class PassengerSerializer(serializers.ModelSerializer):
class Meta:
model = Passenger
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(read_only=True, many=True)
class Meta:
model = Booking
fields = ('booking_number', 'passengers', 'status')
My models:
class Passenger(models.Model):
first_name = models.CharField(max_length=25)
surname = models.CharField(max_length=25)
email = models.EmailField()
phone_number = models.CharField(max_length=12)
class Booking(models.Model):
booking_number = models.IntegerField(unique=True)
passenger = models.ManyToManyField(Passenger)
status = models.CharField(max_length=10)
hold_time = models.DateTimeField()
Any advise on how to get this working would be greatly appreciated.
Btw I was following this: Django rest framework serializing many to many field
If you need to de-serialize fields, you should not use read_only=True:
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(many=True)
...
Note that this won't be enough for saving m2m relationships: as explained in Writable nested serializers, you'll also need to define create() and/or update() methods on your serializer:
class FindBus(serializers.ModelSerializer):
passengers = PassengerSerializer(many=True)
...
def create(self, validated_data):
...
def update(self, validated_data):
...
The reason for the need of the create/update is that you have to decide whether the passenger details that you receive refer to existing objects or need to be created.
You might also need to add fields = ('__all__',) (or specify the fields you're interested in) to your PassengerSerializer:
class PassengerSerializer(serializers.ModelSerializer):
class Meta:
model = Passenger
fields = ('__all__',)

Django Rest Framework - Set current user on model with unique constraint

I have a model that represents a device identifier and I'd like to create a unique constraint on the device identifier and the current user.
I was passing the user on the save method but I had to remove the constraint and now that I'm trying to write tests the poor code that I wrote becomes difficult to test. How can I write a serializer where I could set the currentuser as default and mantain the unique contrasint on the model.
This is my model
class DeviceIdentifier(models.Model):
owner = models.ForeignKey(HDSAuthUser, primary_key=True, on_delete=models.CASCADE)
id_device = models.UUIDField(primary_key=True)
insert_date = models.DateTimeField(auto_now_add=True)
and this is my serializer
class DeviceIdentifierSerializer(serializers.ModelSerializer):
"""
Device identifier serializer
"""
class Meta:
model = DeviceIdentifier
fields = ('id_device', 'owner')
and this is my view
class RegisterDevice(CreateAPIView):
"""
View for registering device for the logged user
"""
serializer_class = DeviceIdentifierSerializer
def post(self, request, *args, **kwargs):
obj = DeviceIdentifierSerializer(data=request.data)
obj.is_valid()
obj.save(owner=request.user)
return Response(True)
Add Unique constaint on models instead of Model Serializer, it will surely work.
class DeviceIdentifier(models.Model):
owner = models.ForeignKey(HDSAuthUser, on_delete=models.CASCADE)
id_device = models.UUIDField(primary_key=True)
insert_date = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('field1', 'field2',)
class DeviceIdentifierSerializer(serializers.ModelSerializer):
"""
Device identifier serializer
"""
class Meta:
model = DeviceIdentifier

How do you filter a nested serializer in Django Rest Framework?

In Django Rest Framework, how do you filter a serializer when it's nested in another serializer?
My filters are imposed in the DRF viewsets, but when you call a serializer from inside another serializer, the viewset of the nested serializer never gets called, so the nested results appear unfiltered.
I have tried adding a filter on originating viewset, but it doesn't seem to filter the nested results because the nested results get called as a separate pre-fretched query. (The nested serializer is a reverse lookup, you see.)
Is it possible to add a get_queryset() override in the nested serializer itself (moving it out of the viewset), to add the filter there? I've tried that, too, with no luck.
This is what I tried, but it doesn't even seem to get called:
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
def get_queryset(self):
query = super(QuestionnaireSerializer, self).get_queryset(instance)
if not self.request.user.is_staff:
query = query.filter(user=self.request.user, edition__hide=False)
return query
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
Here is the relevant code for your sample.
class FilteredListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.context['request'].user, edition__hide=False)
return super(FilteredListSerializer, self).to_representation(data)
class EditionSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredListSerializer
model = Edition
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
While all the above answers work, I find the use of Django's Prefetch object the easiest way of all.
Say a Restaurant obj has a lot of MenuItems, some of which are is_removed == True, and you only want those that are not removed.
In RestaurantViewSet, do something like
from django.db.models import Prefetch
queryset = Restaurant.objects.prefetch_related(
Prefetch('menu_items', queryset=MenuItem.objects.filter(is_removed=False), to_attr='filtered_menu_items')
)
In RestaurantSerializer, do something like
class RestaurantSerializer(serializers.ModelSerializer):
menu_items = MenuItemSerializer(source='filtered_menu_items', many=True, read_only=True)
Tested many solutions from SO and other places.
Found only one working solution for Django 2.0 + DRF 3.7.7.
Define a method in model which has nested class. Craft a filter that will fit your needs.
class Channel(models.Model):
name = models.CharField(max_length=40)
number = models.IntegerField(unique=True)
active = models.BooleanField(default=True)
def current_epg(self):
return Epg.objects.filter(channel=self, end__gt=datetime.now()).order_by("end")[:6]
class Epg(models.Model):
start = models.DateTimeField()
end = models.DateTimeField(db_index=True)
title = models.CharField(max_length=300)
description = models.CharField(max_length=800)
channel = models.ForeignKey(Channel, related_name='onair', on_delete=models.CASCADE)
.
class EpgSerializer(serializers.ModelSerializer):
class Meta:
model = Epg
fields = ('channel', 'start', 'end', 'title', 'description',)
class ChannelSerializer(serializers.ModelSerializer):
onair = EpgSerializer(many=True, read_only=True, source="current_epg")
class Meta:
model = Channel
fields = ('number', 'name', 'onair',)
Pay attention to source="current_epg" and you'll get the point.
I find it easier, and more straight forward, to use a SerializerMethodField on the serializer field you want to filter.
So you would do something like this.
class CarTypesSerializer(serializers.ModelSerializer):
class Meta:
model = CarType
fields = '__all__'
class CarSerializer(serializers.ModelSerializer):
car_types = serializers.SerializerMethodField()
class Meta:
model = Car
fields = '__all__'
def get_car_types(self, instance):
# Filter using the Car model instance and the CarType's related_name
# (which in this case defaults to car_types_set)
car_types_instances = instance.car_types_set.filter(brand="Toyota")
return CarTypesSerializer(car_types_instances, many=True).data
This saves you from having to create many overrides of the serializers.ListSerializer if you need different filtering criteria for different serializers.
It also has the extra benefit of seeing exactly what the filter does within the serializer instead of diving into a subclass definition.
Of course the downside is if you have a serializer with many nested objects that all need to be filtered in some way. It could cause the serializer code to greatly increase. It's up to you how you would like to filter.
Hope this helps!
When a serializer is instantiated and many=True is passed, a
ListSerializer instance will be created. The serializer class then
becomes a child of the parent ListSerializer
This method takes the target of the field as the value argument, and
should return the representation that should be used to serialize the
target. The value argument will typically be a model instance.
Below is the example of the nested serializer
class UserSerializer(serializers.ModelSerializer):
""" Here many=True is passed, So a ListSerializer instance will be
created"""
system = SystemSerializer(many=True, read_only=True)
class Meta:
model = UserProfile
fields = ('system', 'name')
class FilteredListSerializer(serializers.ListSerializer):
"""Serializer to filter the active system, which is a boolen field in
System Model. The value argument to to_representation() method is
the model instance"""
def to_representation(self, data):
data = data.filter(system_active=True)
return super(FilteredListSerializer, self).to_representation(data)
class SystemSerializer(serializers.ModelSerializer):
mac_id = serializers.CharField(source='id')
system_name = serializers.CharField(source='name')
serial_number = serializers.CharField(source='serial')
class Meta:
model = System
list_serializer_class = FilteredListSerializer
fields = (
'mac_id', 'serial_number', 'system_name', 'system_active',
)
In view:
class SystemView(viewsets.GenericViewSet, viewsets.ViewSet):
def retrieve(self, request, email=None):
data = get_object_or_404(UserProfile.objects.all(), email=email)
serializer = UserSerializer(data)
return Response(serializer.data)
The following worked for me, from self.context['view'], You can get the filter params inside the serializer and use it however you want.
class ShipmentDocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ['id', 'created_date', 'consignment', 'document', 'org', 'title' ]
class ShipmentDocumentTypeSerializer(serializers.ModelSerializer):
documents = serializers.SerializerMethodField()
class Meta:
model = DocumentType
fields = ['id', 'type', 'documents']
def get_documents(self, instance):
consignment_id=self.context['view'].kwargs['consignment_id']
queryset = Document.objects.filter(consignment__id=consignment_id)
return ShipmentDocumentSerializer(queryset, many=True).data