Accessing object in perform_create when using many=true - django

How can I get object in perform_create() when I use kwargs['many'] = True?
I get this error message:
Serializers with many=True do not support multiple update by default,
only multiple create. For updates it is unclear how to deal with
insertions and deletions. If you need to support multiple update, use
a ListSerializer class and override .update() so you can specify
the behavior exactly.
class CreateUserApiView(CreateAPIView):
model = User
...
serializer_class = CreateRequesterSerializer
def get_serializer(self, *args, **kwargs):
""" if an array is passed, set serializer to many """
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateUserApiView, self).get_serializer(*args, **kwargs)
def perform_create(self, serializer):
obj = serializer.save(
...,
created_by=self.request.user)
obj.send_invitation()

The problem is in serializer. Basically your serializer knows how to create one object, but you are asking him to create many. In DRF3 this many objects creation should be implemented manually.
So basically you need to rewrite your serializer: inherit it from ListSerializer (because you are expecting many objects input) and implement update method.

Related

Change serializer class for single method in Django Rest Framework

If I want to override a method in my viewset to change the serializer_class for only a single method, how can I do that.
I tried passing serializer_class=CustomSerializer but it doesn't have any effect.
class VoteViewset(viewsets.ModelViewSet):
queryset = Vote.objects.all()
# Use normal serializer for other methods
serializer_class = VoteSerializer
def list(self, request, *args, **kwargs):
# Use custom serializer for list method
return viewsets.ModelViewSet.list(self, request, serializer_class=VoteWebSerializer, *args, **kwargs)
Basically do the same list method as the inherited viewset, but use a different serializer to parse the data.
The main reason for this is because javascript does not handle 64 bit integers, so I need to return the BigInteger fields as a string instead of integer.
Override the get_serializer_class(...) method as
class VoteViewset(viewsets.ModelViewSet):
queryset = Vote.objects.all()
def get_serializer_class(self):
if self.action == "list":
return VoteWebSerializer
return VoteSerializer
What JPG answered is the correct way. Another thing you could do is overriding the list method like you are doing but modifying the default behavior using the serializer that you want.
It would be something like:
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
You can check the default methods in the source code or I recommend using Classy DRF. For example here you can see what DRF is doing in the list method of a ModelViewSet doing and use that as a starting point.

How to dynamically alter field mapping in django restframework's serializer that uses nested serializers?

I use the following serializer in most requests such as GET, POST etc:
class PrescriptionSerializer(serializer.ModelSerializer):
tags = TagSerializer()
setting = SettingSerializer()
But, I want to map setting field to SettingUpdateSerializer() if request.action is UPDATE(=PUT/PATCH). Without diving PrescriptionGetSerializer and PrescriptionUpdateSerialzer and using them accordingly, is there a way to dynamically map serializer-nesting field to other serializer, as below?
class PrescriptionSerializer(serializer.ModelSerializer):
tags = TagSerializer()
setting = SettingUpdateSerializer()
I though about using self.fields.pop on __init__, but this way it is only possible by using different different field names such as update_setting and get_setting.
Thanks for the help in advance.
I think the most clear solution is creating two separate serializer. And chose what serializer to use in a view layer depends on the http verb. If you use viewset it is easy to implement in a get_serializer_class method.
class SomeViewSet(viewsets.ModelViewset):
def get_serializer_class(self):
if self.action === 'update':
return UpdatePrescriptionSerializer
return PrescriptionSerializer
Now when you'll call a get_serializer in actions methods you'll get serializer depends on the action.
But you could also do something like you said:
class PrescriptionSerializer(serializer.ModelSerializer):
def __init__(self, *args, **kwargs):
super(PrescriptionSerializer, self).__init__(*args, **kwargs)
if self.context['request'].method == 'PUT':
self.fields['setting'] = SettingUpdateSerializer()
else:
self.fields['setting'] = SettingSerializer()
tags = TagSerializer()
Just ensure that you pass a request to serializer context. If you use get_serializer method in a viewset then it is already passed.

How to do a PUT (partial update) using generics in Django-Rest-Framework?

If I have a class view that looks like this,
class MovieDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
how do I make the serialize accept partial updates? currently where it stands Put will erase an existing data for said object.
If you are using the DRF route, use PATCH method instead of PUT.
if you write the urls configuration by yourself,
dispatch it to partial_update method in your RetrieveUpdateDestroyAPIView view.
If you get the serialize by yourself,
pass the partial=True to your Serializer
partial = kwargs.pop('partial', False)
serializer = self.get_serializer(instance, data=request.data, partial=partial)
Or you can just overwrite the get_serializer() method as:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(MovieDetail, self).get_serializer(*args, **kwargs)
It is especially useful when the front-end guy use the ngResource of the AngularJS to call your API, which only supports the 'put' instead of the 'patch' by default.
Hope it helps.

django rest framework multiple database

