Skip to content

Commit 262c194

Browse files
authored
Merge pull request #2710 from ohcnetwork/develop
HMIS Release to Staging
2 parents 8f6dc2e + 9009c7f commit 262c194

File tree

350 files changed

+24883
-1362515
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

350 files changed

+24883
-1362515
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ htmlcov
44
staticfiles
55
.coverage
66
care/media/
7+
celerybeat-schedule
8+
celerybeat*

.github/workflows/deploy.yml

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,3 @@ jobs:
194194
service: ${{ env.ECS_SERVICE_BACKEND }}
195195
cluster: ${{ env.ECS_CLUSTER }}
196196
wait-for-service-stability: true
197-
198-
deploy-staging-gcp:
199-
needs: build
200-
if: github.ref == 'refs/heads/staging'
201-
name: Deploy to staging GCP cluster
202-
runs-on: ubuntu-latest
203-
environment:
204-
name: Staging-GCP
205-
url: https://care-staging-api.ohc.network/
206-
steps:
207-
- name: Trigger deploy
208-
run: |
209-
COMMIT_SHA=${{ github.sha }}
210-
JSON='{ "substitutions": { "care_be_tag":"'"$COMMIT_SHA"'", "care_fe_tag": "", "metabase_tag": "" } }'
211-
curl --location "${{ secrets.STAGING_GCP_DEPLOY_URL }}" \
212-
--header 'Content-Type: application/json' --data "$JSON"

.github/workflows/reusable-test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
event_name:
1010
required: false
1111
type: string
12-
default: 'pull_request'
12+
default: "pull_request"
1313

