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
Related
I have a parent model which contains a database of unique records, as follows (truncated - there are many more fields):
models.py - parent
class DBPlatform(models.Model):
description = models.CharField(max_length=300, unique=True)
PDS_date = models.DateField()
PDS_version = models.CharField(max_length=50, blank=True)
I use this model to create a child model to save me copying all of the fields. The child model saves specific user-generated instances of the parent records. They are stored separately as they may be edited by the user:
models.py - child
class Platform(DBPlatform):
scenario = models.ForeignKey(Scenario,
on_delete=models.CASCADE,
related_name="platforms")
database_platform = models.ForeignKey(DBPlatform,
on_delete=models.CASCADE,
related_name="instances")
edited = models.BooleanField()
I am using Django REST Framework to create an API for the eventual app. When a child model is created, I want to update all of its inherited fields with those of the parent model. The incredibly convoluted steps I have taken so far (that do not work) are in the views.py file of the child mode. As follows:
api.views.py - child
class PlatformViewSet(viewsets.ModelViewSet):
lookup_field = "id"
serializer_class = PlatformSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
db_id = self.request.data["database_platform"]
database_platform = get_object_or_404(DBPlatform, id=db_id)
datadict = self.request.data.dict()
datadict.update(database_platform.__dict__)
query_dict = QueryDict('', mutable=True)
query_dict.update(datadict)
self.request.data = query_dict
serializer.save()
How can I achieve what I am looking to do? I surely am taking the wrong approach as this can't be an uncommon thing.
EDIT:
Ruddra's comment has made me consider that the whole design pattern is faulty. Should I just be using a single model and a boolean flag for the "template" instance?
The serializer data is not changed by the operations before serializer.save().
If you want to do it this way, you'll have edit the serializer or re-serialize the data.
Unless this is something that needs to happen only through API and only on this endpoint, I'd suggest overwriting the model's save method or using pre_save signal. To make sure this operation is performed only when creating a new instance, you can check if self (in case of overwriting save) or instance (in case of signal) has id.
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)
I've been using Django for over 3 years now, but have never felt the need to use DRF. However, seeing the growing popularity of DRF, I thought of giving it a try.
Serializing is the concept I find it most difficult. Consider for eg:- I want to save user details. Following is the user related models.
class Users(models.Model):
GENDER_CHOICES = (
('M', 'MALE'),
('F', 'FEMALE'),
('O', 'OTHERS'),
)
first_name = models.CharField(max_length=255, blank=True, null=True)
middle_name = models.CharField(max_length=255, blank=True, null=True)
last_name = models.CharField(max_length=255, blank=True, null=True)
gender = models.CharField(choices=GENDER_CHOICES, max_length=1, blank=True,
null=True)
class UserAddress(models.Model):
ADDRESS_TYPE_CHOICES = (
('P', 'Permanent'),
('Cu', 'Current'),
('Co', 'Correspondence')
)
line1 = models.CharField(max_length=255)
line2 = models.CharField(max_length=255, blank=True, null=True)
pincode = models.IntegerField()
address_type = models.CharField(choices=ADDRESS_TYPE_CHOICES,
max_length=255)
user_id = models.ForeignKey(Users, related_name='uaddress')
class UserPhone(models.Model):
phone = models.CharField(max_length=10)
user_id = models.ForeignKey(Users, related_name='uphone')
class UserPastProfession(models.Model):
profession = models.CharField(max_length=10) # BusinessMan, software Engineer, Artist etc.
user_id = models.ForeignKey(Users, related_name='uprofession')
I'm getting all the user details bundled in one POST endpoint.
{
'first_name': 'first_name',
'middle_name': 'middle_name',
'last_name': 'last_name',
'gender': 'gender',
'address': [{
'line1': 'line1',
'line2': 'line2',
'address_type': 'address_type',
}],
'phone': ['phone1', 'phone2'],
'profession': ['BusinessMan', 'Software Engineer', 'Artist']
}
Without using DRF, I would have made a Users object first, linking it with UserAddress, UserPhone and UserPastProfession object.
How the same could be done using DRF? I mean validating, serializing, and then saving the details. How serializers.py file will be look like?
If you want to make your life easy, you will surely use it.
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
This gives you a generic way to control the output of your responses, as well as a ModelSerializer class which provides a useful shortcut for creating serializers that deal with model instances and querysets.
They save you from writing a lot of custom code. Let’s look at some examples.
Pretend we have an app that tracks a list of tasks that the user has to complete by a certain date. The Task model might look something like the following:
class Task(models.Model):
title = models.CharField(max_length=255)
due_date = models.DateField()
completed = models.BooleanField(default=False)
When the user requests those tasks, our app returns a response in the form of a JSON-serialized string. What happens when we try to serialize our Django objects using the built-in json library?
import json
task = Task.objects.first()
json.dumps(task)
We get a TypeError. Task is not JSON serializable. To bypass this, we have to explicitly create a dictionary with each of the attributes from Task.
json.dumps({
'title': task.title,
'due_date': task.due_date.strftime('%Y-%m-%d'),
'completed': task.completed
})
Serializing a Python object from a JSON string or from request data is just as painful.
from datetime import datetime
title = request.data.get('title')
due_date = datetime.strptime(request.data.get('due_date'), '%Y-%m-%d').date()
completed = request.data.get('completed')
task = Task.objects.create(title=title, due_date=due_date, completed=completed)
Now, imagine having to follow these steps in multiple views if you have more than one API that needs to serialize (or deserialize) JSON data. Also, if your Django model changes, you have to track down and edit all of the custom serialization code.
Creating and using a serializer is easy:
from rest_framework import serializers
class TaskSerializer(serializers.ModelSerializer):
def create(self, validated_data):
return Task.objects.create(**validated_data)
class Meta:
model = Task
fields = ('title', 'due_date', 'completed')
# Serialize Python object to JSON string.
task_data = TaskSerializer(task).data
# Create Python object from JSON string.
task_data = TaskSerializer(request.data)
task = task_data.create()
If you update the Django model, you only have to update the serializer in one place and all of the code that depends on it works. You also get a lot of other goodies including (as you mentioned) data validation.
Hope that helps!
If I got you correctly, my answer is:
It is not necessary to write one serializer for a model, even for method type (POST,GET etc.). You can pretty much create serializers for your model as much as you need and set fields you want to operate on. You can also set those different serializers as serializer_class property of your APIView class per each method.
I strongly recommend you to take some time to look at the Django Rest Framework Tutorial
below is how your serializer can look.. but please go through this DRF serializer realtionship
from rest_framework.serializers import (
ModelSerializer,
PrimaryKeyRelatedField
)
class UserSerializer(ModelSerializer):
"""
Serializer for the users models.. Please dont forget to import the model
"""
class Meta:
model = Users
field = "__all__"
class UserPhoneSerializer(ModelSerializer):
"""
Serializer for the users address model..
Pass the previously created user id within the post.. serializer will automatically validate
it
"""
user_id = PrimaryKeyRelatedField(queryset=Users.objects.all())
class Meta:
model = UserPhone
field = "__all__"
class UserAddressSerializer(ModelSerializer):
"""
Serializer for the users address model..
Pass the previously created user id within the post.. serializer will automatically validate
it
"""
user_id = PrimaryKeyRelatedField(queryset=Users.objects.all())
class Meta:
model = UserAddress
field = "__all__"
class UserPastProfessionSerializer(ModelSerializer):
"""
Serializer for the UserPastProfession model..
Pass the previously created user id within the post.. serializer will automatically validate
it
"""
user_id = PrimaryKeyRelatedField(queryset=Users.objects.all())
class Meta:
model = UserPastProfession
field = "__all__"
In the tutorial, there's this loose one-to-one mapping between serializer fields and model fields. I can expect that if a serializer field and a model field are both CharFields it will save a string of characters when deserializing into a model instance:
models.py:
class Deck(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, unique=True, blank=False, null=False)
serializers.py:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Deck
fields = ('url', 'id', 'created', 'name')
extra_kwargs = {
'url': {
'view_name': 'goals:deck-detail',
}
}
But when I try a relationship, the serializer field is a ReadOnlyField, which from what I understand is essentially a Charfield, but the model field is a ForeignKeyField, and to add to the confusion, it seems like I'm saving an object in the views when I override perform_create:
models.py:
class Deck(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, unique=True, blank=False, null=False)
user = models.ForeignKey('users.User', related_name='decks', on_delete=models.CASCADE, null=False)
serializers.py:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
class Meta:
model = Deck
fields = ('url', 'id', 'created', 'name', 'user')
extra_kwargs = {
'url': {
'view_name': 'goals:deck-detail',
}
}
views.py:
class DeckList(generics.ListCreateAPIView):
serializer_class = DeckSerializer
def get_queryset(self):
return Deck.objects.all().filter(user__username=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
What's going on here? When dealing with relationships, why is it that I am saving an object by overriding perform_create in the views, declaring in the serializers a CharField, and in the models declaring a relationship field?
What's missing in my understanding, or what's really happening under the hood such that the user field (ForeignKey) can be represented as a string but saved as an object?
Edit:
If I'm overriding serializer.save(user=user) in the views and the serializers.py has the user field as
user = serializers.CharField(read_only=True)
And I want to override the save method in serializers.py, how do I pass the proper data so that it will know how to serialize? Do I just grab the whole User object, save it, and it'll do the rest? Is the serializers.save() override in the views the same as serializers.save() in serializers.py?
Not 100% sure that I've understood what you're asking, but if the question is:
What's happening under the hood when a ForeignKey field is saved in the Django ORM?
Then the answer is that:
The relation is saved in the DB as an (e.g.) int field which stores the primary key of the related object.
The ForeignKey field reference section in the Django docs explains how this part of the ORM works, and the "Database Representation" subsection likely touches on the specific bit you're interested in.
For example, for your case of User being a related field in the Deck model the underlying table would likely look like this (assuming postgresql):
myapp_deck
id int
created timestamp
name varChar
user_id int
The Deck -> User relation is mapped by the DB storing the pk for the related User object in the user_id field in the myapp_deck table.
So, all Django (and, consequently, DRF) needs to do to change the User in the Deck model is change the user_id in the myapp_deck table to the PK of a different User object.
Hope this helps, and please let me know if I've missed the point of your question.
Edited to Add Example of Custom .create() method
If you want to override the custom "save" method in a serializer then the methods to override are create() and update() accordingly (see Serializer "Saving instances" section in the DRF docs).
An example of this might be:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
... Rest of your serializer code ...
def create(self, validated_data, **kwargs):
user_obj = User.objects.get(pk=validated_data["user"])
deck = Deck.objects.create(
name=validated_data["name"],
user=user_obj,
)
return deck
Note: This assumes that the pk of the related User object is passed throught the serializer, validates OK, and is available in the validated_data dict.
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.