Skip to content

Commit 06ed06a

Browse files
committed
2 parents 9fe812c + 87c10ce commit 06ed06a

35 files changed

Lines changed: 3704 additions & 505 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: PR Title JIRA Validation
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, edited]
6+
7+
jobs:
8+
validate-jira-ticket:
9+
runs-on: ubuntu-24.04-arm
10+
if: github.event.pull_request.user.type != 'Bot'
11+
12+
steps:
13+
- name: Validate JIRA Ticket in PR Title
14+
uses: actions/github-script@v7
15+
with:
16+
github-token: ${{ secrets.GITHUB_TOKEN }}
17+
script: |
18+
// Configure JIRA project keys here
19+
const jiraProjectKeys = ['ENG'];
20+
21+
const prTitle = context.payload.pull_request.title;
22+
23+
// Check that title starts with [ENG-456] format
24+
const pattern = new RegExp(`^\\[(${jiraProjectKeys.join('|')})-\\d+\\]`, 'i');
25+
26+
if (!pattern.test(prTitle)) {
27+
const examples = jiraProjectKeys.map(k => `[${k}-123]`).join(', ');
28+
core.setFailed(
29+
`PR title must start with a JIRA ticket in brackets.\n` +
30+
`Expected format: [PROJECT-NUMBER] title\n` +
31+
`Examples: ${examples}`
32+
);
33+
}

care/emr/api/otp_viewsets/login.py

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,58 @@
1212
from rest_framework.response import Response
1313

1414
from care.emr.api.viewsets.base import EMRBaseViewSet
15-
from care.facility.models.patient import PatientMobileOTP
15+
from care.facility.models.patient import MobileOTP
1616
from care.utils import sms
1717
from care.utils.models.validators import mobile_validator
18-
from care.utils.sms.utils import get_sms_content
1918
from config.patient_otp_token import PatientToken
2019

2120
logger = logging.getLogger(__name__)
2221

2322

23+
class BaseOTPType:
24+
def render_content(self, otp: str) -> str:
25+
pass
26+
27+
28+
class LoginOTP(BaseOTPType):
29+
@classmethod
30+
def render_content(cls, otp: str) -> str:
31+
return settings.OTP_SMS_LOGIN_CONTENT.format(otp=otp)
32+
33+
2434
def rand_pass(size):
2535
return "".join(secrets.choice(string.digits) for _ in range(size))
2636

2737

28-
class OTPLoginRequestSpec(BaseModel):
38+
def send_otp(phone_number, otp_type: BaseOTPType):
39+
sent_otps = MobileOTP.objects.filter(
40+
created_date__gte=(timezone.now() - timedelta(settings.OTP_REPEAT_WINDOW)),
41+
is_used=False,
42+
phone_number=phone_number,
43+
)
44+
if sent_otps.count() >= settings.OTP_MAX_REPEATS_WINDOW:
45+
raise ValueError("Max Retries has exceeded")
46+
47+
random_otp = ""
48+
if settings.USE_SMS:
49+
random_otp = rand_pass(settings.OTP_LENGTH)
50+
try:
51+
content = otp_type.render_content(random_otp)
52+
sms.send_text_message(
53+
content=content,
54+
recipients=[phone_number],
55+
)
56+
except Exception as e:
57+
raise Exception("Error while sending OTP. Contact admin.") from e
58+
elif settings.IS_PRODUCTION:
59+
random_otp = rand_pass(settings.OTP_LENGTH)
60+
else:
61+
random_otp = "45612"
62+
63+
MobileOTP.objects.create(phone_number=phone_number, otp=random_otp)
64+
65+
66+
class OTPRequestBaseSpec(BaseModel):
2967
phone_number: str
3068

3169
@field_validator("phone_number")
@@ -39,7 +77,7 @@ def validate_phone_number(cls, value):
3977
return value
4078

4179

42-
class OTPLoginSpec(OTPLoginRequestSpec):
80+
class OTPLoginSpec(OTPRequestBaseSpec):
4381
otp: str = Field(min_length=settings.OTP_LENGTH, max_length=settings.OTP_LENGTH)
4482

4583

@@ -48,41 +86,17 @@ class OTPLoginView(EMRBaseViewSet):
4886
permission_classes = []
4987

