Skip to content

Commit

Permalink
Merge branch 'develop' into prafful/tests/questionnaire
Browse files Browse the repository at this point in the history
  • Loading branch information
vigneshhari authored Jan 19, 2025
2 parents 9368366 + a053f09 commit 8d33a6f
Show file tree
Hide file tree
Showing 13 changed files with 2,120 additions and 83 deletions.
26 changes: 18 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ env:
AWS_DEFAULT_OUTPUT: json
ECS_SERVICE_BACKEND: "care-backend"
ECS_SERVICE_CELERY: "care-celery"
ECS_TASK_BACKEND: "care"
ECS_TASK_CELERY: "care-celery"
ECS_CLUSTER: "egov"
CONTAINER_NAME_BACKEND: "care-backend"
CONTAINER_NAME_WORKER: "care-celery-worker"
Expand All @@ -35,7 +37,7 @@ jobs:
build:
needs: test
name: Build & Push to container registries
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -117,7 +119,7 @@ jobs:
needs: build
if: startsWith(github.event.ref, 'refs/tags/v')
name: Notify release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- name: Notify release
run: |
Expand All @@ -127,7 +129,7 @@ jobs:
needs: build
if: github.ref == 'refs/heads/develop'
name: Deploy to ECS API Egov
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
environment:
name: Staging-egov
url: https://careapi.ohc.network
Expand All @@ -148,33 +150,41 @@ jobs:
- name: Download task definition for Celery Service
run: |
set -e
if aws ecs describe-task-definition --task-definition ${{ env.ECS_SERVICE_CELERY }} --query taskDefinition > celery-task-definition.json; then
if aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_CELERY }} --query taskDefinition > celery-task-definition.json; then
echo "Successfully downloaded Celery task definition."
else
echo "Failed to download Celery task definition." >&2
exit 1
fi
- name: Fill in the new image ID in the Celery task definition
id: celery-task-def
- name: Fill in the new image ID for the worker container
id: render-worker
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: celery-task-definition.json
container-name: ${{ env.CONTAINER_NAME_WORKER }}
image: ${{ env.IMAGE_VALUE }}

- name: Fill in the new image ID for the cron container
id: render-cron
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-worker.outputs.task-definition }}
container-name: ${{ env.CONTAINER_NAME_CRON }}
image: ${{ env.IMAGE_VALUE }}

- name: Deploy Celery task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.celery-task-def.outputs.task-definition }}
task-definition: ${{ steps.render-cron.outputs.task-definition }}
service: ${{ env.ECS_SERVICE_CELERY }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true

- name: Download task definition for Backend Service
run: |
set -e
if aws ecs describe-task-definition --task-definition ${{ env.ECS_SERVICE_BACKEND }} --query taskDefinition > backend-task-definition.json; then
if aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_BACKEND }} --query taskDefinition > backend-task-definition.json; then
echo "Successfully downloaded Backend task definition."
else
echo "Failed to download Backend task definition." >&2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ jobs:
build-docs:
if: github.repository == 'ohcnetwork/care'
name: Build docs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: 'pipenv'
cache: "pipenv"

- name: Install pipenv
run: pip install pipenv
Expand All @@ -46,7 +46,7 @@ jobs:
deploy-docs:
if: github.repository == 'ohcnetwork/care' && github.ref == 'refs/heads/develop'
name: Deploy docs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
needs: build-docs
permissions:
contents: write
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/issue-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
jobs:
issue_opened_and_reopened:
name: issue_opened_and_reopened
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
if: github.repository == 'ohcnetwork/care' && github.event_name == 'issues' && github.event.action == 'opened' || github.event.action == 'reopened'
steps:
- name: 'Move issue to "Triage"'
Expand All @@ -21,7 +21,7 @@ jobs:
status_value: "Triage"
issue_closed:
name: issue_closed
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
if: github.repository == 'ohcnetwork/care' && github.event_name == 'issues' && github.event.action == 'closed'
steps:
- name: 'Moved issue to "Done"'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:
lint:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4

Expand Down
105 changes: 76 additions & 29 deletions care/emr/api/viewsets/scheduling/availability.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
from datetime import time, timedelta

