-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2549 from ohcnetwork/vigneshhari/health-details
Re-Architecture Changes
- Loading branch information
Showing
358 changed files
with
25,384 additions
and
1,354,134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,5 @@ htmlcov | |
staticfiles | ||
.coverage | ||
care/media/ | ||
celerybeat-schedule | ||
celerybeat* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from datetime import timedelta | ||
|
||
from django.conf import settings | ||
from django.utils import timezone | ||
from pydantic import BaseModel, Field, field_validator | ||
from rest_framework.decorators import action | ||
from rest_framework.exceptions import ValidationError | ||
from rest_framework.response import Response | ||
|
||
from care.emr.api.viewsets.base import EMRBaseViewSet | ||
from care.facility.api.serializers.patient_otp import rand_pass | ||
from care.facility.models import PatientMobileOTP | ||
from care.utils.models.validators import mobile_validator | ||
from care.utils.sms.send_sms import send_sms | ||
from config.patient_otp_token import PatientToken | ||
|
||
|
||
class OTPLoginRequestSpec(BaseModel): | ||
phone_number: str | ||
|
||
@field_validator("phone_number") | ||
@classmethod | ||
def validate_phone_number(cls, value): | ||
try: | ||
mobile_validator(value) | ||
except Exception as e: | ||
msg = "Invalid phone number" | ||
raise ValueError(msg) from e | ||
return value | ||
|
||
|
||
class OTPLoginSpec(OTPLoginRequestSpec): | ||
otp: str = Field(min_length=settings.OTP_LENGTH, max_length=settings.OTP_LENGTH) | ||
|
||
|
||
class OTPLoginView(EMRBaseViewSet): | ||
authentication_classes = [] | ||
permission_classes = [] | ||
|
||
@action(detail=False, methods=["POST"]) | ||
def send(self, request): | ||
data = OTPLoginRequestSpec(**request.data) | ||
sent_otps = PatientMobileOTP.objects.filter( | ||
created_date__gte=(timezone.now() - timedelta(settings.OTP_REPEAT_WINDOW)), | ||
is_used=False, | ||
phone_number=data.phone_number, | ||
) | ||
if sent_otps.count() >= settings.OTP_MAX_REPEATS_WINDOW: | ||
raise ValidationError({"phone_number": "Max Retries has exceeded"}) | ||
random_otp = "" | ||
if settings.USE_SMS: | ||
random_otp = rand_pass(settings.OTP_LENGTH) | ||
try: | ||
send_sms( | ||
data.phone_number, | ||
( | ||
f"Open Healthcare Network Patient Management System Login, OTP is {random_otp} . " | ||
"Please do not share this Confidential Login Token with anyone else" | ||
), | ||
) | ||
except Exception as e: | ||
import logging | ||
|
||
logging.error(e) | ||
else: | ||
random_otp = "45612" | ||
|
||
otp_obj = PatientMobileOTP(phone_number=data.phone_number, otp=random_otp) | ||
otp_obj.save() | ||
return Response({"otp": "generated"}) | ||
|
||
@action(detail=False, methods=["POST"]) | ||
def login(self, request): | ||
data = OTPLoginSpec(**request.data) | ||
otp_object = PatientMobileOTP.objects.filter( | ||
phone_number=data.phone_number, otp=data.otp, is_used=False | ||
).first() | ||
if not otp_object: | ||
raise ValidationError({"otp": "Invalid OTP"}) | ||
|
||
otp_object.is_used = True | ||
otp_object.save() | ||
|
||
token = PatientToken() | ||
token["phone_number"] = data.phone_number | ||
|
||
return Response({"access": str(token)}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from care.emr.api.viewsets.base import EMRBaseViewSet, EMRCreateMixin, EMRListMixin | ||
from care.emr.models.patient import Patient | ||
from care.emr.resources.patient.otp_based_flow import ( | ||
PatientOTPReadSpec, | ||
PatientOTPWriteSpec, | ||
) | ||
from config.patient_otp_authentication import ( | ||
JWTTokenPatientAuthentication, | ||
OTPAuthenticatedPermission, | ||
) | ||
|
||
|
||
class PatientOTPView(EMRCreateMixin, EMRListMixin, EMRBaseViewSet): | ||
authentication_classes = [JWTTokenPatientAuthentication] | ||
permission_classes = [OTPAuthenticatedPermission] | ||
pydantic_model = PatientOTPWriteSpec | ||
pydantic_read_model = PatientOTPReadSpec | ||
|
||
def perform_create(self, instance): | ||
instance.phone_number = self.request.user.phone_number | ||
instance.save() | ||
|
||
def get_queryset(self): | ||
return Patient.objects.filter(phone_number=self.request.user.phone_number) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from pydantic import UUID4 | ||
from rest_framework.decorators import action | ||
from rest_framework.exceptions import ValidationError | ||
from rest_framework.response import Response | ||
|
||
from care.emr.api.viewsets.base import EMRBaseViewSet, EMRRetrieveMixin | ||
from care.emr.api.viewsets.scheduling import ( | ||
AppointmentBookingSpec, | ||
SlotsForDayRequestSpec, | ||
SlotViewSet, | ||
) | ||
from care.emr.models.patient import Patient | ||
from care.emr.models.scheduling import TokenBooking, TokenSlot | ||
from care.emr.resources.scheduling.slot.spec import ( | ||
TokenBookingReadSpec, | ||
TokenSlotBaseSpec, | ||
) | ||
from config.patient_otp_authentication import ( | ||
JWTTokenPatientAuthentication, | ||
OTPAuthenticatedPermission, | ||
) | ||
|
||
|
||
class SlotsForDayRequestSpec(SlotsForDayRequestSpec): | ||
facility: UUID4 | ||
|
||
|
||
class OTPSlotViewSet(EMRRetrieveMixin, EMRBaseViewSet): | ||
authentication_classes = [JWTTokenPatientAuthentication] | ||
permission_classes = [OTPAuthenticatedPermission] | ||
database_model = TokenSlot | ||
pydantic_read_model = TokenSlotBaseSpec | ||
|
||
@action(detail=False, methods=["POST"]) | ||
def get_slots_for_day(self, request, *args, **kwargs): | ||
request_data = SlotsForDayRequestSpec(**request.data) | ||
return SlotViewSet.get_slots_for_day_handler( | ||
request_data.facility, request.data | ||
) | ||
|
||
@action(detail=True, methods=["POST"]) | ||
def create_appointment(self, request, *args, **kwargs): | ||
request_data = AppointmentBookingSpec(**request.data) | ||
if not Patient.objects.filter( | ||
external_id=request_data.patient, phone_number=request.user.phone_number | ||
).exists(): | ||
raise ValidationError("Patient not allowed ") | ||
return SlotViewSet.create_appointment_handler( | ||
self.get_object(), request.data, None | ||
) | ||
|
||
@action(detail=False, methods=["GET"]) | ||
def get_appointments(self, request, *args, **kwargs): | ||
appointments = TokenBooking.objects.filter( | ||
patient__phone_number=request.user.phone_number | ||
) | ||
return Response( | ||
{ | ||
"results": [ | ||
TokenBookingReadSpec.serialize(obj).model_dump(exclude=["meta"]) | ||
for obj in appointments | ||
] | ||
} | ||
) |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from django_filters import FilterSet, UUIDFilter | ||
from django_filters.rest_framework import DjangoFilterBackend | ||
from drf_spectacular.utils import extend_schema, extend_schema_view | ||
from rest_framework.exceptions import PermissionDenied | ||
|
||
from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase | ||
from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin | ||
from care.emr.models import Encounter | ||
from care.emr.models.allergy_intolerance import AllergyIntolerance | ||
from care.emr.registries.system_questionnaire.system_questionnaire import ( | ||
InternalQuestionnaireRegistry, | ||
) | ||
from care.emr.resources.allergy_intolerance.spec import ( | ||
AllergyIntoleranceSpec, | ||
AllergyIntoleranceWriteSpec, | ||
AllergyIntrolanceSpecRead, | ||
) | ||
from care.emr.resources.questionnaire.spec import SubjectType | ||
from care.security.authorization import AuthorizationController | ||
|
||
|
||
class AllergyIntoleranceFilters(FilterSet): | ||
encounter = UUIDFilter(field_name="encounter__external_id") | ||
|
||
|
||
@extend_schema_view( | ||
create=extend_schema(request=AllergyIntoleranceSpec), | ||
) | ||
class AllergyIntoleranceViewSet( | ||
EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet | ||
): | ||
database_model = AllergyIntolerance | ||
pydantic_model = AllergyIntoleranceSpec | ||
pydantic_read_model = AllergyIntrolanceSpecRead | ||
pydantic_update_model = AllergyIntoleranceWriteSpec | ||
questionnaire_type = "allergy_intolerance" | ||
questionnaire_title = "Allergy Intolerance" | ||
questionnaire_description = "Allergy Intolerance" | ||
questionnaire_subject_type = SubjectType.patient.value | ||
filterset_class = AllergyIntoleranceFilters | ||
filter_backends = [DjangoFilterBackend] | ||
|
||
def validate_data(self, instance: AllergyIntoleranceSpec, model_instance=None): | ||
if not model_instance: | ||
encounter = Encounter.objects.get(external_id=instance.encounter) | ||
if str(encounter.patient.external_id) != self.kwargs["patient_external_id"]: | ||
err = "Malformed request" | ||
raise PermissionDenied(err) | ||
|
||
def get_queryset(self): | ||
if not AuthorizationController.call( | ||
"can_view_clinical_data", self.request.user, self.get_patient_obj() | ||
): | ||
raise PermissionDenied("Permission denied to user") | ||
return ( | ||
super() | ||
.get_queryset() | ||
.filter(patient__external_id=self.kwargs["patient_external_id"]) | ||
.select_related("patient", "encounter", "created_by", "updated_by") | ||
.order_by("-modified_date") | ||
) | ||
|
||
|
||
InternalQuestionnaireRegistry.register(AllergyIntoleranceViewSet) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from rest_framework.exceptions import PermissionDenied | ||
from rest_framework.generics import get_object_or_404 | ||
|
||
from care.emr.models import Encounter, Patient | ||
from care.security.authorization import AuthorizationController | ||
|
||
|
||
class EncounterBasedAuthorizationBase: | ||
def get_patient_obj(self): | ||
return get_object_or_404( | ||
Patient, external_id=self.kwargs["patient_external_id"] | ||
) | ||
|
||
def authorize_update(self, request_obj, model_instance): | ||
if not AuthorizationController.call( | ||
"can_update_encounter_obj", self.request.user, model_instance.encounter | ||
): | ||
raise PermissionDenied("You do not have permission to update encounter") | ||
|
||
def authorize_create(self, instance): | ||
encounter = get_object_or_404(Encounter, external_id=instance.encounter) | ||
if not AuthorizationController.call( | ||
"can_update_encounter_obj", self.request.user, encounter | ||
): | ||
raise PermissionDenied("You do not have permission to update encounter") | ||
|
||
def authorize_delete(self, instance): | ||
if not AuthorizationController.call( | ||
"can_update_encounter_obj", self.request.user, instance.encounter | ||
): | ||
raise PermissionDenied("You do not have permission to update encounter") |
Oops, something went wrong.