Skip to content

Commit

Permalink
adds ecceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
Snorre98 committed Sep 25, 2024
1 parent 8c3062f commit 94eb43c
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 21 deletions.
6 changes: 6 additions & 0 deletions backend/samfundet/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ class InsufficientTimeBlocksError(InterviewAllocationError):
"""Raised when there are not enough time blocks to accommodate all applications."""

pass


class NoFutureTimeSlotsError(InterviewAllocationError):
"""Raised when there are no time slots available at least 24 hours in the future."""

pass
45 changes: 30 additions & 15 deletions backend/samfundet/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,16 @@ def generate_blocks(position, start_dt, end_dt, unavailability, interval):
def allocate_interviews_for_position(position, limit_to_first_applicant=False) -> int:
print(f'Starting allocation for position: {position.name_en}')

# Get the time blocks for the specific position, sorted by rating (descending) and then start time (ascending)
# Define interview duration
interview_duration = timedelta(minutes=30)

timeblocks = sorted(InterviewTimeblock.objects.filter(recruitment_position=position), key=lambda block: (-block.rating, block.start_dt))
print(f'Number of available time blocks: {len(timeblocks)}')
if not timeblocks:
raise NoTimeBlocksAvailableError(f'No available time blocks for position: {position.name_en}')

# Fetch only applications without interviews
applications = list(RecruitmentApplication.objects.filter(recruitment_position=position, withdrawn=False, interview__isnull=True))
print(f'Number of applications without interviews: {len(applications)}')
if not applications:
raise NoApplicationsWithoutInterviewsError(f'No applications without interviews for position: {position.name_en}')

# Prepare unavailability data
applicant_unavailability = defaultdict(list)
Expand All @@ -226,21 +229,20 @@ def allocate_interviews_for_position(position, limit_to_first_applicant=False) -
existing_interviews = Interview.objects.filter(applications__recruitment_position=position)
for interview in existing_interviews:
for interviewer in interview.interviewers.all():
interviewer_unavailability[interviewer.id].append((interview.interview_time, interview.interview_time + timedelta(minutes=30)))
interviewer_unavailability[interviewer.id].append((interview.interview_time, interview.interview_time + interview_duration))
# Mark the interview time as unavailable for all interviewers
for interviewer in position.interviewers.all():
interviewer_unavailability[interviewer.id].append((interview.interview_time, interview.interview_time + timedelta(minutes=30)))
interviewer_unavailability[interviewer.id].append((interview.interview_time, interview.interview_time + interview_duration))

interview_count = 0
interview_duration = timedelta(minutes=30)
all_applicants_unavailable = True
current_time = timezone.now() + timedelta(hours=24) # Ensure interviews are at least 24 hours in the future

for block in timeblocks:
if block.end_dt <= current_time:
continue # Skip blocks that are in the past or within the next 24 hours

print(f'Processing block: {block.start_dt} - {block.end_dt}, Rating: {block.rating}')
future_blocks = [block for block in timeblocks if block.end_dt > current_time]
if not future_blocks:
raise NoFutureTimeSlotsError(f'No time slots available at least 24 hours in the future for position: {position.name_en}')

for block in future_blocks:
block_start = max(block.start_dt, current_time)
current_time = block_start

Expand All @@ -267,6 +269,7 @@ def allocate_interviews_for_position(position, limit_to_first_applicant=False) -
print(f' Checking applicant: {applicant.username}')

if is_applicant_available(applicant, current_time, interview_end_time, applicant_unavailability):
all_applicants_unavailable = False
# Create and assign a new interview
interview = Interview.objects.create(
interview_time=current_time,
Expand All @@ -284,12 +287,15 @@ def allocate_interviews_for_position(position, limit_to_first_applicant=False) -
mark_applicant_unavailable(applicant, current_time, interview_end_time)
mark_interviewers_unavailable(available_interviewers, current_time, interview_end_time, interviewer_unavailability)

# Add the new interview to existing_interviews to prevent double-booking
existing_interviews = list(existing_interviews) + [interview]

interview_count += 1
applications.remove(application)
print(f' Allocated interview for {applicant.username} at {current_time}')

if limit_to_first_applicant:
return interview_count # Exit after allocating for the first applicant
return interview_count

break
else:
Expand All @@ -298,10 +304,19 @@ def allocate_interviews_for_position(position, limit_to_first_applicant=False) -
current_time += interview_duration

if not applications:
print('All applications have been processed')
break

print(f'Finished allocation. Total new interviews allocated: {interview_count}')
if interview_count == 0:
if all_applicants_unavailable:
raise AllApplicantsUnavailableError(f'All applicants are unavailable for the remaining time slots for position: {position.name_en}')
else:
raise NoAvailableInterviewersError(f'No available interviewers for any time slot for position: {position.name_en}')

if applications:
raise InsufficientTimeBlocksError(
f'Not enough time blocks to accommodate all applications for position: {position.name_en}. Allocated {interview_count} interviews.'
)

return interview_count


Expand Down
21 changes: 15 additions & 6 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import hashlib
from typing import Any
from datetime import datetime, timedelta
from .exceptions import *

from guardian.shortcuts import get_objects_for_user

Expand Down Expand Up @@ -1303,19 +1304,27 @@ def post(self, request: Request) -> Response:
class AllocateInterviewsForPositionView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request: Request, pk):
def get(self, request, pk):
try:
# Get the specific recruitment position
position = get_object_or_404(RecruitmentPosition, id=pk)

# Allocate interviews for the position (method explained below)
interview_count = allocate_interviews_for_position(position)

return Response(
{'message': f'Interviews allocated successfully for position {pk}.', 'interviews_allocated': interview_count},
status=status.HTTP_200_OK,
)
except NoTimeBlocksAvailableError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except NoApplicationsWithoutInterviewsError as e:
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
except NoAvailableInterviewersError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except AllApplicantsUnavailableError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except InsufficientTimeBlocksError as e:
return Response({'error': str(e), 'partial_allocation': True}, status=status.HTTP_206_PARTIAL_CONTENT)
except NoFutureTimeSlotsError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except RecruitmentPosition.DoesNotExist:
return Response({'error': f'Recruitment position with id {pk} does not exist.'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'error': f'Failed to allocate interviews: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'error': f'An unexpected error occurred: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

0 comments on commit 94eb43c

Please sign in to comment.