Django Rest Framework - Interrelated serializers - python

Given the following models for which two have a ManyToMany relationship with the first, how can I construct a serializer for all three models?
This is where the importance of ordering in Python is turning to be an issue.
class Trail(models.Model):
'''
Main model for this application. Contains all information for a particular trail
'''
trail_name = models.CharField(max_length=150)
active = models.BooleanField(default=True)
date_uploaded = models.DateTimeField(default=now())
owner = models.ForeignKey(Account, default=1)
class Meta:
ordering = ('trail_name', )
class Activity(models.Model):
'''
A many to many join table to map an activity name to a trail. A trail can have many
activities
'''
name = models.CharField(max_length=40, unique=True)
trails = models.ManyToManyField(Trail)
class Meta:
ordering = ('name', )
class Surface(models.Model):
'''
A many to many join table to map a surface type to many trails. A trail can have many
surface types.
'''
type = models.CharField(max_length=50, db_column='surface_type', unique=True)
trails = models.ManyToManyField(Trail)
class Meta:
ordering = ('type', )
I have the following serializers in serializers.py:
class TrailSerializer(serializers.ModelSerializer):
owner = AccountSerializer()
activities = ActivitySerializer()
class Meta:
model = Trail
fields = ('trail_name', 'active', 'date_uploaded', 'owner', 'activities', )
class ActivitySerializer(serializers.ModelSerializer):
trails = TrailSerializer()
class Meta:
model = Activity
fields = ('trails', 'name', )
class SurfaceSerializer(serializers.ModelSerializer):
trails = TrailSerializer()
class Meta:
model = Surface
fields = ('trails', 'type', )
My question is, short of creating another file to contain ActivitySerializer and SurfaceSerializer, how can I ensure that TrailSerializer works as expected in order to include Activity and Surface references during serialization?

Use Marshmallow (serialization library inspired in part by DRF serializers). It solves this problem, by allowing nested schemas to be reference as strings. See Marshmallow: Two Way Nesting
class AuthorSchema(Schema):
# Make sure to use the 'only' or 'exclude' params to avoid infinite recursion
books = fields.Nested('BookSchema', many=True, exclude=('author', ))
class Meta:
fields = ('id', 'name', 'books')
class BookSchema(Schema):
author = fields.Nested(AuthorSchema, only=('id', 'name'))
class Meta:
fields = ('id', 'title', 'author')
You can use directly or use via django-rest-marshmellow by Tom Christie, which allows use of Marshmallow, while maintaining the same API as REST framework's Serializer class.
I'm not aware of a way to achieve the same result using just DRFs serializers.
See also: Why Marshmallow?

Related

Django serializer, nested relation and get_or_create

