Django Rest Framework invalid serializer data, can not figure out why - python

I am creating a simple model with a many-to-many field. The model works fine and I can create model through the admin panel, and I can make a get request to see that model (except that it only returns user IDs instead of the user models/objects). My problem is when creating a post request to create said model.
I get one of the two errors depending on the changes I make, The serializer field might be named incorrectly and not match any attribute or key on the 'str' instance. or AssertionError: You cannot call '.save()' on a serializer with invalid data., either way it has something to do with my serializer. The following is my model,
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ManyToManyField(MyUser, related_name="users")
class Meta:
ordering = ('week',)
My View,
class SchemaView(APIView):
permission_classes = (SchemaPermissions,)
def get(self, request):
schemas = Schema.objects.all()
serializer = SchemaSerializer(schemas, many=True)
return Response(serializer.data)
def post(self, request):
data = request.data
serializer = SchemaSerializer(data=data)
serializer.is_valid()
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
And my serializer,
class SchemaSerializer(serializers.ModelSerializer):
class Meta:
model = Schema
fields = ('week', 'users')
def create(self, validated_data):
users_data = validated_data.pop('users')
users = MyUser.objects.filter(id__in=users_data)
schema = Schema.objects.create(week=validated_data.week, users=users)
return schema
def update(self, instance, validated_data):
users_data = validated_data.pop('users')
users = MyUser.objects.filter(id__in=users_data)
instance.users.clear()
instance.users.add(*users)
instance.saver()
return instance
The idea is that if a week number already exists then it should call the update() function and then it should simply overwrite the users related to that week number, otherwise it should call create() and create a new week number with relations to the given users. The following is the result of printing the serializer after initializing it in the view.
SchemaSerializer(data={'week': 32, 'users': [1, 2, 3]}):
week = IntegerField(max_value=53, min_value=1, validators=[<UniqueValidator(queryset=Schema.objects.all())>])
users = PrimaryKeyRelatedField(allow_empty=False, many=True, queryset=MyUser.objects.all())
It seems to me that the serializer should be valid for the given model? I am perhaps missing some concepts and knowledge about Django and DRF here, so any help would be greatly appreciated!

First you need set the field for saving users in the SchemaSerializer. And you don't need to customize the create and update method because the logic could be coded in the views.
class SchemaSerializer(serializers.ModelSerializer):
users = UserSerializer(read_only = True, many = True)
user_ids = serializers.ListField(
child = serializers.IntegerField,
write_only = True
)
class Meta:
model = Schema
fields = ('week', 'users', 'user_ids',)
# remove create and update method
And in views.py,
class SchemaView(APIView):
permission_classes = (SchemaPermissions,)
def get(self, request):
...
def post(self, request):
data = request.data
serializer = SchemaSerializer(data=data)
if serializer.is_valid():
input_data = serializer.validated_data
week = input_data.get('week')
user_ids = input_data.get('user_ids')
if Schema.objects.filter(week = week).count() > 0:
schema = Schema.objects.get(week = week).first()
else:
schema = Schema.objects.create(week = week)
schema.users.set(user_ids)
schema.save()
return Response(SchemaSerializer(schema).data, status=status.HTTP_200_OK)
else:
print(serializer.errors)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
And of course, the payload data should be
{'week': 23, 'user_ids': [1,2,3]}

Related

Django Rest Framework, creating a model, (serializer and view) with oneToMany field to users

I am trying to create a rather simple model, all the model holds is a week number (as the primary key), and a oneToMany field with a list of users. The idea is that it should function like a schema, where you can see which users is attached to a specific week number. My problem is currently getting the serializer to work with the oneToMany field.
Model:
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ForeignKey(MyUser, null=True, on_delete=models.SET_NULL)
class Meta:
ordering = ('week',)
Serializer:
class SchemaSerializer(serializers.Serializer):
class Meta:
model = Schema
fields = ('week', 'users')
def create(self, validated_data):
answer, created = Schema.objects.update_or_create(
week=validated_data.get('week', 1),
defaults={'users', validated_data.get('users', None)}
)
return answer
View:
class SchemaView(APIView):
permission_classes = (IsAuthenticated, IsAdminUser)
def get(self, request):
schemas = Schema.objects.all()
serializer = SchemaSerializer(schemas)
return Response(serializer.data)
def post(self, request):
data = request.data
serializer = SchemaSerializer(data=data)
serializer.is_valid()
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
I get the following error TypeError: cannot convert dictionary update sequence element #0 to a sequence. As I interpret that error, something is wrong with the first element (week number) when trying to do serializer.save().

How to create multiple objects in serializers create method?

