DRF PointField 的验证错误 - "Enter a valid location"
Validation error with DRF PointField - "Enter a valid location"
在我的 API 之一中,我能够让 DRF extra field, PointField 工作。
在我在嵌套序列化程序中使用 PointField 的另一个 API 中,它给我一个验证错误。
{
"booking_address": {
"coordinates": [
"Enter a valid location."
]
}
}
有效载荷数据是
{
"booking_address": {
"coordinates" : {
"latitude": 49.87,
"longitude": 24.45
},
"address_text": "A123"
}
}
我的序列化程序如下:
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
我的地址序列化程序定义为:
class BookingAddressSerializer(FlexFieldsModelSerializer):
coordinates = geo_fields.PointField(srid=4326)
customer = serializers.IntegerField(required=False)
预订模式为:
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)
试过调试,现在在这里卡了几个小时,没能找到问题所在。
我们将不胜感激。
嗯,问题是 to_internal_value()
是用正确的 Pointfield 调用的,因为 geofields.PointField 只处理字符串或指令,所以失败了。
这是我用来重现该问题的代码(经过精简,带有导入):
# 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))
如果您 运行 您在输出中看到的测试:
<class 'dict'>
<class 'django.contrib.gis.geos.point.Point'>
而第二次失败了,因为它不能处理一个点。这是你越界造成的validate_booking_address()。这导致 to_internal_value
被调用两次,第二次使用前一次的结果。
您正在尝试在那里处理整个 convert > validate > save
操作,该方法应该只执行 validate
步骤。这意味着,检查数据是否与预期输入匹配。
嵌套字段应该自我验证并能够自行创建。如果您需要 request.user 来创建有效模型,您应该按照文档中的说明覆盖 create()
:
If you're supporting writable nested representations you'll need to write .create() or .update() methods that handle saving multiple objects.
在我的 API 之一中,我能够让 DRF extra field, PointField 工作。 在我在嵌套序列化程序中使用 PointField 的另一个 API 中,它给我一个验证错误。
{
"booking_address": {
"coordinates": [
"Enter a valid location."
]
}
}
有效载荷数据是
{
"booking_address": {
"coordinates" : {
"latitude": 49.87,
"longitude": 24.45
},
"address_text": "A123"
}
}
我的序列化程序如下: 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
我的地址序列化程序定义为:
class BookingAddressSerializer(FlexFieldsModelSerializer):
coordinates = geo_fields.PointField(srid=4326)
customer = serializers.IntegerField(required=False)
预订模式为:
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)
试过调试,现在在这里卡了几个小时,没能找到问题所在。
我们将不胜感激。
嗯,问题是 to_internal_value()
是用正确的 Pointfield 调用的,因为 geofields.PointField 只处理字符串或指令,所以失败了。
这是我用来重现该问题的代码(经过精简,带有导入):
# 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))
如果您 运行 您在输出中看到的测试:
<class 'dict'>
<class 'django.contrib.gis.geos.point.Point'>
而第二次失败了,因为它不能处理一个点。这是你越界造成的validate_booking_address()。这导致 to_internal_value
被调用两次,第二次使用前一次的结果。
您正在尝试在那里处理整个 convert > validate > save
操作,该方法应该只执行 validate
步骤。这意味着,检查数据是否与预期输入匹配。
嵌套字段应该自我验证并能够自行创建。如果您需要 request.user 来创建有效模型,您应该按照文档中的说明覆盖 create()
:
If you're supporting writable nested representations you'll need to write .create() or .update() methods that handle saving multiple objects.