Skip to content

Commit b456779

Browse files
authored
Merge pull request #2308 from coronasafe/develop
Merge Develop to Staging v24.30.0
2 parents d7c0b79 + ac7f696 commit b456779

14 files changed

+646
-32
lines changed

care/facility/api/serializers/patient.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ class Meta:
442442
"facility",
443443
"allow_transfer",
444444
"is_active",
445+
"is_expired",
445446
)
446447

447448

care/facility/api/serializers/patient_consultation.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ class PatientConsentSerializer(serializers.ModelSerializer):
861861
id = serializers.CharField(source="external_id", read_only=True)
862862
created_by = UserBaseMinimumSerializer(read_only=True)
863863
archived_by = UserBaseMinimumSerializer(read_only=True)
864+
files = serializers.SerializerMethodField()
864865

865866
class Meta:
866867
model = PatientConsent
@@ -869,6 +870,7 @@ class Meta:
869870
"id",
870871
"type",
871872
"patient_code_status",
873+
"files",
872874
"archived",
873875
"archived_by",
874876
"archived_date",
@@ -878,13 +880,31 @@ class Meta:
878880

879881
read_only_fields = (
880882
"id",
883+
"files",
881884
"created_by",
882885
"created_date",
883886
"archived",
884887
"archived_by",
885888
"archived_date",
886889
)
887890

891+
def get_files(self, obj):
892+
from care.facility.api.serializers.file_upload import (
893+
FileUploadListSerializer,
894+
check_permissions,
895+
)
896+
897+
user = self.context["request"].user
898+
file_type = FileUpload.FileType.CONSENT_RECORD
899+
if check_permissions(file_type, obj.external_id, user, "read"):
900+
return FileUploadListSerializer(
901+
FileUpload.objects.filter(
902+
associating_id=obj.external_id, file_type=file_type
903+
),
904+
many=True,
905+
).data
906+
return None
907+
888908
def validate_patient_code_status(self, value):
889909
if value == PatientCodeStatusType.NOT_SPECIFIED:
890910
raise ValidationError(

care/facility/api/viewsets/asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ class AssetViewSet(
280280
lookup_field = "external_id"
281281
filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter)
282282
search_fields = ["name", "serial_number", "qr_code_id"]
283-
permission_classes = [IsAuthenticated]
283+
permission_classes = (IsAuthenticated, DRYPermissions)
284284
filterset_class = AssetFilter
285285

286286
def get_queryset(self):

care/facility/api/viewsets/encounter_symptom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def filter_is_cured(self, queryset, name, value):
2424
class EncounterSymptomViewSet(ModelViewSet):
2525
serializer_class = EncounterSymptomSerializer
2626
permission_classes = (IsAuthenticated, DRYPermissions)
27-
queryset = EncounterSymptom.objects.all()
27+
queryset = EncounterSymptom.objects.select_related("created_by", "updated_by")
2828
filter_backends = (filters.DjangoFilterBackend,)
2929
filterset_class = EncounterSymptomFilter
3030
lookup_field = "external_id"

care/facility/api/viewsets/patient.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,32 @@ def filter_by_diagnoses(self, queryset, name, value):
279279
)
280280
return queryset.filter(filter_q)
281281

282+
last_consultation__consent_types = MultiSelectFilter(
283+
method="filter_by_has_consents"
284+
)
285+
286+
def filter_by_has_consents(self, queryset, name, value: str):
287+
288+
if not value:
289+
return queryset
290+
291+
values = value.split(",")
292+
293+
filter_q = Q()
294+
295+
if "None" in values:
296+
filter_q |= ~Q(
297+
last_consultation__has_consents__len__gt=0,
298+
)
299+
values.remove("None")
300+
301+
if values:
302+
filter_q |= Q(
303+
last_consultation__has_consents__overlap=values,
304+
)
305+
306+
return queryset.filter(filter_q)
307+
282308

283309
class PatientDRYFilter(DRYPermissionFiltersBase):
284310
def filter_queryset(self, request, queryset, view):
@@ -565,6 +591,14 @@ def transfer(self, request, *args, **kwargs):
565591
patient = PatientRegistration.objects.get(external_id=kwargs["external_id"])
566592
facility = Facility.objects.get(external_id=request.data["facility"])
567593

