diff --git a/Makefile b/Makefile index 39b8c83..d6e7583 100644 --- a/Makefile +++ b/Makefile @@ -40,3 +40,7 @@ teardown: docker-compose -f ${COMPOSE_ENV}.yml down -v recreate: teardown run + +# DO NOT USE IT IN PRODUCTION as it wipes all the data +load-test-data: + docker-compose -f local.yml exec -T django python manage.py loaddata test_data.yaml diff --git a/tmh_registry/registry/admin.py b/tmh_registry/registry/admin.py index afa5274..bb59730 100644 --- a/tmh_registry/registry/admin.py +++ b/tmh_registry/registry/admin.py @@ -39,10 +39,12 @@ class PatientAdmin(ExportMixin, admin.ModelAdmin): class PatientHospitalMappingAdmin(ExportMixin, admin.ModelAdmin): model = PatientHospitalMapping + @admin.register(PreferredHospital) class PreferredHospitalAdmin(ExportMixin, admin.ModelAdmin): model = PreferredHospital + @admin.register(Episode) class EpisodeAdmin(ExportMixin, admin.ModelAdmin): model = Episode diff --git a/tmh_registry/registry/api/serializers.py b/tmh_registry/registry/api/serializers.py index 25e4899..45ff352 100644 --- a/tmh_registry/registry/api/serializers.py +++ b/tmh_registry/registry/api/serializers.py @@ -25,6 +25,9 @@ class Meta: class EpisodeSerializer(ModelSerializer): + primary_surgeon = MedicalPersonnelSerializer(many=False) + secondary_surgeon = MedicalPersonnelSerializer(many=False) + tertiary_surgeon = MedicalPersonnelSerializer(many=False) surgeons = MedicalPersonnelSerializer(many=True) episode_type = CharField(source="get_episode_type_display") cepod = CharField(source="get_cepod_display") @@ -43,6 +46,9 @@ class Meta: "id", "surgery_date", "episode_type", + "primary_surgeon", + "secondary_surgeon", + "tertiary_surgeon", "surgeons", "cepod", "side", @@ -240,6 +246,7 @@ def to_representation(self, instance): return data + class PreferredHospitalReadSerializer(ModelSerializer): hospital = SerializerMethodField() @@ -251,13 +258,16 @@ def get_hospital(self, obj): request = self.context.get("request") if request and request.user: try: - medical_personnel = MedicalPersonnel.objects.get(user=request.user) + medical_personnel = MedicalPersonnel.objects.get( + user=request.user + ) if obj.medical_personnel == medical_personnel: return {"id": obj.hospital.id} except MedicalPersonnel.DoesNotExist: pass return None + class PatientHospitalMappingWriteSerializer(ModelSerializer): patient_id = PrimaryKeyRelatedField(queryset=Patient.objects.all()) hospital_id = PrimaryKeyRelatedField(queryset=Hospital.objects.all()) @@ -329,6 +339,9 @@ def create(self, validated_data): class EpisodeReadSerializer(ModelSerializer): patient_hospital_mapping = PatientHospitalMappingReadSerializer() + primary_surgeon = MedicalPersonnelSerializer(many=False) + secondary_surgeon = MedicalPersonnelSerializer(many=False) + tertiary_surgeon = MedicalPersonnelSerializer(many=False) surgeons = MedicalPersonnelSerializer(many=True) episode_type = CharField(source="get_episode_type_display") cepod = CharField(source="get_cepod_display") @@ -350,6 +363,9 @@ class Meta: "created", "surgery_date", "episode_type", + "primary_surgeon", + "secondary_surgeon", + "tertiary_surgeon", "surgeons", "cepod", "side", diff --git a/tmh_registry/registry/api/viewsets.py b/tmh_registry/registry/api/viewsets.py index eb32dad..1d3ffa8 100644 --- a/tmh_registry/registry/api/viewsets.py +++ b/tmh_registry/registry/api/viewsets.py @@ -19,6 +19,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from ...users.models import MedicalPersonnel from ..models import ( Discharge, Episode, @@ -42,18 +43,18 @@ PreferredHospitalReadSerializer, ReadPatientSerializer, ) -from ...users.models import MedicalPersonnel class HospitalViewSet(viewsets.ReadOnlyModelViewSet): queryset = Hospital.objects.all() serializer_class = HospitalSerializer + class PreferredHospitalViewSet(viewsets.GenericViewSet): serializer_class = PreferredHospitalReadSerializer permission_classes = [IsAuthenticated] - @action(detail=False, methods=['get']) + @action(detail=False, methods=["get"]) def retrieve_for_current_user(self, request, *args, **kwargs): user = request.user try: @@ -61,12 +62,15 @@ def retrieve_for_current_user(self, request, *args, **kwargs): except MedicalPersonnel.DoesNotExist: return Response({}, status=200) try: - preferred_hospital = PreferredHospital.objects.get(medical_personnel=medical_personnel) + preferred_hospital = PreferredHospital.objects.get( + medical_personnel=medical_personnel + ) except PreferredHospital.DoesNotExist: return Response({}, status=200) serializer = self.get_serializer(preferred_hospital) return Response(serializer.data) + class PatientFilterSet(FilterSet): hospital_id = NumberFilter( method="filter_hospital", diff --git a/tmh_registry/registry/fixtures/test_data.yaml b/tmh_registry/registry/fixtures/test_data.yaml new file mode 100644 index 0000000..b44f13b --- /dev/null +++ b/tmh_registry/registry/fixtures/test_data.yaml @@ -0,0 +1,167 @@ + +- model: auth.User + pk: 2 + fields: + password: 123456 + last_login: null + is_superuser: false + username: sur@doctor.co.uk + first_name: Sur + last_name: Doctor + email: sur@doctor.co.uk + is_staff: false + is_active: true + date_joined: 2024-09-11T00:00:00Z + +- model: auth.User + pk: 3 + fields: + password: 123456 + last_login: null + is_superuser: false + username: sur2@doctor.co.uk + first_name: Sur 2 + last_name: Doctor + email: sur2@doctor.co.uk + is_staff: false + is_active: true + date_joined: 2024-09-11T00:00:00Z + +- model: auth.User + pk: 4 + fields: + password: 123456 + last_login: null + is_superuser: false + username: sur3@doctor.co.uk + first_name: Sur 3 + last_name: Doctor + email: sur3@doctor.co.uk + is_staff: false + is_active: true + date_joined: 2024-09-11T00:00:00Z + +- model: users.MedicalPersonnel + pk: 2 + fields: + user_id: 2 + level: Lead Surgeon + +- model: users.MedicalPersonnel + pk: 3 + fields: + user_id: 3 + level: Lead Surgeon + +- model: users.MedicalPersonnel + pk: 4 + fields: + user_id: 4 + level: Lead Surgeon + +- model: registry.Hospital + pk: 10 + fields: + name: Test Hospital + address: Here and there Address + +- model: registry.Hospital + pk: 11 + fields: + name: Different Hospital + address: Nowhere + + +- model: registry.Patient + pk: 100 + fields: + full_name: Patient Boy + national_id: National Id + day_of_birth: 18 + month_of_birth: 3 + year_of_birth: 2002 + gender: MALE + phone_1: 0123456789 + phone_2: 9876543210 + address: All the way + created_at: 2024-09-11 + updated_at: 2024-09-11 + +- model: registry.PatientHospitalMapping + pk: 1000 + fields: + patient_id: 100 + hospital_id: 10 + patient_hospital_id: 1100 + +- model: registry.Episode + pk: 10000 + fields: + patient_hospital_mapping: 1000 + created: 2024-09-11T00:00:00Z + surgery_date: 2024-09-11T00:00:00Z + episode_type: INGUINAL + primary_surgeon_id: null + secondary_surgeon_id: null + tertiary_surgeon_id: null + surgeons: [ 1, 2, 3, 4 ] + cepod: PLANNED + side: LEFT + occurence: PRIMARY + type: DIRECT + size: VERY_SMALL + complexity: SIMPLE + mesh_type: TNMHP + anaesthetic_type: LOCAL + diathermy_used: false + antibiotic_used: false + antibiotic_type: null + comments: Comment here + +- model: registry.Episode + pk: 10001 + fields: + patient_hospital_mapping: 1000 + created: 2024-09-11T00:00:00Z + surgery_date: 2024-09-11T00:00:00Z + episode_type: INGUINAL + primary_surgeon_id: null + secondary_surgeon_id: null + tertiary_surgeon_id: null + surgeons: [ 1, 2 ] + cepod: PLANNED + side: LEFT + occurence: PRIMARY + type: DIRECT + size: VERY_SMALL + complexity: SIMPLE + mesh_type: TNMHP + anaesthetic_type: LOCAL + diathermy_used: false + antibiotic_used: false + antibiotic_type: null + comments: Comment here + +- model: registry.Episode + pk: 10002 + fields: + patient_hospital_mapping: 1000 + created: 2024-09-11T00:00:00Z + surgery_date: 2024-09-11T00:00:00Z + episode_type: INGUINAL + primary_surgeon_id: null + secondary_surgeon_id: null + tertiary_surgeon_id: null + surgeons: [] + cepod: PLANNED + side: LEFT + occurence: PRIMARY + type: DIRECT + size: VERY_SMALL + complexity: SIMPLE + mesh_type: TNMHP + anaesthetic_type: LOCAL + diathermy_used: false + antibiotic_used: false + antibiotic_type: null + comments: Comment here diff --git a/tmh_registry/registry/migrations/0036_followup_recurrence.py b/tmh_registry/registry/migrations/0036_followup_recurrence.py index 13c0fc8..d676750 100644 --- a/tmh_registry/registry/migrations/0036_followup_recurrence.py +++ b/tmh_registry/registry/migrations/0036_followup_recurrence.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('registry', '0035_episode_comments'), + ("registry", "0035_episode_comments"), ] operations = [ migrations.AddField( - model_name='followup', - name='recurrence', + model_name="followup", + name="recurrence", field=models.BooleanField(null=True), ), ] diff --git a/tmh_registry/registry/migrations/0037_preferredhospital.py b/tmh_registry/registry/migrations/0037_preferredhospital.py index 2ae664a..848e7b8 100644 --- a/tmh_registry/registry/migrations/0037_preferredhospital.py +++ b/tmh_registry/registry/migrations/0037_preferredhospital.py @@ -7,21 +7,43 @@ class Migration(migrations.Migration): dependencies = [ - ('users', '0008_auto_20210630_1407'), - ('registry', '0036_followup_recurrence'), + ("users", "0008_auto_20210630_1407"), + ("registry", "0036_followup_recurrence"), ] operations = [ migrations.CreateModel( - name='PreferredHospital', + name="PreferredHospital", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='preferred_by_medical_personnel', to='registry.hospital')), - ('medical_personnel', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='preferred_hospital', to='users.medicalpersonnel')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "hospital", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="preferred_by_medical_personnel", + to="registry.hospital", + ), + ), + ( + "medical_personnel", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="preferred_hospital", + to="users.medicalpersonnel", + ), + ), ], options={ - 'verbose_name_plural': 'Medical Personnel Preferred Hospitals', - 'unique_together': {('medical_personnel', 'hospital')}, + "verbose_name_plural": "Medical Personnel Preferred Hospitals", + "unique_together": {("medical_personnel", "hospital")}, }, ), ] diff --git a/tmh_registry/registry/migrations/0038_auto_20240915_1210.py b/tmh_registry/registry/migrations/0038_auto_20240915_1210.py new file mode 100644 index 0000000..27a676d --- /dev/null +++ b/tmh_registry/registry/migrations/0038_auto_20240915_1210.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.3 on 2024-09-15 12:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0037_preferredhospital"), + ] + + operations = [ + migrations.AlterModelOptions( + name="patienthospitalmapping", + options={"verbose_name_plural": "Patient-Hospital Mappings"}, + ), + ] diff --git a/tmh_registry/registry/migrations/0039_episode_primary_surgeon.py b/tmh_registry/registry/migrations/0039_episode_primary_surgeon.py new file mode 100644 index 0000000..0cfad03 --- /dev/null +++ b/tmh_registry/registry/migrations/0039_episode_primary_surgeon.py @@ -0,0 +1,51 @@ +# Generated by Django 3.1.3 on 2024-09-18 15:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0008_auto_20210630_1407"), + ("registry", "0038_auto_20240915_1210"), + ] + + operations = [ + migrations.AddField( + model_name="episode", + name="primary_surgeon", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="primary_surgeon", + to="users.medicalpersonnel", + ), + ), + migrations.AddField( + model_name="episode", + name="secondary_surgeon", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="secondary_surgeon", + to="users.medicalpersonnel", + ), + ), + migrations.AddField( + model_name="episode", + name="tertiary_surgeon", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="tertiary_surgeon", + to="users.medicalpersonnel", + ), + ), + ] diff --git a/tmh_registry/registry/migrations/0040_populate_primary_surgeon_20240916_1320.py b/tmh_registry/registry/migrations/0040_populate_primary_surgeon_20240916_1320.py new file mode 100644 index 0000000..73eac83 --- /dev/null +++ b/tmh_registry/registry/migrations/0040_populate_primary_surgeon_20240916_1320.py @@ -0,0 +1,29 @@ +from django.db import migrations + + +def get_primary_surgeon_from_episode_surgeons(apps, schema_editor): + Episode = apps.get_model("registry", "episode") + + for episode in Episode.objects.all(): + print("\n Migrating episode's surgeons: ") + print(episode.id) + MedicalPersonnel = episode.surgeons + surgeons = MedicalPersonnel.all() + if len(surgeons) > 0: + episode.primary_surgeon = surgeons[0] + if len(surgeons) > 1: + episode.secondary_surgeon = surgeons[1] + if len(surgeons) > 2: + episode.tertiary_surgeon = surgeons[2] + episode.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("registry", "0039_episode_primary_surgeon"), + ] + + operations = [ + migrations.RunPython(get_primary_surgeon_from_episode_surgeons) + ] diff --git a/tmh_registry/registry/models.py b/tmh_registry/registry/models.py index a1ec58e..8326b65 100644 --- a/tmh_registry/registry/models.py +++ b/tmh_registry/registry/models.py @@ -2,6 +2,7 @@ from django.db.models import ( CASCADE, + SET_DEFAULT, BooleanField, CharField, DateField, @@ -78,23 +79,25 @@ class Meta: def __str__(self): return f"Patient {self.patient.full_name} ({self.patient_hospital_id}) - Hospital {self.hospital.name}" + class PreferredHospital(Model): medical_personnel = OneToOneField( MedicalPersonnel, on_delete=CASCADE, related_name="preferred_hospital" ) hospital = ForeignKey( - Hospital, on_delete=CASCADE, related_name="preferred_by_medical_personnel" + Hospital, + on_delete=CASCADE, + related_name="preferred_by_medical_personnel", ) class Meta: - unique_together = ( - ("medical_personnel", "hospital"), - ) + unique_together = (("medical_personnel", "hospital"),) verbose_name_plural = "Medical Personnel Preferred Hospitals" def __str__(self): return f"{self.medical_personnel.user.first_name} {self.medical_personnel.user.last_name} ({self.medical_personnel.user.username}) - Hospital {self.hospital.name}" + class Episode(Model): class EpisodeChoices(TextChoices): INGUINAL = ("INGUINAL", "Inguinal Mesh Hernia Repair") @@ -157,6 +160,30 @@ class AnaestheticChoices(TextChoices): created = DateTimeField(auto_now_add=True) surgery_date = DateField(null=True, blank=True) episode_type = CharField(max_length=128, choices=EpisodeChoices.choices) + primary_surgeon = ForeignKey( + MedicalPersonnel, + on_delete=SET_DEFAULT, + blank=True, + null=True, + default=None, + related_name="primary_surgeon", + ) + secondary_surgeon = ForeignKey( + MedicalPersonnel, + on_delete=SET_DEFAULT, + blank=True, + null=True, + default=None, + related_name="secondary_surgeon", + ) + tertiary_surgeon = ForeignKey( + MedicalPersonnel, + on_delete=SET_DEFAULT, + blank=True, + null=True, + default=None, + related_name="tertiary_surgeon", + ) surgeons = ManyToManyField(MedicalPersonnel) cepod = CharField(max_length=16, choices=CepodChoices.choices) side = CharField(max_length=16, choices=SideChoices.choices) diff --git a/tmh_registry/registry/urls.py b/tmh_registry/registry/urls.py index 5cc9600..616e1c8 100644 --- a/tmh_registry/registry/urls.py +++ b/tmh_registry/registry/urls.py @@ -18,7 +18,11 @@ router.register(r"episodes", EpisodeViewset) router.register(r"discharges", DischargeViewset) router.register(r"follow-ups", FollowUpViewset) -router.register(r"preferred-hospital", PreferredHospitalViewSet, basename='preferred-hospital') +router.register( + r"preferred-hospital", + PreferredHospitalViewSet, + basename="preferred-hospital", +) urlpatterns = [ path("", include(router.urls)),