How to save list of objects in DRF - python

I am new to django. I have following model:
class Standup(models.MOdel):
team = models.ForeignKey("Team", on_delete=models.CASCADE)
standup_time = models.DateTimeField(auto_now_add=True)
employee = models.ForeignKey("Employee", on_delete=models.CASCADE)
update_time = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=50)
work_done_yesterday = models.TextField()
work_to_do = models.TextField()
blockers = models.TextField()
Serializer class looks like this:
class StandupSerializer(serializers.ModelSerializer):
class Meta:
model = Standup
fields = '__all__'
Viewset is like this:
class StandupDetail(viewsets.ModelViewSet):
queryset = Standup.objects.all()
serializer_class = StandupSerializer
My task is to hit a single API which will save the data of all employees, instead of saving the data of employees separately. In the current implementation, each employee will have to hit the API separately to save the data in database. Each employee will select team first, as one employee can be a part of multiple team. We will save a list of objects. Any leads on how to do it?

Try to pass a list of data in request body. You need to modify your serializer as well as override the create for bulk creation and saving of data. You can follow this.
https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-create

Django provides bulk_create method for achieving that.
For example you can put the below function in your appropriate class in viewset:
def bulk_update_standup(self, request, *args, **kwargs):
standup_list = request.data.get("standupList", [])
qs = []
for item in standup_list:
serializer = StandupSerializer(data=item)
standup_instance = Standup(**serializer.validated_data)
qs.append(standup_instance)
Standup.objects.bulk_create(qs)
data = {"data": None, "message": "Saved Successfully"}
return Response(data=data, status=status.HTTP_200_OK)

You can override create method.
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
serializer = self.get_serializer(data=request.data, many=True)
else:
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)

Related

Django Rest Framework - Why is serializer.data giving me an empty result set but printing the serializer shows me the data

I have created a view to search posts based on their body text. I added a breakpoint to the view and I tested it with mutliple search terms and it works. The problem I have is when I do a print(serializer) in the console then I see the data of all the posts it found. But doing a print(serializer.data) gives me body:null in the data object in the front end console and an empty dictionary in the back end console.
I am using PostgreSQL
Why am I getting body: null?
Here is the response in the console for print(serializer):
SearchSerializer(<QuerySet [<Post: This is a post>, <Post: This is another post>, <Post: Post THREE>, <Post: Post ONE>, <Post: Post ONE by the user>, <Post: Post TWO by the user>]>):
body = CharField(allow_blank=True, allow_null=True, label='Content', max_length=5000, required=False, style={'base_template': 'textarea.html'})
Here is the view:
class SearchPosts(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
term = request.data.get("term")
posts = Post.objects.filter(body__search=term)
serializer = SearchSerializer(posts)
breakpoint()
return Response(serializer.data, status=HTTP_200_OK)
Here is the serializer:
class SearchSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = [
'body'
]
Here is the post model:
class Post(models.Model):
body = models.TextField("content", blank=True, null=True, max_length=5000)
slug = AutoSlugField(populate_from=["category", "created_at"])
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="posts")
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "post"
verbose_name_plural = "posts"
db_table = "posts"
ordering = ["created_at"]
get_latest_by = "created_at"
def __str__(self):
return self.body[0:30]
def get_absolute_url(self):
return self.slug
How am I then supposed to get the data in the front end?
UPDATE
If in the view the request object looks like this:
<rest_framework.request.Request: GET '/api/v1/posts/search/?term=looking'>
Then how do I get the term? ('looking' at the moment)
Because request.data gives me None
kwargs is an empty dictionary
So I changed the view and now it makes even less sense:
class SearchPosts(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
term = request.GET.get("term")
posts = Post.objects.all().filter(body__search=term)
breakpoint()
serializer = SearchSerializer(posts)
return Response(serializer.data, status=HTTP_200_OK)
Now I get an empty result if I do a print(posts). The term is correct I checked that so why is body__search not working anymore?
Changed it to a ListAPIView and I still get an empty data array in the response:
# GENERIC VIEW TO GET POSTS FOUND BASED ON A SEARCH TERM
class SearchPosts(generics.ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = SearchSerializer
def get_queryset(self):
term = self.request.GET.get("term")
return Post.objects.filter(body__search=term)
If I remove the filter then all the posts data is in the response so why is body__search not working?
def get(self, request, format=None):
all_objects = Model.objects.all()
serializer = YourSerializer(all_objects, many=True)
return Response(serializer.data)
you missed to forgot get function
please refer to the docs https://www.django-rest-framework.org/tutorial/3-class-based-views/
You probably got AttributeError while trying to get serializer.data. You sent Queryset object to the serializer not single instance of Post model. If you want to serialize multiple objects just pass many parameter as True. From docs;
To serialize a queryset or list of objects instead of a single object instance, you should pass the many=True flag when instantiating the serializer.
So, change your usage like;
posts = Post.objects.filter(body__search=term)
serializer = SearchSerializer(posts, many=True)
try def get_serializer_class
because in def post - serializer doesn't mean anything

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

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]}

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

DRF: how to change the value of the model fields before saving to the database

If I need to change some field values before saving to the database as I think models method clear() is suitable. But I can't call him despite all my efforts.
For example fields email I need set to lowercase and fields nda I need set as null
models.py
class Vendors(models.Model):
nda = models.DateField(blank=True, null=True)
parent = models.OneToOneField('Vendors', models.DO_NOTHING, blank=True, null=True)
def clean(self):
if self.nda == "":
self.nda = None
class VendorContacts(models.Model):
....
vendor = models.ForeignKey('Vendors', related_name='contacts', on_delete=models.CASCADE)
email = models.CharField(max_length=80, blank=True, null=True, unique=True)
def clean(self):
if self.email:
self.email = self.email.lower()
serializer.py
class VendorContactSerializer(serializers.ModelSerializer):
class Meta:
model = VendorContacts
fields = (
...
'email',)
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response(request.data, status=status.HTTP_200_OK)
As I learned from the documentation
Django Rest Framework serializers do not call the Model.clean when
validating model serializers
In dealing with this problem, I found two ways to solve it.
1. using the custom method at serializer. For my case, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
def validate(self, attrs):
instance = Vendors(**attrs)
instance.clean()
return attrs
Using full_clean() method. For me, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors(**validated_data)
vendor.full_clean()
vendor.save()
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
But in both cases, the clean() method is not called. I really don't understand what I'm doing wrong.
In my case I had the same problem but with validation feature
I used the way below and it works for me (not excludes the way found above):
class CustomViewClass(APIView):
def post(self, request, format=None):
prepared_data_variable = 'some data in needed format'
serializer = CustomSerializer(data=request.data)
if serializer.is_valid(self):
serializer.validated_data['field_name'] = prepared_data_variable
serializer.save()
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
This string is key for my solution serializer.validated_data['field_name'] = prepared_data_variable
For DRF you can change your serializer before save as below...
First of all, you should check that serializer is valid or not, and if it is valid then change the required object of the serializer and then save that serializer.
if serializer.is_valid():
serializer.object.user_id = 15 # For example
serializer.save()
UPD!
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
data = request.data
if data['nda'] == '':
data['nda'] = None
for contact in data['contacts']:
if contact['email']:
print(contact['email'])
contact['email'] = contact['email'].lower()
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
To answer your question: just override save() method for your models as written in docs. There you can assign any values to your model instance directly before saving it in database.
Also, you should probably use models.EmailField for your email fields which will get rid of your lower() check.

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