From 1038f99d0e3413dc77d4b7c74949e0f8fc9a8763 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Wed, 21 Aug 2024 23:37:12 +0530 Subject: [PATCH] migrating feature field to array field (#2223) * migrating feature to array field * updated serializer for feature field * Added validations and tests * Added tests * added integer choice enum for features * fixed migrations * deleted merge migration and updated the numbers * fixed dummy data in facility.json * fixed dummy data in facility.json * updated migration number --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/api/serializers/facility.py | 17 +++++- ...8_rename_features_facility_old_features.py | 56 +++++++++++++++++ care/facility/models/facility.py | 27 ++++++++- care/facility/tests/test_facility_api.py | 60 ++++++++++++++++++- data/dummy/facility.json | 40 ++++++------- 5 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 care/facility/migrations/0448_rename_features_facility_old_features.py diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index c7380edb9d..dba13cd5b1 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -48,7 +48,10 @@ class FacilityBasicInfoSerializer(serializers.ModelSerializer): state_object = StateSerializer(source="state", read_only=True) facility_type = serializers.SerializerMethodField() read_cover_image_url = serializers.CharField(read_only=True) - features = serializers.MultipleChoiceField(choices=FEATURE_CHOICES) + features = serializers.ListField( + child=serializers.ChoiceField(choices=FEATURE_CHOICES), + required=False, + ) patient_count = serializers.SerializerMethodField() bed_count = serializers.SerializerMethodField() @@ -96,7 +99,10 @@ class FacilitySerializer(FacilityBasicInfoSerializer): # } read_cover_image_url = serializers.URLField(read_only=True) # location = PointField(required=False) - features = serializers.MultipleChoiceField(choices=FEATURE_CHOICES) + features = serializers.ListField( + child=serializers.ChoiceField(choices=FEATURE_CHOICES), + required=False, + ) bed_count = serializers.SerializerMethodField() class Meta: @@ -148,6 +154,13 @@ def validate_middleware_address(self, value): MiddlewareDomainAddressValidator()(value) return value + def validate_features(self, value): + if len(value) != len(set(value)): + raise serializers.ValidationError( + "Features should not contain duplicate values." + ) + return value + def create(self, validated_data): validated_data["created_by"] = self.context["request"].user return super().create(validated_data) diff --git a/care/facility/migrations/0448_rename_features_facility_old_features.py b/care/facility/migrations/0448_rename_features_facility_old_features.py new file mode 100644 index 0000000000..77f3053b83 --- /dev/null +++ b/care/facility/migrations/0448_rename_features_facility_old_features.py @@ -0,0 +1,56 @@ +# Generated by Django 4.2.10 on 2024-06-03 06:24 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +def convert_features_to_array(apps, schema_editor): + Facility = apps.get_model("facility", "Facility") + + facilities_to_update = Facility.objects.filter(old_features__isnull=False) + + updated_facilities = [] + + for facility in facilities_to_update: + try: + facility.features = list(facility.old_features) + updated_facilities.append(facility) + except ValueError: + print(f"facility '{facility.name}' has invalid facility features") + + if updated_facilities: + Facility.objects.bulk_update(updated_facilities, ["features"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0447_patientconsultationevent_taken_at"), + ] + + operations = [ + migrations.RenameField( + model_name="facility", + old_name="features", + new_name="old_features", + ), + migrations.AddField( + model_name="facility", + name="features", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.SmallIntegerField( + choices=[ + (1, "CT Scan Facility"), + (2, "Maternity Care"), + (3, "X-Ray Facility"), + (4, "Neonatal Care"), + (5, "Operation Theater"), + (6, "Blood Bank"), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.RunPython(convert_features_to_array), + ] diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index f5a0013540..6d2f25ac3e 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator from django.db import models from multiselectfield import MultiSelectField @@ -38,6 +39,7 @@ (70, "KASP Ventilator beds"), ] +# to be removed in further PR FEATURE_CHOICES = [ (1, "CT Scan Facility"), (2, "Maternity Care"), @@ -47,9 +49,20 @@ (6, "Blood Bank"), ] + +class FacilityFeature(models.IntegerChoices): + CT_SCAN_FACILITY = 1, "CT Scan Facility" + MATERNITY_CARE = 2, "Maternity Care" + X_RAY_FACILITY = 3, "X-Ray Facility" + NEONATAL_CARE = 4, "Neonatal Care" + OPERATION_THEATER = 5, "Operation Theater" + BLOOD_BANK = 6, "Blood Bank" + + ROOM_TYPES.extend(BASE_ROOM_TYPES) REVERSE_ROOM_TYPES = reverse_choices(ROOM_TYPES) +REVERSE_FEATURE_CHOICES = reverse_choices(FEATURE_CHOICES) FACILITY_TYPES = [ (1, "Educational Inst"), @@ -160,13 +173,17 @@ class Facility(FacilityBaseModel, FacilityPermissionMixin): verified = models.BooleanField(default=False) facility_type = models.IntegerField(choices=FACILITY_TYPES) kasp_empanelled = models.BooleanField(default=False, blank=False, null=False) - features = MultiSelectField( + features = ArrayField( + models.SmallIntegerField(choices=FacilityFeature.choices), + blank=True, + null=True, + ) + old_features = MultiSelectField( choices=FEATURE_CHOICES, null=True, blank=True, max_length=get_max_length(FEATURE_CHOICES, None), ) - longitude = models.DecimalField( max_digits=22, decimal_places=16, null=True, blank=True ) @@ -243,6 +260,12 @@ def save(self, *args, **kwargs) -> None: facility=self, user=self.created_by, created_by=self.created_by ) + @property + def get_features_display(self): + if not self.features: + return [] + return [FacilityFeature(f).label for f in self.features] + CSV_MAPPING = { "name": "Facility Name", "facility_type": "Facility Type", diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 60cdb08c36..0f6fef256d 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -23,7 +23,9 @@ def test_listing(self): def test_create(self): dist_admin = self.create_user("dist_admin", self.district, user_type=30) - sample_data = { + self.client.force_authenticate(user=dist_admin) + + sample_data_with_empty_feature_list = { "name": "Hospital X", "district": self.district.pk, "state": self.state.pk, @@ -33,7 +35,61 @@ def test_create(self): "pincode": 390024, "features": [], } - self.client.force_authenticate(user=dist_admin) + response = self.client.post( + "/api/v1/facility/", sample_data_with_empty_feature_list + ) + self.assertIs(response.status_code, status.HTTP_201_CREATED) + + sample_data_with_invalid_choice = { + "name": "Hospital X", + "district": self.district.pk, + "state": self.state.pk, + "local_body": self.local_body.pk, + "facility_type": "Educational Inst", + "address": "Nearby", + "pincode": 390024, + "features": [1020, 2, 4, 5], + } + response = self.client.post( + "/api/v1/facility/", sample_data_with_invalid_choice + ) + + self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["features"][0][0].code, "invalid_choice") + self.assertEqual( + response.data["features"][0][0], '"1020" is not a valid choice.' + ) + + sample_data_with_duplicate_choices = { + "name": "Hospital X", + "district": self.district.pk, + "state": self.state.pk, + "local_body": self.local_body.pk, + "facility_type": "Educational Inst", + "address": "Nearby", + "pincode": 390024, + "features": [1, 1], + } + response = self.client.post( + "/api/v1/facility/", sample_data_with_duplicate_choices + ) + + self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["features"][0], + "Features should not contain duplicate values.", + ) + + sample_data = { + "name": "Hospital X", + "district": self.district.pk, + "state": self.state.pk, + "local_body": self.local_body.pk, + "facility_type": "Educational Inst", + "address": "Nearby", + "pincode": 390024, + "features": [1, 2], + } response = self.client.post("/api/v1/facility/", sample_data) self.assertIs(response.status_code, status.HTTP_201_CREATED) fac_id = response.data["id"] diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 7b7485df8c..45f4a12b71 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -12,7 +12,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "1,2,3,4,5,6", + "features": [1,2,3,4,5,6], "longitude": null, "latitude": null, "pincode": 670000, @@ -49,7 +49,7 @@ "verified": false, "facility_type": 1300, "kasp_empanelled": false, - "features": "1,6", + "features": [1,6], "longitude": null, "latitude": null, "pincode": 670112, @@ -86,7 +86,7 @@ "verified": false, "facility_type": 1500, "kasp_empanelled": false, - "features": "1,4,6", + "features": [1,4,6], "longitude": "78.6757364624373000", "latitude": "21.4009146842158660", "pincode": 670000, @@ -123,7 +123,7 @@ "verified": false, "facility_type": 1510, "kasp_empanelled": false, - "features": "1,3,5", + "features": [1,3,5], "longitude": "75.2139014820876600", "latitude": "18.2774285038890340", "pincode": 670000, @@ -160,7 +160,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -197,7 +197,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -234,7 +234,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -271,7 +271,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -308,7 +308,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -345,7 +345,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -382,7 +382,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -419,7 +419,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -456,7 +456,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -493,7 +493,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -530,7 +530,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -567,7 +567,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -604,7 +604,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -641,7 +641,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -678,7 +678,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001, @@ -715,7 +715,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": "", + "features": [], "longitude": null, "latitude": null, "pincode": 682001,