Django Rest Framework unique field constraint on array - django

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

Related

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

How to update multiple objects in django rest framework?

I am trying to update multiple objects using IDs which i am passing in every objects that need to be updated but can't find any way to do it successfully. Here is my code
models.py
class EventTicket(models.Model):
id = models.UUIDField(primary_key=True, default=uuid_generate_v1mc, editable=False)
name = models.CharField(max_length=250)
description = models.TextField(max_length=1000)
views.py
class EventTicketView(APIView, PaginationHandlerMixin):
permission_classes = (AllowAny,)
def get_object(self, ticket_id):
try:
return EventTicket.objects.get(id=ticket_id)
except EventTicket.DoesNotExist():
raise status.HTTP_400_BAD_REQUEST
def patch(self, request, *args, **kwargs):
for each_ticket in request.data:
ticket_id = self.get_object(each_ticket['ticket_id'])
serializer = EventTicketSerializer(instance=ticket_id,data=request.data,partial=True)
if serializer.is_valid():
serializer.save()
result = {
'message': "updated sucessfully"
}
return Response(result, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class EventTicketSerializer(serializers.ModelSerializer):
class Meta:
model = EventTicket
fields = ['name', 'description']
```
I have to send data like list of multiple objects :::
[
{
"ticket_id": "054665ea-4fde-11ea-94b2-9f415c43ba4c",
"name": "chris",
"description":"The golden ticket for day only",
},
{
"ticket_id": "054656ea-4fde-11ea-94b2-9f415c43ba4c",
"name": "daut",
"description":"The premium ticket for day only",
}
]
The following code will give you a proper understanding of updating multiple objects in single request.
For updating multiple objects in a single request it is best practice to use the PUT method instead of PATCH.
Here body data given is.
BODY DATA
{
"ids":[
"5e41770d2e8fa013d1f034ec",
"5e41772c2e8fa013d1f034ee",
"5e4177702e8fa013d1f034f2",
"5e453f302e8fa075aa18b277",
"5e4a314f2e8fa070c5251a0a"
]
}
I'am updating the enabled attribute from False to True for given ids of DemoConfig model.
In the same way, you can update your data. As per your requirement, you can write validate methods to validate the body data.
Serializer has written to serialized the instance data for the response.
class DemoAPI(APIView):
def get_object(self, obj_id):
try:
return DemoConfig.objects.get(id=obj_id)
except (DemoConfig.DoesNotExist, ValidationError):
raise status.HTTP_400_BAD_REQUEST
def validate_ids(self, id_list):
for id in id_list:
try:
DemoConfig.objects.get(id=id)
except (DemoConfig.DoesNotExist, ValidationError):
raise status.HTTP_400_BAD_REQUEST
return True
def put(self, request, *args, **kwargs):
id_list = request.data['ids']
self.validate_ids(id_list=id_list)
instances = []
for id in id_list:
obj = self.get_object(obj_id=id)
obj.enabled = True
obj.save()
instances.append(obj)
serializer = DemoSerializer(instances, many=True)
return Response(serializer.data)
Serialiser for this view is:
class DemoSerializer(DocumentSerializer):
class Meta:
model = DemoConfig
fields = '__all__'
Output:
{
"data": [
{
"id": "5e41770d2e8fa013d1f034ec",
"name": "CONFIG_1",
"enabled": true,
},
{
"id": "5e41772c2e8fa013d1f034ee",
"name": "CONFIG_2",
"enabled": true,
},
{
"id": "5e4177702e8fa013d1f034f2",
"name": "CONFIG_3",
"enabled": true,
},
{
"id": "5e453f302e8fa075aa18b277",
"name": "CONFIG_4",
"enabled": true,
},
{
"id": "5e4a314f2e8fa070c5251a0a",
"name": "CONFIG_5",
"enabled": true,
}
]
}
As per your code requirement you need to use put method in follwoing way.
def put(self, request, *args, **kwargs):
data = request.data
ticket_ids = [i['ticket_id'] for i in data]
self.validate_ids(ticket_ids)
instances = []
for temp_dict in data:
ticket_id = temp_dict['ticket_id']
name = temp_dict['name']
description = temp_dict['description']
obj = self.get_object(ticket_id)
obj.name = name
obj.description = description
obj.save()
instances.append(obj)
serializer = DemoSerializer(instances, many=True)
return Response(serializer.data)

Save multiple models in a single post - Django rest frameweok

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)

Django models default error messages into one field

This very simple django restframework code.
models.py
class User(models.Model)
Email = models.CharField(max_length=100)
Username = models.CharField(max_length=100)
State = models.CharField(max_length=100)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
views.py
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
If use this I am getting error out put like this
{
"Email": [
"This field may not be blank."
],
"Username": [
"This field may not be blank."
],
"Country": [
"This field may not be blank."
],
}
But I need to change the error out like this.How I can i archive this and any suggestion greatly appreciated.
{"error":
[
"Email is required",
"Username is required",
"County is required"
]
}
You can always overwrite the create/update methods from the generic views. It would be something like:
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid(raise_exception=False):
# TODO: add here your custom error dict using serializer.errors
return Response({"error":...}, status=...)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Or... you could try to overwrite the serializers... if you don't want to overwrite the view.
(However, there must be a good reason for a JS developer not being able to parse a simple json error object :P)
Hope this helps
You can define your own error messages for any error case:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
for field in self.Meta.fields:
self.fields[field].error_messages['required'] = "%s is required" % field

DRF - ModelViewSet Create - "This field is required."

I have the following json I'm trying to post -
{
"title": "test",
"description": "testing desc",
"username": "admin",
"password": "Welcome1",
"owner": 4
}
The ModelViewSet is below -
class PasswordViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = Password.objects.all()
serializer_class = PasswordSerializer
filter_class = PasswordFilter
def list(self, request):
my_passwords = Password.objects.filter(owner=request.user)
page = self.paginate_queryset(my_passwords)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = PasswordSerializer(my_passwords, many=True)
return Response(serializer.data)
If I remove the def list I can do a post just fine. The problem is trying to post with that def in there. it returns that all my fields are required. Why is it not passing the data?
This is my serializer for reference -
class PasswordSerializer(serializers.ModelSerializer):
class Meta:
model=Password
edit - no idea what's going wrong testing some more here and now it's not even working with the def list removed.
model -
class Password(models.Model):
title = models.CharField(max_length=100)
description = models.CharField(max_length=1024)
username = models.CharField(max_length=50)
password = models.CharField(max_length=200)
owner = models.ForeignKey('MyUser', related_name='MyUser_owner')
The response is telling me all the fields in the model are required so to me on post the request.data is empty even though the json should be it.