I'm writing a serializer to provide multiple partial updates to a django model. I'm following the example implementation that appears in the DRF api guide, reproduced below and linked here: https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update.
The following was retrieved from django-rest-framework documentation:
serializer.py
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
# Maps for id->instance and id->data item.
book_mapping = {book.id: book for book in instance}
data_mapping = {item['id']: item for item in validated_data}
# Perform creations and updates.
ret = []
for book_id, data in data_mapping.items():
book = book_mapping.get(book_id, None)
if book is None:
ret.append(self.child.create(data))
else:
ret.append(self.child.update(book, data))
# Perform deletions.
for book_id, book in book_mapping.items():
if book_id not in data_mapping:
book.delete()
return ret
class BookSerializer(serializers.Serializer):
# We need to identify elements in the list using their primary key,
# so use a writable field here, rather than the default which would be read-only.
id = serializers.IntegerField()
...
class Meta:
list_serializer_class = BookListSerializer
In my code I'm getting a NotImplementedError('update() must be implemented.') in my views.py when .save() is called on the serializer that is returned.
My understanding is that the ListsSerializer overrides the .update(), so could anyone help explain why I'm getting the NotImpletmentedError?
views.py
elif request.method == 'PATCH':
data = JSONParser().parse(request)
books = Book.objects.all()
# both partial and many set to True
serializer = BookSerializer(books, data=data, partial=True, many=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data)
return JsonResponse(serializer.errors, status=400)
With the help of #luistm, I managed to solve this. Continuing with the DRF example above my implementation of the update() override in the bookSerializer class was as below.
serializer.py
class BookSerializer(serializers.Serializer):
# We need to identify elements in the list using their primary key,
# so use a writable field here, rather than the default which would be read-only.
id = serializers.IntegerField()
...
class Meta:
list_serializer_class = BookListSerializer
def update(self, instance, validated_data):
"""update the page number of the book and save"""
instance.page = validated_data.get('page', instance.page)
instance.save()
return instance
Related
I'm working on a small project Django Rest Framework, I already create add contact function as you can see in my create function. now I'm working on bulk import, but when I submit my data as a list not as a dict I get an error message :
{"non_field_errors":["Invalid data. Expected a dictionary, but got list."]}
this is my code to add a contact,
class ContactView(ListModelMixin, viewsets.GenericViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
def create(self, request):
serializeObject = ContactSerializer(data = request.data)
if serializeObject.is_valid():
serializeObject.save()
contactObject = Contact.objects.all()
contactSerializer = ContactSerializer(contactObject, many=True)
return Response(contactSerializer.data, status = status.HTTP_201_CREATED)
return Response(serializeObject.errors, status.HTTP_400_BAD_REQUEST)
Now i would like to create another function, for bulk create, since i have a list
This is my header data structure :
[{"Greeting":"amine","first_name":"alain","last_name":"amine","title":"ricardo","language":"ab#xyz.com","email":43822510594,"phone_1":43822510594,"phone_2":"not yet","mobile":43822510594,"fax":"not yet","internal_id":"xname"},{"Greeting":"bill","first_name":"microsoft","last_name":"bill","title":"microsoft","language":"bill#microsoft.com","email":652565455,"phone_1":652565455,"phone_2":"new york","mobile":652565455,"fax":"new york","internal_id":"microsoft"},{"Greeting":"john","first_name":"Yoyo","last_name":"Ruth","title":"xnameagain","language":"rh#xyz.com","email":5465559852,"phone_1":5465559852,"phone_2":"Vancouver","mobile":5465559852,"fax":"Vancouver","internal_id":"yname"}]
This is my serializer:
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = "__all__"
I found the Solution on https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects
all what i have to do is to add many=True to create multiple object
serializeObject = ContactSerializer(data = request.data, many=True)
Create method should look like this:
class ContactView(ListModelMixin, viewsets.GenericViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
def create(self, request):
valid_objects = []
for data in request.data:
serializeObject = ContactSerializer(data=data)
if serializeObject.is_valid():
valid_objects.append(serializeObject)
else:
return Response(serializeObject.errors, status.HTTP_400_BAD_REQUEST)
for obj in valid_objects:
obj.save()
contactObject = Contact.objects.all()
contactSerializer = ContactSerializer(contactObject, many=True)
return Response(contactSerializer.data, status = status.HTTP_201_CREATED)
Advise
They may not be the best practices but it works.
Context
For a specific use case I need to be able to update a single field of my Visitor model using a GET request instead of a PATCH request.
My relevant Visitor model looks like this:
# models.py
class Visitor(models.Model):
visitor_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True)
customers = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='customer_visitors')
audiences = models.ManyToManyField(Audience, related_name='audience_visitors')
cid = models.CharField(max_length=255, unique=True)
uid = models.CharField(max_length=255)
cup = JSONField(null=True)
def __str__(self):
return self.cid
class Meta:
db_table = 'visitor'
I am using a straightforward serializer like this:
# serializers.py
class VisitorSerializer(serializers.ModelSerializer):
class Meta:
model = Visitor
fields = ('customers', 'cid', 'uid', 'cup')
I am able to update just the cup field for a specific Visitor which is looked up using the unique cid field with a PATCH like this:
# views.py
class VisitorViewSet(viewsets.ModelViewSet):
serializer_class = VisitorSerializer
queryset = Visitor.objects.all()
lookup_field = 'cid'
def list(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.serializer_class(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
Problem
The problem is that I am unable to update the cup field of a Visitor based on a given unique cid field using a GET request.
What I tried
As this answer by Uri Shalit suggested, I tried to override get_serializer() inside my VisitorViewSet and tried to use it in list() like this:
# views.py
class VisitorViewSet(viewsets.ModelViewSet):
serializer_class = VisitorSerializer
queryset = Visitor.objects.all()
lookup_field = 'cid'
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(VisitorViewSet, self).get_serializer(*args, **kwargs)
def list(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
However, updating just the cup field of a specific Visitor based on the cid field works with a PATCH request but does not update said field with a GET request. There is no error either.
Expected behaviour
Making a GET request which contains cid to identify a Visitor and cup with data that needs to be updated for the given Visitor. I know it breaks REST principles but for this use case I need to do this partial update using a GET request instead of a PATCH request.
Any help or pointers in the right direction would be much appreciated!
Add a classmethod in your model.
class Visitor(models.Model):
visitor_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True)
customers = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='customer_visitors')
audiences = models.ManyToManyField(Audience, related_name='audience_visitors')
cid = models.CharField(max_length=255, unique=True)
uid = models.CharField(max_length=255)
cup = JSONField(null=True)
def __str__(self):
return self.cid
class Meta:
db_table = 'visitor'
#classmethod
def update_cup(cls, cid, cup_new):
instance = cls.objects.get(cid=cid)
instance.cup = new_cup
instance.save()
In ModelViewSet override the get_queryset method, see below:
IDK how u calc new_cup I guess u get it as a queryparam
def get_queryset(self):
queryset = Visitor.objects.all()
cup_new = self.request.query_params.get('cup_new', None)
cid = self.request.query_params.get('cid', None)
[obj.update_cup(obj.cid, cup_new) for obj in queryset if obj.cid == cid]
return queryset
I recommend using an api_view to accomplish what you want. api_view is an annotation provided by the rest framework so it should be available already in your case.
#api_view(["GET"])
def update_function(request):
query_params = request.GET # Getting the parameters from request
cid = query_params["cid"]
cup = query_params["cup"]
visitor = Visitor.objects.get(cid = cid)
visitor["cup"] = cup
serializer = VisitorSerializer(data = visitor, partial=True)
if serializer.is_valid():
serializer.save()
else:
print(serializer.errors)
However I am not sure about the syntax but the approch is sufficient for your problem.
Make sure to add the function to urls.py and have a look to the documentation to get better information than mine Api Views. But dont expect it to have information about you specific problem. In your case you have to understand the api_view concept and adapt it for your needs.
I think I don't understand the problem fully. Why can't you simply override the method get_object() in your view and do custom logic in it to update the object?
def get_object(self):
obj = super().get_object()
serializer = self.get_serializer(obj, data=self.request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return obj
I'm trying to serialize some objects whose data is stored in 2 databases, linked by common UUIDs. The second database DB2 stores personal data, so it is run as a segregated microservice to comply with various privacy laws. I receive the data as a decoded list of dicts (rather than an actual queryset of model instances). How can I adapt the ModelSerializer to serialize this data?
Here's a minimal example of interacting with DB2 to get the personal data:
# returns a list of dict objects, approx representing PersonalData.__dict__
# `custom_filter` is a wrapper for the Microservice API using `requests`
personal_data = Microservice.objects.custom_filter(uuid__in=uuids)
And here's a minimal way of serializing it, including the date of birth:
class PersonalDataSerializer(serializers.Serializer):
uuid = serializers.UUIDField() # common UUID in DB1 and DB2
dob = serializers.DateField() # personal, so can't be stored in DB1
In my application, I need to serialize the Person queryset, and related personal_data, into one JSON array.
class PersonSerializer(serializers.ModelSerializer):
dob = serializers.SerializerMethodField()
# can't use RelatedField for `dob` because the relationship isn't
# codified in the RDBMS, due to it being a separate Microservice.
class Meta:
model = Person
# A Person object has `uuid` and `date_joined` fields.
# The `dob` comes from the personal_data, fetched from the Microservice
fields = ('uuid', 'date_joined', 'dob',)
def get_dob(self):
raise NotImplementedError # for the moment
I don't know if there's a nice DRF way to link the two. I definitely don't want to be sending (potentially thousands of) individual requests to the microservice by including a single request in get_dob. The actual view just looks like this:
class PersonList(generics.ListAPIView):
model = Person
serializer_class = PersonSerializer
def get_queryset(self):
self.kwargs.get('some_filter_criteria')
return Person.objects.filter(some_filter_criteria)
Where should the logic go to link the microservice data into the serializer, and what should it look like?
I suggest you to override the serializer and your list method.
Serializer:
class PersonSerializer(models.Serializer):
personal_data = serializers.DictField()
class Meta:
model = Person
make a function to add personal_data dictionary to persons object. Use this method before giving the list of person objects to the serializer.
def prepare_persons(persons):
person_ids = [p.uuid for p in persons]
personal_data_list = Microservice.objects.custom_filter(uuid__in=person_ids)
personal_data_dict = {pd['uuid']: pd for pd in personal_data_list}
for p in persons:
p.personal_data = personal_data_dict[p.id]
return persons
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
page = prepare_persons(page)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
else:
persons = prepare_persons(queryset)
serializer = self.get_serializer(persons, many=True)
return Response(serializer.data)
Because you want to only hit your database one time, a good way to add your extra data to your queryset is by adding a custom version of ListModelMixin to your ViewSet that includes extra context:
class PersonList(generics.ListAPIView):
...
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# Pseudo-code for filtering, adjust to work for your use case
filter_criteria = self.kwargs.get('some_filter_criteria')
personal_data = Microservice.objects.custom_filter(filter_criteria)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(
page,
many=True,
context={'personal_data': personal_data}
)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(
queryset,
many=True,
context={'personal_data': personal_data}
)
return Response(serializer.data)
Then, access the extra context in your serializer by overriding the to_representation method:
def to_representation(self, instance):
"""Add `personal_data` to the object from the Microservice"""
ret = super().to_representation(instance)
personal_data = self.context['personal_data']
ret['personal_data'] = personal_data[instance.uuid]
return ret
I have a model and I want to write an update() method for it in order to update.
The below snippet is my model:
class Klass(models.Model):
title = models.CharField(max_length=50)
description = models.CharField(max_length=500)
university = models.CharField(max_length=50,blank=True, null=True)
teacher = models.ForeignKey(Profile, related_name='teacher', on_delete=models.CASCADE)
and the below snippet is corresponding Serializer:
class KlassSerializer(ModelSerializer):
teacher = ProfileSerializer()
url = HyperlinkedIdentityField(view_name='mainp-api:detail', lookup_field='pk')
klass_settings = KlassSettingsSerializer()
class Meta:
model = Klass
fields = ('url', 'id', 'title', 'description', 'university','teacher')
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('description', instance.description)
instance.university = validated_data.get('university', instance.university)
instance.save()
return instance
And for update, I use below snippet:
class KlassAPIView(APIView):
def put(self, request, pk=None):
if pk == None:
return Response({'message': 'You must specify class ID'}, status=HTTP_400_BAD_REQUEST)
klass = Klass.objects.get(pk=pk)
if request.user.profile.type != 't':
raise PermissionDenied(detail={'message': 'You aren't teacher of this class, so you can't edit information.'})
serializer = KlassSerializer(data=request.data, context={'request': request})
serializer.initial_data['teacher'] = request.user.profile.__dict__
if serializer.is_valid():
serializer.update(instance=klass, validated_data=serializer.data) # Retrieve teacher and store
return Response({'data': serializer.data}, status=HTTP_200_OK)
else:
return Response({'data': serializer.errors}, status=HTTP_400_BAD_REQUEST)
but when I send data with PUT method, it returns below error:
AttributeError at /api/class/49/
'collections.OrderedDict' object has no attribute 'pk'
and the error occurs in serializer.update(instance=klass, validated_data=serializer.data) line.
Just ran into the same error.
In my case the problem was I accessed serializer.data before doing serializer.save().
Google dropped me here, so maybe someone else will also find this helpful.
Source: https://github.com/encode/django-rest-framework/issues/2964
i don't know if this helps. I always add the id field in the serializer due to that similar issue:
id = serializers.ModelField(model_field=YourModel._meta.get_field('id'), required=False)
Make sure it's required=False because when you create a new record the id field is not present.
Well in my case, I was doing:
champions_list = []
for champion in champions_serializer.data:
c = {"id": champion.id}
champions_list.append(c)
And the correct way to do it is:
champions_list = []
for champion in champions_serializer.data:
c = {"id": champion["id"]}
champions_list.append(c)
And make sure that you return the id inside the serializer.
Many answers to this question note that serializer.save() must be called before using serializer.data.
In my case, I was definitely calling serializer.save(), however, I was overriding the save method on my serializer and did not set self.instance on the serializer in that method.
So if you are overriding save be sure to do:
class MySerializer(serializers.ModelSerializer):
def save(self, *args, **kwargs):
...
self.instance = instance
return self.instance
I have Foo objects that are "owned" by an Organization. I get the current Organization from the logged-in User in my request handler:
models.py:
class Organization(models.Model):
name = models.CharField(max_length=255)
class Foo(models.Model):
... some other fields ...
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
serializers.py:
class FooSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
organization = OrganizationSerializer(read_only=True)
""" Create and return a new instance, given the validated data """
def create(self, validatedData):
return OverlayDesign.objects.create(**validatedData)
""" Update and return an existing instance, given the validated_data """
def update(self, instance, validatedData):
instance.organization = validatedData.get('organization', instance.organization)
instance.save()
return instance
views.py:
def createFoo(request):
data = JSONParser().parse(request)
# Get the organization of the logged-in user
data["organization"] = OrganizationSerializer(request.user.account.organization).data
serializer = FooSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONErrorResponse(serializer.errors, status=400)
When I create a new Foo, I want it to be owned by the current Organization. The problem is that I don't understand why the Organization is not deserialized? No matter what I do I just get:
django.db.utils.IntegrityError: null value in column "organization_id" violates not-null constraint
How should I deliver the current Organization to the FooSerializer ?
OrganizationSerializer is set with the readonly option => you won't get anything to match the current organization.
You should take a look at : http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield