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 4 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
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
52 changes: 42 additions & 10 deletions care/emr/api/viewsets/scheduling/booking.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
from typing import Literal

from django.db import transaction
from django_filters import CharFilter, DateFilter, 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
from care.utils.lock import Lock


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


class TokenBookingFilters(FilterSet):
Expand All @@ -40,13 +52,20 @@ def filter_by_user(self, queryset, name, value):
return queryset.filter(token_slot__resource=resource)


def lock_cancel_appointment(token_booking):
token_slot = token_booking.token_slot
with Lock(f"booking:slot:{token_slot.id}"), transaction.atomic():
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
token_slot.allocated -= 1
token_slot.save()

rithviknishad marked this conversation as resolved.
Show resolved Hide resolved

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 +76,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 +104,23 @@ def get_queryset(self):
.order_by("-modified_date")
)

@classmethod
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved
def cancel_appointment_handler(cls, instance, request_data, user):
request_data = CancelBookingSpec(**request_data)
if instance.status not in CANCELLED_STATUS_CHOICES:
# Free up the slot if it is not cancelled already
lock_cancel_appointment(instance)
instance.status = request_data.reason
instance.updated_by = user
instance.save()
return Response({"status": request_data.reason})

@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 Down
20 changes: 20 additions & 0 deletions care/emr/api/viewsets/scheduling/schedule.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from django.db import transaction
from django.utils import timezone
from django_filters import FilterSet, UUIDFilter
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import get_object_or_404

from care.emr.api.viewsets.base import EMRModelViewSet
from care.emr.models.organization import FacilityOrganizationUser
from care.emr.models.scheduling.booking import TokenSlot
from care.emr.models.scheduling.schedule import Schedule
from care.emr.resources.scheduling.schedule.spec import (
ScheduleReadSpec,
Expand All @@ -15,6 +17,7 @@
from care.facility.models import Facility
from care.security.authorization import AuthorizationController
from care.users.models import User
from care.utils.lock import Lock


class ScheduleFilters(FilterSet):
Expand Down Expand Up @@ -43,6 +46,23 @@ def perform_create(self, instance):
availability_obj.schedule = instance
availability_obj.save()

def perform_delete(self, instance):
with Lock(f"booking:resource:{instance.resource.id}"), transaction.atomic():
# Check if there are any tokens allocated for this schedule in the future
availabilities = instance.availability_set.all()
has_future_bookings = TokenSlot.objects.filter(
resource=instance.resource,
availability_id__in=availabilities.values_list("id", flat=True),
start_datetime__gt=timezone.now(),
allocated__gt=0,
).exists()
if has_future_bookings:
raise ValidationError(
"Cannot delete schedule as there are future bookings associated with it"
)
availabilities.update(deleted=True)
super().perform_delete(instance)

def authorize_delete(self, instance):
self.authorize_update({}, instance)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ class AvailabilityExceptionBaseSpec(EMRResource):

class AvailabilityExceptionWriteSpec(AvailabilityExceptionBaseSpec):
facility: UUID4 | None = None
resource: UUID4
user: UUID4

def perform_extra_deserialization(self, is_update, obj):
if not is_update:
resource = None
try:
user = User.objects.get(external_id=self.resource)
user = User.objects.get(external_id=self.user)
resource = SchedulableUserResource.objects.get(
user=user,
facility=Facility.objects.get(external_id=self.facility),
Expand Down
4 changes: 0 additions & 4 deletions care/emr/resources/scheduling/schedule/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ class SlotTypeOptions(str, Enum):
closed = "closed"


class ResourceTypeOptions(str, Enum):
user = "user"


class AvailabilityDateTimeSpec(EMRResource):
day_of_week: int = Field(le=6)
start_time: datetime.time
Expand Down
7 changes: 6 additions & 1 deletion care/emr/resources/scheduling/slot/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import Enum

from pydantic import UUID4
from rest_framework.exceptions import ValidationError

from care.emr.models import TokenBooking
from care.emr.models.scheduling.booking import TokenSlot
Expand Down Expand Up @@ -64,9 +65,13 @@ class TokenBookingBaseSpec(EMRResource):
__exclude__ = ["token_slot", "patient"]


class TokenBookingUpdateSpec(TokenBookingBaseSpec):
class TokenBookingWriteSpec(TokenBookingBaseSpec):
status: BookingStatusChoices

def perform_extra_deserialization(self, is_update, obj):
if self.status in CANCELLED_STATUS_CHOICES:
raise ValidationError("Cannot cancel a booking. Use the cancel endpoint")


class TokenBookingReadSpec(TokenBookingBaseSpec):
id: UUID4 | None = None
Expand Down
Loading