from dateutil.parser import parse
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone
Expand All @@ -18,6 +17,7 @@
from care.emr.models.scheduling.schedule import Availability, SchedulableUserResource
from care.emr.resources.scheduling.schedule.spec import SlotTypeOptions
from care.emr.resources.scheduling.slot.spec import (
CANCELLED_STATUS_CHOICES,
TokenBookingReadSpec,
TokenSlotBaseSpec,
)
Expand Down Expand Up @@ -45,43 +45,76 @@ class AvailabilityStatsRequestSpec(BaseModel):
def validate_period(self):
max_period = 32
if self.from_date > self.to_date:
raise ValidationError("From Date cannot be greater than To Date")
if self.from_date - self.to_date > datetime.timedelta(days=max_period):
raise ValidationError("Period cannot be be greater than max days")
raise ValidationError("From Date cannot be after To Date")
if self.to_date - self.from_date > datetime.timedelta(days=max_period):
msg = f"Period cannot be be greater than {max_period} days"
raise ValidationError(msg)


def convert_availability_to_slots(availabilities):
def convert_availability_and_exceptions_to_slots(availabilities, exceptions, day):
slots = {}
for availability in availabilities:
start_time = parse(availability["availability"]["start_time"])
end_time = parse(availability["availability"]["end_time"])
start_time = datetime.datetime.combine(
day,
time.fromisoformat(availability["availability"]["start_time"]),
tzinfo=None,
)
end_time = datetime.datetime.combine(
day,
time.fromisoformat(availability["availability"]["end_time"]),
tzinfo=None,
)
slot_size_in_minutes = availability["slot_size_in_minutes"]
availability_id = availability["availability_id"]
current_time = start_time
i = 0
while current_time < end_time:
i += 1
if i == 30: # noqa PLR2004
if i == 30: # noqa PLR2004 pragma: no cover
# Failsafe to prevent infinite loop
break
slots[
f"{current_time.time()}-{(current_time + datetime.timedelta(minutes=slot_size_in_minutes)).time()}"
] = {
"start_time": current_time.time(),
"end_time": (
current_time + datetime.timedelta(minutes=slot_size_in_minutes)
).time(),
"availability_id": availability_id,
}

conflicting = False
for exception in exceptions:
exception_start_time = datetime.datetime.combine(
day, exception.start_time, tzinfo=None
)
exception_end_time = datetime.datetime.combine(
day, exception.end_time, tzinfo=None
)
if (
exception_start_time
< (current_time + datetime.timedelta(minutes=slot_size_in_minutes))
) and exception_end_time > current_time:
conflicting = True

if not conflicting:
slots[
f"{current_time.time()}-{(current_time + datetime.timedelta(minutes=slot_size_in_minutes)).time()}"
] = {
"start_time": current_time.time(),
"end_time": (
current_time + datetime.timedelta(minutes=slot_size_in_minutes)
).time(),
"availability_id": availability_id,
}

current_time += datetime.timedelta(minutes=slot_size_in_minutes)
return slots