5088
@extend_schema(
51-
request=OTPLoginRequestSpec,
89+
request=OTPRequestBaseSpec,
5290
)
5391
@action(detail=False, methods=["POST"])
5492
def send(self, request):
55-
data = OTPLoginRequestSpec(**request.data)
56-
sent_otps = PatientMobileOTP.objects.filter(
57-
created_date__gte=(timezone.now() - timedelta(settings.OTP_REPEAT_WINDOW)),
58-
is_used=False,
59-
phone_number=data.phone_number,
60-
)
61-
if sent_otps.count() >= settings.OTP_MAX_REPEATS_WINDOW:
62-
raise ValidationError({"phone_number": "Max Retries has exceeded"})
63-
random_otp = ""
64-
if settings.USE_SMS:
65-
random_otp = rand_pass(settings.OTP_LENGTH)
66-
try:
67-
content = get_sms_content(
68-
settings.OTP_SMS_TEMPLATE_PATH, {"random_otp": random_otp}
69-
)
70-
sms.send_text_message(
71-
content=content,
72-
recipients=[data.phone_number],
73-
)
74-
except Exception as e:
75-
logger.error(e)
76-
return Response(
77-
{"error": "Error while sending OTP. Contact admin."}, status=400
78-
)
79-
elif settings.IS_PRODUCTION:
80-
random_otp = rand_pass(settings.OTP_LENGTH)
81-
else:
82-
random_otp = "45612"
83-
84-
otp_obj = PatientMobileOTP(phone_number=data.phone_number, otp=random_otp)
85-
otp_obj.save()
93+
data = OTPRequestBaseSpec(**request.data)
94+
try:
95+
send_otp(data.phone_number, otp_type=LoginOTP)
96+
except ValueError as e:
97+
raise ValidationError({"phone_number": "Unable to send OTP"}) from e
98+
except Exception:
99+
return Response({"error": "Unable to send OTP"}, status=400)
86100
return Response({"otp": "generated"})
87101

