Django DRF & Updating multiple partial records - django

I'm attempting to update a model that has more fields than what I want to pass to it. I've read the DRF documentation and I'm not finding the right approach. I came across using the UpdateModelMixin but I cannot seem to find a way to implement it successfully.
I've taken a few approaches so far, including using APIView in addition to the methods seen below.
Ultimately, I wish to pass the following to my view and have it update just the "order" field in my model. My model has many fields.
[
{
"id": "1",
"order": "5"
},
{
"id": "2",
"order": "3"
}
]
This is my view:
class WaitlistListGen(generics.ListCreateAPIView):
queryset = Waitlist.objects.all()
serializer_class = WaitlistSerializer
class WaitlistDetailGen(RetrieveUpdateDestroyAPIView):
queryset = Waitlist.objects.all()
serializer_class = WaitlistSerializer
And, this is my serializer:
class WaitlistSerializer(serializers.ModelSerializer):
class Meta:
model = Waitlist
fields = '__all__'

I finally figured it out and believe this is the best approach--I'll take feedback if it's not. I had to create a new view, extend the put method, and iterate over the data. I used the existing WaitlistSerializer:
class WaitlistUpdateGen(generics.UpdateAPIView):
serializer_class = WaitlistSerializer
def put(self, request):
waitlist_data = request.data
for waitlist_item in waitlist_data:
try:
waitlist = Waitlist.objects.get(id=waitlist_item['id'])
except Waitlist.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
serializer = self.serializer_class(
waitlist, data=waitlist_item, partial=True)
if serializer.is_valid():
serializer.save()
return Response(status=status.HTTP_200_OK)

Related

Serializer in patch method does not check fields

