I'm providing JSON to a view like so -
{
"username": "name",
"first_name": "john",
"last_name": "doe",
"email": "jdoe#hotmail.com",
"profile": {
"company": "abc corp"
}
}
I'm then passing it into the following view to post -
def post(self, request, format=None):
uuid = generate_uuid(request.data.get('username'))
data = {'username': request.data.get('username'),
'first_name': request.data.get('first_name'),
'last_name': request.data.get('last_name'),
'email': request.data.get('email'),
'company': request.data.get('profile').('company'),
'uuid': str(uuid)}
serializer = UserSerializer(data=data)
if serializer.is_valid():
print serializer.data
# serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I'm having difficulty in understanding how to structure the data to maintain the nested value as you can see I'm also including a field for a uuid which will also be in the nested profile object.
These are then being passed into my serializer here -
class UserSerializer(serializers.ModelSerializer):
profile = UserProfileSerializer()
class Meta:
model = User
fields = ('username', 'first_name', 'last_name', 'email', 'profile')
My view currently as it is gives a syntax error on this line -
'company': request.data.get('profile').('company'),
I know it's wrong just not sure how it should be structured.
Here is the correct version of this line:
'company': request.data.get('profile', dict()).get('company'),
What this does is supply an empty dict as a default argument if the data is missing a 'profile' object, or if it has one, call get('company') on that.
Related
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
In a Django app (that uses DRF), I have a user profile model. To update the profile info, I want to use PUT. However, I have a field called "meta", that is an object/dict itself. If I am missing any of its properties (gender, mobile, birthday), I will lose that data since the whole "meta" is replaced with the new one. That does not happen with any of the fields (e.g., if I do not specify the first name, the field will just stay the same). Here's an example of a PUT request body:
{
"id": 1,
"first_name": "Jane",
"last_name": "Doe",
"email": "jane#example.com",
"meta": {
"gender": "female",
"mobile": 123456789,
"birthday":"01-01-1970"
}
}
What can I do to assure that the missing properties are not lost? Is there a way to implement or force a merge/update of the previous data with the one in the request?
Here's the method:
def put(self, request, pk):
user = User.objects.get(id=pk)
serializer = UserSerializer(user, data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And here's the serializer:
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = [
'id',
'url',
'first_name',
'last_name',
'email',
'meta'
]
Override the update(...) method of the serializer
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = [
'id',
'url',
'first_name',
'last_name',
'email',
'meta'
]
def update(self, instance, validated_data):
instance_meta = instance.meta.copy()
instance_meta.update(validated_data.get("meta", {}))
validated_data["meta"] = instance_meta
return super().update(instance, validated_data)
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
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)
I have a custom user model, and have its model seriazer to with. Its modelviewset also works in displaying the user list.
class UserSerializer(serializers.ModelSerializer):
password1 = serializers.CharField(write_only=True, style={'input_type': 'password'})
password2 = serializers.CharField(write_only=True, style={'input_type': 'password'})
mobile = serializers.IntegerField()
class Meta:
model = User
fields = ('id', 'first_name', 'last_name', 'email', 'mobile', 'password1', 'password2')
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError({'password1': 'Both password must be equal.'})
return data
def create(self, validated_data):
if self.is_valid(True):
return User.objects.create_user(
validated_data.get('email'),
validated_data.get('first_name'),
validated_data.get('last_name'),
validated_data.get('mobile'),
validated_data.get('password1'),
)
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
However, I am trying to update a User object by posting the model's fields and its value in json key value pair format in PUT http method.
{
"first_name": "Some"
"last_person": "name"
"mobile": "0123456789"
}
But this gives me a 400 bad request error:
{
"password1": [
"This field is required."
],
"password2": [
"This field is required."
],
"email": [
"This field is required."
]
}
How can I update an object with only the partial model fields?
How can I update an object with only the partial model fields?
That's what PATCH is for as opposed to PUT.
It'll pass the partial flag to the serializer will will bypass missing field's validation.
Serializer is considering this fields as obligatory, probably you are putting in your custom model null = False.