def lock_create_appointment(token_slot, patient, created_by, reason_for_visit):
with Lock(f"booking:resource:{token_slot.resource.id}"), transaction.atomic():
if token_slot.start_datetime < timezone.now():
raise ValidationError("Slot is already past")
if token_slot.allocated >= token_slot.availability.tokens_per_slot:
raise ValidationError("Slot is already full")
if (
TokenBooking.objects.filter(token_slot=token_slot, patient=patient)
.exclude(status__in=CANCELLED_STATUS_CHOICES)
.exists()
):
raise ValidationError("Patient already has a booking for this slot")
token_slot.allocated += 1
token_slot.save()
return TokenBooking.objects.create(
Expand Down Expand Up @@ -132,9 +165,15 @@ def get_slots_for_day_handler(cls, facility_external_id, request_data):
"availability_id": schedule_availability.id,
}
)
# Remove anything that has an availability exception
# Generate all slots already created for that day
slots = convert_availability_to_slots(calculated_dow_availabilities)
exceptions = AvailabilityException.objects.filter(
resource=schedulable_resource_obj,
valid_from__lte=request_data.day,
valid_to__gte=request_data.day,
)
# Generate all slots already created for that day, exclude anything that conflicts with availability exception
slots = convert_availability_and_exceptions_to_slots(
calculated_dow_availabilities, exceptions, request_data.day
)
# Fetch all existing slots in that day
created_slots = TokenSlot.objects.filter(
start_datetime__date=request_data.day,
Expand Down Expand Up @@ -184,7 +223,7 @@ def create_appointment_handler(cls, obj, request_data, user):
request_data = AppointmentBookingSpec(**request_data)
patient = Patient.objects.filter(external_id=request_data.patient).first()
if not patient:
raise ValidationError({"Patient not found"})
raise ValidationError("Patient not found")
appointment = lock_create_appointment(
obj, patient, user, request_data.reason_for_visit
)
Expand Down Expand Up @@ -258,8 +297,8 @@ def availability_stats(self, request, *args, **kwargs):
# Calculate availability exception for that day
exceptions = []
for exception in availability_exceptions:
valid_from = timezone.make_naive(exception["valid_from"]).date()
valid_to = timezone.make_naive(exception["valid_to"]).date()
valid_from = exception["valid_from"]
valid_to = exception["valid_to"]
if valid_from <= day <= valid_to:
exceptions.append(exception)
# Calculate slots based on these data
Expand Down Expand Up @@ -311,17 +350,25 @@ def calculate_slots(
end_time = datetime.datetime.combine(
date, time.fromisoformat(available_slot["end_time"]), tzinfo=None
)
while start_time <= end_time:
current_start_time = start_time
while current_start_time < end_time:
conflicting = False
current_end_time = current_start_time + timedelta(
minutes=availability["slot_size_in_minutes"]
)
for exception in exceptions:
exception_start_time = datetime.datetime.combine(
date, exception["start_time"], tzinfo=None
)
exception_end_time = datetime.datetime.combine(
date, exception["end_time"], tzinfo=None
)
if (
exception["start_time"] <= end_time
and exception["end_time"] >= start_time
exception_start_time < current_end_time
and exception_end_time > current_start_time
):
conflicting = True
start_time = start_time + timedelta(
minutes=availability["slot_size_in_minutes"]
)
current_start_time = current_end_time
if conflicting:
continue
slots += availability["tokens_per_slot"]
Expand Down
2 changes: 0 additions & 2 deletions care/emr/api/viewsets/scheduling/booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ class TokenBookingFilters(FilterSet):
patient = UUIDFilter(field_name="patient__external_id")

def filter_by_user(self, queryset, name, value):
if not value:
return queryset
resource = SchedulableUserResource.objects.filter(
user__external_id=value
).first()
Expand Down
6 changes: 3 additions & 3 deletions care/emr/api/viewsets/scheduling/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from care.emr.models.scheduling.schedule import Availability, Schedule
from care.emr.resources.scheduling.schedule.spec import (
AvailabilityForScheduleSpec,
ScheduleCreateSpec,
ScheduleReadSpec,
ScheduleUpdateSpec,
ScheduleWriteSpec,
)
from care.facility.models import Facility
from care.security.authorization import AuthorizationController
Expand All @@ -32,7 +32,7 @@ class ScheduleFilters(FilterSet):

class ScheduleViewSet(EMRModelViewSet):
database_model = Schedule
pydantic_model = ScheduleWriteSpec
pydantic_model = ScheduleCreateSpec
pydantic_update_model = ScheduleUpdateSpec
pydantic_read_model = ScheduleReadSpec
filterset_class = ScheduleFilters
Expand Down Expand Up @@ -189,5 +189,5 @@ def authorize_create(self, instance):
):
raise PermissionDenied("You do not have permission to create schedule")

def authorize_delete(self, instance):
def authorize_destroy(self, instance):
self.authorize_create(instance)
14 changes: 12 additions & 2 deletions care/emr/resources/scheduling/availability_exception/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import UUID4
from rest_framework.exceptions import ValidationError

from care.emr.models import AvailabilityException
from care.emr.models import AvailabilityException, TokenSlot
from care.emr.models.scheduling.schedule import SchedulableUserResource
from care.emr.resources.base import EMRResource
from care.facility.models import Facility
Expand Down Expand Up @@ -34,7 +34,6 @@ class AvailabilityExceptionWriteSpec(AvailabilityExceptionBaseSpec):

def perform_extra_deserialization(self, is_update, obj):
if not is_update:
resource = None
try:
user = User.objects.get(external_id=self.user)
resource = SchedulableUserResource.objects.get(
Expand All @@ -45,6 +44,17 @@ def perform_extra_deserialization(self, is_update, obj):
except ObjectDoesNotExist as e:
raise ValidationError("Object does not exist") from e

slots = TokenSlot.objects.filter(
resource=obj.resource,
start_datetime__date__gte=self.valid_from,
start_datetime__date__lte=self.valid_to,
start_datetime__time__gte=self.start_time,
start_datetime__time__lte=self.end_time,
)
if slots.filter(allocated__gt=0):
raise ValidationError("There are bookings during this exception")
slots.delete()


class AvailabilityExceptionReadSpec(AvailabilityExceptionBaseSpec):
@classmethod
Expand Down
Loading

0 comments on commit 8d33a6f

Please sign in to comment.