I've been bugging on this issue for some time now. I have two models : Acquisitions and RawDatas.
Each RawData have one Acquisition, but many RawDatas can have the same Acquisition.
I want to create or get the instance of Acquisition automatically when I create my RawDatas. And I want to be able to have all informations using the serializer.
class Acquisitions(models.Model):
class Meta:
unique_together = (('implant', 'beg_acq', 'duration_acq'),)
id = models.AutoField(primary_key=True)
implant = models.ForeignKey("Patients", on_delete=models.CASCADE)
beg_acq = models.DateTimeField("Beggining date of the acquisition")
duration_acq = models.DurationField("Duration of the acquisition")
class RawDatas(models.Model):
class Meta:
unique_together = (('acq', 'data_type'),)
id = models.AutoField(primary_key=True)
acq = models.ForeignKey("Acquisitions", on_delete=models.CASCADE)
data_type = models.CharField(max_length=3)
sampling_freq = models.PositiveIntegerField("Sampling frequency")
bin_file = models.FileField(db_index=True, upload_to='media')
And my serializers are these :
class AcquisitionSerializer(serializers.ModelSerializer):
class Meta:
model = Acquisitions
fields = ('id', 'implant', 'beg_acq', 'duration_acq')
class RawDatasSerializer(serializers.ModelSerializer):
acq = AcquisitionSerializer()
class Meta:
model = RawDatas
fields = ('id', 'data_type', 'sampling_freq', 'bin_file', 'acq')
def create(self, validated_data):
acq_data = validated_data.pop('acq')
acq = Acquisitions.objects.get_or_create(**acq_data)
RawDatas.objects.create(acq=acq[0], **validated_data)
return rawdatas
My problem is that, using this, if my instance of Acquisitions already exists, I get a non_field_errors or another constraint validation error.
I would like to know what is the correct way to handle this please ?
So I can automatically create this using the nested serializer, and when I only want to have informations (such as a GET request), I can have all the field I need (every field of the two models).
Thanks in advance for your help !
Try this:
class AcquisitionSerializer(serializers.ModelSerializer):
class Meta:
model = Acquisitions
fields = ('id', 'implant', 'beg_acq', 'duration_acq')
class RawDatasSerializer(serializers.ModelSerializer):
class Meta:
model = RawDatas
fields = ('id', 'data_type', 'sampling_freq', 'bin_file', 'acq')
def create(self, validated_data):
acq_data = validated_data.pop('acq')
acq = Acquisitions.objects.filter(id=acq_data.get('id')).first()
if not acq:
acq = AcquisitionSerializer.create(AcquisitionSerializer(), **acq_data)
rawdata = RawDatas.objects.create(acq=acq, **validated_data)
return rawdata

Nested Relationship in Django Doesn't Work

These are my models here:
class Site(models.Model):
siteID = models.CharField(max_length=255, primary_key=True)
class EndDevice(models.Model):
class Meta:
unique_together = ("edevID", "siteID")
edevID = models.CharField(max_length=255)
siteID = models.ForeignKey(Site, related_name='endDeviceList', on_delete=models.CASCADE)
deviceCategory = models.BigIntegerField()
This is my serilaizer:
class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = EndDevice
fields = ("edevID", "siteID", "deviceCategory")
class SiteSerializer(serializers.ModelSerializer):
endDeviceList = DeviceSerializer(many = True, read_only=True)
class Meta:
model = Site
fields = ("siteID", "endDeviceList")
This is my view:
class IndividualSite(generics.RetrieveUpdateDestroyAPIView):
'''
PUT site/{siteID}/
GET site/{siteID}/
DELETE site/{siteID}/
'''
queryset = EndDevice.objects.all()
serializer_class = SiteSerializer
I am trying to get/put/delete results using this class and I am trying to get all the EndDevice instances which have same siteID. But my serialzer only shows the siteID and doesn't show the endDeviceList (which should have the instants of the model EndDevice)
The problem is quite similar to this link:django rest-farmework nested relationships.
I have been trying different ways to serialize the objects, I think this is probably the smartest way, but have been really unsucccessful. Any help will be appreciated.
The urls.py:
urlpatterns = [
urlpatterns = [path('site/<str:pk>/', IndividualSite.as_view(), name = "get-site"),]
And it is connected to the main urls.
you are using read_only field for the Foreign relationship, remove that, as read_only wont display them
class SiteSerializer(serializers.ModelSerializer):
endDeviceList = DeviceSerializer(many = True)

django-admin-sortable not saving order of the existing objects

I use django-admin-sortable 2.1.2 and django 1.11.
The problem is that the order is not saving when I try to change it from my admin panel. I think this may be due to already existing model instances.
Here is the part of my current code:
// models.py
class Category(SortableMixin):
name = models.CharField(
_('name'),
max_length=150,
)
order = models.PositiveIntegerField(
default=0,
db_index=True,
)
class Meta:
verbose_name = _('category')
verbose_name_plural = _('categories')
ordering = ['order']
// admin.py
class CategoryAdmin(SortableModelAdmin):
class Meta:
model = Category
fields = (
'name',
)
sortable = 'order'
The default value is set as 0 because of already existing objects. I tried to change their order manually in shell console but it did not help.
I want to avoid deleting my objects and creating them again.
Do you have any ideas how to fix this?
I decided to use another class to inheritance from in my admin.py file.
Instead of:
from suit.admin import SortableModelAdmin
class CategoryAdmin(SortableModelAdmin):
class Meta:
model = Category
fields = (
'name',
)
sortable = 'order'
I use:
from adminsortable.admin import SortableAdmin
class CategoryAdmin(SortableAdmin):
class Meta:
model = Category
fields = (
'name',
)
sortable = 'order'
It works a little different but the effect is satisfying for me and solves my problem.

Using filters with model inheritance

I have used Django Filters successfully before to filter models like so:
class ProductFilter(django_filters.FilterSet):
minCost = django_filters.NumberFilter(name="cost", lookup_type='gte')
maxCost = django_filters.NumberFilter(name="cost", lookup_type='lte')
class Meta:
model = Product
fields = ['name', 'minPrice', 'maxPrice', 'manufacturer',]
Now I want to use a Django Filter to filter between many different models which all inherit from a base model, for example (my models are not this simple but to illustrate the point):
class BaseProduct(models.Model):
name = models.CharField(max_length=256)
cost = models.DecimalField(max_digits=10,decimal_places=2)
class FoodProduct(BaseProduct):
farmer = models.CharField(max_length=256)
class ClothingProduct(BaseProduct):
size = models.CharField(max_length=256)
Is there a way to use a Django filter that would work with all models that inherit from BaseProduct? In my case there would be a large number of models some with a large number of variables.
Add to your BaseProduct
class Meta:
abstract = True
https://docs.djangoproject.com/en/dev/topics/db/models/#abstract-base-classes
Base model will not be used to create any database table. Instead, when it is used as a base class for other models, its fields will be added to those of the child class.
https://django-filter.readthedocs.org/en/latest/usage.html#the-filter
Just like with a ModelForm we can also override filters, or add new ones using a declarative syntax
class BaseProductFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_type='icontains')
cost = django_filters.NumberFilter(lookup_type='lt')
class FoodProductFilter(BaseProductFilter):
farmer = django_filters.CharFilter(lookup_type='icontains')
class Meta:
model = FoodProduct
fields = ['name', 'cost', 'farmer']
class ClothingProductFilter(BaseProductFilter):
# size lookup_type will be 'exact'
class Meta:
model = ClothingProduct
fields = ['name', 'cost', 'size']

