DRF 2-way nested serialization with many to many relationship - django

I have a many-to-many relationship between my Student and Macro models, using an intermediary model
class Person(models.Model):
# cf/NIN optional by design
cf = models.CharField(_('NIN'), unique=True, blank=True, null=True, max_length=16)
first_name = models.CharField(_('first name'), blank=False, max_length=40)
last_name = models.CharField(_('last name'), blank=False, max_length=40)
date_of_birth = models.DateField(_('date of birth'), blank=False)
class Meta:
ordering = ['last_name', 'first_name']
abstract = True
def __str__(self):
return self.first_name + ' ' + self.last_name
class Macro(models.Model):
name = models.CharField(_('name'), unique=True, blank=False, max_length=100)
description = models.TextField(_('description'), blank=True, null=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Student(Person):
enrollment_date = models.DateField(_('enrollment date'), blank=True, null=True)
description = models.TextField(_('description'), blank=True, null=True)
macro = models.ManyToManyField(Macro, through='MacroAssignement')
class MacroAssignement(models.Model):
student = models.ForeignKey(Student, related_name='macros', on_delete=models.CASCADE)
macro = models.ForeignKey(Macro, related_name='students', on_delete=models.CASCADE)
def __str__(self):
return str(self.student)
I configure serializers in order to exploit the nested serialization when I serialize students
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('id',
'cf',
'first_name',
'last_name',
'date_of_birth')
abstract = True
class StudentSerializer(PersonSerializer):
macro = serializers.StringRelatedField(many=True, read_only=True)
class Meta(PersonSerializer.Meta):
model = Student
fields = PersonSerializer.Meta.fields + ('enrollment_date',
'description',
'macro')
extra_kwargs = {'enrollment_date': {'default': date.today()},
'description': {'required': False}}
class MacroSerializer(serializers.ModelSerializer):
students = StudentSerializer(many=True, read_only=True)
class Meta:
model = Macro
fields = ('id',
'name',
'description',
'students')
Untill here no problem, when I request student data, the macro related information comes along with it. Here's an example
{
"id": 18,
"cf": "ciaciacia",
"first_name": "Paolo",
"last_name": "Bianchi",
"date_of_birth": "2020-05-01",
"enrollment_date": null,
"description": null,
"macro": [
"macro1"
]
},
Now, on the contrary, when I request for a macro, I would like to view also the related students list. I've tried to implement nested serialization also in the MacroSerializer
class MacroSerializer(serializers.ModelSerializer):
students = StudentSerializer(many=True, read_only=True)
This doesn't work, as I get the following error
AttributeError: Got AttributeError when attempting to get a value for field `first_name` on serializer `StudentSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `MacroAssignement` instance.
Original exception text was: 'MacroAssignement' object has no attribute 'first_name'.
[NOTE: first_name is a field of Student model inherited from Person model]
Of course I could implement a function to query the database and get the name of students assigned to a given macro, but I'm wondering if there's a buil-in django way of doing it. Kind of like 2-way nested serialization

As stated in previous helpful comments, the fix is about using related_name. My code has been modified as following
serializers.py
class StudentSerializer(PersonSerializer):
macro = serializers.StringRelatedField(many=True, read_only=True)
class Meta(PersonSerializer.Meta):
model = Student
fields = PersonSerializer.Meta.fields + ('enrollment_date',
'description',
'macro')
extra_kwargs = {'enrollment_date': {'default': date.today()},
'description': {'required': False}}
class MacroSerializer(serializers.ModelSerializer):
students = StudentSerializer(many=True, read_only=True)
class Meta:
model = Macro
fields = ('id',
'name',
'description',
'students')
models.py
class Student(Person):
enrollment_date = models.DateField(_('enrollment date'), blank=True, null=True)
description = models.TextField(_('description'), blank=True, null=True)
macro = models.ManyToManyField(Macro, through='MacroAssignement', related_name='students')
Note that what I wrote in my question is actually a poor design decision, because it would lead to a circular dependency. Indeed, StudentSerializer would need MacroSerializer and viceversa. I highly suggest to read this question about dependencies in serializers
************ EDITED ************
Quick fix: set depth = 1 in the related model serializer (in this case, StudentSerializer) Thanks to #neverwalkaloner answer on this question

Related

retrieving the last instance of a model in another model's modelSerializer in django rest framework

I am creating rest APIs for a website in which users can purchase one of the provided subscriptions.
In this website there is a user-info API which returns the information about the logged in user which can be used to show their info on the website.
The problem is that, the mentioned API's serializer is a modelSerializer on the "User" model and the information that I want to return is the instance of "Subscription" model which the latest instance of "SubPurchase" model refers to.
These are my serializers, models and views.And I need to somehow return the user's current subscription's ID and name along with the user's information. If you have any further questions, ask me in the comments and I'll answer them.
# models.py
class User(AbstractBaseUser, PermissionsMixin):
userID = models.AutoField(primary_key=True)
username = models.CharField(max_length=100, unique=True, validators=[RegexValidator(regex="^(?=[a-z0-9._]{5,20}$)(?!.*[_.]{2})[^_.].*[^_.]$")])
email= models.EmailField(max_length=100, unique=True, validators=[EmailValidator()])
name = models.CharField(max_length=100)
isSuspended = models.BooleanField(default=False)
isAdmin = models.BooleanField(default=False)
emailActivation = models.BooleanField(default=False)
balance = models.IntegerField(default=0)
objects = UserManager()
USERNAME_FIELD = 'username'
class Subscription(models.Model):
subID = models.AutoField(primary_key=True)
nameOf = models.CharField(max_length=50)
price = models.PositiveIntegerField()
salePercentage = models.PositiveIntegerField(default=0)
saleExpiration = models.DateTimeField(default=datetime.datetime.now, blank=True)
def __str__(self):
return f"{self.nameOf}"
class SubPurchase(models.Model):
price = models.PositiveIntegerField()
dateOf = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
subscription = models.ForeignKey(Subscription, null=True, on_delete=models.SET_NULL)
def __str__(self):
return self.subscription
# serializers.py
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ('userID', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')
read_only_fields = ('userID', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')
# views.py
class UserInfoViewSet(viewsets.ModelViewSet):
queryset = get_user_model().objects.all()
serializer_class = UserInfoSerializer
def get_queryset(self):
uID = getattr(self.request.user,'userID')
return get_user_model().objects.filter(userID=uID)
def get_object(self):
uID = getattr(self.request.user,'userID')
return self.queryset.filter(userID=uID)
Again, I need to change the UserInfoSerializer in a way that would give me the user's current subscription's name, ID and expiration date which would be 30 days after the purchase date
If you are only interested in the returned data, you can override the function to_representation of your serializer and create a serializer for your related model. If I understood correctly, the current subscription of your user is the last one (if sorted by "dateOf"). So something like that could do the trick
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('nameOf', 'id', 'saleExpiration ')
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ('userID', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')
read_only_fields = ('userID', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')
def to_representation(self, instance):
data = super().to_representation(instance)
current_subs = instance.subpurchase_set.order_by('dateOf').last().subscription
data['current_subscription'] = SubscriptionSerializer(instance=current_subs).data
return data
you can use NestedSerializers to achieve what you are looking for
basically, nested serialization is a method in which you can return, create, put..., into a model from another model, it goes like this..
models.py
class User(AbstractBaseUser, PermissionsMixin):
....
#user model data
class SubPurchase(models.Model):
...
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields =["anyfield you wanna include"]
class SubPurchaseSerializer(serializers.ModelSerializer):
class Meta:
model = SubPurchase
fields =["anyfield you wanna include"]
class UserInfoSerializer(serializers.ModelSerializer):
subpurchace = SubPurchaseSerializer()
subscription= SubscriptionSerializer() #later included in the fields of this serializer
class Meta:
model = get_user_model()
fields = ('userID','subpurchace', 'subscription', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')
read_only_fields = ('userID', 'username','email', 'name', 'balance', 'emailActivation', 'isSuspended')

Get fields from through table in Django

I have two models, organization and vendor, which are connected by a 'relational' or 'through' table, VendorOrganization. There is a field description in my through table, which I am trying to access, but do not know. How should I configure my view, query and serializer to be able to access fields from the through table? Thanks for any help!
// models.py
class Organization(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=20, unique=True, validators=[alphanumeric_plus_underscore, MinLengthValidator(4)])
vendors = models.ManyToManyField("thirdparty.Vendor", through="thirdparty.VendorOrganization")
class Vendor(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=60, blank=False, null=False)
class VendorOrganization(models.Model):
vendor = models.ForeignKey(Vendor, blank=False, null=False, on_delete=models.CASCADE)
organization = models.ForeignKey(Organization, blank=True, null=False, on_delete=models.DO_NOTHING)
created_at = models.DateTimeField(_('created at'), auto_now_add=True, null=True)
created_by_user = models.ForeignKey(User, blank=False, null=False, on_delete=models.DO_NOTHING)
description = models.CharField(max_length=300, blank=True, null=False)
// serializers.py (this does not work to get the 'description' field)
class VendorSerializer(serializers.ModelSerializer):
class Meta:
model = Vendor
fields = ('id', 'name', 'created_at', 'created_by_user', 'description')
extra_kwargs = {
'id': {'read_only': True},
'created_at': {'read_only': True},
}
// views.py
class VendorView(mixins.DestroyModelMixin, ListAPIView):
authentication_classes = (TokenAuthentication,)
def get(self, request):
pagination_class = None
current_org = Organization.objects.get(id=request.user.organization.id)
queryset = current_org.vendors.select_related()
vendors = VendorSerializer(queryset, many=True)
return Response(vendors.data)
You can pass your organization_id with context to your serializer and in serializer, use SerializerMethodField(). Like that
in your views.py:
class VendorView(mixins.DestroyModelMixin, ListAPIView):
#your other codes
#add this method for pass data to your serializer
def get_serializer_context(self):
return {'organization_id': request.user.organization.id}
in your serializers.py file
class VendorSerializer(serializers.ModelSerializer):
description = serializers.SerializerMethodField()
class Meta:
model = Vendor
fields = ('id', 'name', 'created_at', 'created_by_user', 'description')
extra_kwargs = {
'id': {'read_only': True},
'created_at': {'read_only': True},
}
def get_description(self,obj):
organization = Organization.objects.get(id=self.context['organization_id'])
return obj.vendororganization_set.filter(organization=organization).first().description()
#NOTE: You can have multiple vendororganization, so you must set a logic in here.Perhaps you can change your vendor model with OneToOneFieldAnd you must check whether your vendor organziation exists.
In serializer.py change the model from Vendor to VendorOrganization. Because you are referencing both through that model

How do I filter data with two parameter from two different Django models

I want to create a JSON object which will
Search the particular Projects from the model "EmpProject" by a specific emp_id
Search whose project status is "Pending" from the model "Project" with the help of (1.) Search result
I am using JSON Parser (no models or generic view)
Models
Below are my models I have not use many to many field instead I created a Intermediate Table if the solution is also possible by using manytomanyfield than also suggest
class Employee(models.Model):
employeeid = models.IntegerField()
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
phone_no = models.CharField(max_length=10)
date_of_birth = models.DateField()
email = models.EmailField(unique=True)
password = models.CharField(max_length=50)
designation = models.CharField(max_length=50)
dept_id = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
ordering = ('id',)
def __str__(self):
return self.emp_name
class Project(models.Model):
projectname = models.CharField(max_length=50, unique=True,)
project_status = models.CharField(max_length=50)
description = models.TextField()
start_date = models.DateField(auto_now_add=True)
due_date = models.DateField()
class Meta:
ordering = ('id',)
def __str__(self):
return self.projectname
class EmpProject(models.Model):
emp_id = models.ForeignKey(Employee,on_delete=models.SET_NULL, null=True, blank=True)
project_id = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
unique_together = [['emp_id','project_id']]
ordering = ('project_id',)
def __str__(self):
return self.emp_id
Serializer
class EmployeeSerializer(serializers.ModelSerializer):
dept_id = serializers.SlugRelatedField(queryset=Department.objects.all(), slug_field='dept_name')
class Meta:
model = Employee
fields = [
'id',
'employeeid',
'first_name',
'last_name',
'phone_no',
'date_of_birth',
'email',
'password',
'designation',
'dept_id',
]
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = [
'id',
'projectname',
'project_status',
'description',
'start_date',
'due_date' ,
]
class EmpProjectSerializer(serializers.ModelSerializer):
emp_id=serializers.SlugRelatedField(queryset=Employee.objects.all(),slug_field='employeeid')
project_id=serializers.SlugRelatedField(queryset=Project.objects.all(),slug_field='projectname')
class Meta:
model = EmpProject
fields = [
'emp_id',
'project_id',
]
You can try something like this:
Project.objects.filter(id__in=EmpProject.objects.filter(id__in=emp_id).values("project_id"), project_status="Pending")
Few points worth mentioning:
It's good idea to use a intermediate table when we want to have extra data related to association. But for that kindly have a look at through attribute supported in ManyToManyField https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.ManyToManyField.through
project_status is CharField, I think you should consider ChoiceField, charfield may introduce lots of dirty data in database.

Multiple Objects Returned

I'm beginner in python and Django rest And I stuck when I fetch data.
Here is my API :- http://127.0.0.1:8000/subjects/course/23/
I want a all subject data according to course which I select..When I hit this api if single data present working awesome but when inside course_id multiple subject present then gives me error such as :
Exception Type: MultipleObjectsReturned
Exception Value: get() returned more than one Subject -- it returned 2!
Here is my model.py
class Subject(models.Model):
course = models.CharField(max_length=255, blank=False, unique=False)
subject = models.CharField(max_length=255, blank=False, unique=False)
description = models.CharField(max_length=255, blank=False, unique=False)
amount = models.CharField(max_length=255, blank=False)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
def __str__(self):
return "{}".format(self.title)
Here is my serializer.py
class SubjectSerializer(serializers.ModelSerializer):
class Meta:
model = Subject
fields = ('id', 'course', 'subject', 'description', 'amount', 'date_created', 'date_modified')
read_only_fields = ('date_created', 'date_modified')
lookup_field = 'course'
Here is my views.py
class ViewSubjectAccordingCourse(generics.RetrieveUpdateDestroyAPIView):
"""This class handles the GET and POSt requests of our rest api."""
queryset = Subject.objects.all()
serializer_class = SubjectSerializer
lookup_field = 'course'
Here is my urls.py
url(r'^subjects/course/(?P<course>[0-9]+)/$', ViewSubjectAccordingCourse.as_view(), name="details"),
so the problem is
course = models.CharField(max_length=255, blank=False, unique=False)
this value is identical for more then one subject, as you send in unique=False this seems to be by design, is the plan to have one or more subjects per course?
If its meant to be many subjects, you need to use the ListAPIView and some get_queryset modifications. If course is ment to only have a single Subject then change the field to unique=True

Django REST Framework join table results in attribute exception

I am building an API in Django using REST Framework but am running into an issue.
Serializers:
class SquadSerializer(serializers.Serializer):
class Meta:
model = Squad
fields = ('name')
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(style={'base_template': 'textarea.html'})
class MembershipSerializer(serializers.Serializer):
class Meta:
model = Membership
fields = ('employee_id', 'squad_id')
squad = SquadSerializer()
employee = EmployeeSerializer()
class EmployeeSerializer(serializers.HyperlinkedModelSerializer):
habitat = HabitatSerializer()
class Meta:
model = Employee
fields = ('id', 'first_name', 'last_name', 'function',
'start_date', 'end_date', 'visible_site', 'habitat')
Models:
class Employee(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
function = models.CharField(max_length=50)
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
visible_site = models.BooleanField()
habitat = models.ForeignKey(Habitat, on_delete=models.SET_NULL, null=True, blank=True)
class Squad(models.Model):
name = models.TextField(max_length=40)
class Membership(models.Model):
class Meta:
unique_together = (('employee', 'squad'))
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, null=False, blank=True, default=1)
squad = models.ForeignKey(Squad, on_delete=models.CASCADE, null=False, blank=True, default=1)
The problem is that I keep running into this error:
AttributeError: Got AttributeError when attempting to get a value for field `name` on serializer `SquadSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Membership` instance.
Original exception text was: 'Membership' object has no attribute 'name'.
When executing this test (and a couple others)
def test_membership_serializer_id_name_field_content(self):
"""
The name field of a squad should contain an id
"""
serializer = create_membership_serializer(self.membership, '')
self.assertEqual(serializer.data['id'], self.membership.id)
I've seen multipe people with the same issues here on Stack Overflow but the often suggest solution (to add many=True to SquadSerializer() and EmployeeSerializer()) doesn't work. I hope anyone here has any knowledge on why this happens.
If you want to map your seriailizer to your model, you should use ModelSerializer. In tupple, if it has only one value, you should write it as (1,) not (1). Your SquadSerializer should be like
class SquadSerializer(serializers.ModelSerializer):
class Meta:
model = Squad
fields = ('name',) # or ('id', 'name')
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(style={'base_template': 'textarea.html'})
Your MembershipSerializer should be like
class MembershipSerializer(serializers.ModelSerializer):
class Meta:
model = Membership
fields = ('employee', 'squad')
squad = SquadSerializer()
employee = EmployeeSerializer()