Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduling and Appointments: Validations and other minor fixes #2711

Merged
merged 16 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
)
vigneshhari marked this conversation as resolved.
Show resolved Hide resolved
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):
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
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
Loading