I am trying to partially update a model and overwriting the patch method. It works fine when I send correct data, but my problem is, that when I send incorrect data the serializer stays True and I still get a 200 status code and nothing gets updated. With incorrect data I mean wrong field names. I could basically send any field name and get a 200 status code. So I think the fields are not checked by the serializer...
class BuildingUpdateAPI(UpdateAPIView):
serializer_class = BuildingSerializer
def patch(self, request, *args, **kwargs):
"""Patches building."""
building = buildings.get(name=self.kwargs['name_building'])
serializer = BuildingSerializer(building, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(status=200, data=serializer.data)
return JsonResponse(status=400, data="Wrong data provided")
serializer:
class BuildingSerializer(serializers.ModelSerializer):
"""Serializer for Buildings."""
class Meta:
model = Building
fields = (
"id",
"name",
"...",
.....
Now I am wondering if this is like this by design or if I am doing something wrong in my view. I thought I could overwrite the validate-method in the serializer but my model has quite a lot of fields... So I would have to check every field single field which is not optimal I think. Or is there a way to do this somehow elegantly?
I am patching like this:
result = requests.request("patch", url=endpoint, json=payload, headers=get_headers())

Django: multiple update

I want to update multiple instances of my models. I can currently update them one by one just fine.
I want to be able to update them with a PUT request to my URL:
www.my-api.com/v1/mymodels/
with the request data like so:
[ { "mymodel_id": "id1", "name": "foo"}, { "mymodel_id": "id2", "alert_name": "bar"} ]
If I try it this way now, I receive the following Django error:
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.
My model has a Serializer class MyModelSerializer
class MyModelSerializer(ModelSerializerWithFields):
class Meta:
model = MyModel
fields = "__all__"
def to_representation(self, instance):
data = super().to_representation(instance)
if instance.name is None:
del data['name']
return data
ModelSerializerWithFields extends serializers.ModelSerializer.
The View for MyModel is very basic:
class MyModelViewSet(MultipleDBModelViewSet):
serializer_class = MyModelSerializer
queryset = MyModel.objects.none()
MultipleDBModelViewSet extends BulkModelViewSet, and contains
def filter_queryset(self, queryset):
ids = self.request.query_params.get("ids", None)
if ids:
return queryset.filter(pk__in=json.loads(ids))
# returns normal query set if no param
return queryset
At which level do I need to use the ListSerializer class? ie: in ModelSerializerWithFields or in MyModelSerializer? Or somewhere else completely?
If anyone has any examples of this implementation, I'd be very grateful
Serializer Must be inherited from BulkSerializerMixin
So the serializer code will be like
from rest_framework_bulk.serializers import BulkListSerializer, BulkSerializerMixin
class SimpleSerializer(BulkSerializerMixin,
ModelSerializer):
class Meta(object):
model = SimpleModel
# only required in DRF3
list_serializer_class = BulkListSerializer
At Viewset don't forget to use the
filter_queryset method.
So your view will be like
class MyModelViewSet(MultipleDBModelViewSet):
serializer_class = MyModelSerializer
queryset = MyModel.objects.none()
def filter_queryset(self, queryset):
return queryset.filter(<some_filtering>)

Django Query from Post

I am trying to pass into my DRF API a post that needs to be queried against the database. For now I have included only 2 fields that can be queried. I am having trouble getting the view to work. Here is what I have so far.
POST
{
"city": "Denver",
"state": "CO"
}
Serializer only lets city and state be accepted
class EventQuerySerializer(serializers.ModelSerializer):
class Meta:
model = Events
fields = ('city', 'state')
View note that EventsSerializer is used in the return and I can confirm it works for a basic get request
class QueryEvents(APIView):
#staticmethod
def post(request):
serializer = EventQuerySerializer(data=request.data)
if serializer.is_valid():
events = Events.objects.get(serializer)
return Response(EventsSerializer(events).data)
Error
AttributeError: 'CharField' object has no attribute 'split'
You need to run save on you serializer to retrieve instance:
# ...
if serializer.is_valid():
event = serializer.save()
events = Events.objects.get(pk=event.pk)
# ...

Posting a foreign key relationship in Django Rest Framework

In my models, I have the following classes:
class Topic(models.Model):
name = models.CharField(max_length=25, unique=True)
class Content(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
topic = models.ForeignKey(Topic, blank=True, null=True)
My serializers is like this:
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = Topic
fields = ('name')
class ContentSerializer(serializers.ModelSerializer):
topic = TopicSerializer(read_only=True)
class Meta:
model = Content
fields = ('title', 'body', 'topic')
Alright, so in my urls file, I have the following pattern:
urlpatterns = [
...
url(r'^api/topic_detail/(?P<name>[a-zA-Z0-9-]+)/content_list/$', views.topic_content_list, name='topic_content_list'),
...
]
Therefore, when the user goes to say /api/topic_detail/sports/content_list/, we get a list of all the contents that has the topic of sports. Now what I want is if we POST the following data to the above URL, then a Content object is created with the topic field related automatically to sports.
I am trying to do this the following way in my views:
#api_view(['GET', 'POST'])
def topic_content_list(request, name):
try:
topic = Topic.objects.get(name=name)
except:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
contents = Content.objects.filter(topic=topic)
serializer = ContentSerializer(contents, many=True)
return Response(serializer.data)
elif request.method == 'POST':
request.data["topic"] = topic
serializer = ContentSerializer(data=request.data)
print request.data
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)
Now lets say I go the URL /api/topic_detail/sports/content_list/ and POST this:
{
"title": "My blog post",
"body" : ".....",
}
The content object gets created and the title and body field is set properly. However, the topic field is set to null. How can I fix this? Any help is appreciated.
Also, please don't suggest using generic viewsets, as I am uncomfortable with so many things happening automatically.
EDIT
Alright, so I fixed my dumb mistake :
class ContentSerializer(serializers.ModelSerializer):
topic = TopicSerializer(read_only=False)
class Meta:
model = Content
fields = ('title', 'body', 'topic')
That is, I set the read_only argument to False. However, now the post creates a new error:
{
"topic": {
"non_field_errors": [
"Invalid data. Expected a dictionary, but got Topic."
]
}
}
This I am pretty sure refers to the fact that the data.website I'm sending in is not JSON, but instead just a Django model instance. How can I JSONify the single instance?
This is from your serializer.
topic = TopicSerializer(read_only=True)
It means your topic is read only so when you are trying to save your serializer, topic is not getting saved. Remove that and problem would be fixed.
EDIT:
Now as per the second error, it is because it is expecting a dict and you are passing the model instance, so you have two options. Either create the dict by hand.
topic_dict = { "name": topic.name }
and pass that as 'topic' in request.data and then save or give the topic_id, as there is a foreign key relationship, it should work.
So it would be something like:
request.data["topic_id"] = topic.id
Now what you choose to do is totally upto you.
Resurrecting this old thread since it seems to be a common issue people are running into. I just got this working on Django 3.1.6.
Since the Topic and Content models are already linked, the serializer is simply
class ContentSerializer(serializers.ModelSerializer):
class Meta:
model = Content
fields = ('title', 'body', 'topic')
No need for topic = TopicSerializer(read_only=False), which will require you to create a new topic with the POST. Now, the body of the POST can be
{
"title": "My blog post",
"body" : ".....",
"topic": 3
}
If you want your GET output to look like
{
"title": "My blog post",
"body" : ".....",
"topic": {
"id": 3
"name": "announcements"
}
}
override to_representation
class ContentSerializer(serializers.ModelSerializer):
class Meta:
model = Content
fields = ('title', 'body', 'topic')
def to_representation(self, instance):
response = super().to_representation(instance)
response['topic'] = TopicSerializer(instance.topic).data
return response
Credit for proper usage of to_representation goes to this answer by #PdotNJ

Unable to get a non-model field in the validated_data of a Django Rest Framework serializer

I have an ItemCollection and Items in my Django models and I want to be able to remove Items from the collection through the UI. In a REST PUT request I add an extra boolean field deleted for each Item to signal that an Item should be deleted.
The correct way to handle this seems to be in the update method of the Serializer.
My problem is that this non-model deleted field gets removed during validation, so it is not available anymore. Adding deleted as a SerializerMethodField did not help. For now I get my deleted information from the initial_data attribute of the Serializer, but that does not feel right.
My current example code is below. Does anybody know a better approach?
Models:
class ItemCollection(models.Model):
description = models.CharField(max_length=256)
class Item(models.Model):
collection = models.ForeignKey(ItemCollection, related_name="items")
Serializers:
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from models import Item, ItemCollection
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
class ItemCollectionSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=False)
class Meta:
model = ItemCollection
def update(self, instance, validated_data):
instance.description = validated_data['description']
for item, item_obj in zip(
self.initial_data['items'], validated_data['items']):
if item['delete']:
instance.items.filter(id=item['id']).delete()
return instance
class ItemCollectionView(APIView):
def get(self, request, ic_id):
item_collection = get_object_or_404(ItemCollection, pk=ic_id)
serialized = ItemCollectionSerializer(item_collection).data
return Response(serialized)
def put(self, request, ic_id):
item_collection = get_object_or_404(ItemCollection, pk=ic_id)
serializer = ItemCollectionSerializer(
item_collection, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)
And an example of the json in the PUT request:
{
"id": 2,
"items": [
{
"id": 3,
"collection": 2,
"delete": true
}
],
"description": "mycoll"
}
You can add non-model fields back by overwriting the to_internal_value fn:
def to_internal_value(self, data):
internal_value = super(MySerializer, self).to_internal_value(data)
my_non_model_field_raw_value = data.get("my_non_model_field")
my_non_model_field_value = ConvertRawValueInSomeCleverWay(my_non_model_field_raw_value)
internal_value.update({
"my_non_model_field": my_non_model_field_value
})
return internal_value
Then you can process it however you want in create or update.
If you're doing a PUT request, your view is probably calling self.perform_update(serializer). Change it for
serializer.save(<my_non_model_field>=request.data.get('<my_non_model_field>', <default_value>)
All kwargs are passed down to validated_data to your serializer.
Make sure to properly transform incoming value (to boolean, to int, etc.)