Skip to content

Commit

Permalink
Merge pull request #2549 from ohcnetwork/vigneshhari/health-details
Browse files Browse the repository at this point in the history
Re-Architecture Changes
  • Loading branch information
vigneshhari authored Dec 31, 2024
2 parents 72a1132 + f55bf2d commit 7d74cda
Show file tree
Hide file tree
Showing 358 changed files with 25,384 additions and 1,354,134 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ htmlcov
staticfiles
.coverage
care/media/
celerybeat-schedule
celerybeat*
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pull:
up:
docker compose -f docker-compose.yaml -f $(docker_config_file) up -d --wait

build-up-live:
docker compose -f docker-compose.yaml -f $(docker_config_file) up --build

down:
docker compose -f docker-compose.yaml -f $(docker_config_file) down

Expand Down
6 changes: 4 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ djangorestframework-simplejwt = "==5.3.1"
dry-rest-permissions = "==0.1.10"
drf-nested-routers = "==0.94.1"
drf-spectacular = "==0.27.2"
"fhir.resources" = "==6.5.0"
gunicorn = "==23.0.0"
healthy-django = "==0.1.0"
json-fingerprint = "==0.14.0"
jsonschema = "==4.23.0"
newrelic = "==10.2.0"
pillow = "==11.0.0"
psycopg = { extras = ["c"], version = "==3.2.3" }
pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2
pydantic = "==2.9.2"
pyjwt = "==2.9.0"
python-slugify = "==8.0.4"
pywebpush = "==2.0.1"
redis = { extras = ["hiredis"], version = "==5.2.0" }
redis-om = "==0.3.3"
requests = "==2.32.3"
simplejson = "==3.19.3"
sentry-sdk = "==2.18.0"
whitenoise = "==6.8.2"
django-anymail = {extras = ["amazon-ses"], version = "*"}

[dev-packages]
boto3-stubs = { extras = ["s3", "boto3"], version = "*" }
Expand Down
2,070 changes: 1,108 additions & 962 deletions Pipfile.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion care/audit_log/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ def __call__(self, request: HttpRequest):
response: HttpResponse = self.get_response(request)
self.save(request, response)

current_user_str = f"{request.user.id}|{request.user}" if request.user else None
if getattr(request.user, "is_alternative_login", False):
current_user_str = f"patient|{request.user.phone_number[-4:]}"
else:
current_user_str = (
f"{request.user.id}|{request.user}" if request.user else None
)

logger.info(
"%s %s %s User:[%s]",
Expand Down
File renamed without changes.
File renamed without changes.
87 changes: 87 additions & 0 deletions care/emr/api/otp_viewsets/login.py
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)})
24 changes: 24 additions & 0 deletions care/emr/api/otp_viewsets/patient.py
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)
64 changes: 64 additions & 0 deletions care/emr/api/otp_viewsets/slot.py
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.
64 changes: 64 additions & 0 deletions care/emr/api/viewsets/allergy_intolerance.py
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)
31 changes: 31 additions & 0 deletions care/emr/api/viewsets/authz_base.py
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")
Loading

0 comments on commit 7d74cda

Please sign in to comment.