I am trying to upload a csv file and then using it to populate a table in the database (creating multiple objects).
serializers.py:
def instantiate_batch_objects(data_list, user):
return [
WorkData(
work=db_obj['work'],
recordTime=db_obj['recordTime'],
user=user
) for db_obj in data_list
]
class FileUploadSerializer(serializers.ModelSerializer):
filedata = serializers.FileField(write_only=True)
class Meta:
model = WorkData
fields = ['user', 'filedata']
def create(self, validated_data):
file = validated_data.pop('filedata')
data_list = csv_file_parser(file)
batch = instantiate_batch_objects(data_list, validated_data['user'])
work_data_objects = WorkData.objects.bulk_create(batch)
# print(work_data_objects[0])
return work_data_objects
views.py:
class FileUploadView(generics.CreateAPIView):
queryset = WorkData.objects.all()
permission_classes = [IsAuthenticated]
serializer_class = FileUploadSerializer
# I guess, this is not need for my case.
def get_serializer(self, *args, **kwargs):
print(kwargs.get('data'))
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
models.py
class WorkData(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='work_data',
)
work = models.IntegerField(blank=False, null=False)
recordTime = models.DateTimeField(blank=False, null=True)
When I upload the file and post it I get this error:
Got AttributeError when attempting to get a value for field user on serializer FileUploadSerializer. The serializer field might be named incorrectly and not match any attribute or key on the list instance. Original exception text was: 'list' object has no attribute 'user'.
But I can see table is populated successfully in the database. What should I return from create method of FileUploadSerializer?
OK, after trying an example myself I was able to reproduce the errors, I have a better understanding of why this is happing now.
First, let's put the implementation of create() on the view class here
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)
The original error of Got AttributeError when attempting to get a value for field user... etc happened because the create() in the FileUploadView is returning serializer.data which is expecting fields user and filedata but create() on FileUploadSerializer is returning a list of objects so you can see now why this is happening.
You can solve this by overriding create() on FileUploadView and serialize the returned serializer data with a WorkDataSerializer that you will create
For ex:
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)
workData = WorkDataSerializer(data=serializer.data)
return Response(workData.data, status=status.HTTP_201_CREATED, headers=headers)
OR, you can do it on serializer level - which I prefer -
For example:
class FileUploadSerializer(serializers.ModelSerializer):
filedata = serializers.FileField(write_only=True)
created_objects_from_file = serializers.SerializerMethodField()
def get_created_objects_from_file(self, obj):
file = self.validated_data.pop('filedata')
data_list = csv_file_parser(file)
batch = instantiate_batch_objects(data_list, self.validated_data['user'])
work_data_objects = WorkData.objects.bulk_create(batch)
return WorkDataSerializer(work_data_objects, many = True).data
class Meta:
model = WorkData
fields = ['user', 'filedata']
class WorkDataSerializer(serializers.Serializer):
# fields of WorkData model you want to return
This should work with no problems, note that SerializerMethodField is read_only by default
see https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield

Django Rest Framework Calculations in Serializer?