I have a view that I'm using for GET and POST to a database that's NOT the default DB.
class DeployResourceFilterView(generics.ListAPIView):
serializer_class = ResourceSerializer
def get(self, request, format=None):
resname = self.request.GET.get('name')
queryset = Resmst.objects.db_manager('Admiral').filter(resmst_name=resname)
serializer = ResourceSerializer(queryset)
if queryset:
return Response(serializer.data)
else:
raise Http404
def post(self, request, format=None):
serializer = ResourceSerializer(data=request.DATA, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The GET works perfectly fine but on the POST it constantly fails complaining that the table does not exist. My assumption is that the reason for this is because it's trying to use the default database not the 'Admiral' one I have defined as my secondary database. How do I assign the POST to use a specific database and not the default?
See this link to the docs: https://docs.djangoproject.com/en/1.7/topics/db/multi-db/#selecting-a-database-for-save
You can specify the database you want to save to, just pass it as a parameter:
my_object.save(using='database-name')
In your case it would be:
serializer.save(using='Admiral')
You should also use it in your queryset like this:
queryset = Resmst.objects.using('Admiral').filter(resmst_name=resname)
Since it is a queryset and not a command that needs a db_manager as creating objects is.
In the code provide by the op, the issue arises when serializer is trying to be saved, i.e. on the line
serializer.save()
-the default database is being used. One cannot use the form serializer.save(using='database_name') as the accepted answer recommends, because the kwarg "using='database_name" will not be understood/expected by a serializer class (in this case the class ResourceSerializer).
The django docs state that if you have a model (model.Model) then yes you can save using
my_object.save(using='database_name') see here for the quote: https://docs.djangoproject.com/en/2.1/topics/db/multi-db/#selecting-a-database-for-save
. But serializer is obviously not a model instance.
In such a case as above, you could subclass (or amend -I prefer amending when I have created the serializer myself) ResourceSerializer and change the create and update methods to work utilizing db_manager('Admiral'). For example:
class MyResourceSerializer(ResourceSerializer):
def create(self, validated_data):
"""
copy the create from ResourceSerializer and amend it here, with code such as
follows in the try section.
"""
ModelClass=Resmst # or whichever desired model you are acting on
try:
instance = ModelClass.objects.db_manager('Admiral').create(**validated_data)
except TypeError: # or whatever error type you are mitigating against, if any
raise TypeError()
return instance
A nice alternative (as elim mentions in one of the comments to this question) is to add a router and have this all handled without having to insert "using" or "db_manager" throughout the code: https://docs.djangoproject.com/en/2.1/topics/db/multi-db/#using-routers
Say for example you're using a ListCreateAPIView
You might might be able to do it at the view level, using get_queryset
When to use get, get_queryset, get_context_data in Django?
class YourModelDRFGetView(generics.ListCreateAPIView):
serializer_class = YourModelDRFViewSerializer
def get_queryset(self):
return YourModel.objects.using('your_read_replica').all()
Where your_read_replica is defined in settings.py:
replica_database_url = os.environ.get("DATABASE_REPLICA_URL") or database_url
DATABASES["your_read_replica"] = dj_database_url.parse(replica_database_url)

Django REST Framework ModelSerializer get_or_create functionality

When I try to deserialize some data into an object, if I include a field that is unique and give it a value that is already assigned to an object in the database, I get a key constraint error. This makes sense, as it is trying to create an object with a unique value that is already in use.
Is there a way to have a get_or_create type of functionality for a ModelSerializer? I want to be able to give the Serializer some data, and if an object exists that has the given unique field, then just return that object.
In my experience nmgeek's solution won't work in DRF 3+ as serializer.is_valid() correctly honors the model's unique_together constraint. You can work around this by removing the UniqueTogetherValidator and overriding your serializer's create method.
class MyModelSerializer(serializers.ModelSerializer):
def run_validators(self, value):
for validator in self.validators:
if isinstance(validator, validators.UniqueTogetherValidator):
self.validators.remove(validator)
super(MyModelSerializer, self).run_validators(value)
def create(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
The Serializer restore_object method was removed starting with the 3.0 version of REST Framework.
A straightforward way to add get_or_create functionality is as follows:
class MyObjectSerializer(serializers.ModelSerializer):
class Meta:
model = MyObject
fields = (
'unique_field',
'other_field',
)
def get_or_create(self):
defaults = self.validated_data.copy()
identifier = defaults.pop('unique_field')
return MyObject.objects.get_or_create(unique_field=identifier, defaults=defaults)
def post(self, request, format=None):
serializer = MyObjectSerializer(data=request.data)
if serializer.is_valid():
instance, created = serializer.get_or_create()
if not created:
serializer.update(instance, serializer.validated_data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
However, it doesn't seem to me that the resulting code is any more compact or easy to understand than if you query if the instance exists then update or save depending upon the result of the query.
#Groady's answer works, but you have now lost your ability to validate the uniqueness when creating new objects (UniqueValidator has been removed from your list of validators regardless the cicumstance). The whole idea of using a serializer is that you have a comprehensive way to create a new object that validates the integrity of the data you want to use to create the object. Removing validation isn't what you want. You DO want this validation to be present when creating new objects, you'd just like to be able to throw data at your serializer and get the right behavior under the hood (get_or_create), validation and all included.
I'd recommend overwriting your is_valid() method on the serializer instead. With the code below you first check to see if the object exists in your database, if not you proceed with full validation as usual. If it does exist you simply attach this object to your serializer and then proceed with validation as usual as if you'd instantiated the serializer with the associated object and data. Then when you hit serializer.save() you'll simply get back your already created object and you can have the same code pattern at a high level: instantiate your serializer with data, call .is_valid(), then call .save() and get returned your model instance (a la get_or_create). No need to overwrite .create() or .update().
The caveat here is that you will get an unnecessary UPDATE transaction on your database when you hit .save(), but the cost of one extra database call to have a clean developer API with full validation still in place seems worthwhile. It also allows you the extensibility of using custom models.Manager and custom models.QuerySet to uniquely identify your model from a few fields only (whatever the primary identifying fields may be) and then using the rest of the data in initial_data on the Serializer as an update to the object in question, thereby allowing you to grab unique objects from a subset of the data fields and treat the remaining fields as updates to the object (in which case the UPDATE call would not be extra).
Note that calls to super() are in Python3 syntax. If using Python 2 you'd want to use the old style: super(MyModelSerializer, self).is_valid(**kwargs)
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
class MyModelSerializer(serializers.ModelSerializer):
def is_valid(self, raise_exception=False):
if hasattr(self, 'initial_data'):
# If we are instantiating with data={something}
try:
# Try to get the object in question
obj = Security.objects.get(**self.initial_data)
except (ObjectDoesNotExist, MultipleObjectsReturned):
# Except not finding the object or the data being ambiguous
# for defining it. Then validate the data as usual
return super().is_valid(raise_exception)
else:
# If the object is found add it to the serializer. Then
# validate the data as usual
self.instance = obj
return super().is_valid(raise_exception)
else:
# If the Serializer was instantiated with just an object, and no
# data={something} proceed as usual
return super().is_valid(raise_exception)
class Meta:
model = models.MyModel
There are a couple of scenarios where a serializer might need to be able to get or create Objects based on data received by a view - where it's not logical for the view to do the lookup / create functionality - I ran into this this week.
Yes it is possible to have get_or_create functionality in a Serializer. There is a hint about this in the documentation here: http://www.django-rest-framework.org/api-guide/serializers#specifying-which-fields-should-be-write-only where:
restore_object method has been written to instantiate new users.
The instance attribute is fixed as None to ensure that this method is not used to update Users.
I think you can go further with this to put full get_or_create into the restore_object - in this instance loading Users from their email address which was posted to a view:
class UserFromEmailSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
'email',
]
def restore_object(self, attrs, instance=None):
assert instance is None, 'Cannot update users with UserFromEmailSerializer'
(user_object, created) = get_user_model().objects.get_or_create(
email=attrs.get('email')
)
# You can extend here to work on `user_object` as required - update etc.
return user_object
Now you can use the serializer in a view's post method, for example:
def post(self, request, format=None):
# Serialize "new" member's email
serializer = UserFromEmailSerializer(data=request.DATA)
if not serializer.is_valid():
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
# Loaded or created user is now available in the serializer object:
person=serializer.object
# Save / update etc.
A better way of doing this is to use the PUT verb instead, then override the get_object() method in the ModelViewSet. I answered this here: https://stackoverflow.com/a/35024782/3025825.
A simple workaround is to use to_internal_value method:
class MyModelSerializer(serializers.ModelSerializer):
def to_internal_value(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
I know it's a hack, but in case if you need a quick solution
P.S. Of course, editing is not supported
class ExpoDeviceViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, ]
serializer_class = ExpoDeviceSerializer
def get_queryset(self):
user = self.request.user
return ExpoDevice.objects.filter(user=user)
def perform_create(self, serializer):
existing_token = self.request.user.expo_devices.filter(
token=serializer.validated_data['token']).first()
if existing_token:
return existing_token
return serializer.save(user=self.request.user)
In case anyone needs to create an object if it does not exist on GET request:
class MyModelViewSet(viewsets.ModelViewSet):
queryset = models.MyModel.objects.all()
serializer_class = serializers.MyModelSerializer
def retrieve(self, request, pk=None):
instance, _ = models.MyModel.objects.get_or_create(pk=pk)
serializer = self.serializer_class(instance)
return response.Response(serializer.data)
Another solution, as I found that UniqueValidator wasn't in the validators for the serializer, but rather in the field's validators.
def is_valid(self, raise_exception=False):
self.fields["my_field_to_fix"].validators = [
v
for v in self.fields["my_field_to_fix"].validators
if not isinstance(v, validators.UniqueValidator)
]
return super().is_valid(raise_exception)