diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 3f14980056..79f06ae5a5 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -141,7 +141,9 @@ def update(self, instance, validated_data): patient.save() validated_data["last_updated_by_telemedicine"] = False - if self.context["request"].user == instance.consultation.assigned_to: + if instance.consultation.assigned_clinicians.contains( + self.context["request"].user + ): validated_data["last_updated_by_telemedicine"] = True instance.consultation.save(update_fields=["last_updated_by_telemedicine"]) @@ -280,9 +282,8 @@ def create(self, validated_data): validated_data["created_by_telemedicine"] = False validated_data["last_updated_by_telemedicine"] = False - if ( + if validated_data["consultation"].assigned_clinicians.contains( self.context["request"].user - == validated_data["consultation"].assigned_to ): validated_data["created_by_telemedicine"] = True validated_data["last_updated_by_telemedicine"] = True diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index e991cf045a..1fe9fb5d8f 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -26,9 +26,10 @@ def check_permissions(file_type, associating_id, user, action="create"): if user == patient.assigned_to: return patient.id if patient.last_consultation: - if patient.last_consultation.assigned_to: - if user == patient.last_consultation.assigned_to: - return patient.id + if patient.last_consultation.assigned_clinicians.filter( + id=user.id + ).exists(): + return patient.id if not has_facility_permission(user, patient.facility): raise Exception("No Permission") return patient.id @@ -43,9 +44,8 @@ def check_permissions(file_type, associating_id, user, action="create"): if consultation.patient.assigned_to: if user == consultation.patient.assigned_to: return consultation.id - if consultation.assigned_to: - if user == consultation.assigned_to: - return consultation.id + if consultation.assigned_clinicians.filter(id=user.id).exists(): + return consultation.id if not ( has_facility_permission(user, consultation.patient.facility) or has_facility_permission(user, consultation.facility) @@ -77,7 +77,7 @@ def check_permissions(file_type, associating_id, user, action="create"): and user == consultation.patient.assigned_to ): return consultation.external_id - if consultation.assigned_to and user == consultation.assigned_to: + if consultation.assigned_clinicians.filter(id=user.id).exists(): return consultation.external_id if not ( has_facility_permission(user, consultation.patient.facility) @@ -91,10 +91,11 @@ def check_permissions(file_type, associating_id, user, action="create"): if patient.assigned_to: if user == patient.assigned_to: return sample.id - if sample.consultation: - if sample.consultation.assigned_to: - if user == sample.consultation.assigned_to: - return sample.id + if ( + sample.consultation + and sample.consultation.assigned_clinicians.filter(id=user.id).exists() + ): + return sample.id if sample.testing_facility: if has_facility_permission( user, diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index ad0cd8ece3..d21c89dda0 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -108,9 +108,20 @@ class PatientConsultationSerializer(serializers.ModelSerializer): patient = ExternalIdSerializerField(queryset=PatientRegistration.objects.all()) facility = ExternalIdSerializerField(read_only=True) - assigned_to_object = UserAssignedSerializer(source="assigned_to", read_only=True) + assigned_to_object = UserAssignedSerializer( + source="assigned_to", read_only=True + ) # deprecated assigned_to = serializers.PrimaryKeyRelatedField( queryset=User.objects.all(), required=False, allow_null=True + ) # deprecated + + assigned_clinicians_object = UserBaseMinimumSerializer( + many=True, read_only=True, source="assigned_clinicians" + ) + assigned_clinicians = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), + many=True, + required=False, ) treating_physician_object = UserBaseMinimumSerializer( @@ -236,15 +247,19 @@ def update(self, instance, validated_data): patient.review_time = None patient.save() - validated_data["last_updated_by_telemedicine"] = ( - self.context["request"].user == instance.assigned_to - ) + validated_data[ + "last_updated_by_telemedicine" + ] = instance.assigned_clinicians.filter( + id=self.context["request"].user.id + ).exists() if "is_kasp" in validated_data: if validated_data["is_kasp"] and (not instance.is_kasp): validated_data["kasp_enabled_date"] = localtime(now()) - _temp = instance.assigned_to + _old_clinicians = list( + instance.assigned_clinicians.values_list("id", flat=True) + ) consultation = super().update(instance, validated_data) @@ -256,8 +271,14 @@ def update(self, instance, validated_data): old_instance, ) - if "assigned_to" in validated_data: - if validated_data["assigned_to"] != _temp and validated_data["assigned_to"]: + if "assigned_clinicians" in validated_data: + _new_clinicians = list( + instance.assigned_clinicians.values_list("id", flat=True) + ) + if ( + set(_old_clinicians) != set(_new_clinicians) + and validated_data["assigned_clinicians"] + ): NotificationGenerator( event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, caused_by=self.context["request"].user, @@ -353,8 +374,11 @@ def create(self, validated_data): if validated_data["patient"].last_consultation: if ( - self.context["request"].user - == validated_data["patient"].last_consultation.assigned_to + validated_data["patient"] + .last_consultation.assigned_clinicians.filter( + id=self.context["request"].user.id + ) + .exists() ): raise ValidationError( { @@ -377,7 +401,9 @@ def create(self, validated_data): validated_data["facility_id"] = validated_data[ "patient" ].facility_id # Coercing facility as the patient's facility + assigned_clinicians = validated_data.pop("assigned_clinicians", []) consultation = super().create(validated_data) + consultation.assigned_clinicians.set(assigned_clinicians) consultation.created_by = self.context["request"].user consultation.last_edited_by = self.context["request"].user patient = consultation.patient @@ -449,7 +475,7 @@ def create(self, validated_data): consultation.created_date, ) - if consultation.assigned_to: + if consultation.assigned_clinicians.exists(): NotificationGenerator( event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, caused_by=self.context["request"].user, @@ -729,7 +755,10 @@ def validate(self, attrs): def update(self, instance: PatientConsultation, validated_data): old_instance = copy(instance) + assigned_clinicians = validated_data.pop("assigned_clinicians", None) with transaction.atomic(): + if assigned_clinicians is not None: + instance.assigned_clinicians.set(assigned_clinicians) instance = super().update(instance, validated_data) patient: PatientRegistration = instance.patient patient.is_active = False diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index e7c5d618af..324757aeb4 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -11,6 +11,7 @@ F, Func, OuterRef, + Prefetch, Q, Subquery, Value, @@ -213,8 +214,8 @@ def filter_by_bed_type(self, queryset, name, value): field_name="last_consultation__new_discharge_reason", choices=NewDischargeReasonEnum.choices, ) - last_consultation_assigned_to = filters.NumberFilter( - field_name="last_consultation__assigned_to" + last_consultation_assigned_clinician = filters.NumberFilter( + field_name="last_consultation__assigned_clinicians__id" ) last_consultation_is_telemedicine = filters.BooleanFilter( field_name="last_consultation__is_telemedicine" @@ -306,7 +307,7 @@ def filter_queryset(self, request, queryset, view): facility__id__in=allowed_facilities ).values("patient_id") ) - q_filters |= Q(last_consultation__assigned_to=request.user) + q_filters |= Q(last_consultation__assigned_clinicians=request.user) q_filters |= Q(assigned_to=request.user) queryset = queryset.filter(q_filters) return queryset @@ -386,6 +387,7 @@ class PatientViewSet( "last_edited", "created_by", ) + .prefetch_related(Prefetch("last_consultation__assigned_clinicians")) .annotate( coalesced_dob=Coalesce( "date_of_birth", @@ -846,7 +848,9 @@ def get_queryset(self): else: allowed_facilities = get_accessible_facilities(user) q_filters = Q(patient_note__patient__facility__id__in=allowed_facilities) - q_filters |= Q(patient_note__patient__last_consultation__assigned_to=user) + q_filters |= Q( + patient_note__patient__last_consultation__assigned_clinicians=user + ) q_filters |= Q(patient_note__patient__assigned_to=user) q_filters |= Q(patient_note__created_by=user) queryset = queryset.filter(q_filters) @@ -897,7 +901,7 @@ def get_queryset(self): else: allowed_facilities = get_accessible_facilities(user) q_filters = Q(patient__facility__id__in=allowed_facilities) - q_filters |= Q(patient__last_consultation__assigned_to=user) + q_filters |= Q(patient__last_consultation__assigned_clinicians=user) q_filters |= Q(patient__assigned_to=user) q_filters |= Q(created_by=user) queryset = queryset.filter(q_filters) diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index 4a31f6354e..74af14ae75 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -75,10 +75,14 @@ def get_permissions(self): def get_queryset(self): if self.serializer_class == PatientConsultationSerializer: self.queryset = self.queryset.prefetch_related( - "assigned_to", Prefetch( - "assigned_to__skills", - queryset=Skill.objects.filter(userskill__deleted=False), + "assigned_clinicians", + queryset=User.objects.prefetch_related( + Prefetch( + "skills", + queryset=Skill.objects.filter(userskill__deleted=False), + ) + ), ), "current_bed", "current_bed__bed", diff --git a/care/facility/api/viewsets/patient_investigation.py b/care/facility/api/viewsets/patient_investigation.py index bb9682abff..2f977f858c 100644 --- a/care/facility/api/viewsets/patient_investigation.py +++ b/care/facility/api/viewsets/patient_investigation.py @@ -139,7 +139,7 @@ def get_queryset(self): ) allowed_facilities = get_accessible_facilities(self.request.user) filters = Q(consultation__patient__facility_id__in=allowed_facilities) - filters |= Q(consultation__assigned_to=self.request.user) + filters |= Q(consultation__assigned_clinicians=self.request.user) filters |= Q(consultation__patient__assigned_to=self.request.user) return queryset.filter(filters) @@ -184,7 +184,7 @@ def get_queryset(self): filters = Q( consultation__patient__facility__users__id__exact=self.request.user.id ) - filters |= Q(consultation__assigned_to=self.request.user) + filters |= Q(consultation__assigned_clinicians=self.request.user) filters |= Q(consultation__patient__assigned_to=self.request.user) return queryset.filter(filters).distinct("id") diff --git a/care/facility/migrations/0429_auto_20240220_1233.py b/care/facility/migrations/0429_auto_20240220_1233.py new file mode 100644 index 0000000000..7a9185468d --- /dev/null +++ b/care/facility/migrations/0429_auto_20240220_1233.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.8 on 2024-02-20 07:03 + +from django.db import migrations + + +def transfer_assigned_to_to_clinicians(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + ConsultationClinician = apps.get_model("facility", "ConsultationClinician") + clinician_links = [] + + consultations = PatientConsultation.objects.filter( + assigned_to__isnull=False + ).select_related("assigned_to") + + for consultation in consultations: + link = ConsultationClinician( + consultation=consultation, clinician=consultation.assigned_to + ) + clinician_links.append(link) + + ConsultationClinician.objects.bulk_create(clinician_links) + + +def reverse_transfer(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + consultations_to_update = [] + + for consultation in PatientConsultation.objects.prefetch_related( + "assigned_clinicians" + ).all(): + clinicians = list(consultation.assigned_clinicians.all()) + consultation.assigned_to = clinicians[0] if clinicians else None + consultations_to_update.append(consultation) + + PatientConsultation.objects.bulk_update(consultations_to_update, ["assigned_to"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0428_alter_patientmetainfo_occupation"), + ] + + operations = [ + migrations.RunPython(transfer_assigned_to_to_clinicians, reverse_transfer), + ] diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 0b5b78ec3c..62b36aa32f 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -542,7 +542,7 @@ def has_read_permission(request): return request.user.is_superuser or ( (request.user in consultation.patient.facility.users.all()) or ( - request.user == consultation.assigned_to + consultation.assigned_clinicians.filter(id=request.user.id).exists() or request.user == consultation.patient.assigned_to ) or ( @@ -577,7 +577,9 @@ def has_object_read_permission(self, request): and request.user in self.consultation.patient.facility.users.all() ) or ( - self.consultation.assigned_to == request.user + self.consultation.assigned_clinicians.filter( + id=request.user.id + ).exists() or request.user == self.consultation.patient.assigned_to ) or ( diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index b814cde15a..3e7c3e012c 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -20,7 +20,9 @@ def has_object_read_permission(self, request): doctor_allowed = False if self.last_consultation: doctor_allowed = ( - self.last_consultation.assigned_to == request.user + self.last_consultation.assigned_clinicians.filter( + id=request.user.id + ).exists() or request.user == self.assigned_to ) return request.user.is_superuser or ( @@ -61,7 +63,9 @@ def has_object_write_permission(self, request): doctor_allowed = False if self.last_consultation: doctor_allowed = ( - self.last_consultation.assigned_to == request.user + self.last_consultation.assigned_clinicians.filter( + id=request.user.id + ).exists() or request.user == self.assigned_to ) @@ -100,7 +104,9 @@ def has_object_update_permission(self, request): doctor_allowed = False if self.last_consultation: doctor_allowed = ( - self.last_consultation.assigned_to == request.user + self.last_consultation.assigned_clinicians.filter( + id=request.user.id + ).exists() or request.user == self.assigned_to ) diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 91dad91de4..e6dcdcfb29 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -174,7 +174,7 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): on_delete=models.SET_NULL, null=True, related_name="patient_assigned_to", - ) + ) # Deprecated assigned_clinicians = models.ManyToManyField( User, @@ -325,16 +325,21 @@ def has_write_permission(request): def has_object_read_permission(self, request): if not super().has_object_read_permission(request): return False + + is_assigned_clinician = self.assigned_clinicians.filter( + id=request.user.id + ).exists() + is_patient_assigned_to = ( + request.user == self.patient.assigned_to + ) # patient.assigned_to is for assigning volunteer to patient + return ( request.user.is_superuser or ( self.patient.facility and request.user in self.patient.facility.users.all() ) - or ( - self.assigned_to == request.user - or request.user == self.patient.assigned_to - ) + or (is_assigned_clinician or is_patient_assigned_to) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index af2d91e4c5..b5932fe596 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -62,9 +62,15 @@ def get_user_type( field_name, value, ): + # TODO: use https://django-filter.readthedocs.io/en/stable/ref/filters.html#multiplechoicefilter if value: - if value in INVERSE_USER_TYPE: - return queryset.filter(user_type=INVERSE_USER_TYPE[value]) + user_types = value.split(",") + filtered_query = queryset.filter( + user_type__in=[ + INVERSE_USER_TYPE.get(user_type) for user_type in user_types + ] + ) + return filtered_query return queryset user_type = filters.CharFilter(method="get_user_type", field_name="user_type") diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index bbb910fb7f..eac9581c76 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -143,24 +143,41 @@ def deserialize_extra_data(self, extra_data): return extra_data def generate_extra_users(self): + assigned_clinicians_fields = ("id", "local_body", "district", "state") if isinstance(self.caused_object, PatientConsultation): - if self.caused_object.assigned_to: - self.extra_users.append(self.caused_object.assigned_to.id) + for clinician in self.caused_object.assigned_clinicians.all().only( + *assigned_clinicians_fields + ): + self.extra_users.append(clinician.id) if isinstance(self.caused_object, PatientRegistration): if self.caused_object.last_consultation: - if self.caused_object.last_consultation.assigned_to: - self.extra_users.append( - self.caused_object.last_consultation.assigned_to.id - ) + for ( + clinician + ) in self.caused_object.last_consultation.assigned_clinicians.all().only( + *assigned_clinicians_fields + ): + self.extra_users.append(clinician.id) if isinstance(self.caused_object, InvestigationSession): - if self.extra_data["consultation"].assigned_to: - self.extra_users.append(self.extra_data["consultation"].assigned_to.id) + for clinician in ( + self.extra_data["consultation"] + .assigned_clinicians.all() + .only(*assigned_clinicians_fields) + ): + self.extra_users.append(clinician.id) if isinstance(self.caused_object, InvestigationValue): - if self.caused_object.consultation.assigned_to: - self.extra_users.append(self.caused_object.consultation.assigned_to.id) + for ( + clinician + ) in self.caused_object.consultation.assigned_clinicians.all().only( + *assigned_clinicians_fields + ): + self.extra_users.append(clinician.id) if isinstance(self.caused_object, DailyRound): - if self.caused_object.consultation.assigned_to: - self.extra_users.append(self.caused_object.consultation.assigned_to.id) + for ( + clinician + ) in self.caused_object.consultation.assigned_clinicians.all().only( + *assigned_clinicians_fields + ): + self.extra_users.append(clinician.id) def generate_system_message(self): message = "" diff --git a/care/utils/queryset/consultation.py b/care/utils/queryset/consultation.py index 49dc632d77..1ec55aa99c 100644 --- a/care/utils/queryset/consultation.py +++ b/care/utils/queryset/consultation.py @@ -23,7 +23,7 @@ def get_consultation_queryset(user): allowed_facilities = get_accessible_facilities(user) q_filters = Q(facility__id__in=allowed_facilities) q_filters |= Q(patient__facility__id__in=allowed_facilities) - q_filters |= Q(assigned_to=user) + q_filters |= Q(assigned_clinicians=user) q_filters |= Q(patient__assigned_to=user) queryset = queryset.filter(q_filters) return queryset diff --git a/care/utils/queryset/patient.py b/care/utils/queryset/patient.py index dfade629d9..fa12b63cee 100644 --- a/care/utils/queryset/patient.py +++ b/care/utils/queryset/patient.py @@ -15,7 +15,7 @@ def get_patient_queryset(user): queryset = queryset.filter(facility__district=user.district) else: q_filters = Q(facility__id=user.home_facility) - q_filters |= Q(last_consultation__assigned_to=user) + q_filters |= Q(last_consultation__assigned_clinicians=user) q_filters |= Q(assigned_to=user) queryset = queryset.filter(q_filters) return queryset @@ -32,7 +32,7 @@ def get_patient_notes_queryset(user): else: allowed_facilities = get_accessible_facilities(user) - q_filters = Q(last_consultation__assigned_to=user) + q_filters = Q(last_consultation__assigned_clinicians=user) q_filters |= Q(assigned_to=user) if user.user_type >= User.TYPE_VALUE_MAP["Doctor"]: q_filters |= Q(facility__id__in=allowed_facilities)