1414
jobs:
1515
test:
@@ -31,6 +31,7 @@ jobs:
3131
3232
- name: Create new cache
3333
run: |
34+
mkdir -p /tmp/.buildx-cache
3435
mkdir -p /tmp/.buildx-cache-new
3536
3637
- name: Bake docker images

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ care/media/*
345345
test_db
346346

347347
celerybeat-schedule*
348+
celerybeat-schedule-shm
349+
celerybeat-schedule-wal
348350

349351
secrets.sh
350352

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pull:
2525
up:
2626
docker compose -f docker-compose.yaml -f $(docker_config_file) up -d --wait
2727

28+
build-up-live:
29+
docker compose -f docker-compose.yaml -f $(docker_config_file) up --build
30+
2831
down:
2932
docker compose -f docker-compose.yaml -f $(docker_config_file) down
3033

@@ -66,7 +69,8 @@ load-db:
6669
docker compose exec db sh -c "pg_restore -U postgres --clean --if-exists -d care /tmp/care_db.dump"
6770

6871
reset-db:
69-
docker compose exec backend bash -c "python manage.py reset_db --noinput"
72+
docker compose exec db sh -c "dropdb -U postgres care -f"
73+
docker compose exec db sh -c "createdb -U postgres care"
7074

7175
ruff-all:
7276
ruff check .

Pipfile

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ name = "pypi"
55

66
[packages]
77
argon2-cffi = "==23.1.0"
8-
authlib = "==1.3.2"
9-
boto3 = "==1.35.59"
8+
authlib = "==1.4.0"
9+
boto3 = "==1.35.90"
1010
celery = "==5.4.0"
1111
django = "==5.1.3"
1212
django-environ = "==0.11.2"
@@ -24,30 +24,32 @@ djangorestframework-simplejwt = "==5.3.1"
2424
dry-rest-permissions = "==0.1.10"
2525
drf-nested-routers = "==0.94.1"
2626
drf-spectacular = "==0.27.2"
27-
"fhir.resources" = "==6.5.0"
2827
gunicorn = "==23.0.0"
2928
healthy-django = "==0.1.0"
29+
json-fingerprint = "==0.14.0"
3030
jsonschema = "==4.23.0"
3131
newrelic = "==10.2.0"
3232
pillow = "==11.0.0"
3333
psycopg = { extras = ["c"], version = "==3.2.3" }
34-
pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2
34+
pydantic = "==2.9.2"
3535
pyjwt = "==2.9.0"
3636
python-slugify = "==8.0.4"
3737
pywebpush = "==2.0.1"
3838
redis = { extras = ["hiredis"], version = "==5.2.0" }
3939
redis-om = "==0.3.3"
4040
requests = "==2.32.3"
41+
simplejson = "==3.19.3"
4142
sentry-sdk = "==2.18.0"
4243
whitenoise = "==6.8.2"
44+
django-anymail = {extras = ["amazon-ses"], version = "*"}
4345

4446
[dev-packages]
4547
boto3-stubs = { extras = ["s3", "boto3"], version = "*" }
4648
coverage = "==7.6.4"
47-
debugpy = "==1.8.8"
49+
debugpy = "==1.8.11"
4850
django-coverage-plugin = "==3.1.0"
4951
django-extensions = "==3.2.3"
50-
django-silk = "==5.2.0"
52+
django-silk = "==5.3.2"
5153
djangorestframework-stubs = "==3.15.1"
5254
factory-boy = "==3.3.1"
5355
freezegun = "==1.5.1"
@@ -58,7 +60,11 @@ requests-mock = "==1.12.1"
5860
tblib = "==3.0.0"
5961
watchdog = "==6.0.0"
6062
werkzeug = "==3.1.3"
61-
ruff = "==0.7.3"
63+
ruff = "==0.8.4"
64+
model-bakery = "==1.20.0"
65+
dirty-equals = "==0.8.0"
66+
polyfactory = "==2.18.1"
67+
Faker = "==33.3.0"
6268

6369
[docs]
6470
furo = "==2024.8.6"

Pipfile.lock

Lines changed: 1216 additions & 1041 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

care/audit_log/middleware.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ def __call__(self, request: HttpRequest):
8585
response: HttpResponse = self.get_response(request)
8686
self.save(request, response)
8787

88-
current_user_str = f"{request.user.id}|{request.user}" if request.user else None
88+
if getattr(request.user, "is_alternative_login", False):
89+
current_user_str = f"patient|{request.user.phone_number[-4:]}"
90+
else:
91+
current_user_str = (
92+
f"{request.user.id}|{request.user}" if request.user else None
93+
)
8994

9095
logger.info(
9196
"%s %s %s User:[%s]",
File renamed without changes.

care/emr/api/otp_viewsets/login.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from datetime import timedelta
2+
3+
from django.conf import settings
4+
from django.utils import timezone
5+
from pydantic import BaseModel, Field, field_validator
6+
from rest_framework.decorators import action
7+
from rest_framework.exceptions import ValidationError
8+
from rest_framework.response import Response
9+
10+
from care.emr.api.viewsets.base import EMRBaseViewSet
11+
from care.facility.api.serializers.patient_otp import rand_pass
12+
from care.facility.models import PatientMobileOTP
13+
from care.utils.models.validators import mobile_validator
14+
from care.utils.sms.send_sms import send_sms
15+
from config.patient_otp_token import PatientToken
16+
17+
18+
class OTPLoginRequestSpec(BaseModel):
19+
phone_number: str
20+
21+
@field_validator("phone_number")
22+
@classmethod
23+
def validate_phone_number(cls, value):
24+
try:
25+
mobile_validator(value)
26+
except Exception as e:
27+
msg = "Invalid phone number"
28+
raise ValueError(msg) from e
29+
return value
30+
31+
32+
class OTPLoginSpec(OTPLoginRequestSpec):
33+
otp: str = Field(min_length=settings.OTP_LENGTH, max_length=settings.OTP_LENGTH)
34+
35+
36+
class OTPLoginView(EMRBaseViewSet):
37+
authentication_classes = []
38+
permission_classes = []
39+
40+
@action(detail=False, methods=["POST"])
41+
def send(self, request):
42+
data = OTPLoginRequestSpec(**request.data)
43+
sent_otps = PatientMobileOTP.objects.filter(
44+
created_date__gte=(timezone.now() - timedelta(settings.OTP_REPEAT_WINDOW)),
45+
is_used=False,
46+
phone_number=data.phone_number,
47+
)
48+
if sent_otps.count() >= settings.OTP_MAX_REPEATS_WINDOW:
49+
raise ValidationError({"phone_number": "Max Retries has exceeded"})
50+
random_otp = ""
51+
if settings.USE_SMS:
52+
random_otp = rand_pass(settings.OTP_LENGTH)
53+
try:
54+
send_sms(
55+
data.phone_number,
56+
(
57+
f"Open Healthcare Network Patient Management System Login, OTP is {random_otp} . "
58+
"Please do not share this Confidential Login Token with anyone else"
59+
),
60+
)
61+
except Exception as e:
62+
import logging
63+
64+
logging.error(e)
65+
else:
66+
random_otp = "45612"
67+
68+
otp_obj = PatientMobileOTP(phone_number=data.phone_number, otp=random_otp)
69+
otp_obj.save()
70+
return Response({"otp": "generated"})
71+
72+
@action(detail=False, methods=["POST"])
73+
def login(self, request):
74+
data = OTPLoginSpec(**request.data)
75+
otp_object = PatientMobileOTP.objects.filter(
76+
phone_number=data.phone_number, otp=data.otp, is_used=False
77+
).first()
78+
if not otp_object:
79+
raise ValidationError({"otp": "Invalid OTP"})
80+
81+
otp_object.is_used = True
82+
otp_object.save()
83+
84+
token = PatientToken()
85+
token["phone_number"] = data.phone_number
86+
87+
return Response({"access": str(token)})

care/emr/api/otp_viewsets/patient.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from care.emr.api.viewsets.base import EMRBaseViewSet, EMRCreateMixin, EMRListMixin
2+
from care.emr.models.patient import Patient
3+
from care.emr.resources.patient.otp_based_flow import (
4+
PatientOTPReadSpec,
5+
PatientOTPWriteSpec,
6+
)
7+
from config.patient_otp_authentication import (
8+
JWTTokenPatientAuthentication,
9+
OTPAuthenticatedPermission,
10+
)
11+
12+
13+
class PatientOTPView(EMRCreateMixin, EMRListMixin, EMRBaseViewSet):
14+
authentication_classes = [JWTTokenPatientAuthentication]
15+
permission_classes = [OTPAuthenticatedPermission]
16+
pydantic_model = PatientOTPWriteSpec
17+
pydantic_read_model = PatientOTPReadSpec
18+
19+
def perform_create(self, instance):
20+
instance.phone_number = self.request.user.phone_number
21+
instance.save()
22+
23+
def get_queryset(self):
24+
return Patient.objects.filter(phone_number=self.request.user.phone_number)

care/emr/api/otp_viewsets/slot.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from pydantic import UUID4
2+
from rest_framework.decorators import action
3+
from rest_framework.exceptions import ValidationError
4+
from rest_framework.response import Response
5+
6+
from care.emr.api.viewsets.base import EMRBaseViewSet, EMRRetrieveMixin
7+
from care.emr.api.viewsets.scheduling import (
8+
AppointmentBookingSpec,
9+
SlotsForDayRequestSpec,
10+
SlotViewSet,
11+
)
12+
from care.emr.models.patient import Patient
13+
from care.emr.models.scheduling import TokenBooking, TokenSlot
14+
from care.emr.resources.scheduling.slot.spec import (
15+
TokenBookingReadSpec,
16+
TokenSlotBaseSpec,
17+
)
18+
from config.patient_otp_authentication import (
19+
JWTTokenPatientAuthentication,
20+
OTPAuthenticatedPermission,
21+
)
22+
23+
24+
class SlotsForDayRequestSpec(SlotsForDayRequestSpec):
25+
facility: UUID4
26+
27+
28+
class OTPSlotViewSet(EMRRetrieveMixin, EMRBaseViewSet):
29+
authentication_classes = [JWTTokenPatientAuthentication]
30+
permission_classes = [OTPAuthenticatedPermission]
31+
database_model = TokenSlot
32+
pydantic_read_model = TokenSlotBaseSpec
33+
34+
@action(detail=False, methods=["POST"])
35+
def get_slots_for_day(self, request, *args, **kwargs):
36+
request_data = SlotsForDayRequestSpec(**request.data)
37+
return SlotViewSet.get_slots_for_day_handler(
38+
request_data.facility, request.data
39+
)
40+
41+
@action(detail=True, methods=["POST"])
42+
def create_appointment(self, request, *args, **kwargs):
43+
request_data = AppointmentBookingSpec(**request.data)
44+
if not Patient.objects.filter(
45+
external_id=request_data.patient, phone_number=request.user.phone_number
46+
).exists():
47+
raise ValidationError("Patient not allowed ")
48+
return SlotViewSet.create_appointment_handler(
49+
self.get_object(), request.data, None
50+
)
51+
52+
@action(detail=False, methods=["GET"])
53+
def get_appointments(self, request, *args, **kwargs):
54+
appointments = TokenBooking.objects.filter(
55+
patient__phone_number=request.user.phone_number
56+
)
57+
return Response(
58+
{
59+
"results": [
60+
TokenBookingReadSpec.serialize(obj).model_dump(exclude=["meta"])
61+
for obj in appointments
62+
]
63+
}
64+
)

0 commit comments

Comments
 (0)