Skip to content

Commit

Permalink
disallow creating availability that is overlapping with other availab…
Browse files Browse the repository at this point in the history
…ilities
  • Loading branch information
rithviknishad committed Jan 30, 2025
1 parent 7111ed6 commit 525ca75
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 14 deletions.
8 changes: 7 additions & 1 deletion care/emr/api/viewsets/scheduling/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ScheduleCreateSpec,
ScheduleReadSpec,
ScheduleUpdateSpec,
AvailabilityCreateSpec,
)
from care.facility.models import Facility
from care.security.authorization import AuthorizationController
Expand Down Expand Up @@ -132,7 +133,8 @@ def get_queryset(self):

class AvailabilityViewSet(EMRCreateMixin, EMRDestroyMixin, EMRBaseViewSet):
database_model = Availability
pydantic_model = AvailabilityForScheduleSpec
pydantic_model = AvailabilityCreateSpec
pydantic_retrieve_model = AvailabilityForScheduleSpec

def get_facility_obj(self):
return get_object_or_404(
Expand Down Expand Up @@ -164,6 +166,10 @@ def get_queryset(self):
.order_by("-modified_date")
)

def clean_create_data(self, request_data):
request_data["schedule"] = self.kwargs["schedule_external_id"]
return request_data

def perform_create(self, instance):
schedule = self.get_schedule_obj()
instance.schedule = schedule
Expand Down
65 changes: 54 additions & 11 deletions care/emr/resources/scheduling/schedule/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,8 @@ class AvailabilityForScheduleSpec(AvailabilityBaseSpec):
@field_validator("availability")
@classmethod
def validate_availability(cls, availabilities: list[AvailabilityDateTimeSpec]):
# Validates if availability overlaps for the same day
for i in range(len(availabilities)):
for j in range(i + 1, len(availabilities)):
if availabilities[i].day_of_week != availabilities[j].day_of_week:
continue
# Check if time ranges overlap
if (
availabilities[i].start_time <= availabilities[j].end_time
and availabilities[j].start_time <= availabilities[i].end_time
):
raise ValueError("Availability time ranges are overlapping")
if has_overlapping_availability(availabilities):
raise ValueError("Availability time ranges are overlapping")
for availability in availabilities:
if availability.start_time >= availability.end_time:
raise ValueError("Start time must be earlier than end time")
Expand Down Expand Up @@ -95,6 +86,31 @@ def validate_for_slot_type(self):
return self


class AvailabilityCreateSpec(AvailabilityForScheduleSpec):
schedule: UUID4

@model_validator(mode="after")
def check_for_overlaps(self):
availabilities = Availability.objects.filter(
schedule__external_id=self.schedule
)
all_availabilities = [*self.availability]
for availability in availabilities:
all_availabilities.extend(
[
AvailabilityDateTimeSpec(
day_of_week=availability["day_of_week"],
start_time=availability["start_time"],
end_time=availability["end_time"],
)
for availability in availability.availability
]
)
if has_overlapping_availability(all_availabilities):
raise ValueError("Availability time ranges are overlapping")
return self


class ScheduleBaseSpec(EMRResource):
__model__ = Schedule
__exclude__ = ["resource", "facility"]
Expand All @@ -116,6 +132,18 @@ def validate_period(self):
raise ValidationError("Valid from cannot be greater than valid to")
return self

@field_validator("availabilities")
@classmethod
def validate_availabilities_not_overlapping(
cls, availabilities: list[AvailabilityForScheduleSpec]
):
all_availabilities = []
for availability in availabilities:
all_availabilities.extend(availability.availability)
if has_overlapping_availability(all_availabilities):
raise ValueError("Availability time ranges are overlapping")
return availabilities

def perform_extra_deserialization(self, is_update, obj):
user = get_object_or_404(User, external_id=self.user)
# TODO Validation that user is in given facility
Expand Down Expand Up @@ -188,3 +216,18 @@ def perform_extra_serialization(cls, mapping, obj):
AvailabilityForScheduleSpec.serialize(o)
for o in Availability.objects.filter(schedule=obj)
]