88102
@extend_schema(
@@ -91,7 +105,7 @@ def send(self, request):
91105
@action(detail=False, methods=["POST"])
92106
def login(self, request):
93107
data = OTPLoginSpec(**request.data)
94-
otp_object = PatientMobileOTP.objects.filter(
108+
otp_object = MobileOTP.objects.filter(
95109
phone_number=data.phone_number, otp=data.otp, is_used=False
96110
).first()
97111
if not otp_object:

care/emr/api/viewsets/account.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,19 @@ def authorize_update(self, request_obj, model_instance):
116116
Encounter, external_id=request_obj.primary_encounter
117117
)
118118
if encounter.facility != model_instance.facility:
119-
raise PermissionDenied(
120-
"Primary encounter is not associated with the facility"
119+
raise ValidationError(
120+
"Primary encounter must belong to the same facility"
121121
)
122122
if encounter.patient != model_instance.patient:
123-
raise PermissionDenied(
124-
"Primary encounter is not associated with the patient"
123+
raise ValidationError(
124+
"Primary encounter must belong to the same patient"
125125
)
126126
if (
127127
Account.objects.exclude(id=model_instance.id)
128128
.filter(primary_encounter=encounter)
129129
.exists()
130130
):
131-
raise PermissionDenied(
132-
"Encounter is already associated with an account"
133-
)
131+
raise ValidationError("Encounter is already associated with an account")
134132
if not AuthorizationController.call(
135133
"can_update_account_in_facility",
136134
self.request.user,

care/emr/api/viewsets/inventory/delivery_order.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ def authorize_location_write(self, location_obj, raise_error=True):
9292
return False
9393
return True
9494

95+
def authorize_location_external_write(self, location_obj, raise_error=True):
96+
if not AuthorizationController.call(
97+
"can_write_facility_external_supply_delivery",
98+
self.request.user,
99+
location_obj,
100+
):
101+
if raise_error:
102+
raise PermissionDenied("Cannot write supply requests")
103+
return False
104+
return True
105+
95106
def perform_create(self, instance):
96107
if (
97108
instance.origin
@@ -102,6 +113,16 @@ def perform_create(self, instance):
102113
)
103114
return super().perform_create(instance)
104115

116+
def authorize_order_write(self, order):
117+
if order.origin:
118+
allowed = self.authorize_location_write(order.origin, raise_error=False)
119+
else:
120+
allowed = self.authorize_location_external_write(
121+
order.destination, raise_error=False
122+
)
123+
if not allowed:
124+
raise PermissionDenied("Cannot write supply requests")
125+
105126
def perform_update(self, instance):
106127
with transaction.atomic():
107128
old_instance = DeliveryOrder.objects.get(id=instance.id)
@@ -150,18 +171,15 @@ def authorize_create(self, instance):
150171
destination_location = get_object_or_404(
151172
FacilityLocation, external_id=instance.destination
152173
)
153-
self.authorize_location_write(destination_location)
174+
self.authorize_location_external_write(destination_location)
154175

155176
def authorize_update(self, request_obj, model_instance):
156177
"""
157178
If the order is an external order, then destination is the owner,
158179
else the owner is the origin.
159180
"""
160181
# TODO: Order Destination permission to be figured out
161-
if model_instance.origin:
162-
self.authorize_location_write(model_instance.origin)
163-
else:
164-
self.authorize_location_write(model_instance.destination)
182+
self.authorize_order_write(model_instance)
165183

166184
def authorize_retrieve(self, model_instance):
167185
allowed = False

care/emr/api/viewsets/inventory/dispense_order.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,18 @@ def perform_update(self, instance):
120120
raise ValidationError(
121121
"Dispense order already abandoned or entered in error"
122122
)
123-
if (
123+
if instance.status in [
124+
MedicationDispenseOrderStatusOptions.abandoned.value,
125+
MedicationDispenseOrderStatusOptions.entered_in_error.value,
126+
]:
127+
cancel_dispense_order(instance, user)
128+
elif (
124129
old_object.status
125130
== MedicationDispenseOrderStatusOptions.completed.value
126131
):
127-
if instance.status not in [
128-
MedicationDispenseOrderStatusOptions.abandoned.value,
129-
MedicationDispenseOrderStatusOptions.entered_in_error.value,
130-
]:
131-
raise ValidationError("Dispense order can only be cancelled")
132-
cancel_dispense_order(instance, user)
132+
raise ValidationError(
133+
"Completed dispense order can only be cancelled"
134+
)
133135
return super().perform_update(instance)
134136

135137
def perform_create(self, instance):

care/emr/api/viewsets/inventory/supply_delivery.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,17 @@ def authorize_location_write(self, location_obj, raise_error=True):
170170
return False
171171
return True
172172

173+
def authorize_location_external_write(self, location_obj, raise_error=True):
174+
if not AuthorizationController.call(
175+
"can_write_facility_external_supply_delivery",
176+
self.request.user,
177+
location_obj,
178+
):
179+
if raise_error:
180+
raise PermissionDenied("Cannot write supply requests")
181+
return False
182+
return True
183+
173184
def authorize_order_read(self, order):
174185
allowed = False
175186
if order.origin:
@@ -188,9 +199,13 @@ def authorize_order_write(self, order):
188199
allowed = allowed or self.authorize_location_write(
189200
order.origin, raise_error=False
190201
)
191-
allowed = allowed or self.authorize_location_write(
192-
order.destination, raise_error=False
193-
)
202+
allowed = allowed or self.authorize_location_write(
203+
order.destination, raise_error=False
204+
)
205+
else:
206+
allowed = allowed or self.authorize_location_external_write(
207+
order.destination, raise_error=False
208+
)
194209
if not allowed:
195210
raise PermissionDenied("Cannot write supply requests")
196211

care/emr/api/viewsets/medication_dispense.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,19 @@ def perform_update(self, instance):
188188
instance.charge_item.save()
189189
super().perform_update(instance)
190190
sync_inventory_item(instance.item.location, instance.item.product)
191-
if instance.authorizing_request:
192-
if instance._fully_dispensed is not None and instance._fully_dispensed: # noqa
191+
if instance.authorizing_request and instance._fully_dispensed is not None: # noqa
192+
if instance._fully_dispensed: # noqa
193193
instance.authorizing_request.dispense_status = (
194194
MedicationRequestDispenseStatus.complete.value
195195
)
196-
instance.authorizing_request.updated_by = self.request.user
197-
instance.authorizing_request.save(
198-
update_fields=["dispense_status", "updated_by", "modified_date"]
199-
)
200-
elif instance.authorizing_request:
196+
else:
201197
instance.authorizing_request.dispense_status = (
202198
MedicationRequestDispenseStatus.partial.value
203199
)
204-
instance.authorizing_request.updated_by = self.request.user
205-
instance.authorizing_request.save(
206-
update_fields=["dispense_status", "updated_by", "modified_date"]
207-
)
200+
instance.authorizing_request.updated_by = self.request.user
201+
instance.authorizing_request.save(
202+
update_fields=["dispense_status", "updated_by", "modified_date"]
203+
)
208204
return instance
209205

210206
def authorize_location_read(self, location):

care/emr/api/viewsets/observation_definition.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def validate_data(self, instance, model_obj=None):
7171
facility = None
7272
if model_obj:
7373
queryset = queryset.exclude(id=model_obj.id)
74-
facility = str(model_obj.facility.external_id)
74+
if model_obj.facility:
75+
facility = str(model_obj.facility.external_id)
7576
else:
7677
facility = instance.facility
7778

0 commit comments

Comments
 (0)