I'm working with a finance application, and due to the way that floating point math works, have decided to store all values in the database as cents (so dollar amount * 100). I have been banging my head against a wall to get the serializer to perform two calculations for me. On create/update accept a float value but then before saving to the database do value*100. Then on get, do value/100.
I got it half working using a SerializerMethodField, but that seemed to remove my ability to do create/update actions. I also at one point had something that kind of worked for create/update by changing the serializer.save() method in the view and adding an IntegerField validator on the field, but then that broke the SerializerMethodField.
In short, I'm stuck. lol
Here is my very simple model:
class Items(models.Model):
user = models.ForeignKey(
'CustomUser',
on_delete=models.CASCADE,
)
name = models.CharField(max_length=60)
total = models.IntegerField()
My views for this item:
class GetItems(generics.ListCreateAPIView):
serializer_class = ItemsSerializer
permission_classes = [permissions.IsAuthenticated, IsAuthorOrDenied]
user = serializers.HiddenField(default=serializers.CurrentUserDefault(), )
def get_queryset(self):
user = self.request.user
return Items.objects.filter(user=user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class SingleItem(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ItemsSerializer
permission_classes = [permissions.IsAuthenticated, IsAuthorOrDenied]
user = serializers.HiddenField(default=serializers.CurrentUserDefault(), )
def get_queryset(self):
user = self.request.user
return Items.objects.filter(user=user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
And my serializer
class ItemsSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'name', 'budget_total')
model = models.Items
I feel like I should be doing more in my Serializer and less in my views, but that may be a completely different question all together.
Thanks in advance for the help!
Custom Serializer Field
You could write a custom field to handle the data:
class BudgetField(serializers.Field):
def to_representation(self, value):
# You can decide here how you want to return your data back
return value / 100
def to_internal_value(self, data):
# this will be passed to validated_data, so will be used to create/update instances
# you could do some validation here to make sure it is a float
# https://www.django-rest-framework.org/api-guide/fields/#raising-validation-errors
return int(data * 100)
Then use the custom field on your serializer.
class ItemsSerializer(serializers.ModelSerializer):
total = BudgetField()
class Meta:
fields = ('id', 'name', 'total')
model = models.Items
Override .update() and .create()
You could also choose to override these methods on the serializer.
class ItemsSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'name', 'budget_total')
model = models.Items
def create(self, validated_data):
# Modify validated_data with the value you need
return super().create(validated_data)
def update(self, instance, validated_data):
# Modify validated_data with the value you need
return super().update(instance, validated_data)

How better way to create a function to pass a JSON by serializer in django rest?

I'm studying django rest framework and would like the create a function. In this function, I need to pass a list in JSON and update by serializer.
For help I wrote a code example below.
Serialzer example:
class GarageViewSet(viewsets.ModelViewSet):
queryset = Garage.objects.all()
serializer_class = GarageSerializer
model = Garage
class CarViewSet(RestrictedQuerysetMixin, viewsets.ModelViewSet):
queryset = Car.objects.all()
serializer_class = CarSerializer
model = Car
Well. I need to update a car list through the garage serializer. I'm thinking anything like this:
(example view)
class GarageViewSet(viewsets.ModelViewSet):
queryset = Garage.objects.all()
serializer_class = GarageSerializer
model = Garage
#action(detail=True, methods=['put'])
def update_car(self, request):
...
serializer = CarSerializer(queryset, many=True)
...
return Response(serializer.data)
Attempt 1:
Searching and reading the doc, I tried this way:
#action(methods=['put'], detail=False)
def update_car(self, request, *args, **kwargs):
if request.method == 'PUT':
data = JSONParser().parse(request)
serializer = CarSerializer(data=data, many=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data)
return JsonResponse(serializer.errors, status=400)
But I received this error:
non_field_errors:
["Expected a list of items but got type "dict"."]
Attempt 2:
With a #fxgx I tried too:
def update_car(self, request):
serializer = CarSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = dict(list(serializer.validated_data.items()))
queryset = Car.objects.update(**validated_data)
return Response(CarSerializer(queryset, many=True).data)
But I received this error:
{
"detail": "Not found."
}
DRF serializers do not support bulk updates, you must pass an object instance to serializer to update it. What you can do is serializing data using serializer, updating objects with validated data and then serializing objects again to get response data:
class GarageViewSet(viewsets.ModelViewSet):
queryset = Garage.objects.all()
serializer_class = GarageSerializer
model = Garage
#action(detail=False, methods=['put'])
def update_car(self, request):
...
# Use partial=True for partial updates.
serializer = CarSerializer(data=request.data)
# Validate data.
serializer.is_valid(raise_exception=True)
# Get validated data in dictionary format.
validated_data = dict(list(serializer.validated_data.items()))
# Update objects
quertset.update(**validated_data)
...
return Response(CarSerializer(queryset, many=True).data)

Django DRF - ListCreateAPIView POST Failing with depth=2

I have a ListCreateAPIViewfor showing a list of contacts, as well as for creating new contacts which uses this serializer:
class ContactPostSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
For POSTing new records, I have to specifically exclude id so that DRF doesn't complain about a null id. But, for listing records with this serializer, the serializer doesn't return the objects in ForeignKey fields. To get these objects, I add depth = 2. So now the serializer looks like this:
class ContactPostSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
depth = 2
However, now, with depth = 2, I can't do POSTs anymore. It complains again of null id values.
Edit: I should add that the errors that come up with I have depth=2 are specific to the models of the Foreign Key objects, not the new record I'm creating.
What am I missing here?
I discovered the problem is that when the serializer has depth=2 that part is not writeable. That's why it was failing. The other thing is that I didn't want to change my URL so that I only had /contacts/ for both listing and for creating. To do that, I had to adjust my class for handling the responses.
Here's what I came up with:
api.py
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
def create(self, request, *args, **kwargs):
# If we're creating (POST) then we switch serializers to the one that doesn't include depth = 2
serializer = ContactCreateSerializer(data = request.data)
if serializer.is_valid():
self.object = serializer.save()
headers = self.get_success_headers(serializer.data)
# Here we serialize the object with the proper depth = 2
new_c = ContactSerializer(self.object)
return Response(new_c.data, status = status.HTTP_201_CREATED, headers = headers)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
serializers
class ContactCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ()
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ()
depth = 2
Credit to this SO answer which helped me figure it out: https://stackoverflow.com/a/26741062/717682
Let's say that the link that calls your view is /example/.
If you want to POST data then you can call this like that: "/example/",
If you want to GET data (with depth) you can call this like that: "/example/?depth="yes"
You must have two serializers. One with the depth and one without it.
class ContactPOSTSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
class ContactGETSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
depth = 2
So then your view will be something like that:
class ExampleView(viewsets.ModelViewSet):
serializer_class = ContactPOSTSerializer
def list(self, request, *args, **kwargs):
depth = self.request.query_params.get('depth', "")
if (depth != "" and depth != "null"):
serializer = ContactGETSerializer(context={'request': request})
return Response(serializer.data)
serializer = ContactPOSTSerializer(context={'request': request})
return Response(serializer.data)
It might not be the best solution but it worked for me :)

Categories

Resources