Ошибка проверки с помощью DRF PointField — «Введите действительное местоположение»

#django #django-rest-framework #geodjango

#django #django-rest-framework #geodjango

Вопрос:

В одном из моих API я могу заставить дополнительное поле DRF, PointField работать. В одном из других API, где я использую PointField во вложенном сериализаторе, он выдает мне ошибку проверки.

 {
"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)
 

Пробовал отладку, застрял здесь на несколько часов и не смог найти проблему.

Любая помощь будет оценена.

Ответ №1:

Ну, проблема в том, что 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'>
 

И во 2-й раз происходит сбой, потому что он не может обработать точку. Это вызвано тем, что вы переступаете свои границы в validate_booking_address(). Это приводит to_internal_value к повторному вызову, 2-й раз с результатом предыдущего.

Вы пытаетесь обработать всю convert > validate > save операцию там, и метод должен выполнять только этот validate шаг. Это означает, что проверьте, соответствуют ли данные ожидаемому вводу.

Вложенное поле должно проверять себя и иметь возможность создавать себя. Если вам нужен request.user для создания действительной модели, вы должны переопределить create() , как описано в документации:

Если вы поддерживаете доступные для записи вложенные представления, вам нужно будет написать методы .create() или .update(), которые обрабатывают сохранение нескольких объектов.

Комментарии:

1. Спасибо, что приложили столько усилий, чтобы ответить на этот вопрос. Теперь я понимаю, что вызывает ошибку, я попробую сейчас и исправлю ее на основе ваших предложений. Что касается того, что вы сказали относительно того, что я создаю объект при проверке, я хотел эмулировать get_or_create . Если я получу идентификатор, я повторно использую адрес, если нет, я создам новый. Позвольте мне посмотреть, как с этим можно справиться.