Как (де)сериализовать плавающий список в модели с помощью сериализатора моделей Django REST Framework?
TL; DR
Как создать serializers.Patient
и serializers.Temperature
таким образом, чтобы:
models.Patient
has one-to-many relationship withmodels.Temperatures
serializers.Patient
is a subclass ofserializers.ModelSerializer
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
:
Вот как будет возвращаться температура пациента:
Надеюсь, это ответит на ваши вопросы.