Save multiple models in a single post - Django rest frameweok - django

I have 4 models
class User(AbstractEmailUser):
first_name = models.CharField(max_length=100, blank=True)
last_name = models.CharField(max_length=100, blank=True)
class Event(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=200)
date = models.DateField()
class EventLocation(models.Model):
event = models.ForeignKey(Event)
ubigeo = ArrayField(models.CharField(max_length=200), blank=True)
class EventStaff(models.Model):
recycler = models.ForeignKey(User)
event = models.ForeignKey(Event)
When I want to register an event and be able to assign users to this same publication at the time of creation, assign users or do not assign them. I have already created a nested serialier that in the documentation is well explained so that the event is saved and at the same time it is saved in the ubigeo field of the EventLocation table (code of the district of the place):
Class EventLocationSerializer(serializers.ModelSerializer):
class Meta:
model = EventLocation
fields = ('id', 'ubigeo')
class EventSerializer(serializers.ModelSerializer):
event_location = EventLocationSerializer(required=True, write_only=True)
def to_representation(self, instance):
representation = super(EventSerializer, self).to_representation(instance)
event_location = EventLocation.objects.filter(event=instance.id)
if event_location:
representation['event_location'] = event_location.values('ubigeo')[0]
return representation
class Meta:
model = Event
fields = ('id', 'date', 'name', 'address', 'schedule', 'event_location')
def create(self, validated_data):
location_data = validated_data.pop('event_location')
event = Event.objects.create(**validated_data)
EventLocation.objects.create(event=event, **location_data)
return event
and it works correctly, but how would you add the users you want to assign to the event at the same time? I know I have to save them in the EventStaff table but how do I insert them in that same post?
This is my viewset:
#transaction.atomic
def create(self, request, *args, **kwargs):
with transaction.atomic():
try:
data = request.data
serializer = EventSerializer(data=data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response({"status": True, "results": "Evento registrado correctamente"},
status=status.HTTP_201_CREATED)
except ValidationError as err:
return Response({"status": False, "error_description": err.detail}, status=status.HTTP_400_BAD_REQUEST)
This is the json format:
{
"date": "2018-03-01",
"name": "La prueba reciclaje",
"address": "Av españa trujillo",
"users": [
{"id": 40, "first_name": "Raul"},
{"id": 23, "first_name": "ALejandro"}
],
"eventlocation": {
"ubigeo": ["130101"]
}
}

In my opinion, we can custom your def create a bit more.
So we create one Serializer for User, get params user and save it after Event saved.
Maybe like this:
#transaction.atomic
def create(self, request, *args, **kwargs):
with transaction.atomic():
try:
data = request.data
serializer = EventSerializer(data=data)
if serializer.is_valid(raise_exception=True):
serializer.save()
// recheck , this loop have input is all users in json
for user in data.get('users'):
user_serializer = UserSerializer(data=user)
if user_serializer.is_valid(raise_exception=True):
user_serializer.save()
return Response({"status": True, "results": "Evento registrado correctamente"},
status=status.HTTP_201_CREATED)
except ValidationError as err:
return Response({"status": False, "error_description": err.detail}, status=status.HTTP_400_BAD_REQUEST)
Hoop this help

As I said in the commentary, it works wonderfully :D
#transaction.atomic
def create(self, request, *args, **kwargs):
with transaction.atomic():
try:
data = request.data
users = request.data.get('users', None)
serializer = EventSerializer(data=data)
if serializer.is_valid(raise_exception=True):
instance = serializer.save()
if users:
for user in users:
EventStaff.objects.create(recycler_id=user['id'], event_id=instance.id)
return Response({"status": True, "results": "Evento registrado correctamente"},
status=status.HTTP_201_CREATED)
except ValidationError as err:
return Response({"status": False, "error_description": err.detail}, status=status.HTTP_400_BAD_REQUEST)

Related

Django Rest Framework unique field constraint on array

So, I'm trying to make an endpoint where I insert a list of objects.
My issue is the behavior and response when inserting duplicates.
What I want to accomplish is to:
Send the duplicate lead external_id(s) in the error response
Insert any other non duplicated object
I'm pretty sure this logic (for the response and behavior) could be accomplished in the modifying the serializer.is_valid() method... but before doing that, I wanted to know if anyone had experience with this kind of request.. Maybe there is a "clean" way to do this while keeping the unique validation in the model.
Data on OK response:
[
{
"external_id": "1",
"phone_number": "1234567890"
}
]
Data for a FAIL request (1 is dup, but 2 should be inserted. Expecting a response like "external_id" : "id 1 is duplicated"):
[
{
"external_id": "1",
"phone_number": "1234567890"
},
{
"external_id": "2",
"phone_number": "2234567890"
}
]
models.py
class Lead(models.Model):
external_id = models.CharField(max_length=20, unique=True)
phone_number = models.CharField(max_length=50)
serializers.py
class LeadSerializer(serializers.ModelSerializer):
class Meta:
model = Lead
fields = '__all__'
def create(self, validated_data):
lead = Lead.objects.create(**validated_data)
log.info(f"Lead created: {lead.import_queue_id}")
return lead
views.py
class LeadView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
#extend_schema(description="Insert campaign data", request=LeadSerializer(many=True), responses=None, tags=["Leads"])
def post(self, request):
serializer = LeadSerializer(data=request.data, many=True)
valid = serializer.is_valid()
if serializer.is_valid():
serializer.save()
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
return Response({"status": "error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
You can customize the behaviour of list of object through list_serializer_class option in meta class. Like this:
class LeadListSerializer(serializers.ListSerializer):
def validate(self, data):
items = list(map(lambda x: x['phone_number'], data))
if len(set(items)) == len(items)):
return super().validate(data)
raise ValidationError()
class LeadSerializer(serializers.ModelSerializer):
class Meta:
model = Lead
fields = '__all__'
list_serializer_class = LeadListSerializer

How do I modify my Django REST endpoint to automatically create a dependent model when creating the submitted model?

I'm using the djangorestframework module to set up an API to update/read my models. I have these models ...
from django.db import models
from address.models import AddressField
from phonenumber_field.modelfields import PhoneNumberField
from address.models import State
from address.models import Country
class CoopTypeManager(models.Manager):
def get_by_natural_key(self, name):
return self.get_or_create(name=name)[0]
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False)
objects = CoopTypeManager()
class Meta:
unique_together = ("name",)
class Coop(models.Model):
name = models.CharField(max_length=250, null=False)
type = models.ForeignKey(CoopType, on_delete=None)
address = AddressField(on_delete=models.CASCADE)
enabled = models.BooleanField(default=True, null=False)
phone = PhoneNumberField(null=True)
email = models.EmailField(null=True)
web_site = models.TextField()
and I have these view classes ...
class CoopList(APIView):
"""
List all coops, or create a new coop.
"""
def get(self, request, format=None):
coops = Coop.objects.all()
serializer = CoopSerializer(coops, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = CoopSerializer(data=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)
class CoopDetail(APIView):
"""
Retrieve, update or delete a coop instance.
"""
def get_object(self, pk):
try:
return Coop.objects.get(pk=pk)
raise Http404
def get(self, request, pk, format=None):
coop = self.get_object(pk)
serializer = CoopSerializer(coop)
return Response(serializer.data)
def put(self, request, pk, format=None):
coop = self.get_object(pk)
serializer = CoopSerializer(coop, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
coop = self.get_object(pk)
coop.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Problem is, I would like to submit JSON like this
{
"name": "1872",
"type": {
"name": "Coworking Space"
},
with the intention being that for the dependent CoopType model, I would either create one of use an existing one before creating my Coop model. However, right now, submitting the above results in a 400 response ...
{"type":["Incorrect type. Expected pk value, received dict."]
How should I modify my view class to accommodate what I'm trying to do?
Edit: the serializers ...
from rest_framework import serializers
from maps.models import Coop, CoopType
from address.models import Address, AddressField, Locality, State, Country
class CoopSerializer(serializers.ModelSerializer):
class Meta:
model = Coop
fields = ['id', 'name', 'type', 'address', 'enabled', 'phone', 'email', 'web_site']
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['type'] = CoopTypeSerializer(instance.type).data
rep['address'] = AddressSerializer(instance.address).data
return rep
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Coop.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `Coop` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.type = validated_data.get('type', instance.type)
instance.address = validated_data.get('address', instance.address)
instance.enabled = validated_data.get('enabled', instance.enabled)
instance.phone = validated_data.get('phone', instance.phone)
instance.email = validated_data.get('email', instance.email)
instance.web_site = validated_data.get('web_site', instance.web_site)
instance.web_site = validated_data.get('web_site', instance.web_site)
instance.save()
return instance
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
Edit 2: The curl request ...
#!/bin/bash
read -d '' req << EOF
{
"name": "1872",
"type": {
"name": "Coworking Space"
},
"address": {
"id": 1,
"street_number": "222",
"route": "1212",
"raw": "222 W. Merchandise Mart Plaza, Suite 1212",
"formatted": "222 W. Merchandise Mart Plaza, Suite 1212",
"latitude": 41.88802611,
"longitude": -87.63612199,
"locality": {
"id": 29,
"name": "Chicago",
"postal_code": "60654",
"state": {
"id": 1,
"name": "IL",
"code": "",
"country": {
"id": 484,
"name": "United States",
"code": "US"
}
}
}
},
"enabled": true,
"phone": null,
"email": null,
"web_site": "http://www.1871.com/"
}
EOF
echo $req
curl -v --header "Content-type: application/json" --data "$req" --request POST "http://127.0.0.1/coops/"
This should work:
class CoopTypeField(serializers.PrimaryKeyRelatedField):
queryset = CoopType.objects
def to_internal_value(self, data):
if type(data) == dict:
cooptype, created = CoopType.objects.get_or_create(**data)
# Replace the dict with the ID of the newly obtained object
data = cooptype.pk
return super().to_internal_value(data)
class CoopSerializer(serializers.ModelSerializer):
type = CoopTypeField()
# ... the rest of this class is unchanged
Where the changes are:
Define a custom CoopTypeField(). The to_internal_value() method would ordinarily be expecting an ID - so we override it to accept data in the form of a dictionary and convert this to an ID (by getting or creating a CoopType) and then pass this ID to the parent class method.
Define a type on the CoopSerializer that uses this new CoopTypeField().
Now your CoopSerializer will accept data in two forms:
{'name': 'Coop 1', 'web_site': 'http://example.com', 'type': {'name': 'Coop Type 1'}}
or
{'name': 'Coop 1', 'web_site': 'http://example.com', 'type': 1}
(I've omitted the other fields that Coop requires for brevity).
Based on the current serializer, the CoopType needs to be made ahead of time (or fetched ahead of time if it exists) so that the id can be passed along. If you want to still make the CoopType with nested data in the Coop serializer (or use it to find the CoopType to use), Django Rest Framework talks about how in their docs under Writable nested serializers.
Oh boy... There is so much we could improve upon here. So let's start with your question first.
#tredzko(https://stackoverflow.com/a/60310659/3627387) was right and you should look into https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
By the look of your CoopType model I see that name field is unique.
from rest_framework import serializers
from maps.models import Coop, CoopType
from address.models import Address, AddressField, Locality, State, Country
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
class CoopSerializer(serializers.ModelSerializer):
# type field should be defined here instead of `to_representation`
type = CoopTypeSerializer()
class Meta:
model = Coop
fields = ['id', 'name', 'type', 'address', 'enabled', 'phone', 'email', 'web_site']
def to_representation(self, instance):
# this is the correct way of extending to_representation
# we set update self.fields and after that
# Serializer class handles everything automatically
self.fields['address'] = AddressSerializer(read_only=True)
return super().to_representation(instance)
def validate_type(self, value):
coop_type, __ = CoopType.objects.get_or_create(**coop_type_data)
return coop_type
Something along this lines should work.
Now let's dive into other improvements
I suggest using ModelViewSet(https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset)
So you could do this(see below) instead of your views code
from rest_framework import viewsets
class CoopViewSet(viewsets.ModelViewSet):
serializer_class = CoopSerializer
queryset = Coop.objects.all().select_related('type')
Your CoopType model is defined in an odd way, I think you wanted to actually do this.
class CoopType(models.Model):
# https://docs.djangoproject.com/en/3.0/ref/models/fields/#unique
name = models.CharField(max_length=200, null=False, unique=True)
class Meta:
pass

Django Rest Framework many-to-many serialization is not working as expected

I dug into stackoverflow and couldn't find anything related to this error/mistake.
Well, I have this model:
class ChatRoom(models.Model):
class Meta:
app_label = 'chatbot'
group = models.CharField(max_length=255)
category = models.CharField(max_length=255)
name = models.CharField(max_length=255)
url = models.URLField()
online = models.IntegerField(default=0)
recaptcha_hash = models.TextField(null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
bots = models.ManyToManyField(MarkBot, through='ChatBot')
def __str__(self):
return '{0}/{1}/{2}'.format(self.group, self.category, self.name)
This serializer:
class ChatRoomSerializer(ModelSerializer):
bots = MarkBotSerializer(read_only=True, many=True, allow_null=True)
class Meta:
model = ChatRoom
fields = ['group', 'category', 'name', 'url',
'online', 'recaptcha_hash', 'bots']
read_only_fields = ['pk', 'created_at', 'updated_at']
My many-to-many (with "through") middle model is:
class ChatBot(models.Model):
class Meta:
app_label = 'chatbot'
room_token = models.TextField()
bot = models.ForeignKey(MarkBot, on_delete=models.CASCADE)
chat_room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE)
Finally, my APIview, that retrieves the data to the client:
class ChatRoomsActive(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
user = request.user
try:
campaign = Campaign.objects.get(user=user, is_active=True)
chat_rooms = ChatRoom.objects.filter(campaign=campaign)
rooms = ChatRoomSerializer(
data=list(chat_rooms.values()),
many=True
)
if rooms.is_valid():
return Response({
'data': rooms.data,
'error': None
})
except Campaign.DoesNotExist:
return Response({
'data': [],
'error': 'Campanha inválida'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
return Response({
'data': [],
'error': 'Ocorreu um erro inesperado em sua requisição'
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'data': [],
'error': 'Não foi possível obter as salas ativas'
}, status=status.HTTP_400_BAD_REQUEST)
And, in all my JSON serialized results I receive the attribute "bots" equals "null", even when I have a bot attached to a room.
Expected Output
A list of bots attached to that specific room or null if it has none.
Thank you, in advance!
chat_rooms.values() is likely not to have the related bots and calling is_valid for serialization doesn't make sense - I'm not sure it works at all.
rooms = ChatRoomSerializer(
instance=list(chat_rooms),
many=True
)
return Response({
'data': rooms.data,
'error': None
})

serializers validation method is not validating list of objects for the first time

models.py
class Customer(models.Model):
name = models.CharField(max_length=128)
email = models.EmailField(null=True, blank=True)
phone = models.CharField(max_length=128)
serializers.py
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ("name", "email", "phone")
extra_kwargs = {
"email":{"required":True},
}
def validate_email(self, email):
qs = Customer.objects.filter(email__iexact=email)
if qs.exists():
raise serializers.ValidationError("This email is already existed")
return email
def validate_phone(self, phone):
qs = Customer.objects.filter(phone__iexact=phone)
if qs.exists():
raise serializers.ValidationError("This Phone is already existed")
return phone
views.py
class CustomerApi(SerializerMixin, APIView):
......
.........
def post(self, request):
serializer = CustomerSerializer(data=request.data, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(
serializer.errors,
status = status.HTTP_400_BAD_REQUEST
)
json
[
{
"phone": "123",
"name": "name1",
"email": "123#ll.cc"
},
{
"phone": "123",
"name": "nam32",
"email": "123#ll.cc"
}
]
the above customer json list have two objects with same email and phone . But it is not validating the email and phone(validation methods are not working) when i create if for the first time. But once it get created, the second time when i try to create the customer with the same list, validation method is working as expected.
i couldn't trace out the problem.
you can try:
class CustomerSerializer(serializers.ModelSerializer):
# Your code here
def validate(self, validated_data):
emails = [x['email'] for x in validated_data]
if len(emails) > len(set(emails):
raise serializers.ValidationError("Duplication email in data")
phones = [x['phone'] for x in validated_data]
if len(phones) > len(set(phones):
raise serializers.ValidationError("Duplication phone in data")
return validated_data

POST json array and get response as json array

my client is sending me a POST with a json-array and is awaiting a response with the complete details of the requested data. I have no problems with single requests and single responses, but to minimize the overhead, I'd like to process an array.
my models.py
class RoFile(models.Model):
user = models.ForeignKey('auth.User', null=True)
filename = models.CharField(max_length=120, null=True)
deleted = models.BooleanField(default=False)
info = models.CharField(max_length=120, null=True)
md5check = models.CharField(max_length=120, null=True)
one try of my serializer:
class RoFileSerializer(serializers.ModelSerializer):
deleted = serializers.ReadOnlyField(required=False)
user = serializers.ReadOnlyField(required=False)
info = serializers.ReadOnlyField(required=False)
class Meta:
model = RoFile
fields = (
'filename', 'md5check', 'deleted', 'user', 'info',
)
def create(self, validated_data):
return RoFile(**validated_data)
on try of my views:
#api_view(['POST'])
def rofile_detaillist(request, format=None):
data = JSONParser().parse(request)
serializer = RoFileSerializer(data=data, many=True)
if serializer.is_valid():
json_add = []
for x in serializer.validated_data:
try:
rofile = RoFile.objects.filter(md5check=x['md5check'])
except ObjectDoesNotExist:
continue
*invalid code here*
return Response(jsonarraywithallinfos)
else:
return Resonse(status=status.HTTP_400_BAD_REQUEST)
another view try:
class RoFileDetailList(viewsets.ModelViewSet):
model = RoFile
serializer_class = RoFileSerializer(many=True)
def get_queryset(self):
return Rofile.objects.filter(md5check=self.request.data['md5check'])
a POST example:
{"filename": "filename1.exe", "md5check": "f8541061779b1efc5c30c4783edfb8f8"},
{"filename": "filename2.exe", "md5check": "16cdac5eb0ec829c2e2c199488681f6e"}
what I need as a response back:
{"filename": "filename1.exe", "md5check": "f8541061779b1efc5c30c4783edfb8f8", user: "testuser1", deleted: "True", info: ""},
{"filename": "filename2.exe", "md5check": "16cdac5eb0ec829c2e2c199488681f6e", user: "testuser1", deleted: "False", info: ""}
Sorry for the invalid code part, but I have already tried so much, so I deleted that part (by accident).
Thank you!
EDIT:
I don't need to create with POST, I only need to retrieve additional data (the rest of the model). I had to change the create function in the serializer, because I don't want to create the entries, I only want to retrieve the data associated with the md5check from the db.
thanks to #zaphod100.10
my actual serializer:
class RoFileSerializer(serializers.ModelSerializer):
class Meta:
model = RoFile
fields = '__all__'
read_only_fields = ('deleted',)
def create(self, validated_data):
return RoFile(**validated_data)
my view:
class RoFileListDetailApi(generics.ListCreateAPIView):
serializer_class = RoFileSerializer
def get_queryset(self):
return RoFile.objects.filter(md5check=self.request.data['md5check'])
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
my post:
{"filename": "filename1.exe", "md5check": "f8541061779b1efc5c30c4783edfb8f8"},
{"filename": "filename2.exe", "md5check": "16cdac5eb0ec829c2e2c199488681f6e"}
my actual response is now a list but only with my POST-data and not the real data from the db:
{"filename": "filename1.exe", "md5check": "f8541061779b1efc5c30c4783edfb8f8", deleted: false, info: null, user: null},
{"filename": "filename2.exe", "md5check": "16cdac5eb0ec829c2e2c199488681f6e", deleted: false, info: null, user: null}
should be:
{"filename": "filename1.exe", "md5check": "f8541061779b1efc5c30c4783edfb8f8", deleted: true, info: "some info", user: "usertest1"},
{"filename": "filename2.exe", "md5check": "16cdac5eb0ec829c2e2c199488681f6e", deleted: false, info: "some info2", user: "usertest2"}
use this:
class RoFileSerializer(serializers.ModelSerializer):
class Meta:
model = RoFile
fields = '__all__'
read_only_fields = ('deleted', 'user', 'info')
class RoFileListCreateApi(generics.ListCreateAPIView):
serializer_class = RoFileSerializer
def get_queryset(self):
return Rofile.objects.filter(md5check=self.request.data['md5check'])
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
# override perform_create or the serializers create method for custom create logic
self.perform_create(serializer)
# assign other fields to the objs and save again
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
You just have to pass many=True to the serializer for handling lists.
Override perform_create method in the generic view or the serializers create method for applying custom creation logic.
EDIT:
based on new info provided I have changed the create method.
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
# don't create anything just insert required data
for rof_data in serializer.data:
md5check = rof_data['md5check']
# code to retrieve data from db based on md5check
....
# code to insert values in rof_data
rof_data['user'] = user.username
rof_data['deleted'] = deleted
rof_data['info'] = info
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)