Skip to content

Commit

Permalink
Merge pull request #2711 from ohcnetwork/rithviknishad/feat/schedule-…
Browse files Browse the repository at this point in the history
…validations

Scheduling and Appointments: Validations and other minor fixes
  • Loading branch information
vigneshhari authored Jan 8, 2025
2 parents 98641ae + f47ee49 commit 8b753ad
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 53 deletions.
27 changes: 25 additions & 2 deletions care/emr/api/otp_viewsets/slot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import UUID4
from pydantic import UUID4, BaseModel
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response

from care.emr.api.viewsets.base import EMRBaseViewSet, EMRRetrieveMixin
Expand All @@ -9,9 +10,11 @@
SlotsForDayRequestSpec,
SlotViewSet,
)
from care.emr.api.viewsets.scheduling.booking import TokenBookingViewSet
from care.emr.models.patient import Patient
from care.emr.models.scheduling import TokenBooking, TokenSlot
from care.emr.resources.scheduling.slot.spec import (
BookingStatusChoices,
TokenBookingReadSpec,
TokenSlotBaseSpec,
)
Expand All @@ -25,6 +28,11 @@ class SlotsForDayRequestSpec(SlotsForDayRequestSpec):
facility: UUID4


class CancelAppointmentSpec(BaseModel):
patient: UUID4
appointment: UUID4


class OTPSlotViewSet(EMRRetrieveMixin, EMRBaseViewSet):
authentication_classes = [JWTTokenPatientAuthentication]
permission_classes = [OTPAuthenticatedPermission]
Expand All @@ -44,11 +52,26 @@ def create_appointment(self, request, *args, **kwargs):
if not Patient.objects.filter(
external_id=request_data.patient, phone_number=request.user.phone_number
).exists():
raise ValidationError("Patient not allowed ")
raise ValidationError("Patient not allowed")
return SlotViewSet.create_appointment_handler(
self.get_object(), request.data, None
)

@action(detail=False, methods=["POST"])
def cancel_appointment(self, request, *args, **kwargs):
request_data = CancelAppointmentSpec(**request.data)
patient = get_object_or_404(
Patient,
external_id=request_data.patient,
phone_number=request.user.phone_number,
)
token_booking = get_object_or_404(
TokenBooking, external_id=request_data.appointment, patient=patient
)
return TokenBookingViewSet.cancel_appointment_handler(
token_booking, {"reason": BookingStatusChoices.cancelled}, None
)