594+
if patient.is_expired:
595+
return Response(
596+
{
597+
"Patient": "Patient transfer cannot be completed because the patient is expired"
598+
},
599+
status=status.HTTP_406_NOT_ACCEPTABLE,
600+
)
601+
568602
if patient.is_active and facility == patient.facility:
569603
return Response(
570604
{
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Generated by Django 4.2.10 on 2024-07-04 16:20
2+
3+
import uuid
4+
5+
import django.contrib.postgres.fields
6+
import django.db.models.deletion
7+
from django.db import migrations, models
8+
from django.db.models import Subquery
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
def migrate_has_consents(apps, schema_editor):
14+
FileUpload = apps.get_model("facility", "FileUpload")
15+
PatientConsent = apps.get_model("facility", "PatientConsent")
16+
17+
consents = PatientConsent.objects.filter(archived=False)
18+
for consent in consents:
19+
consultation = consent.consultation
20+
consent_types = (
21+
PatientConsent.objects.filter(consultation=consultation, archived=False)
22+
.annotate(
23+
str_external_id=models.functions.Cast(
24+
"external_id", models.CharField()
25+
)
26+
)
27+
.annotate(
28+
has_files=models.Exists(
29+
FileUpload.objects.filter(
30+
associating_id=models.OuterRef("str_external_id"),
31+
file_type=7,
32+
is_archived=False,
33+
)
34+
)
35+
)
36+
.filter(has_files=True)
37+
.distinct("type")
38+
.values_list("type", flat=True)
39+
)
40+
consultation.has_consents = list(consent_types)
41+
consultation.save()
42+
43+
dependencies = [
44+
("facility", "0443_remove_patientconsultation_consent_records_and_more"),
45+
]
46+
47+
operations = [
48+
migrations.AddField(
49+
model_name="patientconsultation",
50+
name="has_consents",
51+
field=django.contrib.postgres.fields.ArrayField(
52+
base_field=models.IntegerField(
53+
choices=[
54+
(1, "Consent for Admission"),
55+
(2, "Patient Code Status"),
56+
(3, "Consent for Procedure"),
57+
(4, "High Risk Consent"),
58+
(5, "Others"),
59+
]
60+
),
61+
default=list,
62+
size=None,
63+
),
64+
),
65+
migrations.AlterField(
66+
model_name="patientconsent",
67+
name="consultation",
68+
field=models.ForeignKey(
69+
on_delete=django.db.models.deletion.CASCADE,
70+
related_name="consents",
71+
to="facility.patientconsultation",
72+
),
73+
),
74+
migrations.RunPython(
75+
migrate_has_consents, reverse_code=migrations.RunPython.noop
76+
),
77+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Generated by Django 4.2.10 on 2024-07-14 21:31
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("facility", "0444_alter_medicineadministration_dosage_and_more"),
10+
("facility", "0444_patientconsultation_has_consents_and_more"),
11+
]
12+
13+
operations = []

care/facility/models/asset.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ def delete(self, *args, **kwargs):
178178
AssetBed.objects.filter(asset=self).update(deleted=True)
179179
super().delete(*args, **kwargs)
180180

181+
@staticmethod
182+
def has_write_permission(request):
183+
if request.user.asset or request.user.user_type in User.READ_ONLY_TYPES:
184+
return False
185+
return (
186+
request.user.is_superuser
187+
or request.user.verified
188+
and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"]
189+
)
190+
191+
def has_object_write_permission(self, request):
192+
return self.has_write_permission(request)
193+
194+
@staticmethod
195+
def has_read_permission(request):
196+
return request.user.is_superuser or request.user.verified
197+
198+
def has_object_read_permission(self, request):
199+
return self.has_read_permission(request)
200+
181201
def __str__(self):
182202
return self.name
183203

care/facility/models/file_upload.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import uuid
23
from uuid import uuid4
34

45
import boto3
@@ -163,5 +164,45 @@ class FileType(models.IntegerChoices):
163164
FileTypeChoices = [(x.value, x.name) for x in FileType]
164165
FileCategoryChoices = [(x.value, x.name) for x in BaseFileUpload.FileCategory]
165166

167+
def save(self, *args, **kwargs):
168+
from care.facility.models import PatientConsent
169+
170+
if self.file_type == self.FileType.CONSENT_RECORD:
171+
new_consent = False
172+
if not self.pk and not self.is_archived:
173+
new_consent = True
174+
consent = PatientConsent.objects.filter(
175+
external_id=uuid.UUID(self.associating_id), archived=False
176+
).first()
177+
consultation = consent.consultation
178+
consent_types = (
179+
PatientConsent.objects.filter(consultation=consultation, archived=False)
180+
.annotate(
181+
str_external_id=models.functions.Cast(
182+
"external_id", models.CharField()
183+
)
184+
)
185+
.annotate(
186+
has_files=(
187+
models.Exists(
188+
FileUpload.objects.filter(
189+
associating_id=models.OuterRef("str_external_id"),
190+
file_type=self.FileType.CONSENT_RECORD,
191+
is_archived=False,
192+
).exclude(pk=self.pk if self.is_archived else None)
193+
)
194+
if not new_consent
195+
else models.Value(True)
196+
)
197+
)
198+
.filter(has_files=True)
199+
.distinct("type")
200+
.values_list("type", flat=True)
201+
)
202+
consultation.has_consents = list(consent_types)
203+
consultation.save()
204+
205+
return super().save(*args, **kwargs)
206+
166207
def __str__(self):
167208
return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}"

care/facility/models/patient.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,10 @@ class TestTypeEnum(enum.Enum):
436436

437437
objects = BaseManager()
438438

439+
@property
440+
def is_expired(self) -> bool:
441+
return self.death_datetime is not None
442+
439443
def __str__(self):
440444
return f"{self.name} - {self.year_of_birth} - {self.get_gender_display()}"
441445

care/facility/models/patient_consultation.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
from care.utils.models.base import BaseModel
3030

3131

32+
class ConsentType(models.IntegerChoices):
33+
CONSENT_FOR_ADMISSION = 1, "Consent for Admission"
34+
PATIENT_CODE_STATUS = 2, "Patient Code Status"
35+
CONSENT_FOR_PROCEDURE = 3, "Consent for Procedure"
36+
HIGH_RISK_CONSENT = 4, "High Risk Consent"
37+
OTHERS = 5, "Others"
38+
39+
3240
class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin):
3341
SUGGESTION_CHOICES = [
3442
(SuggestionChoices.HI, "HOME ISOLATION"),
@@ -248,6 +256,11 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin):
248256
prn_prescription = JSONField(default=dict)
249257
discharge_advice = JSONField(default=dict)
250258

