Как (де)сериализовать плавающий список в модели с помощью сериализатора моделей Django REST Framework?

TL; DR

Как создать serializers.Patient и serializers.Temperature таким образом, чтобы:

  1. models.Patient has one-to-many relationship with models.Temperatures
  2. serializers.Patient is a subclass of serializers.ModelSerializer
  3. serializers.Patient (de)serialize temperatures as a list of floats

Детали

Предлагается quick-dirty medical records пациента RESTful API, реализованный с помощью Django framework.

Пациент определяется в models.Patient как:

class Patient(models.Model):
    created_at = models.DateField()
    name = models.CharField(max_length=200)
    updated_at = models.DateField()

и models.Temperature:

class Temperature(models.Model):
    created_at = models.DateField()
    patient = models.ForeignKey(
        Patient,
        db_column='patient',
        related_name='temperatures',
        on_delete=models.CASCADE,
    )
    updated_at = models.DateField()

CRUD операции на /patients (де)сериализованы models.Temperature как float списки, таким образом POST должны требовать только:

{
  "name": "John Connor",
  "temperatures": [36.7, 40, 35.9]
}

тем временем операция GET

{
  "created_at": "1985-03-25",
  "name": "John Connor",
  "temperatures": [36.7, 40, 35.9],
  "updated_at": "2021-08-29"
}

Однако операции в /patients/<id>/temperatures/ конечной точке должны возвращать все свойства:

[
  {
    "created_at": "1985-03-25",
    "value": 36.7,
    "updated_at": "2021-08-29"
  },
  {
    "created_at": "1985-03-25",
    "value": 40.0,
    "updated_at": "2021-08-29"
  },
  {
    "created_at": "1985-03-25",
    "value": 35.9,
    "updated_at": "2021-08-29"
  }
]

Можно ли реализовать эту возможность, используя подклассы стандартных DRF сериализаторов, или для этого требуется специализированный serializers.Serializer подкласс?

Попробуем проанализировать сложившуюся ситуацию. Нам нужно ввести данные о температуре с помощью списка. Поэтому воспользуемся для этой цели ListField. Итак, мы должны начать следующим образом:

class PatientSerializer(ModelSerializer):
    temperatures = ListField(child=FloatField())

Что это будет делать, так это ожидать от пользователя список плавающих значений. Но как мы получим массив температур созданного пациента при использовании операции GET. Для этого добавим в поле источник. Все, что будет записано в параметре source, будет преобразовано в patient.field, например:

class PatientSerializer(ModelSerializer):
    temperatures = ListField(child=FloatField(), source="temperature_list")

Это source="temperature_list будет пользователем patient.temperature_list и покажет результат при сериализации объекта. Теперь добавим некоторое свойство к исходной модели, чтобы получить чистый массив температур следующим образом:

class Patient(models.Model):
    created_at = models.DateField()
    name = models.CharField(max_length=200)
    updated_at = models.DateField()

    @property
    def temperature_list(self):
        return [temperature.value for temperature in self.temperatures.all()]

Мы почти закончили. Теперь нам нужно переопределить метод create для PatientSerializer, чтобы он мог сохранять температуры, присутствующие в списке. Теперь, чтобы получить данные о температуре из validated_data, нам нужно использовать то же поле, которое используется в аргументе source на ListField, то есть temperature_list. Итак, нам нужно извлечь эти данные, создать пользователя, подтвердить элементы списка температур, создать объекты температуры, связанные с пациентом. Давайте создадим TemperatureSerializer следующим образом:

class TemperatureSerializer(ModelSerializer):
    class Meta:
        model = Temperature
        fields = ("created_at", "value", "updated_at", "patient")
        extra_kwargs = {
            "patient": {"write_only": True},
            "created_at": {"required": False},
            "updated_at": {"required": False},
        }

Теперь нам осталось написать метод create. Мы можем написать его следующим образом:

def create(self, validated_data):
    temperatures = validated_data.pop("temperature_list")
    now = datetime.now().strftime("%Y-%m-%d")
    patient = Patient.objects.create(
        created_at=now, updated_at=now, **validated_data
    )
    for temperature in temperatures:
        now = datetime.now().strftime("%Y-%m-%d")
        temperature_serializer = TemperatureSerializer(
            data={
                "value": temperature,
                "created_at": now,
                "updated_at": now,
                "patient": patient.id,
            }
        )
        temperature_serializer.is_valid(raise_exception=True)
        temperature_serializer.save()
    return patient

Наконец, давайте соберем все вместе, и вот как будет выглядеть файл serializers.py:

from rest_framework.serializers import ModelSerializer, ListField, FloatField
from datetime import datetime

from .models import Patient, Temperature


class TemperatureSerializer(ModelSerializer):
    class Meta:
        model = Temperature
        fields = ("created_at", "value", "updated_at", "patient")
        extra_kwargs = {
            "patient": {"write_only": True},
            "created_at": {"required": False},
            "updated_at": {"required": False},
        }


class PatientSerializer(ModelSerializer):
    temperatures = ListField(child=FloatField(), source="temperature_list")

    def create(self, validated_data):
        temperatures = validated_data.pop("temperature_list")
        now = datetime.now().strftime("%Y-%m-%d")
        patient = Patient.objects.create(
            created_at=now, updated_at=now, **validated_data
        )
        for temperature in temperatures:
            now = datetime.now().strftime("%Y-%m-%d")
            temperature_serializer = TemperatureSerializer(
                data={
                    "value": temperature,
                    "created_at": now,
                    "updated_at": now,
                    "patient": patient.id,
                }
            )
            temperature_serializer.is_valid(raise_exception=True)
            temperature_serializer.save()
        return patient

    class Meta:
        model = Patient
        fields = ("created_at", "name", "temperatures", "updated_at")
        extra_kwargs = {
            "created_at": {"required": False},
            "updated_at": {"required": False},
        }

Вот как будет выглядеть views.py:

from rest_framework.generics import ListCreateAPIView, ListAPIView

from .models import Patient, Temperature
from .serializers import PatientSerializer, TemperatureSerializer

class PatientListCreateView(ListCreateAPIView):
    serializer_class = PatientSerializer
    queryset = Patient.objects.all()


class TemperatureListView(ListAPIView):
    serializer_class = TemperatureSerializer

    def get_queryset(self, *args, **kwargs):
        patient_id = self.kwargs["pk"]
        return Temperature.objects.filter(patient_id=patient_id)

И файл urls.py:

from django.urls import path

from . import views

urlpatterns = [
    path("patients", views.PatientListCreateView.as_view(), name="patient-list"),
    path(
        "patients/<int:pk>/temperatures/",
        views.TemperatureListView.as_view(),
        name="patient-temperature-list",
    ),
]

Вот как будет работать метод POST: enter image description here

Вот как будет возвращаться температура пациента: enter image description here

Надеюсь, это ответит на ваши вопросы.

Вернуться на верх