From 908db8885d12d50d9479d3e83b3173e56f0c0c20 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Sun, 19 Sep 2021 21:28:02 +0300 Subject: [PATCH 1/4] added patient_hospital_id --- tmh_registry/registry/factories.py | 39 +++++++++++-------- .../migrations/0008_auto_20210919_1826.py | 30 ++++++++++++++ tmh_registry/registry/models.py | 10 +++-- 3 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 tmh_registry/registry/migrations/0008_auto_20210919_1826.py diff --git a/tmh_registry/registry/factories.py b/tmh_registry/registry/factories.py index 246bb73..3e1387c 100644 --- a/tmh_registry/registry/factories.py +++ b/tmh_registry/registry/factories.py @@ -1,10 +1,10 @@ import random -import factory -from factory.django import DjangoModelFactory +from django import DjangoModelFactory +from factory import LazyAttribute, SubFactory from faker import Faker -from .models import Hospital, Patient +from .models import Hospital, Patient, PatientHospitalMapping faker = Faker() @@ -13,26 +13,33 @@ class HospitalFactory(DjangoModelFactory): class Meta: model = Hospital - name = factory.LazyAttribute(lambda n: "Hospital '%s'" % faker.name()) - address = factory.LazyAttribute(lambda n: faker.address()) + name = LazyAttribute(lambda n: "Hospital '%s'" % faker.name()) + address = LazyAttribute(lambda n: faker.address()) class PatientFactory(DjangoModelFactory): class Meta: model = Patient - full_name = factory.LazyAttribute(lambda n: faker.name()) - national_id = factory.LazyAttribute( + full_name = LazyAttribute(lambda n: faker.name()) + national_id = LazyAttribute( lambda n: faker.numerify(text="####################") ) - day_of_birth = factory.LazyAttribute(lambda n: faker.date_of_birth().day) - month_of_birth = factory.LazyAttribute( - lambda n: faker.date_of_birth().month - ) - year_of_birth = factory.LazyAttribute(lambda n: faker.date_of_birth().year) - gender = factory.LazyAttribute( + day_of_birth = LazyAttribute(lambda n: faker.date_of_birth().day) + month_of_birth = LazyAttribute(lambda n: faker.date_of_birth().month) + year_of_birth = LazyAttribute(lambda n: faker.date_of_birth().year) + gender = LazyAttribute( lambda n: random.choice([gender.value for gender in Patient.Gender]) ) - phone_1 = factory.LazyAttribute(lambda n: faker.numerify(text="#########")) - phone_2 = factory.LazyAttribute(lambda n: faker.numerify(text="#########")) - address = factory.LazyAttribute(lambda n: faker.address()) + phone_1 = LazyAttribute(lambda n: faker.numerify(text="#########")) + phone_2 = LazyAttribute(lambda n: faker.numerify(text="#########")) + address = LazyAttribute(lambda n: faker.address()) + + +class PatientHospitalMappingFactory(DjangoModelFactory): + class Meta: + model = PatientHospitalMapping + + patient = SubFactory(PatientFactory) + hospital = SubFactory(HospitalFactory) + patient_hospital_id = LazyAttribute(lambda n: faker.ssn()) diff --git a/tmh_registry/registry/migrations/0008_auto_20210919_1826.py b/tmh_registry/registry/migrations/0008_auto_20210919_1826.py new file mode 100644 index 0000000..c6c3f83 --- /dev/null +++ b/tmh_registry/registry/migrations/0008_auto_20210919_1826.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.3 on 2021-09-19 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0007_episode_followup"), + ] + + operations = [ + migrations.AlterModelOptions( + name="patienthospitalmapping", + options={"verbose_name_plural": "Patient-Hospital mappings"}, + ), + migrations.AddField( + model_name="patienthospitalmapping", + name="patient_hospital_id", + field=models.CharField(default="111111", max_length=256), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name="patienthospitalmapping", + unique_together={ + ("hospital", "patient_hospital_id"), + ("patient", "hospital"), + }, + ), + ] diff --git a/tmh_registry/registry/models.py b/tmh_registry/registry/models.py index 36ba99d..cf7da7d 100644 --- a/tmh_registry/registry/models.py +++ b/tmh_registry/registry/models.py @@ -51,13 +51,17 @@ def get_year_of_birth_from_age(age): class PatientHospitalMapping(models.Model): patient = models.ForeignKey(Patient, on_delete=models.CASCADE) hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE) + patient_hospital_id = models.CharField(max_length=256) class Meta: - unique_together = ("patient", "hospital") - verbose_name_plural = "Patient-Hospital mapping" + unique_together = ( + ("patient", "hospital"), + ("hospital", "patient_hospital_id"), + ) + verbose_name_plural = "Patient-Hospital mappings" def __str__(self): - return f"{self.patient.full_name} - {self.hospital.name}" + return f"Patient {self.patient.full_name} ({self.patient_hospital_id}) - Hospital {self.hospital.name}" class Episode(models.Model): From 54e31bd16ca17ddd07926d8462c677579ca4b772 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Sun, 19 Sep 2021 21:56:13 +0300 Subject: [PATCH 2/4] updated post endpoint --- tmh_registry/registry/api/serializers.py | 49 ++++++++++++++----- tmh_registry/registry/factories.py | 2 +- .../tests/api/viewsets/test_patients.py | 21 +++++++- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index e618e38..d2c553d 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -1,17 +1,19 @@ -from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from rest_framework.fields import IntegerField, SerializerMethodField +from rest_framework.serializers import ModelSerializer from ..models import Hospital, Patient, PatientHospitalMapping -class HospitalSerializer(serializers.ModelSerializer): +class HospitalSerializer(ModelSerializer): class Meta: model = Hospital fields = ["id", "name", "address"] -class ReadPatientSerializer(serializers.ModelSerializer): - age = serializers.IntegerField(allow_null=True) - hospitals = serializers.SerializerMethodField() +class ReadPatientSerializer(ModelSerializer): + age = IntegerField(allow_null=True) + hospitals = SerializerMethodField() def get_hospitals(self, obj): hospitals = Hospital.objects.filter( @@ -51,10 +53,11 @@ def to_representation(self, instance): return data -class CreatePatientSerializer(serializers.ModelSerializer): - age = serializers.IntegerField(allow_null=True) - hospital_id = serializers.IntegerField(write_only=True) - year_of_birth = serializers.IntegerField(allow_null=True) +class CreatePatientSerializer(ModelSerializer): + age = IntegerField(allow_null=True) + hospital_id = IntegerField(write_only=True) + patient_hospital_id = IntegerField(write_only=True) + year_of_birth = IntegerField(allow_null=True) class Meta: model = Patient @@ -71,6 +74,7 @@ class Meta: "phone_2", "address", "hospital_id", + "patient_hospital_id", ] def to_representation(self, instance): @@ -85,7 +89,7 @@ def create(self, validated_data): "year_of_birth" ] = Patient.get_year_of_birth_from_age(validated_data["age"]) else: - raise serializers.ValidationError( + raise ValidationError( { "error": "Either 'age' or 'year_of_birth' should be populated." } @@ -97,14 +101,31 @@ def create(self, validated_data): id=validated_data["hospital_id"] ) except Hospital.DoesNotExist: - raise serializers.ValidationError( + raise ValidationError( { "error": "The hospital you are trying to register this patient does not exist." } ) validated_data.pop("hospital_id", None) else: - raise serializers.ValidationError( + raise ValidationError( + {"error": "The patient needs to be registered to a hospital."} + ) + + patient_hospital_id = validated_data.pop("patient_hospital_id", None) + if patient_hospital_id: + if PatientHospitalMapping.objects.filter( + hospital_id=hospital.id, + patient_hospital_id=patient_hospital_id, + ).exists(): + raise ValidationError( + { + "error": f"The patient hospital id {patient_hospital_id} is already " + f"registered to another patient of this hospital." + } + ) + else: + raise ValidationError( {"error": "The patient needs to be registered to a hospital."} ) @@ -113,7 +134,9 @@ def create(self, validated_data): validated_data ) PatientHospitalMapping.objects.create( - patient=new_patient, hospital=hospital + patient=new_patient, + hospital=hospital, + patient_hospital_id=patient_hospital_id, ) return new_patient diff --git a/tmh_registry/registry/factories.py b/tmh_registry/registry/factories.py index 3e1387c..e95ccd6 100644 --- a/tmh_registry/registry/factories.py +++ b/tmh_registry/registry/factories.py @@ -1,7 +1,7 @@ import random -from django import DjangoModelFactory from factory import LazyAttribute, SubFactory +from factory.django import DjangoModelFactory from faker import Faker from .models import Hospital, Patient, PatientHospitalMapping diff --git a/tmh_registry/registry/tests/api/viewsets/test_patients.py b/tmh_registry/registry/tests/api/viewsets/test_patients.py index c714521..3823d19 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_patients.py +++ b/tmh_registry/registry/tests/api/viewsets/test_patients.py @@ -13,7 +13,11 @@ from rest_framework.test import APIClient from .....users.factories import MedicalPersonnelFactory, UserFactory -from ....factories import HospitalFactory, PatientFactory +from ....factories import ( + HospitalFactory, + PatientFactory, + PatientHospitalMappingFactory, +) from ....models import PatientHospitalMapping @@ -47,6 +51,7 @@ def get_patient_test_data(self): "phone_2": 324362141, "address": "16 Test Street, Test City, Test Country", "hospital_id": self.hospital.id, + "patient_hospital_id": "1111", } ###################### @@ -168,6 +173,7 @@ def test_create_patients_only_with_mandatory_fields(self): data["full_name"] = "John Doe" data["year_of_birth"] = 1989 data["hospital_id"] = self.hospital.id + data["patient_hospital_id"] = "1111" response = self.client.post( "/api/v1/patients/", data=data, format="json" @@ -181,6 +187,19 @@ def test_create_patients_only_with_mandatory_fields(self): response.data["age"], ) + def test_create_patients_with_already_existing_patient_hospital_id(self): + data = self.get_patient_test_data() + PatientHospitalMappingFactory( + patient_hospital_id=data["patient_hospital_id"], + hospital_id=data["hospital_id"], + ) + + response = self.client.post( + "/api/v1/patients/", data=data, format="json" + ) + + self.assertEqual(HTTP_400_BAD_REQUEST, response.status_code) + def test_create_patients_non_medical_personnel_user(self): self.non_mp_user = UserFactory() self.token = Token.objects.create(user=self.non_mp_user) From 3785da3239cdb90a683707f92a89690b0107f132 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Sun, 19 Sep 2021 23:29:07 +0300 Subject: [PATCH 3/4] added patient_hospital_id in get endpoints --- tmh_registry/registry/api/serializers.py | 21 ++++- .../migrations/0009_auto_20210919_1941.py | 32 +++++++ tmh_registry/registry/models.py | 8 +- .../tests/api/viewsets/test_patients.py | 84 ++++++++++++++----- 4 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 tmh_registry/registry/migrations/0009_auto_20210919_1941.py diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index d2c553d..62b5835 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -17,11 +17,24 @@ class ReadPatientSerializer(ModelSerializer): def get_hospitals(self, obj): hospitals = Hospital.objects.filter( - id__in=PatientHospitalMapping.objects.filter( - patient=obj - ).values_list("hospital_id", flat=True) + id__in=obj.hospital_mappings.all().values_list( + "hospital_id", flat=True + ) ) - return HospitalSerializer(hospitals, many=True).data + hospital_data = HospitalSerializer(hospitals, many=True).data + + # enrich with patient_hospital_id + idx = 0 + for hospital in hospital_data: + patient_hospital_id = PatientHospitalMapping.objects.get( + hospital_id=hospital["id"], patient_id=obj.id + ).patient_hospital_id + hospital_data[idx].update( + {"patient_hospital_id": patient_hospital_id} + ) + idx += 1 + + return hospital_data class Meta: model = Patient diff --git a/tmh_registry/registry/migrations/0009_auto_20210919_1941.py b/tmh_registry/registry/migrations/0009_auto_20210919_1941.py new file mode 100644 index 0000000..d3ca52b --- /dev/null +++ b/tmh_registry/registry/migrations/0009_auto_20210919_1941.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.3 on 2021-09-19 19:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0008_auto_20210919_1826"), + ] + + operations = [ + migrations.AlterField( + model_name="patienthospitalmapping", + name="hospital", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="patient_mappings", + to="registry.hospital", + ), + ), + migrations.AlterField( + model_name="patienthospitalmapping", + name="patient", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="hospital_mappings", + to="registry.patient", + ), + ), + ] diff --git a/tmh_registry/registry/models.py b/tmh_registry/registry/models.py index cf7da7d..35533d2 100644 --- a/tmh_registry/registry/models.py +++ b/tmh_registry/registry/models.py @@ -49,8 +49,12 @@ def get_year_of_birth_from_age(age): class PatientHospitalMapping(models.Model): - patient = models.ForeignKey(Patient, on_delete=models.CASCADE) - hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE) + patient = models.ForeignKey( + Patient, on_delete=models.CASCADE, related_name="hospital_mappings" + ) + hospital = models.ForeignKey( + Hospital, on_delete=models.CASCADE, related_name="patient_mappings" + ) patient_hospital_id = models.CharField(max_length=256) class Meta: diff --git a/tmh_registry/registry/tests/api/viewsets/test_patients.py b/tmh_registry/registry/tests/api/viewsets/test_patients.py index 3823d19..eedd197 100644 --- a/tmh_registry/registry/tests/api/viewsets/test_patients.py +++ b/tmh_registry/registry/tests/api/viewsets/test_patients.py @@ -31,6 +31,9 @@ def setUpClass(cls) -> None: cls.hospital = HospitalFactory() cls.patient = PatientFactory() + cls.mapping = PatientHospitalMappingFactory( + hospital=cls.hospital, patient=cls.patient + ) cls.medical_personnel = MedicalPersonnelFactory() cls.token = Token.objects.create(user=cls.medical_personnel.user) @@ -64,9 +67,14 @@ def test_get_patients_list_successful(self): self.assertEqual(HTTP_200_OK, response.status_code) self.assertEqual(1, response.data["count"]) self.assertEqual(self.patient.id, response.data["results"][0]["id"]) - self.assertEqual([], response.data["results"][0]["hospitals"]) self.assertNotIn("hospital_id", response.data["results"][0]) + self.assertEqual(1, len(response.data["results"][0]["hospitals"])) + self.assertEqual( + self.mapping.patient_hospital_id, + response.data["results"][0]["hospitals"][0]["patient_hospital_id"], + ) + def test_get_patients_list_unauthorized(self): self.client = APIClient() response = self.client.get("/api/v1/patients/", format="json") @@ -74,22 +82,22 @@ def test_get_patients_list_unauthorized(self): self.assertEqual(HTTP_401_UNAUTHORIZED, response.status_code) def test_get_patients_list_from_non_admin_user(self): - self.non_admin_mp = MedicalPersonnelFactory(user__is_staff=False) - self.token = Token.objects.create(user=self.non_admin_mp.user) + non_admin_mp = MedicalPersonnelFactory(user__is_staff=False) + non_admin_token = Token.objects.create(user=non_admin_mp.user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - response = self.client.get("/api/v1/patients/", format="json") + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + non_admin_token.key) + response = client.get("/api/v1/patients/", format="json") self.assertEqual(HTTP_403_FORBIDDEN, response.status_code) def test_get_patients_list_from_non_medical_personnel_user(self): - self.non_mp_user = UserFactory() - self.token = Token.objects.create(user=self.non_mp_user) + non_mp_user = UserFactory() + non_mp_token = Token.objects.create(user=non_mp_user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - response = self.client.get("/api/v1/patients/", format="json") + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + non_mp_token.key) + response = client.get("/api/v1/patients/", format="json") self.assertEqual(HTTP_403_FORBIDDEN, response.status_code) @@ -104,6 +112,17 @@ def test_get_patients_detail_successful(self): self.assertEqual(HTTP_200_OK, response.status_code) self.assertEqual(self.patient.id, response.data["id"]) + self.assertEqual(1, len(response.data["hospitals"])) + + patient_hospital_id = PatientHospitalMapping.objects.get( + hospital=self.hospital.id, patient=self.patient.id + ).patient_hospital_id + + self.assertEqual(self.hospital.id, response.data["hospitals"][0]["id"]) + self.assertEqual( + patient_hospital_id, + response.data["hospitals"][0]["patient_hospital_id"], + ) def test_get_patients_detail_unauthorized(self): self.client = APIClient() @@ -114,29 +133,48 @@ def test_get_patients_detail_unauthorized(self): self.assertEqual(HTTP_401_UNAUTHORIZED, response.status_code) def test_get_patients_detail_from_non_admin_user(self): - self.non_admin_mp = MedicalPersonnelFactory(user__is_staff=False) - self.token = Token.objects.create(user=self.non_admin_mp.user) + non_admin_mp = MedicalPersonnelFactory(user__is_staff=False) + non_admin_token = Token.objects.create(user=non_admin_mp.user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - response = self.client.get( + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + non_admin_token.key) + response = client.get( f"/api/v1/patients/{self.patient.id}/", format="json" ) self.assertEqual(HTTP_403_FORBIDDEN, response.status_code) def test_get_patients_detail_from_non_medical_personnel_user(self): - self.non_mp_user = UserFactory() - self.token = Token.objects.create(user=self.non_mp_user) + non_mp_user = UserFactory() + non_mp_token = Token.objects.create(user=non_mp_user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - response = self.client.get( + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + non_mp_token.key) + response = client.get( f"/api/v1/patients/{self.patient.id}/", format="json" ) self.assertEqual(HTTP_403_FORBIDDEN, response.status_code) + def test_get_patients_detail_with_multiple_hospitals(self): + PatientHospitalMappingFactory(patient=self.patient) + + response = self.client.get( + f"/api/v1/patients/{self.patient.id}/", format="json" + ) + + self.assertEqual(HTTP_200_OK, response.status_code) + self.assertEqual(self.patient.id, response.data["id"]) + self.assertEqual(2, len(response.data["hospitals"])) + + for hospital in response.data["hospitals"]: + patient_hospital_id = PatientHospitalMapping.objects.get( + hospital_id=hospital["id"], patient_id=self.patient.id + ).patient_hospital_id + self.assertEqual( + patient_hospital_id, hospital["patient_hospital_id"] + ) + ######################## # Test create endpoint # ######################## @@ -159,6 +197,10 @@ def test_create_patients_successful(self): self.assertEqual(1, len(response.data["hospitals"])) self.assertEqual(self.hospital.id, response.data["hospitals"][0]["id"]) + self.assertEqual( + data["patient_hospital_id"], + response.data["hospitals"][0]["patient_hospital_id"], + ) self.assertEqual( 1, PatientHospitalMapping.objects.filter( From e76e2a4286db7812bd734676b11bafdb6818731c Mon Sep 17 00:00:00 2001 From: Dimitris Koutsonikolis Date: Wed, 20 Oct 2021 12:48:07 +0300 Subject: [PATCH 4/4] reverted version to isort --- requirements/local.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index dcde2a7..7cc8276 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -23,7 +23,7 @@ coverage==5.5 # https://github.com/nedbat/coveragepy black==20.8b1 # https://github.com/ambv/black pylint-django==2.4.4 # https://github.com/PyCQA/pylint-django pre-commit==2.10.1 # https://github.com/pre-commit/pre-commit -isort==5.9.2 # https://github.com/PyCQA/isort +isort==4.3.21 # https://github.com/PyCQA/isort # Django # ------------------------------------------------------------------------------