In one of my APIs, I am able to get the DRF extra field, PointField working.
In one of the other API where I am using PointField in a nested serializer, it is giving me a validation error.
{
"booking_address": {
"coordinates": [
"Enter a valid location."
]
}
}
And the payload data is
{
"booking_address": {
"coordinates" : {
"latitude": 49.87,
"longitude": 24.45
},
"address_text": "A123"
}
}
My serializers are below:
BookingSerializer
class BookingSerializer(FlexFieldsModelSerializer):
booked_services = BookedServiceSerializer(many=True)
booking_address = BookingAddressSerializer(required=False)
------
def validate_booking_address(self, address):
if address.get("id"):
address = BookingAddress.objects.get(id=address.get("id"))
else:
address["customer"] = self.context.get("request").user.id
serializer = BookingAddressSerializer(data=address)
if serializer.is_valid(): <---- error is coming from here
address = serializer.save()
else:
raise ValidationError(serializer.errors)
return address
My Address Serializer is defined as:
class BookingAddressSerializer(FlexFieldsModelSerializer):
coordinates = geo_fields.PointField(srid=4326)
customer = serializers.IntegerField(required=False)
And booking model is:
class BookingAddress(BaseTimeStampedModel):
customer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="booking_addresses", on_delete=models.CASCADE)
coordinates = models.PointField()
address_text = models.CharField(max_length=256)
Tried Debugging, been stuck here for a few hours now and not able to find the issue.
Any help will be appreciated.
Well, the problem is that to_internal_value() is called with a correct Pointfield, and because geofields.PointField only handles strings or dicts it fails.
Here's the code I used to reproduce the problem (trimmed down, with imports):
# models.py
from __future__ import annotations
import typing as t
from django.contrib.gis.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
if t.TYPE_CHECKING:
from django.contrib.auth.models import User
class BaseTimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class BookingAddress(BaseTimeStampedModel):
customer = models.ForeignKey(
User, related_name="booking_addresses", on_delete=models.CASCADE,
)
coordinates = models.PointField(geography=True, srid=4326)
address_text = models.CharField(max_length=256)
class Booking(BaseTimeStampedModel):
service = models.CharField(max_length=20)
address = models.ForeignKey(
BookingAddress, on_delete=models.CASCADE, related_name="booking"
)
# serializers.py
import json
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from drf_extra_fields import geo_fields
from .models import BookingAddress, Booking
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos.error import GEOSException
from django.contrib.gis.geos.point import Point
EMPTY_VALUES = (None, "", [], (), {})
class PointField(geo_fields.PointField):
default_error_messages = {
"invalid": "Enter a valid location.",
"json": "Invalid json",
"unknown": "Unknown cause",
"wrong_type": "Expected string or dict",
}
def to_internal_value(self, value):
if value in EMPTY_VALUES and not self.required:
return None
if isinstance(value, str):
try:
value = value.replace("'", '"')
value = json.loads(value)
except ValueError:
self.fail("json")
print(type(value))
if value and isinstance(value, dict):
try:
latitude = value.get("latitude")
longitude = value.get("longitude")
return GEOSGeometry(
"POINT(%(longitude)s %(latitude)s)"
% {"longitude": longitude, "latitude": latitude},
srid=self.srid,
)
except (GEOSException, ValueError) as e:
msg = e.args[0] if len(e.args) else "Unknown"
self.fail(f"unknown", msg=msg)
if isinstance(value, Point):
raise TypeError("Point received")
self.fail(f"wrong_type")
class BookingAddressSerializer(serializers.ModelSerializer):
coordinates = PointField(srid=4326)
class Meta:
model = BookingAddress
fields = ("coordinates", "customer_id")
class BookingSerializer(serializers.ModelSerializer):
booking_address = BookingAddressSerializer(required=False)
def validate_booking_address(self, address):
if address.get("id"):
address = BookingAddress.objects.get("id")
else:
address["customer"] = self.context.get("request").user.id
serializer = BookingAddressSerializer(data=address)
if serializer.is_valid():
address = serializer.save()
else:
raise ValidationError(serializer.errors)
return address
class Meta:
model = Booking
fields = ("service", "created_at", "last_modified", "booking_address")
read_only_fields = ("created_at", "last_modified")
# tests.py
import json
from django.contrib.auth import get_user_model
from django.test import TestCase
from .serializers import BookingSerializer
User = get_user_model()
class DummyRequest:
user = None
class BookingSerializerTest(TestCase):
payload = json.dumps(
{
"service": "Dry cleaning",
"booking_address": {
"coordinates": {"longitude": 24.45, "latitude": 49.87},
"address_text": "123 Main Street",
},
}
)
def test_serializer(self):
user = User.objects.create_user(username="test_user", password="pass123456")
request = DummyRequest()
request.user = user
serializer = BookingSerializer(
data=json.loads(self.payload), context={"request": request}
)
self.assertTrue(serializer.is_valid(raise_exception=True))
If you run the test you see in the output:
<class 'dict'>
<class 'django.contrib.gis.geos.point.Point'>
And the 2nd time is where it fails, because it cannot handle a Point. This is caused by you overstepping your boundaries in validate_booking_address(). This causes to_internal_value to be called twice, the 2nd time with the result of the previous.
You're trying to handle the entire convert > validate > save operation there and the method should only do the validate step. This means, check to see if the data matches the expected input.
A nested field should validate itself and be able to create itself. If you need request.user to create a valid model you should override create() as explained by the documentation:
If you're supporting writable nested representations you'll need to write .create() or .update() methods that handle saving multiple objects.
Related
New to Django and DRF, I have a method in the model properties which accept arguments. I have managed to call it successful though a serializer class with default paramenters and getting a JSON response. My problem is I can't pass argument to that function named balance. I have successful pass my argument from view to serializer class but from serializer to model that where I have failed. I thought will be appreciated.
model.py
class Item(models.Model):
entered_by = models.ForeignKey(User, on_delete=models.PROTECT)
name = models.CharField(max_length=50, blank=True)
#property
def balance(self, stock_type='Retail'):
stock = Stock.objects.filter(item=self, type=stock_type, status='InStock').aggregate(models.Sum('quantity')).get('quantity__sum')
return stock or 0
views.py
def getItemInfo(request):
if request.is_ajax and request.method == "GET":
id = request.GET.get("item_id", None)
sell_type = request.GET.get("sell_type", None)
item = Item.objects.get(id=id)
if item:
serializer = ItemSerializer(item, context={'sell_type':sell_type})
return JsonResponse(serializer.data, status = 200, safe=False)
else:
return JsonResponse({"data":False}, status = 400)
serializer.py
from rest_framework import serializers
from .models import Item
class ItemSerializer(serializers.ModelSerializer):
balance = serializers.SerializerMethodField()
class Meta:
model = Item
fields = ('entered_by', 'name', 'balance', 'sell_mode')
def get_balance(self, object):
sell_type = self.context.get("sell_type")
if sell_type:
return object.balance(sell_type)
return object.balance
The error I'm getting
'int' object is not callable
#property couldn't be called. So I made member variable and setter (with calculation) methods in Item model, then make sure setter method will be called in get_balance serializer method, just before returning balance.
Django ORM model itself is just a class; you can do anything class could, not just only linking with ORM.
My Code:
models.py
from django.db import models
from django.contrib.auth.models import User
class Item(models.Model):
entered_by = models.ForeignKey(User, on_delete=models.PROTECT)
name = models.CharField(max_length=50, blank=True)
_balance = 0
def calculate_balance(self, stock_type='Retail'):
stock = Stock.objects.filter(item=self, type=stock_type, status='InStock').aggregate(
models.Sum('quantity')).get('quantity__sum')
self._balance = stock or 0
#property
def balance(self):
return self._balance
serializer.py
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import Item
class ItemSerializer(serializers.ModelSerializer):
balance = serializers.SerializerMethodField()
class Meta:
model = Item
fields = ('entered_by', 'name', 'balance')
def get_balance(self, object):
sell_type = self.context.get("sell_type")
if sell_type:
object.calculate_balance(sell_type)
return object.balance
views.py
from .models import Item
from .serializer import ItemSerializer
from django.http.response import JsonResponse
def getItemInfo(request):
if request.is_ajax and request.method == "GET":
id = request.GET.get("item_id", None)
if id is None:
return JsonResponse({"data": False}, status=400)
sell_type = request.GET.get("sell_type", None)
try:
item = Item.objects.get(id=id)
serializer = ItemSerializer(item, context={'sell_type': sell_type})
return JsonResponse(serializer.data, status=200, safe=False)
except Item.DoesNotExist:
return JsonResponse({"data": False}, status=400)
I have the following models:
from django.db import models
MNL = 50
MCL = 5
class Continent(models.Model):
"""
Fields
"""
name = models.CharField("name", max_length=MNL, unique=True)
code = models.CharField("code", max_length=MCL, default="", unique=True)
class Meta:
ordering = ['name']
"""
Methods
"""
def __str__(self):
return "%s, %s" % (self.name, self.code)
class Country(models.Model):
"""
Fields
"""
name = models.CharField("name", max_length=MNL, unique=True)
capital = models.CharField("capital", max_length=MNL)
code = models.CharField("code", max_length=MCL, default="", unique=True)
population = models.PositiveIntegerField("population")
area = models.PositiveIntegerField("area")
continent = models.ForeignKey(Continent, on_delete=models.CASCADE,
related_name="countries")
class Meta:
ordering = ['name']
"""
Methods
"""
def __str__(self):
return "%s, %s" % (self.name, self.code)
I need to be able to retrieve 2 things in JSON(P):
individual Country's capital, population and area fields in the form {"area":<area>,"population":<population>,"capital":<capital_name>} and
in the case of a Continent, all of the countries in that continent in the form {"code1":"name1", "code2":"name2",...}
I've tried implementing the following views to achieve this:
from django.http import HttpResponse, Http404, JsonResponse
from django.forms.models import model_to_dict
import json
from .models import Continent, Country
def continent_json(request, continent_code):
""" Write your answer in 7.2 here. """
try:
print("CONTINENT QuerySet: ", Continent.objects.filter(
code__exact=continent_code).values("countries"))
continent_data = json.dumps( list(Continent.objects.filter(
code__exact=continent_code).values("countries") ) )
print("CONTINENT JSON: ",continent_data)
except Continent.DoesNotExist:
raise Http404("Requested continent does not exist.")
# If JSONP
if "callback" in request.GET:
continent_data = "{}({})".format(
request.GET["callback"],
continent_data
)
return HttpResponse(continent_data)
# Normal JSON
return HttpResponse(continent_data, content_type="application/json")
def country_json(request, continent_code, country_code):
""" Write your answer in 7.2 here. """
try:
#print("COUNTRY_OBJECT: "Country.objects.filter(code__exact=country_code).values())
print("MODEL_LIST: ",list(Country.objects.filter(code__exact=country_code).values("capital","population","area")))
country_data = json.dumps( list(Country.objects.filter(
code__exact=country_code).values("code","name") ) )
print("COUNTRY DATA: ", country_data)
except Country.DoesNotExist:
raise Http404("Requested country does not exist.")
# If JSONP
if "callback" in request.GET:
country_data = "{}({})".format(
request.GET["callback"],
country_data
)
return HttpResponse(country_data)
# Normal JSON
return HttpResponse(country_data, content_type="application/json")
However, this is not producing the results I want: the data is not actually coming back as JSON(P), but as either a dict or a list. This is a lot of code to shift through, but I'm at my wits end here.
What am I doing wrong?
From what you explained on chat:
You need to change your views to something like this
country_data = json.dumps(dict(Country.objects.filter(
code__exact=country_code).values("code","name")[0])))
and for continent view:
continent = Continent.objects.get(code__exact=continent_code)
country_data = json.dumps(dict(continent.countries.values_list('code', 'name')))
My models are:
class User(models.Model):
id = models.AutoField(primary_key=True)
email = models.EmailField()
class Lawyer(models.Model):
user = models.OnetoOneField(User)
class Session(models.Model):
name = models.CharField()
lawyer = models.ForeignKey(Lawyer)
I am trying to create multiple objects with a list serializer for Session.
From the app side they don't have the id of lawyer, but have the email of each lawyer. How can I write a list serializer where I can take the following json input and use email to fetch lawyer to store multiple session objects?
The json input sent will be like:
[
{
"name": "sess1",
"email": "lawyer1#gmail.com",
},
{
"name": "sess1",
"email": "lawyer1#gmail.com",
}
]
You can do it in this way but I think email should be unique=True.
Then use a serializer like this:
from django.utils.translation import ugettext_lazy as _
class SessionCreateManySerializer(serializers.ModelSerializer):
email = serializers.SlugRelatedField(
source='lawyer',
slug_field='user__email',
queryset=Lawyer.objects.all(),
write_only=True,
error_messages={"does_not_exist": _('lawyer with email={value} does not exist.')}
)
class Meta:
model = Session
fields = ('name', 'email')
and a generic create view (just override get_serializer and place many=True in kwargs ):
from rest_framework.response import Response
from rest_framework import generics
class SessionCreateManyView(generics.CreateAPIView):
serializer_class = SessionCreateManySerializer
def get_serializer(self, *args, **kwargs):
kwargs['many'] = True
return super(SessionCreateManyView, self).get_serializer(*args, **kwargs)
You can use bulk creation as well:
# serializers.py
from myapp.models import Session
from rest_framework import serializers
class SessionSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
name = serializers.CharField(required=True)
def validate_email(self, email):
lawyer = Lawyer.objects.filter(user__email=email).first()
if not lawyer:
raise ValidationError(detail="user dose not exist.")
return lawyer
def create(self, validated_data):
return Session.objects.create(name=validated_data.get('name'), lawyer=validated_data.get('email'))
and in your api.py file allow bulk creation:
# api.py
from rest_framework import generics
class SessionCreateAPIView(generics.CreateAPIView):
"""Allows bulk creation of a resource."""
def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
Here is my MSV:
models.py
import mongoengine
class PersonAddressModel(mongoengine.DynamicEmbeddedDocument):
country = mongoengine.fields.StringField()
town = mongoengine.fields.StringField()
class PersonModel(mongoengine.DynamicDocument):
name = mongoengine.fields.StringField()
age = mongoengine.IntField()
is_married = mongoengine.fields.BooleanField()
address = EmbeddedDocumentListField(PersonAddressModel)
serializers.py
from rest_framework_mongoengine import serializers
from .models import PersonInfoModel, PersonAddressModel
import mongoengine
class PersonAddressSerializer(serializers.EmbeddedDocumentSerializer):
class Meta:
model = PersonAddressModel
fields = '__all__'
class PersonSerializer(serializers.DynamicDocumentSerializer):
class Meta:
model = PersonModel
fields = '__all__'
views.py
from rest_framework_mongoengine import viewsets
from .serializers import PersonSerializer, PersonAddressSerializer
from rest_framework.response import Response
from rest_framework import status
import djongo
from .models import PersonModel
class PersonView(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = PersonSerializer
def create(self, request):
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid()
serializer.save()
except djongo.sql2mongo.SQLDecodeError:
return Response(
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
return Response(
status=status.HTTP_201_CREATED
)
I'm sending the following json object:
{
"name": "Helmut",
"age": 21,
"is_married": true,
"address": [{"country": "Germany", "town": "Berlin", "street": "Wolfstraße 1"}]
}
However when I check the database I see the following object:
{
"_id" : ObjectId("5b5e201c540d1c3b7a4491e8"),
"name" : "Helmut",
"age" : 21,
"is_married" : true,
"address" : [
{
"country" : "Germany",
"town" : "Berlin"
}
]
}
That's to say, Helmut's street is missing. I wonder why! My bet is that it has something to do with the serializers but I can't figure out what it is exactly.
EDIT
I want both my documents to be dynamic because some jsons may contain fields with unpredictable names and values and I have to save them as well, so I make my both model classes dynamic. However I can expand the PersonInfoModel but can't do the same with the PersonAddressModel. The street field won't show up in the DB (MongoDB).
On the other hand, I want some of the fields present in the address list to be required.
In your PersonAddressModel you defined only two fields, which are country and town. Which means, in DB, you defined the schema with those two fields, street is not included.
So, change your models to add extra field, as
class PersonAddressModel(mongoengine.DynamicEmbeddedDocument):
country = mongoengine.fields.StringField()
town = mongoengine.fields.StringField()
street = mongoengine.fields.StringField()
UPDATE
If your address field is dynamic and it's a dict like field, then use mongoengine.fields.DictField() as,
import mongoengine
class PersonModel(mongoengine.DynamicDocument):
name = mongoengine.fields.StringField()
age = mongoengine.IntField()
is_married = mongoengine.fields.BooleanField()
address = mongoengine.fields.DictField()
Update-2
You can do a field level validation in PersonSerializer as,
from rest_framework_mongoengine import serializers
from rest_framework import serializers as drf_serializer
class PersonSerializer(serializers.DynamicDocumentSerializer):
def validate_address(self, address):
required_fields = ['street', 'country']
if not all([True for field in required_fields if field in address]):
raise drf_serializer.ValidationError('some required fields are missing')
return address
class Meta:
model = PersonModel
fields = '__all__'
Since rest_framework_mongoengine doesnt have a ValidationError class, we using DRF's ValidationError class
UPDATE-3
Inorder to raise validation error, you have to pass True to .is_valid() (reff doc - Raising an exception on invalid data)method, as
class PersonView(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = PersonSerializer
def create(self, request):
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid(True) # Change is here <<<<
serializer.save()
except djongo.sql2mongo.SQLDecodeError:
return Response(
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
return Response(
status=status.HTTP_201_CREATED
)
All other data is saved ideally but as shown below, the user id part shows as a pull down bar and a null value which should be a signed-in username.
What's wrong with my code?
The database page
Here's my code.
views.py
from .models import Markers
from .forms import AddMarkersInfo
from django.http import HttpResponse
def addinfo(request):
if request.method == 'POST':
mks = AddMarkersInfo(request.POST)
if mks.is_valid():
submit = mks.save(commit=False)
submit.user = request.user
submit.save()
name = mks.cleaned_data['name']
address = mks.cleaned_data['address']
description = mks.cleaned_data['description']
type = mks.cleaned_data['type']
lat = mks.cleaned_data['lat']
lng = mks.cleaned_data['lng']
Markers.objects.get_or_create(name=name, address=address, description=description, type=type, lat=lat, lng=lng)
return render(request, 'home.html', {'mks': mks })
else:
mks = AddMarkersInfo()
return render(request, 'home.html', {'mks': mks})
models.py
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from django.contrib.auth import get_user_model
def get_sentinel_user():
return get_user_model().objects.get_or_create(username='deleted')[0]
class Markers(models.Model):
User = settings.AUTH_USER_MODEL
use_id= models.ForeignKey(User, null=True, on_delete=models.SET(get_sentinel_user),)
name = models.CharField(max_length=60,default = 'name')
address = models.CharField(max_length=100,default = 'address')
description = models.CharField(max_length=150, default='description')
types = (
('m', 'museum'),
('s', 'school'),
('r', 'restaurant'),
('o', 'other'),
)
type = models.CharField(max_length=60, choices=types, default='museum')
lat = models.IntegerField()
lng = models.IntegerField()
forms.py
from django import forms
from maps.models import Markers
class AddMarkersInfo(forms.ModelForm):
class Meta:
model = Markers
fields = ['name','address','description', 'type','lat','lng',]
Well, first of all, you should remove the lines from django.contrib.auth.models import User and User = settings.AUTH_USER_MODEL in models.py if you are going to use settings.AUTH_USER_MODEL. You should use only one of the two.
And you can change your field to:
use_id= models.ForeignKey(settings.AUTH_USER_MODEL, ...
Secondly, it seems like you are duplicating the creation. The lines
submit = mks.save(commit=False)
submit.user = request.user
submit.save()
already create an Markers instance, so there is no need to call Markers.objects.get_or_create(... after that.
And, according to you models, the field should be submit.use_id instead of submit.user.
Now, if I understand your question correctly you want to make the use_id field read-only in your form/template.
I don't know why that field is even showing up in your form, since it is not listed in your forms Meta.fields.
You could try something like setting the widget attribute readonly:
class AddMarkersInfo(forms.ModelForm):
class Meta:
model = Markers
fields = ['use_id', 'name', 'address', 'description', 'type', 'lat', 'lng']
widgets = {
'use_id': forms.Textarea(attrs={'readonly': 'readonly'}),
}