Include intermediary (through model) in responses in Django Rest Framework

I have a question about dealing with m2m / through models and their presentation in django rest framework. Let's take a classic example:
models.py:
from django.db import models
class Member(models.Model):
name = models.CharField(max_length = 20)
groups = models.ManyToManyField('Group', through = 'Membership')
class Group(models.Model):
name = models.CharField(max_length = 20)
class Membership(models.Model):
member = models.ForeignKey('Member')
group = models.ForeignKey('Group')
join_date = models.DateTimeField()
serializers.py:
imports...
class MemberSerializer(ModelSerializer):
class Meta:
model = Member
class GroupSerializer(ModelSerializer):
class Meta:
model = Group
views.py:
imports...
class MemberViewSet(ModelViewSet):
queryset = Member.objects.all()
serializer_class = MemberSerializer
class GroupViewSet(ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
When GETing an instance of Member, I successfully receive all of the member's fields and also its groups - however I only get the groups' details, without extra details that comes from the Membership model.
In other words I expect to receive:
{
'id' : 2,
'name' : 'some member',
'groups' : [
{
'id' : 55,
'name' : 'group 1'
'join_date' : 34151564
},
{
'id' : 56,
'name' : 'group 2'
'join_date' : 11200299
}
]
}
Note the join_date.
I have tried oh so many solutions, including of course Django Rest-Framework official page about it and no one seems to give a proper plain answer about it - what do I need to do to include these extra fields? I found it more straight-forward with django-tastypie but had some other problems and prefer rest-framework.
How about.....
On your MemberSerializer, define a field on it like:
groups = MembershipSerializer(source='membership_set', many=True)
and then on your membership serializer you can create this:
class MembershipSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.Field(source='group.id')
name = serializers.Field(source='group.name')
class Meta:
model = Membership
fields = ('id', 'name', 'join_date', )
That has the overall effect of creating a serialized value, groups, that has as its source the membership you want, and then it uses a custom serializer to pull out the bits you want to display.
EDIT: as commented by #bryanph, serializers.field was renamed to serializers.ReadOnlyField in DRF 3.0, so this should read:
class MembershipSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField(source='group.id')
name = serializers.ReadOnlyField(source='group.name')
class Meta:
model = Membership
fields = ('id', 'name', 'join_date', )
for any modern implementations
I was facing this problem and my solution (using DRF 3.6) was to use SerializerMethodField on the object and explicitly query the Membership table like so:
class MembershipSerializer(serializers.ModelSerializer):
"""Used as a nested serializer by MemberSerializer"""
class Meta:
model = Membership
fields = ('id','group','join_date')
class MemberSerializer(serializers.ModelSerializer):
groups = serializers.SerializerMethodField()
class Meta:
model = Member
fields = ('id','name','groups')
def get_groups(self, obj):
"obj is a Member instance. Returns list of dicts"""
qset = Membership.objects.filter(member=obj)
return [MembershipSerializer(m).data for m in qset]
This will return a list of dicts for the groups key where each dict is serialized from the MembershipSerializer. To make it writable, you can define your own create/update method inside the MemberSerializer where you iterate over the input data and explicitly create or update Membership model instances.
I just had the same problem and I ended it up solving it with an annotation on the group queryset.
from django.db.models import F
class MemberSerializer(ModelSerializer):
groups = serializers.SerializerMethodField()
class Meta:
model = Member
def get_groups(self, instance):
groups = instance.groups.all().annotate(join_date=F(membership__join_date))
return GroupSerializer(groups, many=True).data
class GroupSerializer(ModelSerializer):
join_date = serializers.CharField(required=False) # so the serializer still works without annotation
class Meta:
model = Group
fields = ..., 'join_date']
NOTE: As a Software Engineer, I love to use Architectures and I have deeply worked on Layered Approach for Development so I am gonna be Answering it with Respect to Tiers.
As i understood the Issue, Here's the Solution
models.py
class Member(models.Model):
member_id = models.AutoField(primary_key=True)
member_name = models.CharField(max_length =
class Group(models.Model):
group_id = models.AutoField(primary_key=True)
group_name = models.CharField(max_length = 20)
fk_member_id = models.ForeignKey('Member', models.DO_NOTHING,
db_column='fk_member_id', blank=True, null=True)
class Membership(models.Model):
membershipid = models.AutoField(primary_key=True)
fk_group_id = models.ForeignKey('Group', models.DO_NOTHING,
db_column='fk_member_id', blank=True, null=True)
join_date = models.DateTimeField()
serializers.py
import serializer
class AllSerializer(serializer.Serializer):
group_id = serializer.IntegerField()
group_name = serializer.CharField(max_length = 20)
join_date = serializer.DateTimeField()
CustomModels.py
imports...
class AllDataModel():
group_id = ""
group_name = ""
join_date = ""
BusinessLogic.py
imports ....
class getdata(memberid):
alldataDict = {}
dto = []
Member = models.Members.objects.get(member_id=memberid) #or use filter for Name
alldataDict["MemberId"] = Member.member_id
alldataDict["MemberName"] = Member.member_name
Groups = models.Group.objects.filter(fk_member_id=Member)
for item in Groups:
Custommodel = CustomModels.AllDataModel()
Custommodel.group_id = item.group_id
Custommodel.group_name = item.group_name
Membership = models.Membership.objects.get(fk_group_id=item.group_id)
Custommodel.join_date = Membership.join_date
dto.append(Custommodel)
serializer = AllSerializer(dto,many=True)
alldataDict.update(serializer.data)
return alldataDict
You would technically, have to pass the Request to DataAccessLayer which would return the Filtered Objects from Data Access Layer but as I have to Answer the Question in a Fast Manner so i adjusted the Code in Business Logic Layer!

Categories