def has_overlapping_availability(availabilities: list[AvailabilityDateTimeSpec]):
for i in range(len(availabilities)):
for j in range(i + 1, len(availabilities)):
# Skip checking for overlap if it's not the same day of week
if availabilities[i].day_of_week != availabilities[j].day_of_week:
continue
# Check if time ranges overlap
if (
availabilities[i].start_time <= availabilities[j].end_time
and availabilities[j].start_time <= availabilities[i].end_time
):
return True
return False
82 changes: 80 additions & 2 deletions care/emr/tests/test_schedule_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,55 @@ def test_create_schedule_with_invalid_dates(self):
valid_from=valid_from.isoformat(), valid_to=valid_to.isoformat()
)
response = self.client.post(self.base_url, schedule_data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertContains(
response, "Valid from cannot be greater than valid to", status_code=400
)

def test_create_schedule_with_overlapping_availability(self):
"""Schedule creation fails when availability sessions overlap"""
permissions = [UserSchedulePermissions.can_write_user_schedule.name]
role = self.create_role_with_permissions(permissions)
self.attach_role_facility_organization_user(self.organization, self.user, role)

schedule_data = self.generate_schedule_data(
availabilities=[
{
"name": "Availability 1",
"slot_type": SlotTypeOptions.appointment.value,
"slot_size_in_minutes": 30,
"tokens_per_slot": 1,
"create_tokens": True,
"reason": "Regular schedule",
"availability": [
{
"day_of_week": 1,
"start_time": "09:00:00",
"end_time": "13:00:00",
},
],
},
{
"name": "Availability 2",
"slot_type": SlotTypeOptions.appointment.value,
"slot_size_in_minutes": 30,
"tokens_per_slot": 1,
"create_tokens": True,
"reason": "Regular schedule",
"availability": [
{
"day_of_week": 1,
"start_time": "08:00:00",
"end_time": "10:00:00",
},
],
},
]
)
response = self.client.post(self.base_url, schedule_data, format="json")
self.assertContains(
response, "Availability time ranges are overlapping", status_code=400
)

def test_create_schedule_with_user_not_part_of_facility(self):
"""Users cannot write schedules for user not belonging to the facility."""
permissions = [UserSchedulePermissions.can_write_user_schedule.name]
Expand Down Expand Up @@ -715,6 +759,40 @@ def test_create_availability_with_permissions(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["name"], availability_data["name"])

def test_create_availability_overlapping_with_existing_availabilities(self):
"""Users cannot create availability that overlaps with existing availabilities."""
permissions = [UserSchedulePermissions.can_write_user_schedule.name]
role = self.create_role_with_permissions(permissions)
self.attach_role_facility_organization_user(self.organization, self.user, role)

self.create_availability(
availability=[
{"day_of_week": 1, "start_time": "08:00:00", "end_time": "10:00:00"},
]
)

availability_data = self.generate_availability_data()
response = self.client.post(self.base_url, availability_data, format="json")
self.assertContains(
response, "Availability time ranges are overlapping", status_code=400
)

def test_create_availability_not_overlapping_with_existing_availabilities(self):
"""Users can create availability that does not overlap with existing availabilities."""
permissions = [UserSchedulePermissions.can_write_user_schedule.name]
role = self.create_role_with_permissions(permissions)
self.attach_role_facility_organization_user(self.organization, self.user, role)

self.create_availability(
availability=[
{"day_of_week": 1, "start_time": "14:00:00", "end_time": "20:00:00"},
]
)

availability_data = self.generate_availability_data()
response = self.client.post(self.base_url, availability_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_create_availability_without_permissions(self):
"""Users without can_write_user_schedule permission cannot create availability."""
availability_data = self.generate_availability_data()
Expand Down Expand Up @@ -865,7 +943,7 @@ def test_create_availability_validate_duration_multiple_of_slot_size_in_minutes(
{
"day_of_week": 1, # Monday
"start_time": "09:00:00",
"end_time": "13:10:00",
"end_time": "13:13:00",
},
]
)
Expand Down

0 comments on commit 525ca75

Please sign in to comment.