259+
has_consents = ArrayField(
260+
models.IntegerField(choices=ConsentType.choices),
261+
default=list,
262+
)
263+
251264
def get_related_consultation(self):
252265
return self
253266

@@ -359,14 +372,6 @@ def has_object_generate_discharge_summary_permission(self, request):
359372
return self.has_object_read_permission(request)
360373

361374

362-
class ConsentType(models.IntegerChoices):
363-
CONSENT_FOR_ADMISSION = 1, "Consent for Admission"
364-
PATIENT_CODE_STATUS = 2, "Patient Code Status"
365-
CONSENT_FOR_PROCEDURE = 3, "Consent for Procedure"
366-
HIGH_RISK_CONSENT = 4, "High Risk Consent"
367-
OTHERS = 5, "Others"
368-
369-
370375
class PatientCodeStatusType(models.IntegerChoices):
371376
NOT_SPECIFIED = 0, "Not Specified"
372377
DNH = 1, "Do Not Hospitalize"
@@ -387,7 +392,9 @@ class ConsultationClinician(models.Model):
387392

388393

389394
class PatientConsent(BaseModel, ConsultationRelatedPermissionMixin):
390-
consultation = models.ForeignKey(PatientConsultation, on_delete=models.CASCADE)
395+
consultation = models.ForeignKey(
396+
PatientConsultation, on_delete=models.CASCADE, related_name="consents"
397+
)
391398
type = models.IntegerField(choices=ConsentType.choices)
392399
patient_code_status = models.IntegerField(
393400
choices=PatientCodeStatusType.choices, null=True, blank=True

care/facility/tests/test_asset_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from rest_framework.test import APITestCase
44

55
from care.facility.models import Asset, Bed
6+
from care.users.models import User
67
from care.utils.assetintegration.asset_classes import AssetClasses
78
from care.utils.tests.test_utils import TestUtils
89

@@ -17,6 +18,11 @@ def setUpTestData(cls) -> None:
1718
cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body)
1819
cls.asset_location = cls.create_asset_location(cls.facility)
1920
cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility)
21+
cls.state_admin_ro = cls.create_user(
22+
"stateadmin-ro",
23+
cls.district,
24+
user_type=User.TYPE_VALUE_MAP["StateReadOnlyAdmin"],
25+
)
2026
cls.patient = cls.create_patient(
2127
cls.district, cls.facility, local_body=cls.local_body
2228
)
@@ -38,6 +44,16 @@ def test_create_asset(self):
3844
response = self.client.post("/api/v1/asset/", sample_data)
3945
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
4046

47+
def test_create_asset_read_only(self):
48+
sample_data = {
49+
"name": "Test Asset",
50+
"asset_type": 50,
51+
"location": self.asset_location.external_id,
52+
}
53+
self.client.force_authenticate(self.state_admin_ro)
54+
response = self.client.post("/api/v1/asset/", sample_data)
55+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
56+
4157
def test_create_asset_with_warranty_past(self):
4258
sample_data = {
4359
"name": "Test Asset",

0 commit comments

Comments
 (0)