@action(detail=False, methods=["GET"])
def get_appointments(self, request, *args, **kwargs):
appointments = TokenBooking.objects.filter(
Expand Down
27 changes: 17 additions & 10 deletions care/emr/api/viewsets/scheduling/availability.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ def get_slots_for_day_handler(cls, facility_external_id, request_data):
TokenSlot.objects.create(
resource=schedulable_resource_obj,
start_datetime=datetime.datetime.combine(
request_data.day, slot["start_time"], tzinfo=timezone.now().tzinfo
request_data.day, slot["start_time"], tzinfo=None
),
end_datetime=datetime.datetime.combine(
request_data.day, slot["end_time"], tzinfo=timezone.now().tzinfo
request_data.day, slot["end_time"], tzinfo=None
),
availability_id=slot["availability_id"],
)
Expand Down Expand Up @@ -251,12 +251,16 @@ def availability_stats(self, request, *args, **kwargs):
# Calculate all matching schedules
current_schedules = []
for schedule in schedules:
if schedule["valid_from"].date() <= day <= schedule["valid_to"].date():
valid_from = timezone.make_naive(schedule["valid_from"]).date()
valid_to = timezone.make_naive(schedule["valid_to"]).date()
if valid_from <= day <= valid_to:
current_schedules.append(schedule)
# Calculate availability exception for that day
exceptions = []
for exception in availability_exceptions:
if exception["valid_from"] <= day <= exception["valid_to"]:
valid_from = timezone.make_naive(exception["valid_from"]).date()
valid_to = timezone.make_naive(exception["valid_to"]).date()
if valid_from <= day <= valid_to:
exceptions.append(exception)
# Calculate slots based on these data

Expand Down Expand Up @@ -301,8 +305,12 @@ def calculate_slots(
for available_slot in availability["availability"]:
if available_slot["day_of_week"] != day_of_week:
continue
start_time = time.fromisoformat(available_slot["start_time"])
end_time = time.fromisoformat(available_slot["end_time"])
start_time = datetime.datetime.combine(
date, time.fromisoformat(available_slot["start_time"]), tzinfo=None
)
end_time = datetime.datetime.combine(
date, time.fromisoformat(available_slot["end_time"]), tzinfo=None
)
while start_time <= end_time:
conflicting = False
for exception in exceptions:
Expand All @@ -311,10 +319,9 @@ def calculate_slots(
and exception["end_time"] >= start_time
):
conflicting = True
start_time = (
datetime.datetime.combine(date.today(), start_time)
+ timedelta(minutes=availability["slot_size_in_minutes"])
).time()
start_time = start_time + timedelta(
minutes=availability["slot_size_in_minutes"]
)
if conflicting:
continue
slots += availability["tokens_per_slot"]
Expand Down
12 changes: 6 additions & 6 deletions care/emr/api/viewsets/scheduling/availability_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from rest_framework.generics import get_object_or_404

from care.emr.api.viewsets.base import EMRModelViewSet
from care.emr.models import AvailabilityException, SchedulableUserResource
from care.emr.models import AvailabilityException
from care.emr.resources.scheduling.availability_exception.spec import (
AvailabilityExceptionReadSpec,
AvailabilityExceptionWriteSpec,
)
from care.facility.models import Facility
from care.security.authorization import AuthorizationController
from care.users.models import User


class AvailabilityExceptionFilters(FilterSet):
Expand Down Expand Up @@ -38,14 +39,13 @@ def authorize_delete(self, instance):
self.authorize_update({}, instance)

def authorize_create(self, instance):
user_resource = get_object_or_404(
SchedulableUserResource, external_id=instance.resource
)
facility = self.get_facility_obj()
schedule_user = get_object_or_404(User, external_id=instance.user)
if not AuthorizationController.call(
"can_write_user_schedule",
self.request.user,
user_resource.facility,
user_resource.user,
facility,
schedule_user,
):
raise PermissionDenied("You do not have permission to view user schedule")

Expand Down
54 changes: 41 additions & 13 deletions care/emr/api/viewsets/scheduling/booking.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
from django_filters import CharFilter, DateFilter, FilterSet, UUIDFilter
from typing import Literal

from django.db import transaction
from django_filters import CharFilter, DateFromToRangeFilter, FilterSet, UUIDFilter
from django_filters.rest_framework import DjangoFilterBackend
from pydantic import BaseModel
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response

from care.emr.api.viewsets.base import (
EMRBaseViewSet,
EMRDeleteMixin,
EMRListMixin,
EMRRetrieveMixin,
EMRUpdateMixin,
)
from care.emr.models.scheduling import SchedulableUserResource, TokenBooking
from care.emr.resources.scheduling.slot.spec import (
CANCELLED_STATUS_CHOICES,
BookingStatusChoices,
TokenBookingReadSpec,
TokenBookingUpdateSpec,
TokenBookingWriteSpec,
)
from care.emr.resources.user.spec import UserSpec
from care.facility.models import Facility, FacilityOrganizationUser
from care.security.authorization import AuthorizationController


class CancelBookingSpec(BaseModel):
reason: Literal[
BookingStatusChoices.cancelled, BookingStatusChoices.entered_in_error
]


class TokenBookingFilters(FilterSet):
status = CharFilter(field_name="status")
date = DateFilter(field_name="token_slot__start_datetime__date")
date = DateFromToRangeFilter(field_name="token_slot__start_datetime__date")
slot = UUIDFilter(field_name="token_slot__external_id")
user = UUIDFilter(method="filter_by_user")
patient = UUIDFilter(field_name="patient__external_id")
Expand All @@ -41,12 +52,12 @@ def filter_by_user(self, queryset, name, value):


class TokenBookingViewSet(
EMRRetrieveMixin, EMRUpdateMixin, EMRListMixin, EMRDeleteMixin, EMRBaseViewSet
EMRRetrieveMixin, EMRUpdateMixin, EMRListMixin, EMRBaseViewSet
):
database_model = TokenBooking
pydantic_model = TokenBookingReadSpec
pydantic_model = TokenBookingWriteSpec
pydantic_read_model = TokenBookingReadSpec
pydantic_update_model = TokenBookingUpdateSpec
pydantic_update_model = TokenBookingWriteSpec

filterset_class = TokenBookingFilters
filter_backends = [DjangoFilterBackend]
Expand All @@ -57,18 +68,14 @@ def get_facility_obj(self):
Facility, external_id=self.kwargs["facility_external_id"]
)

def authorize_delete(self, instance):
# TODO, need more depth to handle this case
pass

def authorize_update(self, request_obj, model_instance):
if not AuthorizationController.call(
"can_write_user_booking",
self.request.user,
model_instance.token_slot.resource.facility,
model_instance.token_slot.resource.user,
):
raise PermissionDenied("You do not have permission to view user schedule")
raise PermissionDenied("You do not have permission to update bookings")

def get_queryset(self):
facility = self.get_facility_obj()
Expand All @@ -89,6 +96,27 @@ def get_queryset(self):
.order_by("-modified_date")
)

@classmethod
def cancel_appointment_handler(cls, instance, request_data, user):
request_data = CancelBookingSpec(**request_data)
with transaction.atomic():
if instance.status not in CANCELLED_STATUS_CHOICES:
# Free up the slot if it is not cancelled already
instance.token_slot.allocated -= 1
instance.token_slot.save()
instance.status = request_data.reason
instance.updated_by = user
instance.save()
return Response(
TokenBookingReadSpec.serialize(instance).model_dump(exclude=["meta"])
)

@action(detail=True, methods=["POST"])
def cancel(self, request, *args, **kwargs):
instance = self.get_object()
self.authorize_update({}, instance)
return self.cancel_appointment_handler(instance, request.data, request.user)

@action(detail=False, methods=["GET"])
def available_users(self, request, *args, **kwargs):
facility = Facility.objects.get(external_id=self.kwargs["facility_external_id"])
Expand All @@ -102,7 +130,7 @@ def available_users(self, request, *args, **kwargs):
return Response(
{
"users": [
UserSpec.serialize(facility_user.user).model_dump(exclude=["meta"])
UserSpec.serialize(facility_user.user).to_json()
for facility_user in facility_users
]
}
Expand Down
Loading

0 comments on commit 8b753ad

Please sign in to comment.