From 1115a8fe9a4de6be090b34fec42872abf9c89fa6 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 2 Apr 2024 22:37:20 +0530 Subject: [PATCH 1/9] Dropped `age` column from patient registration and annotated age in patient viewset (#1966) * drop field `age` and compute age on demand * Update filters and related usages * add min value validator for year of birth * remove age from test utils * remove unnecessary default * Add field `death_datetime` in patient registration * annotate age in days * suggestion from code review * revert unintended changes * skip serializing age * rebase migrations * rebase migrations * drop age from dummy data * Revert "drop age from dummy data" This reverts commit 9964b853e24e30ff332a8c38128cd484510f8c6f. * drop patient registration's age from dummy data * select only `last_consultation` in custom migration Co-authored-by: Aakash Singh * format code * rebase migrations * rebase migrations --------- Co-authored-by: Aakash Singh --- care/facility/admin.py | 2 +- care/facility/api/serializers/patient.py | 23 +++-- care/facility/api/viewsets/patient.py | 87 ++++++++++++++----- ..._add_patientregistration_death_datetime.py | 65 ++++++++++++++ care/facility/models/patient.py | 29 ++----- care/facility/models/patient_consultation.py | 14 +++ care/facility/models/patient_icmr.py | 27 +++--- care/utils/tests/test_utils.py | 1 - data/dummy/facility.json | 18 ---- 9 files changed, 177 insertions(+), 89 deletions(-) create mode 100644 care/facility/migrations/0424_remove_patientregistration_age_and_add_patientregistration_death_datetime.py diff --git a/care/facility/admin.py b/care/facility/admin.py index bc5b3fb650..db0ac6a173 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -147,7 +147,7 @@ class AmbulanceDriverAdmin(DjangoQLSearchMixin, admin.ModelAdmin): class PatientAdmin(DjangoQLSearchMixin, admin.ModelAdmin): - list_display = ("id", "name", "age", "gender") + list_display = ("id", "name", "year_of_birth", "gender") djangoql_completion_enabled_by_default = True diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 28993af4ca..5ba675a734 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -121,13 +121,12 @@ class Meta: "created_by", "deleted", "ongoing_medication", - "year_of_birth", "meta_info", "countries_travelled_old", "allergies", "external_id", ) - read_only = TIMESTAMP_FIELDS + read_only = TIMESTAMP_FIELDS + ("death_datetime",) class PatientContactDetailsSerializer(serializers.ModelSerializer): @@ -223,12 +222,16 @@ class Meta: model = PatientRegistration exclude = ( "deleted", - "year_of_birth", "countries_travelled_old", "external_id", ) include = ("contacted_patients",) - read_only = TIMESTAMP_FIELDS + ("last_edited", "created_by", "is_active") + read_only = TIMESTAMP_FIELDS + ( + "last_edited", + "created_by", + "is_active", + "death_datetime", + ) # def get_last_consultation(self, obj): # last_consultation = PatientConsultation.objects.filter(patient=obj).last() @@ -250,13 +253,15 @@ def validate_countries_travelled(self, value): def validate(self, attrs): validated = super().validate(attrs) - if ( - not self.partial - and not validated.get("age") - and not validated.get("date_of_birth") + if not self.partial and not ( + validated.get("year_of_birth") or validated.get("date_of_birth") ): raise serializers.ValidationError( - {"non_field_errors": ["Either age or date_of_birth should be passed"]} + { + "non_field_errors": [ + "Either year_of_birth or date_of_birth should be passed" + ] + } ) if validated.get("is_vaccinated"): diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 704dc98bbb..f55a60030a 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -5,7 +5,18 @@ from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity from django.db import models -from django.db.models import Case, OuterRef, Q, Subquery, When +from django.db.models import ( + Case, + ExpressionWrapper, + F, + Func, + OuterRef, + Q, + Subquery, + Value, + When, +) +from django.db.models.functions import Coalesce, ExtractDay, Now from django.db.models.query import QuerySet from django_filters import rest_framework as filters from djqscsv import render_to_csv_response @@ -333,25 +344,61 @@ class PatientViewSet( ] permission_classes = (IsAuthenticated, DRYPermissions) lookup_field = "external_id" - queryset = PatientRegistration.objects.all().select_related( - "local_body", - "district", - "state", - "ward", - "assigned_to", - "facility", - "facility__ward", - "facility__local_body", - "facility__district", - "facility__state", - # "nearest_facility", - # "nearest_facility__local_body", - # "nearest_facility__district", - # "nearest_facility__state", - "last_consultation", - "last_consultation__assigned_to", - "last_edited", - "created_by", + queryset = ( + PatientRegistration.objects.all() + .select_related( + "local_body", + "district", + "state", + "ward", + "assigned_to", + "facility", + "facility__ward", + "facility__local_body", + "facility__district", + "facility__state", + # "nearest_facility", + # "nearest_facility__local_body", + # "nearest_facility__district", + # "nearest_facility__state", + "last_consultation", + "last_consultation__assigned_to", + "last_edited", + "created_by", + ) + .annotate( + coalesced_dob=Coalesce( + "date_of_birth", + Func( + F("year_of_birth"), + Value(1), + Value(1), + function="MAKE_DATE", + output_field=models.DateField(), + ), + output_field=models.DateField(), + ), + age_end=Case( + When(death_datetime__isnull=True, then=Now()), + default=F("death_datetime__date"), + ), + ) + .annotate( + age=Func( + Value("year"), + Func( + F("age_end"), + F("coalesced_dob"), + function="age", + ), + function="date_part", + output_field=models.IntegerField(), + ), + age_days=ExpressionWrapper( + ExtractDay(F("age_end") - F("coalesced_dob")), + output_field=models.IntegerField(), + ), + ) ) ordering_fields = [ "facility__name", diff --git a/care/facility/migrations/0424_remove_patientregistration_age_and_add_patientregistration_death_datetime.py b/care/facility/migrations/0424_remove_patientregistration_age_and_add_patientregistration_death_datetime.py new file mode 100644 index 0000000000..e051559ba8 --- /dev/null +++ b/care/facility/migrations/0424_remove_patientregistration_age_and_add_patientregistration_death_datetime.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.8 on 2024-03-13 07:03 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0423_patientconsultation_consent_records_and_more"), + ] + + def populate_patientregistration_death_datetime(apps, schema_editor): + PatientRegistration = apps.get_model("facility", "PatientRegistration") + + patients = ( + PatientRegistration.objects.only("last_consultation") + .filter(last_consultation__death_datetime__isnull=False) + .annotate(new_death_datetime=models.F("last_consultation__death_datetime")) + ) + + for patient in patients: + patient.death_datetime = patient.new_death_datetime + + PatientRegistration.objects.bulk_update( + patients, ["death_datetime"], batch_size=1000 + ) + + operations = [ + migrations.RemoveField( + model_name="historicalpatientregistration", + name="age", + ), + migrations.RemoveField( + model_name="patientregistration", + name="age", + ), + migrations.AddField( + model_name="historicalpatientregistration", + name="death_datetime", + field=models.DateTimeField(default=None, null=True), + ), + migrations.AddField( + model_name="patientregistration", + name="death_datetime", + field=models.DateTimeField(default=None, null=True), + ), + migrations.AlterField( + model_name="historicalpatientregistration", + name="year_of_birth", + field=models.IntegerField( + null=True, validators=[django.core.validators.MinValueValidator(1900)] + ), + ), + migrations.AlterField( + model_name="patientregistration", + name="year_of_birth", + field=models.IntegerField( + null=True, validators=[django.core.validators.MinValueValidator(1900)] + ), + ), + migrations.RunPython( + populate_patientregistration_death_datetime, + migrations.RunPython.noop, + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 1c5408bebe..090fee13fe 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -1,10 +1,10 @@ -import datetime import enum from django.contrib.postgres.aggregates import ArrayAgg from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import JSONField +from django.utils import timezone from simple_history.models import HistoricalRecords from care.abdm.models import AbhaNumber @@ -109,7 +109,6 @@ class TestTypeEnum(enum.Enum): # name_old = EncryptedCharField(max_length=200, default="") name = models.CharField(max_length=200, default="") - age = models.PositiveIntegerField(null=True, blank=True) gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) # phone_number_old = EncryptedCharField(max_length=14, validators=[phone_number_regex], default="") @@ -128,7 +127,8 @@ class TestTypeEnum(enum.Enum): pincode = models.IntegerField(default=0, blank=True, null=True) date_of_birth = models.DateField(default=None, null=True) - year_of_birth = models.IntegerField(default=0, null=True) + year_of_birth = models.IntegerField(validators=[MinValueValidator(1900)], null=True) + death_datetime = models.DateTimeField(default=None, null=True) nationality = models.CharField( max_length=255, default="", verbose_name="Nationality of Patient" @@ -426,7 +426,7 @@ class TestTypeEnum(enum.Enum): objects = BaseManager() def __str__(self): - return "{} - {} - {}".format(self.name, self.age, self.get_gender_display()) + return f"{self.name} - {self.year_of_birth} - {self.get_gender_display()}" @property def tele_consultation_history(self): @@ -458,30 +458,13 @@ def save(self, *args, **kwargs) -> None: if self.district is not None: self.state = self.district.state - self.year_of_birth = ( - self.date_of_birth.year - if self.date_of_birth is not None - else datetime.datetime.now().year - self.age - ) - - today = datetime.date.today() - if self.date_of_birth: - self.age = ( - today.year - - self.date_of_birth.year - - ( - (today.month, today.day) - < (self.date_of_birth.month, self.date_of_birth.day) - ) - ) - elif self.year_of_birth: - self.age = today.year - self.year_of_birth + self.year_of_birth = self.date_of_birth.year self.date_of_receipt_of_information = ( self.date_of_receipt_of_information if self.date_of_receipt_of_information is not None - else datetime.datetime.now() + else timezone.now() ) self._alias_recovery_to_recovered() diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 8f6797f997..91dad91de4 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -286,6 +286,20 @@ def get_related_consultation(self): def __str__(self): return f"{self.patient.name}<>{self.facility.name}" + def save(self, *args, **kwargs): + """ + # Removing Patient Hospital Change on Referral + if not self.pk or self.referred_to is not None: + # pk is None when the consultation is created + # referred to is not null when the person is being referred to a new facility + self.patient.facility = self.referred_to or self.facility + self.patient.save() + """ + if self.death_datetime and self.patient.death_datetime != self.death_datetime: + self.patient.death_datetime = self.death_datetime + self.patient.save(update_fields=["death_datetime"]) + super(PatientConsultation, self).save(*args, **kwargs) + class Meta: constraints = [ models.CheckConstraint( diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py index 5d33c25ed3..677b278322 100644 --- a/care/facility/models/patient_icmr.py +++ b/care/facility/models/patient_icmr.py @@ -1,6 +1,7 @@ import datetime from dateutil.relativedelta import relativedelta +from django.utils import timezone from care.facility.models import ( DISEASE_CHOICES_MAP, @@ -45,26 +46,18 @@ class Meta: # instance.__class__ = PatientSampleICMR # return instance + def get_age_delta(self): + start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date() + end = (self.death_datetime or timezone.now()).date() + return relativedelta(end, start) + @property - def age_years(self): - if self.date_of_birth is not None: - age_years = relativedelta(datetime.datetime.now(), self.date_of_birth).years - else: - age_years = relativedelta( - datetime.datetime.now(), - datetime.datetime(year=self.year_of_birth, month=1, day=1), - ).years - return age_years + def age_years(self) -> int: + return self.get_age_delta().year @property - def age_months(self): - if self.date_of_birth is None or self.year_of_birth is None: - age_months = 0 - else: - age_months = relativedelta( - datetime.datetime.now(), self.date_of_birth - ).months - return age_months + def age_months(self) -> int: + return self.get_age_delta().months @property def email(self): diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 446249e949..fcc93584b4 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -238,7 +238,6 @@ def create_facility( def get_patient_data(cls, district, state) -> dict: return { "name": "Foo", - "age": 32, "date_of_birth": date(1992, 4, 1), "gender": 2, "is_medical_worker": True, diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 2104a23cd1..9976ca3646 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -4393,7 +4393,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient", - "age": 18, "gender": 1, "phone_number": "+919987455444", "emergency_phone_number": "+919898797775", @@ -4474,7 +4473,6 @@ "nearest_facility": null, "meta_info": null, "name": "Test E2E User", - "age": 22, "gender": 1, "phone_number": "+919765259927", "emergency_phone_number": "+919228973557", @@ -4555,7 +4553,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 1", - "age": 22, "gender": 1, "phone_number": "+919192495353", "emergency_phone_number": "+919460491040", @@ -4636,7 +4633,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 2", - "age": 22, "gender": 1, "phone_number": "+919112608904", "emergency_phone_number": "+919110616234", @@ -4717,7 +4713,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 3", - "age": 22, "gender": 1, "phone_number": "+919640229897", "emergency_phone_number": "+919135436547", @@ -4798,7 +4793,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 4", - "age": 22, "gender": 1, "phone_number": "+919762277015", "emergency_phone_number": "+919342634016", @@ -4879,7 +4873,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 5", - "age": 22, "gender": 1, "phone_number": "+919303212282", "emergency_phone_number": "+919229738916", @@ -4960,7 +4953,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 6", - "age": 22, "gender": 1, "phone_number": "+919740701377", "emergency_phone_number": "+919321666516", @@ -5041,7 +5033,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 7", - "age": 22, "gender": 1, "phone_number": "+919148299129", "emergency_phone_number": "+919267280161", @@ -5122,7 +5113,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 8", - "age": 22, "gender": 1, "phone_number": "+919490490290", "emergency_phone_number": "+919828674710", @@ -5203,7 +5193,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 9", - "age": 22, "gender": 1, "phone_number": "+919983927490", "emergency_phone_number": "+919781111140", @@ -5284,7 +5273,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 10", - "age": 22, "gender": 1, "phone_number": "+919849511866", "emergency_phone_number": "+919622326248", @@ -5365,7 +5353,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 11", - "age": 22, "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5446,7 +5433,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 12", - "age": 22, "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", @@ -5527,7 +5513,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 13", - "age": 22, "gender": 1, "phone_number": "+919292990239", "emergency_phone_number": "+919992258784", @@ -5608,7 +5593,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 14", - "age": 22, "gender": 1, "phone_number": "+919650206292", "emergency_phone_number": "+919596454242", @@ -5689,7 +5673,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 15", - "age": 22, "gender": 1, "phone_number": "+919266236581", "emergency_phone_number": "+919835286558", @@ -5770,7 +5753,6 @@ "nearest_facility": null, "meta_info": null, "name": "Dummy Patient 16", - "age": 22, "gender": 1, "phone_number": "+919243083817", "emergency_phone_number": "+919924971004", From 182b43fcc15e1d61328d7b5284de39734f5c3296 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:12:52 +0530 Subject: [PATCH 2/9] Bump the boto group with 2 updates (#2044) Bumps the boto group with 2 updates: [boto3](https://github.com/boto/boto3) and [boto3-stubs](https://github.com/youtype/mypy_boto3_builder). Updates `boto3` from 1.34.65 to 1.34.75 - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.34.65...1.34.75) Updates `boto3-stubs` from 1.34.65 to 1.34.75 - [Release notes](https://github.com/youtype/mypy_boto3_builder/releases) - [Commits](https://github.com/youtype/mypy_boto3_builder/commits) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: boto - dependency-name: boto3-stubs dependency-type: direct:development update-type: version-update:semver-patch dependency-group: boto ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vignesh Hari --- Pipfile | 4 ++-- Pipfile.lock | 29 +++++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Pipfile b/Pipfile index 61b01767b0..d4ffa91468 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.2.1" -boto3 = "==1.34.65" +boto3 = "==1.34.75" celery = "==5.3.6" django = "==4.2.10" django-environ = "==0.11.2" @@ -48,7 +48,7 @@ redis-om = "==0.2.1" [dev-packages] black = "==23.9.1" -boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.65"} +boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.75"} coverage = "==7.4.0" debugpy = "==1.8.1" django-coverage-plugin = "==3.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index 90f6080148..8bbef9e4ff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ea4fe23094588fde2796a4107473d14dc5a47a9795a08582da5182dac2b5c4fb" + "sha256": "d3f8439435571930893eb20d0599cf4de93bfb4646965c3725af4fdd966b8138" }, "pipfile-spec": 6, "requires": { @@ -94,20 +94,20 @@ }, "boto3": { "hashes": [ - "sha256:b611de58ab28940a36c77d7ef9823427ebf25d5ee8277b802f9979b14e780534", - "sha256:db97f9c29f1806cf9020679be0dd5ffa2aff2670e28e0e2046f98b979be498a4" + "sha256:ba5d2104bba4370766036d64ad9021eb6289d154265852a2a821ec6a5e816faa", + "sha256:eaec72fda124084105a31bcd67eafa1355b34df6da70cadae0c0f262d8a4294f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.65" + "version": "==1.34.75" }, "botocore": { "hashes": [ - "sha256:92560f8fbdaa9dd221212a3d3a7609219ba0bbf308c13571674c0cda9d8f39e1", - "sha256:fd7d8742007c220f897cb126b8916ca0cf3724a739d4d716aa5385d7f9d8aeb1" + "sha256:06113ee2587e6160211a6bd797e135efa6aa21b5bde97bf455c02f7dff40203c", + "sha256:1d7f683d99eba65076dfb9af3b42fa967c64f11111d9699b65757420902aa002" ], "markers": "python_version >= '3.8'", - "version": "==1.34.66" + "version": "==1.34.75" }, "celery": { "hashes": [ @@ -1309,7 +1309,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.6'", "version": "==2.2.1" }, "vine": { @@ -1412,11 +1412,12 @@ "s3" ], "hashes": [ - "sha256:105da4a04dcb5e4ddc90f21ab8b24a3423ecfacb4775b8ccd3879574e5dce358", - "sha256:2afd696c8bb4daf8890ecd75a720e1733cd8b8556eaecc92c36f9b56fc6013bd" + "sha256:78093a0bf5a03bc66a79d6cddb9f0eb67b67ed6b008cba4cf394c0c9d11de2c1", + "sha256:bb55fe97f474ea800c762592d81369bb6c23a8e53a5b2d8497145f87c1d7640c" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.65" + "version": "==1.34.75" }, "botocore": { "hashes": [ @@ -1428,11 +1429,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:530ea7d66022ec6aa0ba0c5200a2aede5d30b839c632d00962f0cf4f806c6a51", - "sha256:a5aa1240c3c8ccc62d43916395943896afa81399dc5d4203127cc0ffba20f999" + "sha256:0c3835c775db1387246c1ba8063b197604462fba8603d9b36b5dc60297197b2f", + "sha256:463248fd1d6e7b68a0c57bdd758d04c6bd0c5c2c3bfa81afdf9d64f0930b59bc" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.66" + "version": "==1.34.69" }, "certifi": { "hashes": [ From ce619ef580c596bbee7423e69b0c90bc78471569 Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:05:27 +0530 Subject: [PATCH 3/9] Add validations for negative values in inventory and min quanityt (#2005) Add validations for negative values in inventory Co-authored-by: Aakash Singh --- ..._facilityinventorylog_quantity_and_more.py | 27 +++++++++++++++++++ care/facility/models/inventory.py | 5 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 care/facility/migrations/0422_alter_facilityinventorylog_quantity_and_more.py diff --git a/care/facility/migrations/0422_alter_facilityinventorylog_quantity_and_more.py b/care/facility/migrations/0422_alter_facilityinventorylog_quantity_and_more.py new file mode 100644 index 0000000000..561a0dbf61 --- /dev/null +++ b/care/facility/migrations/0422_alter_facilityinventorylog_quantity_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.10 on 2024-03-22 11:21 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0421_merge_20240318_1434"), + ] + + operations = [ + migrations.AlterField( + model_name="facilityinventorylog", + name="quantity", + field=models.FloatField( + default=0, validators=[django.core.validators.MinValueValidator(0.0)] + ), + ), + migrations.AlterField( + model_name="facilityinventoryminquantity", + name="min_quantity", + field=models.FloatField( + default=0, validators=[django.core.validators.MinValueValidator(0.0)] + ), + ), + ] diff --git a/care/facility/models/inventory.py b/care/facility/models/inventory.py index 934b0128f4..e0eb056e49 100644 --- a/care/facility/models/inventory.py +++ b/care/facility/models/inventory.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.core.validators import MinValueValidator from django.db import models from django.db.models import Index @@ -108,7 +109,7 @@ class FacilityInventoryLog(FacilityBaseModel, FacilityRelatedPermissionMixin): ) current_stock = models.FloatField(default=0) quantity_in_default_unit = models.FloatField(default=0) - quantity = models.FloatField(default=0) + quantity = models.FloatField(default=0, validators=[MinValueValidator(0.0)]) unit = models.ForeignKey( FacilityInventoryUnit, on_delete=models.SET_NULL, null=True, blank=False ) @@ -157,7 +158,7 @@ class FacilityInventoryMinQuantity(FacilityBaseModel, FacilityRelatedPermissionM item = models.ForeignKey( FacilityInventoryItem, on_delete=models.SET_NULL, null=True, blank=False ) - min_quantity = models.FloatField(default=0) + min_quantity = models.FloatField(default=0, validators=[MinValueValidator(0.0)]) class Meta: constraints = [ From 7aa73b9f5c624c4dca0e680b0b92bf71d9718bdb Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 3 Apr 2024 19:47:48 +0530 Subject: [PATCH 4/9] Added github workflow for automated release (#2045) --- .github/workflows/release.yml | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..1a418a889a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Create Release on Branch Push + +on: + push: + branches: + - production + +permissions: + contents: write + +jobs: + release: + name: Release on Push + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Necessary to fetch all tags + + - name: Calculate next tag + id: calc_tag + run: | + YEAR=$(date +"%y") + WEEK=$(date +"%V") + LAST_TAG=$(git tag -l "v$YEAR.$WEEK.*" | sort -V | tail -n1) + LAST_TAG=$(echo "$LAST_TAG" | tr -d '\r' | sed 's/[[:space:]]*$//') + echo "Last Tag: $LAST_TAG" + if [[ $LAST_TAG == "" ]]; then + MINOR=0 + else + MINOR=$(echo $LAST_TAG | awk -F '.' '{print $NF}') + echo "Minor Version: $MINOR" + MINOR=$((MINOR + 1)) + fi + TAG="v$YEAR.$WEEK.$MINOR" + echo "TAG=$TAG" >> $GITHUB_ENV + echo "Next Tag: $TAG" + - name: Configure git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + - name: Create and push tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + - name: Create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "$TAG" \ + --repo="$GITHUB_REPOSITORY" \ + --title="$TAG" \ + --generate-notes \ + --draft From ef53bec2dd44ce78588980ef76767c4a41086d79 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 3 Apr 2024 21:48:10 +0530 Subject: [PATCH 5/9] Fixed race condition that caused service to be unhealthy and merge migrations (#2048) --- .../migrations/0425_merge_20240403_2055.py | 15 +++++++++++++++ docker-compose.local.yaml | 10 ++++++++-- docker-compose.pre-built.yaml | 19 +++++++++++++++---- docker-compose.yaml | 9 +++++---- docker/dev.Dockerfile | 2 +- scripts/celery-dev.sh | 2 +- scripts/celery_beat-ecs.sh | 2 +- scripts/celery_beat.sh | 2 +- scripts/celery_worker-ecs.sh | 2 +- scripts/celery_worker.sh | 2 +- scripts/start-dev.sh | 2 +- scripts/start-ecs.sh | 2 +- scripts/start.sh | 2 +- 13 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 care/facility/migrations/0425_merge_20240403_2055.py diff --git a/care/facility/migrations/0425_merge_20240403_2055.py b/care/facility/migrations/0425_merge_20240403_2055.py new file mode 100644 index 0000000000..6668820484 --- /dev/null +++ b/care/facility/migrations/0425_merge_20240403_2055.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.10 on 2024-04-03 15:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0422_alter_facilityinventorylog_quantity_and_more"), + ( + "facility", + "0424_remove_patientregistration_age_and_add_patientregistration_death_datetime", + ), + ] + + operations = [] diff --git a/docker-compose.local.yaml b/docker-compose.local.yaml index 27d2a2ff63..aeb971ccd6 100644 --- a/docker-compose.local.yaml +++ b/docker-compose.local.yaml @@ -14,15 +14,21 @@ services: ports: - "9000:9000" - "9876:9876" #debugpy + restart: unless-stopped depends_on: - - db - - redis + db: + condition: service_started + redis: + condition: service_started + celery: + condition: service_healthy celery: image: care_local env_file: - ./docker/.local.env entrypoint: [ "bash", "scripts/celery-dev.sh" ] + restart: unless-stopped depends_on: - db - redis diff --git a/docker-compose.pre-built.yaml b/docker-compose.pre-built.yaml index 2d614adfe8..19dbd14194 100644 --- a/docker-compose.pre-built.yaml +++ b/docker-compose.pre-built.yaml @@ -6,9 +6,14 @@ services: env_file: - ./docker/.prebuilt.env entrypoint: [ "bash", "start-ecs.sh" ] + restart: unless-stopped depends_on: - - db - - redis + db: + condition: service_started + redis: + condition: service_started + celery-beat: + condition: service_healthy ports: - "9000:9000" @@ -17,15 +22,21 @@ services: env_file: - ./docker/.prebuilt.env entrypoint: [ "bash", "celery_worker-ecs.sh" ] + restart: unless-stopped depends_on: - - db - - redis + db: + condition: service_started + redis: + condition: service_started + celery-beat: + condition: service_healthy celery-beat: image: "ghcr.io/coronasafe/care:latest" env_file: - ./docker/.prebuilt.env entrypoint: [ "bash", "celery_beat-ecs.sh" ] + restart: unless-stopped depends_on: - db - redis diff --git a/docker-compose.yaml b/docker-compose.yaml index 2972849920..7959c927d3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ networks: services: db: image: postgres:alpine - restart: always + restart: unless-stopped env_file: - ./docker/.prebuilt.env volumes: @@ -15,12 +15,13 @@ services: redis: image: redis/redis-stack-server:6.2.6-v10 - restart: always + restart: unless-stopped volumes: - redis-data:/data localstack: image: localstack/localstack:latest + restart: unless-stopped environment: - AWS_DEFAULT_REGION=ap-south-1 - EDGE_PORT=4566 @@ -34,8 +35,8 @@ services: - "4566:4566" fidelius: - image: khavinshankar/fidelius:v1.0 - restart: always + image: khavinshankar/fidelius:latest + restart: unless-stopped volumes: postgres-data: diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index f71f5f6464..3a916db775 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -25,7 +25,7 @@ HEALTHCHECK \ --interval=10s \ --timeout=5s \ --start-period=10s \ - --retries=12 \ + --retries=24 \ CMD ["/app/scripts/healthcheck.sh"] WORKDIR /app diff --git a/scripts/celery-dev.sh b/scripts/celery-dev.sh index 4e1c0ad22c..ac63afe1b3 100755 --- a/scripts/celery-dev.sh +++ b/scripts/celery-dev.sh @@ -1,6 +1,6 @@ #!/bin/bash -printf "celery" >> /tmp/container-role +printf "celery" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/celery_beat-ecs.sh b/scripts/celery_beat-ecs.sh index beeb0e1ca6..e664458132 100755 --- a/scripts/celery_beat-ecs.sh +++ b/scripts/celery_beat-ecs.sh @@ -1,5 +1,5 @@ #!/bin/bash -printf "celery-beat" >> /tmp/container-role +printf "celery-beat" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/celery_beat.sh b/scripts/celery_beat.sh index abdc97efba..e4aa5d083f 100755 --- a/scripts/celery_beat.sh +++ b/scripts/celery_beat.sh @@ -1,5 +1,5 @@ #!/bin/bash -printf "celery-beat" >> /tmp/container-role +printf "celery-beat" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/celery_worker-ecs.sh b/scripts/celery_worker-ecs.sh index 701378b461..840b9e73b5 100755 --- a/scripts/celery_worker-ecs.sh +++ b/scripts/celery_worker-ecs.sh @@ -1,5 +1,5 @@ #!/bin/bash -printf "celery-worker" >> /tmp/container-role +printf "celery-worker" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/celery_worker.sh b/scripts/celery_worker.sh index bc291f737e..9c9e4d739d 100755 --- a/scripts/celery_worker.sh +++ b/scripts/celery_worker.sh @@ -1,5 +1,5 @@ #!/bin/bash -printf "celery-worker" >> /tmp/container-role +printf "celery-worker" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index f79ff45a20..b4996c3490 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -printf "api" >> /tmp/container-role +printf "api" > /tmp/container-role cd /app diff --git a/scripts/start-ecs.sh b/scripts/start-ecs.sh index ac9fb2c745..fb731bf198 100755 --- a/scripts/start-ecs.sh +++ b/scripts/start-ecs.sh @@ -4,7 +4,7 @@ set -o errexit set -o pipefail set -o nounset -printf "api" >> /tmp/container-role +printf "api" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" diff --git a/scripts/start.sh b/scripts/start.sh index d660d8ec9c..bebefa9a66 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -4,7 +4,7 @@ set -o errexit set -o pipefail set -o nounset -printf "api" >> /tmp/container-role +printf "api" > /tmp/container-role if [ -z "${DATABASE_URL}" ]; then export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" From 397695dd4faee85377071bac893204248b5c54e6 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 4 Apr 2024 21:15:16 +0530 Subject: [PATCH 6/9] Update discharge summary to reflect age and year of birth (#2053) * Update discharge summary to reflect age and year of birth fixes #2052 * suggestions from codereview --- care/facility/models/patient.py | 6 ++++++ .../reports/patient_discharge_summary_pdf.html | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 090fee13fe..d40733515e 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -1,5 +1,6 @@ import enum +from dateutil.relativedelta import relativedelta from django.contrib.postgres.aggregates import ArrayAgg from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -470,6 +471,11 @@ def save(self, *args, **kwargs) -> None: self._alias_recovery_to_recovered() super().save(*args, **kwargs) + def get_age(self) -> int: + start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date() + end = (self.death_datetime or timezone.now()).date() + return relativedelta(end, start).years + def annotate_diagnosis_ids(*args, **kwargs): return ArrayAgg( "last_consultation__diagnoses__diagnosis_id", diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html index 8884e1d43a..48c05155b1 100644 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -46,11 +46,17 @@

Gender: {{patient.get_gender_display}}

- Age: {{patient.age}} -

-

- Date of Birth: {{patient.date_of_birth}} + Age: {{patient.get_age}}

+ {% if patient.date_of_birth %} +

+ Date of Birth: {{patient.date_of_birth}} +

+ {% else %} +

+ Year of Birth: {{patient.year_of_birth}} +

+ {% endif %}

Blood Group: {{patient.blood_group}}

From cda2db978ded57d6ec3b0a8d73a177165e40c0c6 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 8 Apr 2024 16:46:40 +0530 Subject: [PATCH 7/9] fix custom user authentication (#2059) --- config/authentication.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/authentication.py b/config/authentication.py index ee86acab94..cfea6116ae 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,4 +1,5 @@ import json +from datetime import datetime import jwt import requests @@ -151,7 +152,7 @@ def get_user(self, validated_token, facility): user_type=User.TYPE_VALUE_MAP["Nurse"], verified=True, asset=asset_obj, - age=10, + date_of_birth=datetime.now().date(), ) asset_user.save() return asset_user @@ -202,7 +203,7 @@ def get_user(self, validated_token): phone_number="917777777777", user_type=User.TYPE_VALUE_MAP["Volunteer"], verified=True, - age=10, + date_of_birth=datetime.now().date(), ) user.save() return user From 8b9bd0b0a80b5f8c1c243784e3581a3c8b6e1e94 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 8 Apr 2024 17:18:35 +0530 Subject: [PATCH 8/9] Staging release 3 for v24.15.0 (#2060) fix custom user authentication (#2059) --- config/authentication.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/authentication.py b/config/authentication.py index ee86acab94..cfea6116ae 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,4 +1,5 @@ import json +from datetime import datetime import jwt import requests @@ -151,7 +152,7 @@ def get_user(self, validated_token, facility): user_type=User.TYPE_VALUE_MAP["Nurse"], verified=True, asset=asset_obj, - age=10, + date_of_birth=datetime.now().date(), ) asset_user.save() return asset_user @@ -202,7 +203,7 @@ def get_user(self, validated_token): phone_number="917777777777", user_type=User.TYPE_VALUE_MAP["Volunteer"], verified=True, - age=10, + date_of_birth=datetime.now().date(), ) user.save() return user From 76a72d8df1ed1a6da29dcca5a5e6094896ec8765 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 8 Apr 2024 20:41:10 +0530 Subject: [PATCH 9/9] Switch to using year of birth for patient transfer confirmation (#2061) * Switch to using year of birth for patient transfer confirmation * update tests --- care/facility/api/serializers/patient.py | 8 ++++---- care/facility/tests/test_patient_api.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 5ba675a734..e94fa0d25c 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -452,11 +452,11 @@ class PatientTransferSerializer(serializers.ModelSerializer): class Meta: model = PatientRegistration - fields = ("facility", "date_of_birth", "patient", "facility_object") + fields = ("facility", "year_of_birth", "patient", "facility_object") - def validate_date_of_birth(self, value): - if self.instance and self.instance.date_of_birth != value: - raise serializers.ValidationError("Date of birth does not match") + def validate_year_of_birth(self, value): + if self.instance and self.instance.year_of_birth != value: + raise serializers.ValidationError("Year of birth does not match") return value def create(self, validated_data): diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index c3621e66eb..01b017c34d 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -448,7 +448,7 @@ def test_patient_transfer(self): response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", { - "date_of_birth": "1992-04-01", + "year_of_birth": 1992, "facility": self.destination_facility.external_id, }, ) @@ -477,7 +477,7 @@ def test_transfer_with_active_consultation_same_facility(self): response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", { - "date_of_birth": "1992-04-01", + "year_of_birth": 1992, "facility": self.facility.external_id, }, ) @@ -496,7 +496,7 @@ def test_transfer_disallowed_by_facility(self): response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", { - "date_of_birth": "1992-04-01", + "year_of_birth": 1992, "facility": self.destination_facility.external_id, }, )