diff --git a/vsvs_scheduler/__init__.py b/vsvs_scheduler/__init__.py index d3f5a12..93b7576 100644 --- a/vsvs_scheduler/__init__.py +++ b/vsvs_scheduler/__init__.py @@ -1 +1 @@ - +COLUMN_NAMES diff --git a/vsvs_scheduler/applicants/classroom.py b/vsvs_scheduler/applicants/classroom.py index 4b4c653..f9a6b31 100644 --- a/vsvs_scheduler/applicants/classroom.py +++ b/vsvs_scheduler/applicants/classroom.py @@ -5,7 +5,7 @@ class Classroom: - def __init__(self, group_number: int, teacher: Teacher, start_time: str, end_time: str, weekdays: []): + def __init__(self, group_number: int, teacher: Teacher, start_time: str, end_time: str): """ Classroom object that holds information about a classroom and its volunteers. @@ -18,14 +18,11 @@ def __init__(self, group_number: int, teacher: Teacher, start_time: str, end_tim self.group_number = group_number self.teacher = teacher - self.weekdays = weekdays - self.weekday_idx = 0 # index of the weekday in weekdays - self.weekday = weekdays[self.weekday_idx] # weekday the class is held on - self.volunteers = [] self.possible_volunteers = 0 self.possible_partner_groups = 0 - self.team_leader = False + self.team_leader = False + self.weekday = None self.start_time = datetime.strptime(start_time, '%I:%M:%S %p') self.end_time = datetime.strptime(end_time, '%I:%M:%S %p') @@ -53,22 +50,20 @@ def unassign_volunteers(self): self.volunteers = [] self.team_leader = False + def change_to_next_preferred_day(self): + """ Updates the weekday to the next weekday preference. """ + self.teacher.next_weekday() + + def freeze_weekday(self): + """ Freezes the weekday to the current weekday preference. """ + if self.weekday is None: + self.weekday = self.teacher.weekday def duration(self): """ Returns the duration of the class in minutes. (includes travel time) """ return (self.end_time - self.start_time + timedelta(hours=1)).seconds/60 - - def change_to_next_preferred_day(self): - """ Changes the weekday to the next preferred day.""" - possible_weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday"] - if self.weekday_idx < 3 and self.weekdays[self.weekday_idx + 1] in possible_weekdays: - self.weekday_idx += 1 - self.weekday = self.weekdays[self.weekday_idx] - - - def __repr__(self): - return f"{self.teacher.name} at {self.teacher.school} on {self.weekday} ({self.start_time.strftime( '%I:%M %p')} - {strftime(self.end_time.strftime('%I:%M %p'))})\n" + return f"{self.teacher.name} at {self.teacher.school} on {self.teacher.weekday} ({self.start_time.strftime( '%I:%M %p')} - {strftime(self.end_time.strftime('%I:%M %p'))})\n" diff --git a/vsvs_scheduler/applicants/schedule.py b/vsvs_scheduler/applicants/schedule.py index 412ffd1..ce7b15b 100644 --- a/vsvs_scheduler/applicants/schedule.py +++ b/vsvs_scheduler/applicants/schedule.py @@ -83,9 +83,13 @@ def find_free_time_duration(self, availability: dict): - def can_make_class(self, classroom: Classroom): + def can_make_class(self, classroom: Classroom, last_round: bool = False) -> bool: """ Returns True if the volunteer can make the class, False otherwise.""" - + if last_round and classroom.weekday != None: + weekday = classroom.weekday + else: + weekday = classroom.teacher.weekday + # Standardize the start time to the nearest 15 minute interval time_deviation = classroom.start_time.minute % 15 if time_deviation != 0: @@ -94,7 +98,7 @@ def can_make_class(self, classroom: Classroom): start_time = classroom.start_time # Volunteer schedule for the day of the class - weekday_schedule = self.processed_schedule[classroom.weekday] + weekday_schedule = self.processed_schedule[weekday] if start_time in weekday_schedule: return weekday_schedule[start_time] >= classroom.duration() diff --git a/vsvs_scheduler/applicants/teacher.py b/vsvs_scheduler/applicants/teacher.py index 6cb5785..101cd7e 100644 --- a/vsvs_scheduler/applicants/teacher.py +++ b/vsvs_scheduler/applicants/teacher.py @@ -1,11 +1,37 @@ + class Teacher: - def __init__(self, name: str, phone: str, school: str, email: str) -> None: + def __init__(self, name: str, phone: str, school: str, email: str, weekday_preferences: list) -> None: """ Teacher object that holds information about a teacher.""" self.name = name self.phone = phone #USE REGEX self.school = school self.email = email #USE REGEX + self.weekdays = weekday_preferences + self.weekday_idx = 0 + self.weekday = self.weekdays[self.weekday_idx] + self.classrooms = [] + + def next_weekday(self) -> None: + """ Updates the weekday to the next weekday preference. """ + possible_weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday"] + if self.weekday_idx < 3 and self.weekdays[self.weekday_idx + 1] in possible_weekdays: + self.weekday_idx += 1 + self.weekday = self.weekdays[self.weekday_idx] + + def add_classrooms(self, classroom) -> None: + """ Adds classrooms to the teacher's list of classrooms. """ + self.classrooms.append(classroom) + + def unassign_volunteers(self) -> None: + """ Unassigns volunteers from the classroom and resets their group number and assigned_leader status.""" + for classroom in self.classrooms: + classroom.unassign_volunteers() + + def reset_weekday(self) -> None: + """ Resets the weekday to the first weekday preference. """ + self.weekday_idx = 0 + self.weekday = self.weekdays[self.weekday_idx] diff --git a/vsvs_scheduler/applicants/volunteer.py b/vsvs_scheduler/applicants/volunteer.py index 4fa155e..2aed11b 100644 --- a/vsvs_scheduler/applicants/volunteer.py +++ b/vsvs_scheduler/applicants/volunteer.py @@ -38,7 +38,10 @@ def can_make_class(self, classroom: Classroom): return self.availability.can_make_class(classroom) + def can_make_class_last_round(self, classroom: Classroom): + """Returns boolean of whether volunteer can make a classroom based on the schedule parameter.""" + return self.availability.can_make_class(classroom, last_round=True) def assign_classroom(self, classroom: Classroom): """Assigns the volunteer to the classroom and updates the volunteer's group number.""" diff --git a/vsvs_scheduler/class_data_uploader.py b/vsvs_scheduler/class_data_uploader.py index aa9c0b3..8fa1580 100644 --- a/vsvs_scheduler/class_data_uploader.py +++ b/vsvs_scheduler/class_data_uploader.py @@ -15,32 +15,31 @@ def __init__(self): def process_row_data(self, row: dict): """ Process row data from csv/excel file into Classroom objects.""" - number_of_classes = row['Number of Classes'] + number_of_classes = int(row['Number of Classes']) teacher = Teacher ( name= row['Name'], phone= row['Cell Phone Number'], school= row['School'], - email= row['Email Address'] - ) + email= row['Email Address'], + weekday_preferences=[ + row[f'Days (Class {number_of_classes} of {number_of_classes}) [1st Preference]'], + row[f'Days (Class {number_of_classes} of {number_of_classes}) [2nd Preference]'], + row[f'Days (Class {number_of_classes} of {number_of_classes}) [3rd Preference]'], + row[f'Days (Class {number_of_classes} of {number_of_classes}) [4th Preference]'] + ] + ) # for each class a teacher has, create a classroom object and add it to the list 'applicants' - for i in range(int(number_of_classes)): + for i in range(number_of_classes): self.group_num += 1 class_num = i + 1 # class_num keeps track of which class out of the total being created classroom = Classroom( group_number=self.group_num, teacher=teacher, start_time=row[f'Start Time (Class {class_num} of {number_of_classes})'], - end_time=row[f'End Time (Class {class_num} of {number_of_classes})'], - weekdays=[ - row[f'Days (Class {class_num} of {number_of_classes}) [1st Preference]'], - row[f'Days (Class {class_num} of {number_of_classes}) [2nd Preference]'], - row[f'Days (Class {class_num} of {number_of_classes}) [3rd Preference]'], - row[f'Days (Class {class_num} of {number_of_classes}) [4th Preference]'], - ] - + end_time=row[f'End Time (Class {class_num} of {number_of_classes})'] ) - + teacher.add_classrooms(classroom) self.applicants.append(classroom) diff --git a/vsvs_scheduler/main.py b/vsvs_scheduler/main.py index e203644..8d5423b 100644 --- a/vsvs_scheduler/main.py +++ b/vsvs_scheduler/main.py @@ -58,7 +58,7 @@ def main(): '', classroom.start_time.strftime('%I:%M %p'), classroom.end_time.strftime('%I:%M %p'), - classroom.weekday + classroom.teacher.weekday ] ) csv_writer.writerow(['']*6) diff --git a/vsvs_scheduler/scheduler.py b/vsvs_scheduler/scheduler.py index 8426117..36216a4 100644 --- a/vsvs_scheduler/scheduler.py +++ b/vsvs_scheduler/scheduler.py @@ -39,13 +39,35 @@ def create_assignments(self): num_days_to_try = 4 while num_days_to_try > 0 and self.incomplete_classrooms: + updated_teachers = [] for classroom in self.incomplete_classrooms: classroom.unassign_volunteers() - classroom.change_to_next_preferred_day() - self.assign_volunteers() + if classroom.teacher not in updated_teachers: + classroom.change_to_next_preferred_day() + updated_teachers.append(classroom.teacher) + self.assign_volunteers() num_days_to_try -= 1 + num_days_to_try = 4 + updated_teachers = [] + for classroom in self.incomplete_classrooms: + if classroom.teacher not in updated_teachers: + classroom.teacher.reset_weekday() + updated_teachers.append(classroom.teacher) + + while num_days_to_try > 0 and self.incomplete_classrooms: + updated_teachers = [] + for classroom in self.incomplete_classrooms: + classroom.unassign_volunteers() + if classroom.teacher not in updated_teachers: + classroom.change_to_next_preferred_day() + updated_teachers.append(classroom.teacher) + self.assign_volunteers("second_to_last_round") + num_days_to_try -= 1 + self.assign_volunteers("last_round") + for classroom in self.classrooms: + classroom.freeze_weekday() if missing_team_leaders: warnings.warn(f'WARNING: Classrooms are missing team leaders: {missing_team_leaders}') @@ -53,8 +75,6 @@ def create_assignments(self): warnings.warn(f'WARNING: Classrooms without necessary number of volunteers {self.incomplete_classrooms}') return {"unassigned": self.unassigned_partners} - - def assign_partners(self): """Assigns partners to classrooms and returns a list of unassigned partners.""" @@ -94,26 +114,47 @@ def assign_volunteers(self, volunteer_type: str = "default"): idx = 0 while volunteer.group_number == -1 and idx < len(self.classrooms): classroom = self.classrooms[idx] - if volunteer.can_make_class(classroom) and (len(classroom.volunteers) < self.max_size): + if volunteer.can_make_class_last_round(classroom) and (len(classroom.volunteers) < self.max_size): classroom.assign_volunteer(volunteer) volunteer.assign_classroom(classroom) else: idx += 1 + elif volunteer_type == "second_to_last_round": + for volunteer in volunteer_list: + idx = 0 + while volunteer.group_number == -1 and idx < len(self.incomplete_classrooms): + classroom = self.incomplete_classrooms[idx] + if volunteer.can_make_class(classroom) and (volunteer_type == "default" or not classroom.team_leader): + classroom.assign_volunteer(volunteer) + volunteer.assign_classroom(classroom) + if len(classroom.volunteers) >= self.min_size: + self.incomplete_classrooms.remove(classroom) + classroom.freeze_weekday() + else: + idx += 1 + else: + for volunteer in volunteer_list: + idx = 0 + while volunteer.group_number == -1 and idx < len(self.incomplete_classrooms): + classroom = self.incomplete_classrooms[idx] + if volunteer.can_make_class(classroom) and (volunteer_type == "default" or not classroom.team_leader): + classroom.assign_volunteer(volunteer) + volunteer.assign_classroom(classroom) + if len(classroom.volunteers) >= self.min_size: + self.incomplete_classrooms.remove(classroom) + else: + idx += 1 + self.unassign_volunteers_for_incomplete_classes() - - for volunteer in volunteer_list: - idx = 0 - while volunteer.group_number == -1 and idx < len(self.incomplete_classrooms): - classroom = self.incomplete_classrooms[idx] - if volunteer.can_make_class(classroom) and (volunteer_type == "default" or not classroom.team_leader): - classroom.assign_volunteer(volunteer) - volunteer.assign_classroom(classroom) - if len(classroom.volunteers) >= self.min_size: - self.incomplete_classrooms.remove(classroom) - else: - idx += 1 self.incomplete_classrooms.sort(key=lambda classroom: len(classroom.volunteers), reverse=True) + + def unassign_volunteers_for_incomplete_classes(self): + for classroom in self.incomplete_classrooms: + classroom.teacher.unassign_volunteers() + for room in classroom.teacher.classrooms: + if room not in self.incomplete_classrooms: + self.incomplete_classrooms.append(room) def find_possible_classroom_and_partners_matches(self):