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)
Related
I'm writing a REST API using Django Rest Framework and I would like that one of my routes accepts bulk adding on POST method, to create multiple objects. Others methods (GET, PUT, PATCH, DELETE) will still remain the same, accepting only one at a time.
What I have so far is below and it's currently working just fine for posting one at a time.
In my urls.py:
path('book', books.BookViewSet.as_view()),
books.py:
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
queryset = Book.objects.all()
permission_classes = (IsAuthenticated, )
serializer.py:
class BookSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# I assume this is the method to be overridden to get this
class Meta:
model = Book
fields = ('id', 'name', 'author_id', 'page_number', 'active')
Serializer create method, unfortunatelly creates data object by object.You can override create method of ModelViewSet and after validation use bulk_create method.
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = BookSerializer(data=request.data, many=many)
serializer.is_valid(raise_exception=True)
author = request.user # you can change here
book_list = [Book(**data, author=author) for data in serializer.validated_data]
Book.objects.bulk_create(book_list)
return Response({}, status=status.HTTP_201_CREATED)
I have defined AViewSet and ASerializer for my AModel:
class AModel(Model):
name = CharField(16)
text = TextField()
related = ForeignField('ARelated')
class AViewSet(ModelViewSet):
queryset = AModel.objects.all()
serializer_class = ASerializer
class ASerializer(Serializer):
class Meta(object):
model = AModel
fields = '__all__'
I wrote a RESTful client that posts a lot of data to that view/endpoint in multiple requests, creating many AModel records. I have noticed, however that a significant part of the server time is spent on generating the response, and upon googling for a bit I found several references to the nested relationship hazard which seems like a decent fix, but got me wondering:
I already know what I posted and I don't need the pks, so could I prevent that serialization response from happening entirely? Can I instead just serialize the number of rows inserted?
Taking a look at DRF's CreateModelMixin class:
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
I realized I could override the create method and reimplement it without returning the serializer.data as part of the response, so it'll look similar to this:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response({}, status=status.HTTP_201_CREATED, headers=headers)
I have two questions regarding this approach:
Does this practice of preventing the full serialization of objects created with a POST makes sense wrt RESTful design patterns, approach, ideology, etc?
Will this actually avoid selecting all the related data (as well as execute any SerializerMethodFields, etc?
Let's take the basic ModelViewset and ModelSerializer combination here :) It will be like,
# serializers.py
class SampleSerializer(serializers.ModelSerializer):
class Meta:
model = SampleModel
fields = '__all__'
# views.py
class SampleViewset(viewsets.ModelViewSet):
queryset = SampleModel.objects.all()
serializer_class = SampleSerializer
Why DRF returning all the data back to client ?
Here, the SampleViewset is using SampleSerializer everytime and it will serialilze all fields defined in the serializer class. As per the current configuration
What is the possible solution?
The possible solution is for this is stop serialization process of certain fields by some means :)
How to do?
As far as I knew, this can be done in two ways.
1. Use a minimal SampleSerializer class for POST method
2. override the to_representation() method of SampleSerializer on POST requests
Method-1 : Use different serializer
Define a new serializer class with fields which are you wish to send and retrive while POST request
class SampleSerializerMinimal(serializers.ModelSerializer):
class Meta:
model = SampleModel
fields = ('id', 'name', 'age')
Now, we've to tell viewsets to use this serializer for POST methods, it can be done in get_serializer_class() of viewset
class SampleViewset(viewsets.ModelViewSet):
queryset = SampleModel.objects.all()
serializer_class = SampleSerializer
def get_serializer_class(self):
if self.request.method == 'POST':
return SampleSerializerMinimal
return SampleSerializer
Method-2 : Override the to_representation() method
class SampleSerializer(serializers.ModelSerializer):
class Meta:
model = SampleModel
fields = '__all__'
def to_representation(self, instance):
try:
if self.context['view'].request.method == 'POST':
return {
"id": instance.id,
"name": instance.name,
"age": instance.age
}
except KeyError:
return super(SampleSerializer, self).to_representation(instance)
return super(SampleSerializer, self).to_representation(instance)
What is the best way ?
I felt Method-1 is more DRF way of doing things, but you can't add id only to the fields because, POST request may require more fields.
The Method-2 to is also good, but its not much clean if you want to return n fields, and writing it in your to_representation() method
UPDATE-1
Method-3: Combination of method-1 and method-2
# serializer.py
class SampleSerializerMinimal(serializers.ModelSerializer):
class Meta:
model = SampleModel
fields = ('id', 'name', 'age')
def to_representation(self, instance):
"""
we don't have to check the request method, because DRF routing only POSt requests to this serializer
"""
return {"id": instance.id}
# views.py
class SampleViewset(viewsets.ModelViewSet):
queryset = SampleModel.objects.all()
serializer_class = SampleSerializer
def get_serializer_class(self):
if self.action.method == 'POST':
return SampleSerializerMinimal
return SampleSerializer
UPDATE-2
Does this practice of preventing the full serialization of objects created with a POST makes sense wrt RESTful design patterns, approach, ideology, etc?
the .data is calling to_representation() method, which calls the related objects and all other fields in the serializer. ( Source code of data property of serializer ) So, if you can avoid that .data call, it would be nice!.
Since I've seen many API responses with single detail like {"status":true} after a POST request, I don't think your approch overkill the DRF patterns and other stuff
Will this actually avoid selecting all the related data (as well as execute any SerializerMethodFields, etc?
Yes. As I said above, It won't call the serializations process unless calling the .data
I have defined a Model like this:
class Doctor(models.Model):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
My Serializer:
class DoctorSerializer(serializers.ModelSerializer):
class Meta:
model = Doctor
fields = ('id', 'name', )
In View:
class DoctorViewSet(viewsets.ModelViewSet):
queryset = Doctor.objects.all()
serializer_class = DoctorSerializer
Now, I can delete a doctor by calling the url: 'servername/doctors/id/', with the http method DELETE. However, I want to override the delete behavior for this model. I want that, when the user deletes a record, it's is_active field is set to false, without actually deleting the record from the database. I also want to keep the other behaviors of Viewset like the list, put, create as they are.
How do I do that? Where do I write the code for overriding this delete behavior?
class DoctorViewSet(viewsets.ModelViewSet):
queryset = Doctor.objects.all()
serializer_class = DoctorSerializer
def destroy(self, request, *args, **kwargs):
doctor = self.get_object()
doctor.is_active = False
doctor.save()
return Response(data='delete success')
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
I'm using Django Rest Framework in an API project and am trying to figure out if there's a way to use two different serializers with the generic views (e.g. CreateAPIView). I want to use one serializer for deserializing a POST request, and a different one for serializing the resulting response.
Here's what I'm trying to do; I'll illustrate using the Album/Track examples from the docs:
The model that I'm working with has a ForeignKey relationship. In the API, I'd like to just be able to include the FK in the request when assigning the relationship, so in the serializer I'm using a PrimaryKeyRelatedField, similar to how the AlbumSerializer handles the relationship to Tracks:
class CreateAlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
However, on the response, I'd like to include a full representation of the Album using a ModelSerializer, not just the PK, slug, etc., something like this:
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ('order', 'title', 'duration')
The generic DRF views allow you to either specify the serializer_class or override the get_serializer_class method, but I can't figure out how to use that to accomplish what I'm after.
Is there something obvious that I'm missing? This seems like a reasonable thing to want to do, but I can't seem to grok how to get it done.
Approach #1
Overwrite the generic mixins in the DRF ViewSet. For example:
class MyViewSet(CreateModelMixin, MultipleSerializersViewMixin, ViewSet):
serializer_class = CreateAlbumSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
saved = self.perform_create(serializer)
serializer = self.get_serializer(instance=saved, serializer_class=AlbumSerializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save()
MultipleSerializersViewMixin is taken from django-rest-framework-braces.
Approach #2
Customize to_representation of the CreateAlbumSerializer. For example:
class MyViewSet(CreateModelMixin, ViewSet):
serializer_class = CreateAlbumSerializer
class CreateAlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
def to_representation(self, instance):
data = super(CreateAlbumSerializer, self).to_representation(instance)
data['tracks'] = TrackSerializer(instance=instance.tracks).data
return data
Comparison
I personally like approach #1 instead of #2 even though it is more verbose since it does not leak any of the custom create/response logic to serializers. I think serializer should just know how to serialize and all custom requirements to pick different serializers for the job should be done in the views.