diff --git a/tests/scheduler_test.py b/tests/scheduler_test.py index deca1c6..7bca673 100644 --- a/tests/scheduler_test.py +++ b/tests/scheduler_test.py @@ -108,7 +108,7 @@ def setUpClass(cls): def test_import_individual_functions(self): with patch('builtins.input', return_value='test_data/individuals.csv'): self.scheduler.import_volunteers() - self.assertEqual(len(self.scheduler.individuals), 360) + self.assertEqual(len(self.scheduler.volunteers), 360) with patch('builtins.input', return_value='test_data/partners.csv'): self.scheduler.import_partners() diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index deca1c6..7bca673 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -108,7 +108,7 @@ def setUpClass(cls): def test_import_individual_functions(self): with patch('builtins.input', return_value='test_data/individuals.csv'): self.scheduler.import_volunteers() - self.assertEqual(len(self.scheduler.individuals), 360) + self.assertEqual(len(self.scheduler.volunteers), 360) with patch('builtins.input', return_value='test_data/partners.csv'): self.scheduler.import_partners() diff --git a/vsvs_scheduler/__init__.py b/vsvs_scheduler/__init__.py index 93b7576..5b5e039 100644 --- a/vsvs_scheduler/__init__.py +++ b/vsvs_scheduler/__init__.py @@ -1 +1,34 @@ -COLUMN_NAMES +from enum import Enum + +class TEACHER_COLUMNS(Enum): + NAME = 'Name' + PHONE = 'Cell Phone Number' + SCHOOL = 'School' + EMAIL = 'Email Address' + NUM_CLASSES = 'Number of Classes' + +class VOLUNTEER_COLUMNS(Enum): + FIRST_NAME = 'First Name' + LAST_NAME = 'Last Name' + PHONE = 'Phone' + EMAIL = 'Email Address' + LEADER = 'Team Leader' + BOARD = 'Board Member' + +class PARTNER_COLUMNS(Enum): + NUM_PARTNERS = 'Number of Partners' + EMAIL = 'Email Address' + + +CLASSROOM_RAW_DATA_FILE = "data/classrooms.csv" +VOLUNTEER_RAW_DATA_FILE = "data/individuals.csv" +PARTNER_RAW_DATA_FILE = "data/partners.csv" + +ASSIGNMENTS_DIRECTORY = "results" + + +EARLIEST_TIME = "7:15" +LATEST_TIME = "15:30" +TIME_BLOCK_DURATION = 15 +MAX_TEAM_SIZE = 5 +MIN_TEAM_SIZE = 3 diff --git a/vsvs_scheduler/abstract_data_uploader.py b/vsvs_scheduler/abstract_data_uploader.py deleted file mode 100644 index f2773e4..0000000 --- a/vsvs_scheduler/abstract_data_uploader.py +++ /dev/null @@ -1,52 +0,0 @@ -from abc import ABC, abstractmethod -import csv -import os - -class AbstractDataUploader(ABC): - def __init__(self, applicant_type: str): - """ Abstract class for uploading data from csv/excel file into objects.""" - - super().__init__() - self.filename = self.file_prompt(applicant_type) - self.applicants = [] - - - - def file_prompt(self, applicant_file: str) -> str: - """ Prompt user for file path to csv/excel file.""" - - filename = input(f"Enter the file path to the {applicant_file} file:\n") - - # Keep prompting user until they enter a valid file path - while (not os.path.isfile(filename)): - if filename == "" or filename is None: - filename = f"data/{applicant_file}.csv" - else: - filename = input(f"Enter the file path to the {applicant_file} file:\n") - - print(f"\nImporting {applicant_file} ...") - - return filename - - - - def import_data(self): - """ Import data from csv/excel file into objects. """ - - with open(self.filename) as current_csv: - - # DictReader creates an ordered dictionary for each row in the csv file - csv_reader = csv.DictReader(current_csv) - - for row in csv_reader: - self.process_row_data(row) - - print (f"\nImport complete. {len(self.applicants)} records added.\n") - - - - @abstractmethod - def process_row_data(self, row: dict): - """ Abstract method for processing row data from csv/excel file into objects. """ - pass - diff --git a/vsvs_scheduler/class_data_uploader.py b/vsvs_scheduler/class_data_uploader.py deleted file mode 100644 index 8fa1580..0000000 --- a/vsvs_scheduler/class_data_uploader.py +++ /dev/null @@ -1,48 +0,0 @@ -from applicants.classroom import Classroom -from applicants.teacher import Teacher -from abstract_data_uploader import AbstractDataUploader - -class ClassDataUploader(AbstractDataUploader): - def __init__(self): - """ This uploads class data from csv/excel file into Classroom objects""" - - super().__init__("classrooms") - self.group_num = 0 - self.import_data() - - - - def process_row_data(self, row: dict): - """ Process row data from csv/excel file into Classroom objects.""" - - 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'], - 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(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})'] - ) - teacher.add_classrooms(classroom) - self.applicants.append(classroom) - - - - - diff --git a/vsvs_scheduler/data_uploader.py b/vsvs_scheduler/data_uploader.py new file mode 100644 index 0000000..a79b718 --- /dev/null +++ b/vsvs_scheduler/data_uploader.py @@ -0,0 +1,129 @@ +import csv +import os +from applicants.teacher import Teacher +from applicants.classroom import Classroom +from applicants.volunteer import Volunteer +from applicants.schedule import Schedule +from applicants.partners import Partners +from __init__ import CLASSROOM_RAW_DATA_FILE, TEACHER_COLUMNS, VOLUNTEER_COLUMNS, VOLUNTEER_RAW_DATA_FILE, PARTNER_RAW_DATA_FILE, PARTNER_COLUMNS + + +class DataUploader(): + def __init__(self): + + self.classrooms = [] + self.partners = [] + self.volunteers = [] + self.partners_not_found = [] + self.import_data() + + + + def import_data(self) -> str: + """ Prompt user for file path to csv/excel file.""" + + files = [CLASSROOM_RAW_DATA_FILE, VOLUNTEER_RAW_DATA_FILE, PARTNER_RAW_DATA_FILE] + + for file_name in files: + if (not os.path.isfile(file_name)): + raise FileNotFoundError(f"File not found at {file_name}") + + print (f"\nImporting {file_name} ...") + + with open(file_name) as current_csv: + # DictReader creates an ordered dictionary for each row in the csv file + csv_reader = csv.DictReader(current_csv) + + if file_name is CLASSROOM_RAW_DATA_FILE: + for row in csv_reader: + self.process_classroom_data(row) + elif file_name is VOLUNTEER_RAW_DATA_FILE: + for row in csv_reader: + self.process_volunteer_data(row) + elif file_name is PARTNER_RAW_DATA_FILE: + for row in csv_reader: + self.process_partner_data(row) + else: + raise ValueError(f"Invalid file name: {file_name}") + + + def process_classroom_data(self, row: dict): + """ Process row data from csv/excel file into Classroom objects.""" + group_num = 0 + + number_of_classes = int(row[TEACHER_COLUMNS.NUM_CLASSES.value]) + + teacher = Teacher ( + name = row[TEACHER_COLUMNS.NAME.value], + phone = row[TEACHER_COLUMNS.PHONE.value], + school = row[TEACHER_COLUMNS.SCHOOL.value], + email = row[TEACHER_COLUMNS.EMAIL.value], + 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(number_of_classes): + group_num += 1 + class_num = i + 1 # class_num keeps track of which class out of the total being created + classroom = Classroom( + group_number = 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})'] + ) + teacher.add_classrooms(classroom) + + self.classrooms.append(classroom) + + def process_volunteer_data(self, row: dict): + volunteer = Volunteer( + first = row[VOLUNTEER_COLUMNS.FIRST_NAME.value].strip().lower().capitalize(), + last = row[VOLUNTEER_COLUMNS.LAST_NAME.value].strip().lower().capitalize(), + phone = row[VOLUNTEER_COLUMNS.PHONE.value], + email = row[VOLUNTEER_COLUMNS.EMAIL.value].strip(), + leader_app = (lambda x: True if x == 'Yes' else False)(row[VOLUNTEER_COLUMNS.LEADER.value]), + schedule = Schedule(list(row.values())[15:55]), + board_member = (lambda current_board_member: True if current_board_member == 'Yes' else False)(row[VOLUNTEER_COLUMNS.BOARD.value]) + ) + + self.volunteers.append(volunteer) + + def process_partner_data(self, row: dict): + """ Process row data from csv/excel file into Partner objects.""" + + number_of_partners = int(row[PARTNER_COLUMNS.NUM_PARTNERS.value]) + partner_emails = [row[PARTNER_COLUMNS.EMAIL.value].lower()] + + # add all the partners' emails to the list 'partner_emails' + for i in range(1, number_of_partners): + partner_email = row[f'Group Member #{i + 1}'].lower() + partner_emails.append(partner_email) + + # create a list of volunteers that are in the partner group + group = [volunteer for volunteer in self.volunteers if (volunteer.email in partner_emails)] + + # Remove duplicate volunteers + for partner in group: + duplicates = [partner_group for partner_group in self.partners if partner in partner_group.members] + if len(duplicates) != 0 and len(group) > 1: + print(f'{partner.email} was in 2 groups. One deleted.') + self.partners.remove(duplicates[0]) + + if len(group) > 1: + self.partners.append(Partners(group)) + + if len(group) < number_of_partners: + self.partners_not_found.append(partner_emails) + + + + + + + + diff --git a/vsvs_scheduler/main.py b/vsvs_scheduler/main.py index 32bc798..96478cd 100644 --- a/vsvs_scheduler/main.py +++ b/vsvs_scheduler/main.py @@ -2,6 +2,7 @@ import os from scheduler import Scheduler +from __init__ import ASSIGNMENTS_DIRECTORY, MIN_TEAM_SIZE def main(): @@ -10,10 +11,12 @@ def main(): partner_errors = vsvs_scheduler.create_assignments() print(partner_errors) - if not os.path.isdir("results/"): - os.mkdir("results") + if not os.path.isdir(ASSIGNMENTS_DIRECTORY): + os.mkdir(ASSIGNMENTS_DIRECTORY) + + results_file_path = os.path.join(ASSIGNMENTS_DIRECTORY, 'assignments.csv') - with open('results/assignments.csv', 'a', newline='') as assignments_csv: + with open(results_file_path, 'w', newline='') as assignments_csv: csv_writer = csv.writer(assignments_csv, delimiter=',') csv_writer.writerow( ['Group Number', 'First Name', 'Last Name', 'Email', 'Phone Number', 'Team Leader', 'Board Member', @@ -22,7 +25,7 @@ def main(): group_num = 1 for classroom in vsvs_scheduler.classrooms: - if len(classroom.volunteers) >= 3: + if len(classroom.volunteers) >= MIN_TEAM_SIZE: for volunteer in classroom.volunteers: csv_writer.writerow( [ @@ -41,14 +44,15 @@ def main(): ) group_num += 1 - with open('results/unassigned.csv', 'w', newline='') as unassigned_csv: + unassigned_file_path = os.path.join(ASSIGNMENTS_DIRECTORY, 'unassigned.csv') + with open(unassigned_file_path, 'w', newline='') as unassigned_csv: csv_writer = csv.writer(unassigned_csv, delimiter=',') csv_writer.writerow( [ 'Group', 'First Name', 'Last Name', 'Email', 'Phone Number', 'Team Leader', 'Board Member', 'Teacher', 'Availability', 'Start', 'End', 'Day'] ) for classroom in vsvs_scheduler.classrooms: - if len(classroom.volunteers) < 3: + if len(classroom.volunteers) < MIN_TEAM_SIZE: csv_writer.writerow( [ '', @@ -67,7 +71,7 @@ def main(): ) csv_writer.writerow(['']*6) - for volunteer in vsvs_scheduler.individuals: + for volunteer in vsvs_scheduler.volunteers: if volunteer.group_number == -1: csv_writer.writerow( [ diff --git a/vsvs_scheduler/partner_data_uploader.py b/vsvs_scheduler/partner_data_uploader.py deleted file mode 100644 index 0241c54..0000000 --- a/vsvs_scheduler/partner_data_uploader.py +++ /dev/null @@ -1,41 +0,0 @@ -from abstract_data_uploader import AbstractDataUploader -from applicants.partners import Partners - - -class PartnerDataUploader(AbstractDataUploader): - def __init__(self, individual_volunteers: list): - """ This uploads partner data from csv/excel file into Partner objects""" - - super().__init__("partners") - self.individuals = individual_volunteers - self.partners_not_found = [] - self.import_data() - - - - def process_row_data(self, row: dict): - """ Process row data from csv/excel file into Partner objects.""" - - number_of_partners = int(row['Number of Partners']) - partner_emails = [row['Email Address'].lower()] - - # add all the partners' emails to the list 'partner_emails' - for i in range(1, number_of_partners): - partner_email = row[f'Group Member #{i + 1}'].lower() - partner_emails.append(partner_email) - - # create a list of volunteers that are in the partner group - group = [volunteer for volunteer in self.individuals if (volunteer.email in partner_emails)] - - # Remove duplicate volunteers - for partner in group: - for partnered in self.applicants: - if partner in partnered.members and len(group) > 1: - print(f'{partner.email} was in 2 groups. One deleted.') - self.applicants.remove(partnered) - - if len(group) > 1: - self.applicants.append(Partners(group)) - - if len(group) < number_of_partners: - self.partners_not_found.append(partner_emails) \ No newline at end of file diff --git a/vsvs_scheduler/scheduler.py b/vsvs_scheduler/scheduler.py index 2824de0..aede43d 100644 --- a/vsvs_scheduler/scheduler.py +++ b/vsvs_scheduler/scheduler.py @@ -3,26 +3,25 @@ from applicants.classroom import Classroom from applicants.volunteer import Volunteer from applicants.partners import Partners -from volunteer_data_uploader import VolunteerDataUploader -from class_data_uploader import ClassDataUploader -from partner_data_uploader import PartnerDataUploader +from data_uploader import DataUploader +from __init__ import MAX_TEAM_SIZE, MIN_TEAM_SIZE, EARLIEST_TIME, LATEST_TIME + class Scheduler: - def __init__(self, earliest: str = "7:15", latest: str = "15:30", max_team_size: int = 5, min_team_size: int = 3): + def __init__(self): """Scheduler object that holds information about the schedule and the volunteers and classrooms.""" - self.earliest_time = datetime.strptime(earliest, "%H:%M") - self.latest_time = datetime.strptime(latest, "%H:%M") + self.earliest_time = datetime.strptime(EARLIEST_TIME, "%H:%M") + self.latest_time = datetime.strptime(LATEST_TIME, "%H:%M") - self.individuals: list[Volunteer] = VolunteerDataUploader().applicants - self.classrooms: list[Classroom] = ClassDataUploader().applicants - self.partners: list[Partners] = PartnerDataUploader(self.individuals).applicants + data = DataUploader() + self.volunteers: list[Volunteer] = data.volunteers + self.classrooms: list[Classroom] = data.classrooms + self.partners: list[Partners] = data.partners self.unassigned_partners = [] self.incomplete_classrooms = self.classrooms.copy() - self.max_size = max_team_size - self.min_size = min_team_size @@ -84,9 +83,9 @@ def assign_partners(self): for group in self.partners: while group.group_number == -1 and idx < len(self.incomplete_classrooms): curr_class = self.incomplete_classrooms[idx] - if group.can_make_class(curr_class, self.max_size): + if group.can_make_class(curr_class, MAX_TEAM_SIZE): group.assign_partners(curr_class) - if len(curr_class.volunteers) >= self.max_size: + if len(curr_class.volunteers) >= MAX_TEAM_SIZE: self.incomplete_classrooms.remove(curr_class) else: idx += 1 @@ -102,19 +101,19 @@ def assign_volunteers(self, volunteer_type: str = "default"): self.find_possible_classroom_and_volunteer_matches() - volunteer_list = self.individuals + volunteer_list = self.volunteers if volunteer_type == "leaders": - volunteer_list = [volunteer for volunteer in self.individuals if volunteer.leader_app] + volunteer_list = [volunteer for volunteer in self.volunteers if volunteer.leader_app] elif volunteer_type == "board": - volunteer_list = [volunteer for volunteer in self.individuals if volunteer.board] + volunteer_list = [volunteer for volunteer in self.volunteers if volunteer.board] if volunteer_type == "last_round": for volunteer in volunteer_list: idx = 0 while volunteer.group_number == -1 and idx < len(self.classrooms): classroom = self.classrooms[idx] - if volunteer.can_make_class_last_round(classroom) and (len(classroom.volunteers) < self.max_size): + if volunteer.can_make_class_last_round(classroom) and (len(classroom.volunteers) < MAX_TEAM_SIZE): classroom.assign_volunteer(volunteer) volunteer.assign_classroom(classroom) else: @@ -127,7 +126,7 @@ def assign_volunteers(self, volunteer_type: str = "default"): 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: + if len(classroom.volunteers) >= MIN_TEAM_SIZE: self.incomplete_classrooms.remove(classroom) classroom.freeze_weekday() else: @@ -140,7 +139,7 @@ def assign_volunteers(self, volunteer_type: str = "default"): 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: + if len(classroom.volunteers) >= MIN_TEAM_SIZE: self.incomplete_classrooms.remove(classroom) else: idx += 1 @@ -162,7 +161,7 @@ def find_possible_classroom_and_partners_matches(self): for group in self.partners: for classroom in self.incomplete_classrooms: - if group.can_make_class(classroom, self.max_size): + if group.can_make_class(classroom, MAX_TEAM_SIZE): group.increment_possible_classrooms() classroom.possible_partner_groups += 1 self.partners.sort(key=lambda person: person.possible_classrooms) @@ -172,12 +171,12 @@ def find_possible_classroom_and_partners_matches(self): def find_possible_classroom_and_volunteer_matches(self): """Finds the number of possible classrooms each volunteer can make and the number of possible volunteers each classroom can have.""" - for volunteer in self.individuals: + for volunteer in self.volunteers: for classroom in self.incomplete_classrooms: if volunteer.group_number == -1 and volunteer.can_make_class(classroom): volunteer.possible_classrooms += 1 classroom.possible_volunteers += 1 - self.individuals.sort(key=lambda person: person.possible_classrooms) + self.volunteers.sort(key=lambda person: person.possible_classrooms) self.incomplete_classrooms.sort(key=lambda clsroom: clsroom.possible_volunteers) diff --git a/vsvs_scheduler/volunteer_data_uploader.py b/vsvs_scheduler/volunteer_data_uploader.py deleted file mode 100644 index 4bf6a41..0000000 --- a/vsvs_scheduler/volunteer_data_uploader.py +++ /dev/null @@ -1,28 +0,0 @@ -from abstract_data_uploader import AbstractDataUploader -from applicants.schedule import Schedule -from applicants.volunteer import Volunteer - - -class VolunteerDataUploader(AbstractDataUploader): - def __init__(self): - """ This uploads volunteer data from csv/excel file into Volunteer objects """ - - super().__init__("individuals") - self.import_data() - - - - def process_row_data(self, row: dict): - """ Process row data from csv/excel file into Volunteer objects.""" - - volunteer = Volunteer( - first=row['First Name'].strip().lower().capitalize(), - last=row['Last Name'].strip().lower().capitalize(), - phone=row['Phone'], - email=row['Email Address'].strip(), - leader_app=(lambda x: True if x == 'Yes' else False)(row['Team Leader']), - schedule=Schedule(list(row.values())[15:55]), - board_member=(lambda current_board_member: True if current_board_member == 'Yes' else False)(row['Board Member']) - ) - - self.applicants.append(volunteer) \ No newline at end of file