How to create multiple objects in serializers create method? - python

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

Related

How to save list of objects in DRF

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)

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 can I fix my PATCH request to append to ArrayField, rather than delete and replace the whole object?

I have a problem with my PATCH request instance. Currently, data that is being PATCHED and sent from a request is overriding every item in my list of strings ArrayField inside my model object.
I need my patch request behavior to append to the rest of the items in ArrayField object, not delete/override.
How can I go about doing that?
I assume I need to override the patch method within RetrieveUpdateAPIView,
so I've started out here:
def patch(self, request, **kwargs):
item = self.kwargs.get('slug')
serializer = StockListSerializer(item, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
Response(serializer, status=status.HTTP_200_OK)
return Response(status=status.HTTP_400_BAD_REQUEST)
serializer.py:
class StringArrayField(serializers.ListField):
def to_representation(self, obj):
obj = super().to_representation(obj)
return ",".join([str(element) for element in obj])
def to_internal_value(self, data):
data = data.split(",")
return super().to_internal_value(data)
class StockListSerializer(serializers.ModelSerializer):
stock_list = StringArrayField()
class Meta:
model = Bucket
fields = ("stock_list",)
view.py
class EditBucketSymbols(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = StockListSerializer
queryset = Bucket.objects.all()
def get_object(self, queryset=queryset, **kwargs):
item = self.kwargs.get('slug')
return get_object_or_404(Bucket, slug=item)
url.py:
path('bucket/symbols/<str:slug>/', EditBucketSymbols.as_view(), name='editsymbols')
model.py:
stock_list = ArrayField(models.CharField(max_length=6,null=True),size=30,null=True, blank=True)
You can make use of the field-lever validation technique here,
class StockListSerializer(serializers.ModelSerializer):
stock_list = StringArrayField()
class Meta:
model = Bucket
fields = ("stock_list",)
def validate_stock_list(self, stock_list):
existing_stock_list = []
if self.instance and self.instance.stock_list:
# Patch or Put request
existing_stock_list = self.instance.stock_list
return existing_stock_list + stock_list
I would do this within the .validate() method of the serializer. something like:
def validate(self, validated_data):
# Make sure that this only runs for Patch requests
if self.context["request"].method == "PATCH"
# Get list of existing stocks from instance and append incoming list
stock_list = self.instance.stock_list + validated_data["stock_list"]
# Replace data
validated_data["stock_list"] = stock_list
return validated_data

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.

How to add additional fields on the base of inputs in django rest framework mongoengine

I am developing an API, using django-rest-framework-mongoengine with MongoDb, I want to append additional fields to the request from the serializer on the base of user inputs, for example If user enters keyword=#rohit49khatri, I want to append two more fields to the request by manipulating keyword, like type=username, username=rohit49khatri
Here's my code:
Serializer
class SocialFeedCreateSerializer(DocumentSerializer):
type = 'username'
class Meta:
model = SocialFeedSearchTerm
fields = [
'keyword',
'type',
]
read_only_fields = [
'type'
]
View
class SocialFeedCreateAPIView(CreateAPIView):
queryset = SocialFeed.objects.all()
serializer_class = SocialFeedCreateSerializer
def perform_create(self, serializer):
print(self.request.POST.get('type'))
But when I print type parameter, It gives None
Please help me save some time. Thanks.
for the additional question: how to get type parameter?
# access it via `django rest framework request`
self.request.data.get('type', None)
# or via `django request`
self.request.request.POST.get('type', None)
for the original question:
situation 1) IMHO for you situation, perform_create can handle it:
def perform_create(self, serializer):
foo = self.request.data.get('foo', None)
bar = extract_bar_from_foo(foo)
serializer.save(
additional_foo='abc',
additional_bar=bar,
)
situation 2) If you need to manipulate it before the data goes to serializer (so that the manipulated data will pass through serializer validation):
class SocialFeedCreateAPIView(CreateAPIView):
queryset = SocialFeed.objects.all()
serializer_class = SocialFeedCreateSerializer
def create(self, request, *args, **kwargs):
# you can check the original snipeet in rest_framework/mixin
# original: serializer = self.get_serializer(data=request.data)
request_data = self.get_create_data() if hasattr(self, 'get_create_data') else request.data
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)
def get_create_data(self):
data = self.request.data.copy()
# manipulte your data
data['foo'] = 'foo'
return data
situation 3) If you do need to manipulate the request:
(here's just an example, you can try to find out another place to manipulate the request.)
class SocialFeedCreateAPIView(CreateAPIView):
queryset = SocialFeed.objects.all()
serializer_class = SocialFeedCreateSerializer
def initial(self, request, *args, **kwargs):
# or any other condition you want
if self.request.method.lower() == 'post':
data = self.request.data
# manipulate it
data['foo'] = 'foo'
request._request.POST = data
return super(SocialFeedCreateAPIView, self).initial(request, *args, **kwargs)

Categories

Resources