I am making am app to control the presence of students. I have 4 models:
class Student(models.Model):
name = models.CharField(max_length=70)
class Justification(models.Model):
name = models.CharField(max_length=70)
class Session(models.Model):
date = models.DateTimeField()
present = models.ManyToManyField(Student)
absences = models.ManyToManyField(Student, related_name='absences_set', through='Absence')
class Absence(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE)
atleta = models.ForeignKey(Student, on_delete=models.CASCADE)
justification = models.ForeignKey(Justification, on_delete=models.CASCADE)
The models have more fields and different names (I translated the names to English) but this is basically it.
I am using DRF framework to make an API. I have setup the endpoints (and serializers) for Student, Justification and Absence but I can't figure out how to make the serializer for the Session model. I want it to work when someone makes the following POST (I only need an endpoint to create Sessions) request (I am using a ViewSet for the view):
{
"date": "2019-02-01T10:08:52-02:00"
"present": [
2
],
"absences": [
{
"student": 1,
"justification": 1
}
]
}
But the absences are not created. How can I make this nested relationship work?
ps: I can only make one request that's why I don't want to make one request to create a Session and then many requests to create Absences and need it all together. If there is a way to create all of them on the same request (but not only the same JSON object) I am okay with this solution
If i understand properly you want to create corresponding absences and season at same Season end-point. I think Justification and Student both model serve same, they are just student's instance and keep student information if i am not wrong. So i don't think there is actually any need to keep Justfication model. Corresponding absences ( students ) in Season Model need to ask for Justification. So my advice to keep model structure as like these
class Student(models.Model):
name = models.CharField(max_length=70)
class Session(models.Model):
date = models.DateTimeField()
present = models.ManyToManyField(Student)
absences = models.ManyToManyField(Student, related_name='absences_set', through='Absence')
class Absence(models.Model):
session = models.OneToOneField(Session, on_delete=models.CASCADE) # You can also keep Foreign-key
atleta = models.ForeignKey(Student, on_delete=models.CASCADE)
And then there are two possible way to create Absence model instance corresponding to Season Post endpoint. We can overwrite the post method of SeasonViewset and write our logic there or even can overwrite the SeasonSrealizer-create method to do same.
My preferable option is to overwrite post method of SeasonViewset. And these can be done as like following - over writing DRF CreateMixins
class SeasonViewSet(viewsets.ModelViewSet):
# your declare serializers and others thing
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
season_instance = self.perform_create(serializer)
# creating Absence's instance and you need to add other fields as necessary
Absence.objects.create(season=season_instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Related
supposing a models.py like:
class User(AbstractUser):
pass
class Notification(models.Model):
value = models.CharField(max_length=255)
class NotificationUserLink(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
notification = models.ForeignKey(Notification, on_delete=models.CASCADE)
active = models.BooleanField(default=True)
The point behind normalizing instead of just having a user field on the Notification itself is for when I want to send the same Notification to hundreds of people I only need 1 Notification object. Suppose I also have an endpoint in views.py like below where the user can see all their notifications and only their notifications, which is achieved by filtering the queryset.
class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = serializers.NotificationSerializer
model = Notification
lookup_field='pk'
def get_queryset(self):
user = self.request.user
return Notification.objects.filter(notificationuserlink__user=user)
Now the "user" and "notification" pair on the NotificationUserLink model form a candidate key (they are unique together. currently just by business logic but I'll add the actual constraint to the db at some point).
Therefore, given a user in the request, each Notification (in the filtered queryset) will have 1 and only 1 NotificationUserLink to that User. Therefore, from the User's perspective, that notification is either active or it isn't, depending on the "active" field on the NotificationUserLink.
What I want to accomplish is to include that field ("active") in the serialization of the Notification. I.e. if the user hits this endpoint they should see notifications like:
[
{
"value": "Bob commented on your post",
"active": False
},
{...}, ...
]
etc
I think that to achieve this I will need to override the "list" method on the view. But I don't know how. The serializer is being passed a queryset in the list method so I'm not sure how to inject data into that.
The serializer currently just looks like so:
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model=Notification
fields=("value",)
You can annotate the status of the link like this in your queryset:
Notification.objects.filter(
notificationuserlink__user=user
).annotate(
active=F('notificationuserlink__active')
)
And then you can add a read-only field active field in your serializer:
class NotificationSerializer(serializers.ModelSerializer):
active = serializers.BooleanField(read_only=True)
class Meta:
model = Notification
fields = ("active", "value",)
I need to assign ForeignKey to object without having it in Serializer class. Here's over-simplified version of my case:
I have models called Company, User and Order:
class Company(models.Model):
...
class User(AbstractBaseUser)
company = models.ForeignKey('Company', null=False)
...
class Order(models.Model):
company = models.ForeignKey('Company', null=False)
some_other_field = ...
...
This way there can be several Companies and each Company can have multiple users and orders. User is allowed to retrieve and create orders.
In a ModelViewSet that handles Order retrieval and creation operations I'm filtering queryset against requesting user:
.filter(company=self.request.user.company)
This way I can leave company field out of Serializer class for Order:
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('some_other_field', ...)
Problem arises when user needs to create order using POST request: company field can not be blank, but I don't want to add this field to serializer either because User is always assigned to company thus I internally can add this field by checking which user is sending this request.
So far I came up with very brutal solution to override entire create method from CreateModelMixin and assign fields manually after serializer validation:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
Reservation.objects.create(
company=self.request.user.company,
some_other_field=request.data['some_other_field']
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Add this in you CreateAPIView
This can be used to override serializer.save() method and add extra parameters before saving it.
def perform_create(self, serializer):
# Do any operatons, and get all the data you need
serializer.save( param1 = data1,
param2 = data2,
)
In your case:
def perform_create(self, serializer):
company=self.request.user.company
serializer.save(company = company)
You can use
serializer.save(company=company)
In the viewset, you can customize at the method perform_create, with something like
serializer.save(company=self.request.user.company)
When making querysets in Django, you can use the double underscore method, to filter on specific criteria of a model:
E.g. instead of filtering restaurant by their pk like this:
restaurants = Restaurant.objects.filter(restaurant=<some_pk>)
You can filter them by some attribute of the model like this:
restaurants = Restaurant.objects.filter(restaurant__uuid=<some_uuid>)
Or
restaurants = Restaurant.objects.filter(restaurant__name=<some_name>)
Now in function in the api views you (e.g. in get_queryset) you can use this to filter by kwargs of the request:
def get_queryset(self):
restaurants = Restaurant.objects.filter(restaurant__uuid=self.kwargs['uuid'])
return restaurants
Now I want to do the same using serializer.save(). In my perform create I want to call:
def perform_create(self, serializer):
serializer.save(restaurant__uuid=self.request.data['uuid'])
This unfortunately throws an error. My workaround is this:
def perform_create(self, serializer):
restaurant = Restaurant.objects.get(restaurant__uuid=self.kwargs['uuid'])
serializer.save(restaurant=restaurant)
but this obviously has bad performance, because it needs to do an additional queryset. Is there any way to get it work in one step within serializer.save()?
Edit
Upon the request I added the restaurant model:
class Restaurant(models.Model):
name = models.Charfield(max_length=255)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
owner = models.ForeignkeyField(
Owner,
on_delete=models.CASCADE
)
class Chef(models.Model):
restaurant = models.ForeignKey(
Restaurant,
on_delete=models.CASCADE
)
training = models.Charfield(max_length=255)
Just to clarify I want to call serializer.save() for a Chef, dynamically giving him the restaurant based on the UUID.
Give a try with a single underscore.
def perform_create(self, serializer):
serializer.save(restaurant_uuid=self.request.data['uuid'])
I am new to Django. Reading a lot of ways to do the same thing--but not finding the proverbial needle in a haystack. One such needle is a simple "Find or Create" pattern for Django Rest.
I am trying to find a simple example of how to go about implementing a find or create pattern for one of my model data using Django Rest ModelSerializer and CreateAPIView methods. Let say that I have a model Location with a unique field 'address'. I want to return an existing instance when the address already exists on my database. If the address does not exist, I want to create an entry in the database and populate other computed values for the object.
class Location(models.Model):
address = models.CharField(max_length=100, unique=True,)
thing1 = models.CharField(max_length=100, blank=True, null=True, )
thing2 = models.CharField(max_length=100, blank=True, null=True, )
def compute_things(self, address):
somevalue1 = ...
somevalue2 = ....
return somevalue1, somevalue2
Now, I am not exactly sure how to write the serializer and view so that:
A new location is created and returned with all the fields
initialized when a new address is seen for the first time
An existing location that matches 'address' in the database is
returned in lieu of step 1
What else should I define for the model? How do I write APIView and CreateSerializer to get the right thing? Where should I call the compute_thing() in order to populate the missing fields.
For the serializer:
class LocationCreateSerializer(ModelSerializer):
class Meta:
model = Location
And for the APIView:
class LocationCreateAPIView(CreateAPIView):
serializer_class = LocationCreateSerializer
queryset = Location.objects.all()
The APIView and Serializer above are not enough for what I need. What else do I need to add to model, View and Serializer to get that behavior that I am seeking?
I don't want the View nor the Serializer to return validation errors for duplicate 'addresses'--just the existing instance and not an error. It looks like restore_object() is deprecated. Is there a way to accomplish what I am seeking?
You missed one thing, that is,
fields =("Here will be your models fields. That you want to serialize.")
That is after the model = Location in serializer.
And you can follow official doc of Django-REST-Framework
Ok, I figured out the answer to my own question. I am not sure this is the best solution; however, for anyone that needs a solution, here is what I ended up doing:
class LocationCreateAPIView(CreateAPIView):
serializer_class = LocationCreateSerializer
queryset = Location.objects.all()
def post(self, request, format=None):
address = None
if 'address' in self.request.data:
address = self.request.data['address']
else:
return Response(status=HTTP_400_BAD_REQUEST)
try:
location = Location.objects.get(address=address)
serializer = self.get_serializer(location)
return Response(serializer.data, status=HTTP_200_OK)
except Location.DoesNotExist:
pass
serializer = LocationCreateSerializer(data=self.request.data)
if serializer.is_valid():
somevalue1, somevalue2 = Location.compute_things(self, address=address)
if (not somevalue1) | (not somevalue2):
return Response(status=HTTP_400_BAD_REQUEST)
serializer.save(address=address, thing1=somevalue1, thing2=somevalue2)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(status=HTTP_400_BAD_REQUEST)
If you have a better solution, please post it. I'd like to continue learning.
I'm novice in python and django rest. But I'm confused. What is the best way to update many to many relation in django rest framework.
I read the docs
http://www.django-rest-framework.org/api-guide/relations/#manytomanyfields-with-a-through-model
By default, relational fields that target a ManyToManyField with a through model specified are set to read-only.
If you explicitly specify a relational field pointing to a ManyToManyField with a through model, be sure to set read_only to True.
So if I have a code
class Master(models.Model):
# other fields
skills = models.ManyToManyField(Skill)
class MasterSerializer(serializers.ModelSerializer):
skills = SkillSerializer(many=True, read_only=False)
This will return skills as list of objects. And I don't have a way to update them. As far as I understood Django prefers work with objects vs object id when it comes to M2M. If I work with yii or rails I will work with "through" models. I would like to get skill_ids field. That I could read and write. And I can do this for write operation
class MasterSerializer(serializers.ModelSerializer):
skill_ids = serializers.ListField(write_only=True)
def update(self, instance, validated_data):
# ...
validated_data['skill_ids'] = filter(None, validated_data['skill_ids'])
for skill_id in validated_data['skill_ids']:
skill = Skill.objects.get(pk=skill_id)
instance.skills.add(skill)
return instance
But I cannot make it return skill_ids in field. And work for read and write operations.
A few things to note.
First, you don't have an explicit through table in your example. Therefore you can skip that part.
Second, you are trying to use nested serializers which are far more complex than what you're trying to achieve.
You can simply read/write related id by using a PrimaryKeyRelatedField:
class MasterSerializer(serializers.ModelSerializer):
skills_ids = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=Skill.objects.all(), source='skills')
Which should be able to read/write:
{id: 123, first_name: "John", "skill_ids": [1, 2, 3]}
Note that the mapping from JSON's "skill_ids" to model's "skills" is done by using the optional argument source
I will try to bring some light in terms of design: in Django if you specify the model for a ManyToManyRelation, then the relation field on the model becomes read-only. If you need to alter the associations you do it directly on the through model, by deleting or registering new records.
This means that you may need to use a completely different serializer for the through model, or to write custom update/create methods.
There are some sets back with custom through model, are you sure you're not good enough with the default implementation of ManyToManyFields ?
tl;dr:
For a much simpler, one-liner solution for M2M, I sussed out a solution of the form:
serializer = ServiceSerializer(instance=inst, data={'name':'updated', 'countries': [1,3]}, partial=True)
if serializer.is_valid():
serializer.save()
For a more complete example, I have included the following:
models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=50, null=False, blank=False)
class Service(models.Model):
name = models.CharField(max_length=20, null=True)
countries = models.ManyToManyField('Country')
serializers.py
from rest_framework import serializers
from .models import *
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ('name',)
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ('name', 'countries',)
Make sure some dummy service and country instances are created for testing. Then you can update an instance in a function like so:
Update example
# get an object instance by key:
inst = ServiceOffering.objects.get(pk=1)
# Pass the object instance to the serializer and a dictionary
# Stating the fields and values to update. The key here is
# Passing an instance object and the 'partial' argument:
serializer = ServiceSerializer(instance=inst, data={'name':'updated', 'countries': [1,3]}, partial=True)
# validate the serializer and save
if serializer.is_valid():
serializer.save()
return 'Saved successfully!'
else:
print("serializer not valid")
print(serializer.errors)
print(serializer.data)
return "Save failed"
If you inspect the relevant tables, the updates are carried through including to the M2M bridging table.
To extend this example, we could create an object instance in a very similar way:
### Create a new instance example:
# get the potential drop down options:
countries = ['Germany', 'France']
# get the primary keys of the objects:
countries = list(Country.objects.filter(name__in=countries).values_list('pk', flat=True))
# put in to a dictionary and serialize:
data = {'countries': countries, 'name': 'hello-world'}
serializer = ServiceOfferingSerializer(data=data)
I have dealt with this issue for quite some time and I have found that the best way to solve the general problem of updating any many to many field is by working around it.
In my case there is a model called Listing and a user can make a Subscription(the other model) to an instance of the Listing model. The Subscription works with a Generic Foreign Key and the Listing imports the Subscriptions of the users via Many2Many.
Instead of making a PUT request to the Listing Model via API, I simply add the Subscription instance to the right model in the POST Method of the API View of Subscription. Here is my adjusted code:
#Model
class Listing(models.Model):
#Basics
user = models.ForeignKey(settings.AUTH_USER_MODEL)
slug = models.SlugField(unique=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
#Listing
title = models.CharField(max_length=200)
price = models.CharField(max_length=50, null=True, blank=True)
subscriptions = models.ManyToManyField(Subscription, blank=True)
class Subscription(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
#Views
class APISubscriptionCreateView(APIView): #Retrieve Detail
def post(self, request, format=None):
serializer = SubscriptionCreateSerializer(data=request.data)
if serializer.is_valid():
sub = serializer.save(user=self.request.user)
object_id = request.data['object_id']
lis = Listing.objects.get(pk=object_id)
lis.subscriptions.add(sub)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I hope this will help, it took me a while to figure this out