From a77becf57986cf36aa3e492a6c2357ebaaf4c01e Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 12:45:33 +0200 Subject: [PATCH 1/9] renamed FollowUp follow_up_date to date --- .../migrations/0015_auto_20220222_1043.py | 29 +++++++++++++++++++ tmh_registry/registry/models.py | 4 +-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tmh_registry/registry/migrations/0015_auto_20220222_1043.py diff --git a/tmh_registry/registry/migrations/0015_auto_20220222_1043.py b/tmh_registry/registry/migrations/0015_auto_20220222_1043.py new file mode 100644 index 0000000..dbaa5c9 --- /dev/null +++ b/tmh_registry/registry/migrations/0015_auto_20220222_1043.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.3 on 2022-02-22 10:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0014_remove_episode_discharge_date"), + ] + + operations = [ + migrations.RenameField( + model_name="followup", + old_name="follow_up_date", + new_name="date", + ), + migrations.AddField( + model_name="followup", + name="created_at", + field=models.DateField(auto_now_add=True, default=1998), + preserve_default=False, + ), + migrations.AddField( + model_name="followup", + name="updated_at", + field=models.DateField(auto_now=True), + ), + ] diff --git a/tmh_registry/registry/models.py b/tmh_registry/registry/models.py index 74c7f9c..1e9f716 100644 --- a/tmh_registry/registry/models.py +++ b/tmh_registry/registry/models.py @@ -165,7 +165,7 @@ class Meta: verbose_name_plural = "Discharges" -class FollowUp(Model): +class FollowUp(TimeStampMixin): class PainSeverityChoices(TextChoices): NO_PAIN = ("NO_PAIN", "No Pain") MINIMAL = ("MINIMAL", "Minimal") @@ -174,7 +174,7 @@ class PainSeverityChoices(TextChoices): SEVERE = ("SEVERE", "Severe") episode = ForeignKey(Episode, on_delete=CASCADE) - follow_up_date = DateField() + date = DateField() pain_severity = CharField( max_length=16, choices=PainSeverityChoices.choices ) From 6d586a805ee76942d04c8211dd89e8957e3ce6d2 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 12:49:47 +0200 Subject: [PATCH 2/9] added follow up factory --- tmh_registry/registry/factories.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tmh_registry/registry/factories.py b/tmh_registry/registry/factories.py index 405ab10..deca219 100644 --- a/tmh_registry/registry/factories.py +++ b/tmh_registry/registry/factories.py @@ -8,6 +8,7 @@ from .models import ( Discharge, Episode, + FollowUp, Hospital, Patient, PatientHospitalMapping, @@ -108,3 +109,30 @@ class Meta: date = LazyAttribute(lambda _: faker.date_object()) aware_of_mesh = LazyAttribute(lambda _: faker.boolean()) infection = LazyAttribute(lambda _: faker.boolean()) + + +class FollowUpFactory(DjangoModelFactory): + class Meta: + model = FollowUp + + episode = SubFactory(EpisodeFactory) + date = LazyAttribute(lambda _: faker.date_object()) + pain_severity = LazyAttribute( + lambda _: faker.random_element(FollowUp.PainSeverityChoices.values) + ) + mesh_awareness = LazyAttribute(lambda _: faker.boolean()) + seroma = LazyAttribute(lambda _: faker.boolean()) + infection = LazyAttribute(lambda _: faker.boolean()) + numbness = LazyAttribute(lambda _: faker.boolean()) + + @post_generation + def attendees(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + self.attendees.add(*extracted) + else: + self.attendees.add(MedicalPersonnelFactory()) From 58732020f5b79d8f5b205b811fe56abbeecd6490 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 12:51:06 +0200 Subject: [PATCH 3/9] renamed test file --- .../tests/api/viewsets/{test_discharge.py => test_discharges.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tmh_registry/registry/tests/api/viewsets/{test_discharge.py => test_discharges.py} (100%) diff --git a/tmh_registry/registry/tests/api/viewsets/test_discharge.py b/tmh_registry/registry/tests/api/viewsets/test_discharges.py similarity index 100% rename from tmh_registry/registry/tests/api/viewsets/test_discharge.py rename to tmh_registry/registry/tests/api/viewsets/test_discharges.py From 7805c447d1b50761ca68e60aabf1c89b39e7a875 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 13:11:21 +0200 Subject: [PATCH 4/9] added FollowUp serializers --- tmh_registry/registry/api/serializers.py | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index bd293d3..db3c621 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -9,6 +9,7 @@ from ..models import ( Discharge, Episode, + FollowUp, Hospital, Patient, PatientHospitalMapping, @@ -396,3 +397,79 @@ def create(self, validated_data): ) return discharge + + +class FollowUpReadSerializer(ModelSerializer): + episode = EpisodeReadSerializer() + + class Meta: + model = FollowUp + fields = [ + "id", + "episode", + "date", + "pain_severity", + "attendees", + "mesh_awareness", + "seroma", + "infection", + "numbness", + ] + + +class FollowUpWriteSerializer(ModelSerializer): + episode_id = PrimaryKeyRelatedField( + write_only=True, queryset=Episode.objects.exclude(discharge=None) + ) + attendee_ids = PrimaryKeyRelatedField( + write_only=True, many=True, queryset=MedicalPersonnel.objects.all() + ) + + class Meta: + model = FollowUp + fields = [ + "episode_id", + "date", + "pain_severity", + "attendee_ids", + "mesh_awareness", + "seroma", + "infection", + "numbness", + ] + + def to_representation(self, instance): + serializer = FollowUpReadSerializer(instance) + return serializer.data + + def create(self, validated_data): + episode = validated_data["episode_id"] + attendees = validated_data["attendee_ids"] + + if episode.surgery_date > validated_data["date"]: + raise ValidationError( + { + "error": "Episode surgery date cannot be after Follow Up date" + } + ) + + if episode.discharge.date > validated_data["date"]: + raise ValidationError( + { + "error": "Episode Discharge date cannot be after Follow Up date" + } + ) + + follow_up = FollowUp.objects.create( + episode_id=episode.id, + date=validated_data["date"], + pain_severity=validated_data.get("pain_severity", ""), + mesh_awareness=validated_data["aware_of_mesh"], + seroma=validated_data["seroma"], + infection=validated_data["infection"], + numbness=validated_data["numbness"], + ) + + follow_up.attendees.set(attendees) + + return follow_up From 78fa18133cf4a0fa7a85d659e07575599725b752 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 13:56:15 +0200 Subject: [PATCH 5/9] added POST follow up endpoint --- tmh_registry/registry/api/serializers.py | 6 +- tmh_registry/registry/api/viewsets.py | 19 ++++ .../migrations/0015_auto_20220222_1043.py | 2 +- .../tests/api/viewsets/test_discharges.py | 11 +-- .../tests/api/viewsets/test_follow_ups.py | 97 +++++++++++++++++++ tmh_registry/registry/urls.py | 2 + tmh_registry/users/api/serializers.py | 2 +- 7 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 tmh_registry/registry/tests/api/viewsets/test_follow_ups.py diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index db3c621..5d551f9 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -1,6 +1,6 @@ from drf_yasg.utils import swagger_serializer_method from rest_framework.exceptions import ValidationError -from rest_framework.fields import IntegerField +from rest_framework.fields import CharField, IntegerField from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.serializers import ModelSerializer, SerializerMethodField @@ -401,6 +401,8 @@ def create(self, validated_data): class FollowUpReadSerializer(ModelSerializer): episode = EpisodeReadSerializer() + pain_severity = CharField(source="get_pain_severity_display") + attendees = MedicalPersonnelSerializer(many=True) class Meta: model = FollowUp @@ -464,7 +466,7 @@ def create(self, validated_data): episode_id=episode.id, date=validated_data["date"], pain_severity=validated_data.get("pain_severity", ""), - mesh_awareness=validated_data["aware_of_mesh"], + mesh_awareness=validated_data["mesh_awareness"], seroma=validated_data["seroma"], infection=validated_data["infection"], numbness=validated_data["numbness"], diff --git a/tmh_registry/registry/api/viewsets.py b/tmh_registry/registry/api/viewsets.py index 5131884..0122781 100644 --- a/tmh_registry/registry/api/viewsets.py +++ b/tmh_registry/registry/api/viewsets.py @@ -15,6 +15,7 @@ from ..models import ( Discharge, Episode, + FollowUp, Hospital, Patient, PatientHospitalMapping, @@ -25,6 +26,8 @@ DischargeWriteSerializer, EpisodeReadSerializer, EpisodeWriteSerializer, + FollowUpReadSerializer, + FollowUpWriteSerializer, HospitalSerializer, PatientHospitalMappingReadSerializer, PatientHospitalMappingWriteSerializer, @@ -171,3 +174,19 @@ def get_serializer_class(self): return DischargeWriteSerializer raise NotImplementedError + + +@method_decorator( + name="create", + decorator=swagger_auto_schema(responses={201: FollowUpReadSerializer()}), +) +class FollowUpViewset(CreateModelMixin, GenericViewSet): + queryset = FollowUp.objects.all() + + def get_serializer_class(self): + if self.action in ["list", "retrieve"]: + return FollowUpReadSerializer + if self.action == "create": + return FollowUpWriteSerializer + + raise NotImplementedError diff --git a/tmh_registry/registry/migrations/0015_auto_20220222_1043.py b/tmh_registry/registry/migrations/0015_auto_20220222_1043.py index dbaa5c9..3f82498 100644 --- a/tmh_registry/registry/migrations/0015_auto_20220222_1043.py +++ b/tmh_registry/registry/migrations/0015_auto_20220222_1043.py @@ -18,7 +18,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="followup", name="created_at", - field=models.DateField(auto_now_add=True, default=1998), + field=models.DateField(auto_now_add=True, default="1998-1-1"), preserve_default=False, ), migrations.AddField( diff --git a/tmh_registry/registry/tests/api/viewsets/test_discharges.py b/tmh_registry/registry/tests/api/viewsets/test_discharges.py index 0000251..f4b48bd 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_discharges.py +++ b/tmh_registry/registry/tests/api/viewsets/test_discharges.py @@ -21,7 +21,7 @@ def setUp(self) -> None: self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - def test_discharge_data(self): + def get_discharge_data(self): return { "episode_id": self.episode.id, "date": "2022-02-22", @@ -30,8 +30,7 @@ def test_discharge_data(self): } def test_successful(self): - data = self.test_discharge_data() - print(f"{data['episode_id']=}") + data = self.get_discharge_data() response = self.client.post( "/api/v1/discharges/", data=data, format="json" ) @@ -44,7 +43,7 @@ def test_successful(self): self.assertEqual(response.data["infection"], data["infection"]) def test_when_episode_id_does_not_exist(self): - data = self.test_discharge_data() + data = self.get_discharge_data() data["episode_id"] = -1 response = self.client.post( "/api/v1/discharges/", data=data, format="json" @@ -54,7 +53,7 @@ def test_when_episode_id_does_not_exist(self): def test_when_episode_is_already_discharged(self): DischargeFactory(episode=self.episode, date="2022-02-22") - data = self.test_discharge_data() + data = self.get_discharge_data() response = self.client.post( "/api/v1/discharges/", data=data, format="json" ) @@ -62,7 +61,7 @@ def test_when_episode_is_already_discharged(self): self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) def test_when_episode_surgery_date_is_after_discharge_date(self): - data = self.test_discharge_data() + data = self.get_discharge_data() data["date"] = "2021-12-03" response = self.client.post( "/api/v1/discharges/", data=data, format="json" diff --git a/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py b/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py new file mode 100644 index 0000000..6f1685d --- /dev/null +++ b/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py @@ -0,0 +1,97 @@ +from django.test import TestCase +from rest_framework.authtoken.models import Token +from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST +from rest_framework.test import APIClient + +from tmh_registry.registry.factories import DischargeFactory, EpisodeFactory +from tmh_registry.users.factories import MedicalPersonnelFactory + + +class TestFollowUpsCreate(TestCase): + @classmethod + def setUpClass(cls) -> None: + super(TestFollowUpsCreate, cls).setUpClass() + + cls.episode = EpisodeFactory(surgery_date="2022-01-23") + cls.discharge = DischargeFactory( + episode=cls.episode, date="2022-02-19" + ) + + cls.medical_personnel = MedicalPersonnelFactory() + cls.token = Token.objects.create(user=cls.medical_personnel.user) + + def setUp(self) -> None: + self.client = APIClient() + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + + def get_follow_up_data(self): + return { + "episode_id": self.episode.id, + "date": "2022-02-22", + "attendee_ids": [self.medical_personnel.id], + "pain_severity": "MILD", + "mesh_awareness": False, + "seroma": True, + "infection": False, + "numbness": True, + } + + def test_successful(self): + data = self.get_follow_up_data() + response = self.client.post( + "/api/v1/follow-ups/", data=data, format="json" + ) + + self.assertEqual(HTTP_201_CREATED, response.status_code) + print(f"{response.data['attendees']=}") + self.assertEqual(response.data["episode"]["id"], self.episode.id) + self.assertEqual(response.data["date"], data["date"]) + self.assertEqual( + response.data["attendees"][0]["id"], data["attendee_ids"][0] + ) + self.assertEqual( + response.data["pain_severity"], + data["pain_severity"].replace("_", " ").title(), + ) + self.assertEqual( + response.data["mesh_awareness"], data["mesh_awareness"] + ) + self.assertEqual(response.data["seroma"], data["seroma"]) + self.assertEqual(response.data["infection"], data["infection"]) + self.assertEqual(response.data["numbness"], data["numbness"]) + + def test_when_episode_id_does_not_exist(self): + data = self.get_follow_up_data() + data["episode_id"] = -1 + response = self.client.post( + "/api/v1/follow-ups/", data=data, format="json" + ) + + self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) + + def test_when_episode_is_not_discharged_yet(self): + data = self.get_follow_up_data() + data["episode_id"] = EpisodeFactory().id + response = self.client.post( + "/api/v1/follow-ups/", data=data, format="json" + ) + + self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) + + def test_when_episode_surgery_date_is_after_follow_up_date(self): + data = self.get_follow_up_data() + data["date"] = "2021-12-03" + response = self.client.post( + "/api/v1/follow-ups/", data=data, format="json" + ) + + self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) + + def test_when_episode_discharge_date_is_after_follow_up_date(self): + data = self.get_follow_up_data() + data["date"] = "2022-02-05" + response = self.client.post( + "/api/v1/follow-ups/", data=data, format="json" + ) + + self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) diff --git a/tmh_registry/registry/urls.py b/tmh_registry/registry/urls.py index e0c4c34..67a6f44 100644 --- a/tmh_registry/registry/urls.py +++ b/tmh_registry/registry/urls.py @@ -4,6 +4,7 @@ from .api.viewsets import ( DischargeViewset, EpisodeViewset, + FollowUpViewset, HospitalViewSet, PatientHospitalMappingViewset, PatientViewSet, @@ -15,6 +16,7 @@ router.register(r"patient-hospital-mappings", PatientHospitalMappingViewset) router.register(r"episodes", EpisodeViewset) router.register(r"discharges", DischargeViewset) +router.register(r"follow-ups", FollowUpViewset) urlpatterns = [ path("", include(router.urls)), diff --git a/tmh_registry/users/api/serializers.py b/tmh_registry/users/api/serializers.py index 4e0e759..354e946 100644 --- a/tmh_registry/users/api/serializers.py +++ b/tmh_registry/users/api/serializers.py @@ -34,7 +34,7 @@ class MedicalPersonnelSerializer(ModelSerializer): class Meta: model = MedicalPersonnel - fields = ["user", "level"] + fields = ["id", "user", "level"] class SignInSerializer(Serializer): From f816583e24a85d49fbf80b229c903c6cc977eb3e Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 15:25:26 +0200 Subject: [PATCH 6/9] handle labels and values --- tmh_registry/common/utils/__init__.py | 0 tmh_registry/common/utils/functions.py | 4 ++++ tmh_registry/registry/api/serializers.py | 10 +++++++++- .../tests/api/viewsets/test_follow_ups.py | 19 ++++++++++++++++--- 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tmh_registry/common/utils/__init__.py create mode 100644 tmh_registry/common/utils/functions.py diff --git a/tmh_registry/common/utils/__init__.py b/tmh_registry/common/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tmh_registry/common/utils/functions.py b/tmh_registry/common/utils/functions.py new file mode 100644 index 0000000..73a1b1f --- /dev/null +++ b/tmh_registry/common/utils/functions.py @@ -0,0 +1,4 @@ +def get_text_choice_value_from_label(choices, label): + return [ + choice[0] for choice in choices if choice[1].upper() == label.upper() + ][0] diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index 5d551f9..62b335a 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.serializers import ModelSerializer, SerializerMethodField +from ...common.utils.functions import get_text_choice_value_from_label from ...users.api.serializers import MedicalPersonnelSerializer from ...users.models import MedicalPersonnel from ..models import ( @@ -426,6 +427,7 @@ class FollowUpWriteSerializer(ModelSerializer): attendee_ids = PrimaryKeyRelatedField( write_only=True, many=True, queryset=MedicalPersonnel.objects.all() ) + pain_severity = CharField() class Meta: model = FollowUp @@ -462,10 +464,16 @@ def create(self, validated_data): } ) + pain_severity = validated_data.get("pain_severity", "") + print(f"{pain_severity=}") follow_up = FollowUp.objects.create( episode_id=episode.id, date=validated_data["date"], - pain_severity=validated_data.get("pain_severity", ""), + pain_severity=get_text_choice_value_from_label( + FollowUp.PainSeverityChoices.choices, pain_severity + ) + if pain_severity + else "", mesh_awareness=validated_data["mesh_awareness"], seroma=validated_data["seroma"], infection=validated_data["infection"], diff --git a/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py b/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py index 6f1685d..386eda0 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py +++ b/tmh_registry/registry/tests/api/viewsets/test_follow_ups.py @@ -3,7 +3,11 @@ from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST from rest_framework.test import APIClient +from tmh_registry.common.utils.functions import ( + get_text_choice_value_from_label, +) from tmh_registry.registry.factories import DischargeFactory, EpisodeFactory +from tmh_registry.registry.models import FollowUp from tmh_registry.users.factories import MedicalPersonnelFactory @@ -29,7 +33,7 @@ def get_follow_up_data(self): "episode_id": self.episode.id, "date": "2022-02-22", "attendee_ids": [self.medical_personnel.id], - "pain_severity": "MILD", + "pain_severity": "Mild", "mesh_awareness": False, "seroma": True, "infection": False, @@ -43,7 +47,7 @@ def test_successful(self): ) self.assertEqual(HTTP_201_CREATED, response.status_code) - print(f"{response.data['attendees']=}") + self.assertEqual(response.data["episode"]["id"], self.episode.id) self.assertEqual(response.data["date"], data["date"]) self.assertEqual( @@ -51,7 +55,7 @@ def test_successful(self): ) self.assertEqual( response.data["pain_severity"], - data["pain_severity"].replace("_", " ").title(), + data["pain_severity"], ) self.assertEqual( response.data["mesh_awareness"], data["mesh_awareness"] @@ -60,6 +64,15 @@ def test_successful(self): self.assertEqual(response.data["infection"], data["infection"]) self.assertEqual(response.data["numbness"], data["numbness"]) + # check value stored in db + follow_up = FollowUp.objects.get(id=response.data["id"]) + self.assertEqual( + follow_up.pain_severity, + get_text_choice_value_from_label( + FollowUp.PainSeverityChoices.choices, data["pain_severity"] + ), + ) + def test_when_episode_id_does_not_exist(self): data = self.get_follow_up_data() data["episode_id"] = -1 From 750ce2b408178230026bf58d77f8acd13158035f Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 15:47:57 +0200 Subject: [PATCH 7/9] updated swagger documentation --- tmh_registry/registry/api/viewsets.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tmh_registry/registry/api/viewsets.py b/tmh_registry/registry/api/viewsets.py index 0122781..54ac5eb 100644 --- a/tmh_registry/registry/api/viewsets.py +++ b/tmh_registry/registry/api/viewsets.py @@ -146,7 +146,11 @@ def get_serializer_class(self): @method_decorator( name="create", - decorator=swagger_auto_schema(responses={201: EpisodeReadSerializer()}), + decorator=swagger_auto_schema( + operation_summary="Register an Episode", + operation_description="Use this endpoint to register an episode. \n ", + responses={201: EpisodeReadSerializer()}, + ), ) class EpisodeViewset(CreateModelMixin, GenericViewSet): queryset = Episode.objects.all() @@ -162,7 +166,12 @@ def get_serializer_class(self): @method_decorator( name="create", - decorator=swagger_auto_schema(responses={201: DischargeReadSerializer()}), + decorator=swagger_auto_schema( + operation_summary="Discharge a Patient", + operation_description="Use this endpoint to discharge a patient. Only one Discharge can be registered " + "for the same Episode.", + responses={201: DischargeReadSerializer()}, + ), ) class DischargeViewset(CreateModelMixin, GenericViewSet): queryset = Discharge.objects.all() @@ -178,7 +187,13 @@ def get_serializer_class(self): @method_decorator( name="create", - decorator=swagger_auto_schema(responses={201: FollowUpReadSerializer()}), + decorator=swagger_auto_schema( + operation_summary="Register a Follow Up", + operation_description="Use this endpoint to register a Follow Up. Multiple Follow Ups can be registered" + "for the same Episode.\n\n The accepted values for `pain_severity` are" + f" `{FollowUp.PainSeverityChoices.labels}`", + responses={201: FollowUpReadSerializer()}, + ), ) class FollowUpViewset(CreateModelMixin, GenericViewSet): queryset = FollowUp.objects.all() From 0a5c1400e097f9b6b29e56866aedf21f7df930d8 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 16:29:33 +0200 Subject: [PATCH 8/9] updated enums and swagger on episodes and patients --- tmh_registry/registry/api/serializers.py | 81 +++++++++++++++---- tmh_registry/registry/api/viewsets.py | 22 +++-- .../migrations/0016_auto_20220222_1415.py | 23 ++++++ tmh_registry/registry/models.py | 4 +- .../tests/api/viewsets/test_episodes.py | 71 ++++++++++++++-- .../tests/api/viewsets/test_patients.py | 13 ++- 6 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 tmh_registry/registry/migrations/0016_auto_20220222_1415.py diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index 62b335a..a66955b 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -52,6 +52,7 @@ class ReadPatientSerializer(ModelSerializer): age = IntegerField(allow_null=True) hospital_mappings = PatientHospitalMappingPatientSerializer(many=True) episodes = SerializerMethodField() + gender = CharField(source="get_gender_display") @swagger_serializer_method(serializer_or_field=EpisodeSerializer) def get_episodes(self, obj): @@ -99,11 +100,11 @@ class CreatePatientSerializer(ModelSerializer): hospital_id = IntegerField(write_only=True) patient_hospital_id = IntegerField(write_only=True) year_of_birth = IntegerField(allow_null=True) + gender = CharField(allow_null=True) class Meta: model = Patient fields = [ - "id", "full_name", "national_id", "age", @@ -170,6 +171,14 @@ def create(self, validated_data): {"error": "The patient needs to be registered to a hospital."} ) + validated_data["gender"] = ( + get_text_choice_value_from_label( + Patient.Gender.choices, validated_data["gender"] + ) + if validated_data["gender"] + else validated_data["gender"] + ) + validated_data.pop("age", None) new_patient = super(CreatePatientSerializer, self).create( validated_data @@ -251,6 +260,14 @@ def create(self, validated_data): class EpisodeReadSerializer(ModelSerializer): patient_hospital_mapping = PatientHospitalMappingReadSerializer() surgeons = MedicalPersonnelSerializer(many=True) + episode_type = CharField(source="get_episode_type_display") + cepod = CharField(source="get_cepod_display") + side = CharField(source="get_side_display") + occurence = CharField(source="get_occurence_display") + type = CharField(source="get_type_display") + complexity = CharField(source="get_complexity_display") + mesh_type = CharField(source="get_mesh_type_display") + anaesthetic_type = CharField(source="get_anaesthetic_type_display") class Meta: model = Episode @@ -283,6 +300,14 @@ class EpisodeWriteSerializer(ModelSerializer): surgeon_ids = PrimaryKeyRelatedField( write_only=True, many=True, queryset=MedicalPersonnel.objects.all() ) + episode_type = CharField() + cepod = CharField() + side = CharField() + occurence = CharField() + type = CharField() + complexity = CharField() + mesh_type = CharField() + anaesthetic_type = CharField() class Meta: model = Episode @@ -327,20 +352,46 @@ def create(self, validated_data): } ) - episode = Episode.objects.create( - patient_hospital_mapping=patient_hospital_mapping, - surgery_date=validated_data["surgery_date"], - episode_type=validated_data["episode_type"], - comments=validated_data["comments"], - cepod=validated_data["cepod"], - side=validated_data["side"], - occurence=validated_data["occurence"], - type=validated_data["type"], - complexity=validated_data["complexity"], - mesh_type=validated_data["mesh_type"], - anaesthetic_type=validated_data["anaesthetic_type"], - diathermy_used=validated_data["diathermy_used"], - ) + try: + episode = Episode.objects.create( + patient_hospital_mapping=patient_hospital_mapping, + surgery_date=validated_data["surgery_date"], + episode_type=get_text_choice_value_from_label( + Episode.EpisodeChoices.choices, + validated_data["episode_type"], + ), + comments=validated_data["comments"], + cepod=get_text_choice_value_from_label( + Episode.CepodChoices.choices, validated_data["cepod"] + ), + side=get_text_choice_value_from_label( + Episode.SideChoices.choices, validated_data["side"] + ), + occurence=get_text_choice_value_from_label( + Episode.OccurenceChoices.choices, + validated_data["occurence"], + ), + type=get_text_choice_value_from_label( + Episode.TypeChoices.choices, validated_data["type"] + ), + complexity=get_text_choice_value_from_label( + Episode.ComplexityChoices.choices, + validated_data["complexity"], + ), + mesh_type=get_text_choice_value_from_label( + Episode.MeshTypeChoices.choices, + validated_data["mesh_type"], + ), + anaesthetic_type=get_text_choice_value_from_label( + Episode.AnaestheticChoices.choices, + validated_data["anaesthetic_type"], + ), + diathermy_used=validated_data["diathermy_used"], + ) + except IndexError: + raise ValidationError( + {"error": "Not supported value provided for ChoiceField."} + ) if surgeons: episode.surgeons.set(surgeons) diff --git a/tmh_registry/registry/api/viewsets.py b/tmh_registry/registry/api/viewsets.py index 54ac5eb..aa30034 100644 --- a/tmh_registry/registry/api/viewsets.py +++ b/tmh_registry/registry/api/viewsets.py @@ -76,7 +76,11 @@ class Meta: @method_decorator( name="create", decorator=swagger_auto_schema( - responses={201: ReadPatientSerializer(many=True)} + operation_summary="Register a Patient", + operation_description="Use this endpoint to register a patient. A PatientHospitalMapping will be created " + "automatically for the newly created Patient and the provided Hospital.\n " + f"Accepted values for `gender` are `{Patient.Gender.labels}`. \n ", + responses={201: ReadPatientSerializer(many=True)}, ), ) @method_decorator( @@ -148,7 +152,15 @@ def get_serializer_class(self): name="create", decorator=swagger_auto_schema( operation_summary="Register an Episode", - operation_description="Use this endpoint to register an episode. \n ", + operation_description="Use this endpoint to register an episode. \n " + f"Accepted values for `episode_type` are `{Episode.EpisodeChoices.labels}`. \n " + f"Accepted values for `cepod` are `{Episode.CepodChoices.labels}`. \n " + f"Accepted values for `side` are `{Episode.SideChoices.labels}`. \n " + f"Accepted values for `occurence` are `{Episode.OccurenceChoices.labels}`. \n " + f"Accepted values for `type` are `{Episode.TypeChoices.labels}`. \n " + f"Accepted values for `complexity` are `{Episode.ComplexityChoices.labels}`. \n " + f"Accepted values for `mesh_type` are `{Episode.MeshTypeChoices.labels}`. \n " + f"Accepted values for `anaesthetic_type` are `{Episode.AnaestheticChoices.labels}`. \n ", responses={201: EpisodeReadSerializer()}, ), ) @@ -189,9 +201,9 @@ def get_serializer_class(self): name="create", decorator=swagger_auto_schema( operation_summary="Register a Follow Up", - operation_description="Use this endpoint to register a Follow Up. Multiple Follow Ups can be registered" - "for the same Episode.\n\n The accepted values for `pain_severity` are" - f" `{FollowUp.PainSeverityChoices.labels}`", + operation_description="Use this endpoint to register a Follow Up. Multiple Follow Ups can be registered " + "for the same Episode.\n The accepted values for `pain_severity` are " + f"`{FollowUp.PainSeverityChoices.labels}`.", responses={201: FollowUpReadSerializer()}, ), ) diff --git a/tmh_registry/registry/migrations/0016_auto_20220222_1415.py b/tmh_registry/registry/migrations/0016_auto_20220222_1415.py new file mode 100644 index 0000000..a430b3f --- /dev/null +++ b/tmh_registry/registry/migrations/0016_auto_20220222_1415.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.3 on 2022-02-22 14:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0015_auto_20220222_1043"), + ] + + operations = [ + migrations.AlterField( + model_name="patient", + name="gender", + field=models.CharField( + blank=True, + choices=[("MALE", "Male"), ("FEMALE", "Female")], + max_length=32, + null=True, + ), + ), + ] diff --git a/tmh_registry/registry/models.py b/tmh_registry/registry/models.py index 1e9f716..ed9662c 100644 --- a/tmh_registry/registry/models.py +++ b/tmh_registry/registry/models.py @@ -29,8 +29,8 @@ def __str__(self): class Patient(TimeStampMixin): class Gender(TextChoices): - MALE = ("Male", "Male") - FEMALE = ("Female", "Female") + MALE = ("MALE", "Male") + FEMALE = ("FEMALE", "Female") full_name = CharField(max_length=255) national_id = CharField(max_length=20, null=True, blank=True, unique=True) diff --git a/tmh_registry/registry/tests/api/viewsets/test_episodes.py b/tmh_registry/registry/tests/api/viewsets/test_episodes.py index c3fe0dd..7c88fde 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_episodes.py +++ b/tmh_registry/registry/tests/api/viewsets/test_episodes.py @@ -6,6 +6,9 @@ from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST from rest_framework.test import APIClient +from tmh_registry.common.utils.functions import ( + get_text_choice_value_from_label, +) from tmh_registry.registry.factories import ( HospitalFactory, PatientFactory, @@ -34,16 +37,16 @@ def get_episode_test_data(self): "patient_id": self.patient.id, "hospital_id": self.hospital.id, "surgery_date": "2021-10-12", - "episode_type": Episode.EpisodeChoices.UMBILICAL.value, + "episode_type": Episode.EpisodeChoices.UMBILICAL.label, "surgeon_ids": [self.medical_personnel.id], "comments": "A random comment", - "cepod": Episode.CepodChoices.PLANNED.value, - "side": Episode.SideChoices.LEFT.value, - "occurence": Episode.OccurenceChoices.RECURRENT.value, - "type": Episode.TypeChoices.INDIRECT.value, - "complexity": Episode.ComplexityChoices.INCARCERATED.value, - "mesh_type": Episode.MeshTypeChoices.TNMHP.value, - "anaesthetic_type": Episode.AnaestheticChoices.SPINAL.value, + "cepod": Episode.CepodChoices.PLANNED.label, + "side": Episode.SideChoices.LEFT.label, + "occurence": Episode.OccurenceChoices.RECURRENT.label, + "type": Episode.TypeChoices.INDIRECT.label, + "complexity": Episode.ComplexityChoices.INCARCERATED.label, + "mesh_type": Episode.MeshTypeChoices.TNMHP.label, + "anaesthetic_type": Episode.AnaestheticChoices.SPINAL.label, "diathermy_used": True, } @@ -121,3 +124,55 @@ def test_create_episode_successful(self): self.assertEqual( response.data["diathermy_used"], data["diathermy_used"] ) + + # assert that values are stored in the db, not labels + episode = Episode.objects.get(id=response.data["id"]) + + self.assertEqual( + episode.episode_type, + get_text_choice_value_from_label( + Episode.EpisodeChoices.choices, data["episode_type"] + ), + ) + self.assertEqual( + episode.cepod, + get_text_choice_value_from_label( + Episode.CepodChoices.choices, data["cepod"] + ), + ) + self.assertEqual( + episode.side, + get_text_choice_value_from_label( + Episode.SideChoices.choices, data["side"] + ), + ) + self.assertEqual( + episode.occurence, + get_text_choice_value_from_label( + Episode.OccurenceChoices.choices, data["occurence"] + ), + ) + self.assertEqual( + episode.type, + get_text_choice_value_from_label( + Episode.TypeChoices.choices, data["type"] + ), + ) + self.assertEqual( + episode.complexity, + get_text_choice_value_from_label( + Episode.ComplexityChoices.choices, data["complexity"] + ), + ) + self.assertEqual( + episode.mesh_type, + get_text_choice_value_from_label( + Episode.MeshTypeChoices.choices, data["mesh_type"] + ), + ) + self.assertEqual( + episode.anaesthetic_type, + get_text_choice_value_from_label( + Episode.AnaestheticChoices.choices, data["anaesthetic_type"] + ), + ) diff --git a/tmh_registry/registry/tests/api/viewsets/test_patients.py b/tmh_registry/registry/tests/api/viewsets/test_patients.py index cad1d61..3ed5aa9 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_patients.py +++ b/tmh_registry/registry/tests/api/viewsets/test_patients.py @@ -12,6 +12,7 @@ ) from rest_framework.test import APIClient +from .....common.utils.functions import get_text_choice_value_from_label from .....users.factories import MedicalPersonnelFactory, UserFactory from ....factories import ( EpisodeFactory, @@ -19,7 +20,7 @@ PatientFactory, PatientHospitalMappingFactory, ) -from ....models import PatientHospitalMapping +from ....models import Patient, PatientHospitalMapping @mark.registry @@ -325,6 +326,16 @@ def test_create_patients_successful(self): ).count(), ) + # assert db value + patient = Patient.objects.get(id=response.data["id"]) + + self.assertEqual( + get_text_choice_value_from_label( + Patient.Gender.choices, data["gender"] + ), + patient.gender, + ) + def test_create_patients_only_with_mandatory_fields(self): data = self.get_patient_test_data() for key in data.keys(): From 3d4bacb0e2d98a5a469f5034718bd5ccd2bc6aaa Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Tue, 22 Feb 2022 16:47:44 +0200 Subject: [PATCH 9/9] updated swagger --- tmh_registry/registry/api/viewsets.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tmh_registry/registry/api/viewsets.py b/tmh_registry/registry/api/viewsets.py index aa30034..2734748 100644 --- a/tmh_registry/registry/api/viewsets.py +++ b/tmh_registry/registry/api/viewsets.py @@ -79,7 +79,7 @@ class Meta: operation_summary="Register a Patient", operation_description="Use this endpoint to register a patient. A PatientHospitalMapping will be created " "automatically for the newly created Patient and the provided Hospital.\n " - f"Accepted values for `gender` are `{Patient.Gender.labels}`. \n ", + f"\nAccepted values for `gender` are `{Patient.Gender.labels}`. \n ", responses={201: ReadPatientSerializer(many=True)}, ), ) @@ -152,15 +152,17 @@ def get_serializer_class(self): name="create", decorator=swagger_auto_schema( operation_summary="Register an Episode", - operation_description="Use this endpoint to register an episode. \n " - f"Accepted values for `episode_type` are `{Episode.EpisodeChoices.labels}`. \n " - f"Accepted values for `cepod` are `{Episode.CepodChoices.labels}`. \n " - f"Accepted values for `side` are `{Episode.SideChoices.labels}`. \n " - f"Accepted values for `occurence` are `{Episode.OccurenceChoices.labels}`. \n " - f"Accepted values for `type` are `{Episode.TypeChoices.labels}`. \n " - f"Accepted values for `complexity` are `{Episode.ComplexityChoices.labels}`. \n " - f"Accepted values for `mesh_type` are `{Episode.MeshTypeChoices.labels}`. \n " - f"Accepted values for `anaesthetic_type` are `{Episode.AnaestheticChoices.labels}`. \n ", + operation_description="Use this endpoint to register an episode. Keep in mind that you need to create a " + "`PatientHospitalMapping`(through the POST /patient-hospital-mappings/ endpoint) " + "if one does not already exist for this specific Patient/Hospital pair.\n " + f"\nAccepted values for `episode_type` are `{Episode.EpisodeChoices.labels}`. \n " + f"\nAccepted values for `cepod` are `{Episode.CepodChoices.labels}`. \n " + f"\nAccepted values for `side` are `{Episode.SideChoices.labels}`. \n " + f"\nAccepted values for `occurence` are `{Episode.OccurenceChoices.labels}`. \n " + f"\nAccepted values for `type` are `{Episode.TypeChoices.labels}`. \n " + f"\nAccepted values for `complexity` are `{Episode.ComplexityChoices.labels}`. \n " + f"\nAccepted values for `mesh_type` are `{Episode.MeshTypeChoices.labels}`. \n " + f"\nAccepted values for `anaesthetic_type` are `{Episode.AnaestheticChoices.labels}`. \n ", responses={201: EpisodeReadSerializer()}, ), ) @@ -202,7 +204,8 @@ def get_serializer_class(self): decorator=swagger_auto_schema( operation_summary="Register a Follow Up", operation_description="Use this endpoint to register a Follow Up. Multiple Follow Ups can be registered " - "for the same Episode.\n The accepted values for `pain_severity` are " + "for the same Episode.\n " + "\nThe accepted values for `pain_severity` are " f"`{FollowUp.PainSeverityChoices.labels}`.", responses={201: FollowUpReadSerializer()}, ),