From fe2561a54300eb8143df0025fc8422197346ceb7 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 30 Nov 2022 19:57:54 -0800 Subject: [PATCH] add course dataclass --- app/generator.py | 46 ++++++++++++++-------------- app/util/convertRawData.py | 11 ++++--- app/util/courses.py | 61 ++++++++++++++++++++++---------------- main.py | 15 +++++----- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/app/generator.py b/app/generator.py index a80284f..2a04c41 100644 --- a/app/generator.py +++ b/app/generator.py @@ -6,6 +6,7 @@ from string import hexdigits from app.util.globals import flex, Error from app.util.estimateGrade import getGrade from app.util.students import Student, studentsToDict +from app.util.courses import Course, coursesToDict # Takes in information to create or add a new conflict # Returns if the particular student has a previous error @@ -45,17 +46,17 @@ def insertConflictSolutions( # Then it starts to attempt to fit all classes into a timetable, making corretions along # the way. Corrections being moving a students class def generateScheduleV3( - students: list[Student], # Refer to /util/students.py to see the students list structure - courses: dict, # Refer to /util/courses.py to see the courses dictionary structure - minReq: int = 18, # minimum requests for a class to run - classCap: int = 30, # maximum students per class - blockClassLimit: int = 40, # Block class limit is the number of classrooms available per block. Default 40 classes per block - totalBlocks: int = 10, # total blocks between two semesters -> default is 10 for 5 per semester... or this can be 8 for 4 blocks per semester + students: list[Student], # Refer to /util/students.py to see the students list structure + courses: dict[str: Course], # Refer to /util/courses.py to see the courses dictionary structure + minReq: int = 18, # minimum requests for a class to run + classCap: int = 30, # maximum students per class + blockClassLimit: int = 40, # Block class limit is the number of classrooms available per block. Default 40 classes per block + totalBlocks: int = 10, # total blocks between two semesters -> default is 10 for 5 per semester... or this can be 8 for 4 blocks per semester studentsDir: str = "../output/raw/students.json", conflictsDir: str = "../output/raw/conflicts.json", coursesDir: str = "../output/raw/courses.json" -) -> tuple[dict, Error]: # Returns the completed 'running' dictionary from above - +) -> tuple[dict, Error]: # Returns the completed 'running' dictionary + # Return error that totalBlocks is invalid if totalBlocks not in (10, 8): totalBlockError = Error('Invalid totalBlocks', 'An invalid \'totalBlocks\' value was provided -> must be 10 or 8') @@ -80,9 +81,9 @@ def generateScheduleV3( # Tally class request for request in (request for request in student.Requests if not request.Alt and request.CrsNo not in flex): code = request.CrsNo - courses[code]["Requests"] += 1 + courses[code].Requests += 1 # Add course to active list if enough requests - if courses[code]["Requests"] > minReq and courses[code]["CrsNo"] not in activeCourses: + if courses[code].Requests > minReq and courses[code].CrsNo not in activeCourses: activeCourses[code] = courses[code] @@ -94,14 +95,14 @@ def generateScheduleV3( for i in range(len(activeCourses)): index = list(activeCourses)[i] if index not in emptyClasses: emptyClasses[index] = {} - classRunCount = activeCourses[index]["Requests"] // median - remaining = activeCourses[index]["Requests"] % median + classRunCount = activeCourses[index].Requests // median + remaining = activeCourses[index].Requests % median # Put number of classRunCount classes in emptyClasses for j in range(classRunCount): emptyClasses[index][f"{index}-{hexdigits[j]}"] = { "CrsNo": index, - "Description": activeCourses[index]["Description"], + "Description": activeCourses[index].Description, "expectedLen": median # Number of students expected in this class / may be altered later } @@ -118,7 +119,7 @@ def generateScheduleV3( # Create a class using remaining emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { "CrsNo": index, - "Description": activeCourses[index]["Description"], + "Description": activeCourses[index].Description, "expectedLen": remaining } @@ -145,7 +146,7 @@ def generateScheduleV3( # Create a class using remaining + required amount from existing classes emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { "CrsNo": index, - "Description": activeCourses[index]["Description"], + "Description": activeCourses[index].Description, "expectedLen": remaining } @@ -225,7 +226,7 @@ def generateScheduleV3( "index": student.StudentIndex }], "CrsNo": course, - "Description": courses[course]["Description"] + "Description": courses[course].Description } getAvailableCourse = False break @@ -550,13 +551,14 @@ def generateScheduleV3( # Read timetable and collect data on courses for block in running: for course in running[block]: - sem = "Sem1" if int(block[5:]) <= blockPerSem else "Sem2" - courses[running[block][course]["CrsNo"]][sem] += 1 - courses[running[block][course]["CrsNo"]]["Occupied"] += len(running[block][course]["students"]) + c = running[block][course]["CrsNo"] + if int(block[5:]) <= blockPerSem: courses[c].Sem1 += 1 + else: courses[c].Sem2 += 1 + courses[c].Occupied += len(running[block][course]["students"]) for course in courses: - courses[course]["Total"] = courses[course]["Sem1"] + courses[course]["Sem2"] - courses[course]["Seats"] = courses[course]["Total"] * classCap + courses[course].Total = courses[course].Sem1 + courses[course].Sem2 + courses[course].Seats = courses[course].Total * classCap # Update/log new student records with open(studentsDir, "w") as outfile: @@ -564,7 +566,7 @@ def generateScheduleV3( # Update/log new course records with open(coursesDir, "w") as outfile: - json.dump(courses, outfile, indent=2) + json.dump(coursesToDict(courses), outfile, indent=2) # Save timetable to json with open('./output/raw/json/timetable.json', 'w') as outfile: diff --git a/app/util/convertRawData.py b/app/util/convertRawData.py index 8d511bb..f437c0d 100644 --- a/app/util/convertRawData.py +++ b/app/util/convertRawData.py @@ -4,8 +4,11 @@ from app.util.globals import flex import xlsxwriter import docx +from app.util.courses import Course +from app.util.students import Student + def putMasterTimetable( - table: dict, + table: dict[str: dict], output_dir: str = './output/final' ) -> None: @@ -36,8 +39,8 @@ def putMasterTimetable( workbook.close() def putScheduleToWord( - courses: dict, - student: dict, + courses: dict[str: Course], + student: Student, output_dir: str = './output/final/student_schedules' ) -> None: @@ -64,7 +67,7 @@ def putScheduleToWord( for block in student['schedule']: courseCode = student.Schedule[block][0] if courseCode in flex: courseName = 'Study' - else: courseName = courses[courseCode[:-2]]["Description"] + else: courseName = courses[courseCode[:-2]].Description blockNum = int(block.split('block')[1]) semester = 2 if blockNum > 5 else 1 if blockNum > (len(student.Schedule)/2): blockNum -= (len(student.Schedule)/2) diff --git a/app/util/courses.py b/app/util/courses.py index 71a8946..f0deb16 100644 --- a/app/util/courses.py +++ b/app/util/courses.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3.11 import json import csv -from dataclasses import dataclass +from dataclasses import dataclass, asdict from app.util.estimateGrade import getGrade @dataclass @@ -10,45 +10,54 @@ class Request: Description: str Alt: bool +@dataclass +class Course: + CrsNo: str + Description: str + Grade: int + Requests: int = 0 # number of requests for this + Sem1: int = 0 # number of classes running in sem1 + Sem2: int = 0 # number of classes running in sem2 + Total: int = 0 # total number of classes running + Seats: int = 0 # total number of seats + Occupied: int = 0 # number of occupied seats + +def coursesToDict(courses: dict[str: Course]) -> dict[str: dict]: + return [asdict(courses[c]) for c in courses] + # Get all requested courses from data def getCourses( data_dir: str, log: int = False, log_dir: str = './output/raw/courses.json', -) -> dict[dict[str: any]]: +) -> dict[str: Course]: - courses = {} + courses: dict[str: Course] = {} with open(data_dir, newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: exists = False for course in courses: - exists = True if courses[course]["CrsNo"] == row["CrsNo"] else False + exists = True if courses[course].CrsNo == row["CrsNo"] else False if exists: break if not exists: grade = getGrade(row["CrsNo"], row["Description"]) - courses[row["CrsNo"]] = { - "CrsNo": row["CrsNo"], - "Description": row["Description"], - "Grade": grade, - "Requests": 0, - "Sem1": 0, # number of classes running in sem1 - "Sem2": 0, # number of classes running in sem2 - "Total": 0, # total number of classes running - "Seats": 0, # total number of seats - "Occupied": 0, # number of occupied seats - } + courses[row["CrsNo"]] = Course( + row["CrsNo"], + row["Description"], + grade + ) if log: with open(log_dir, "w") as outfile: - json.dump(courses, outfile, indent=2) + json.dump(coursesToDict(courses), outfile, indent=2) return courses # Writes all course data to csv file def writeCoursesToCSV( - courses: dict, + courses: dict[str: Course], output_dir: str = './output/raw/csv/courses.csv' ) -> None: with open(output_dir, 'w') as file: @@ -69,14 +78,14 @@ def writeCoursesToCSV( for course in courses: courseData = ( - courses[course]["CrsNo"], - courses[course]["Description"], - courses[course]["Grade"], - courses[course]["Requests"], - courses[course]["Sem1"], - courses[course]["Sem2"], - courses[course]["Total"], - courses[course]["Seats"], - courses[course]["Occupied"] + courses[course].CrsNo, + courses[course].Description, + courses[course].Grade, + courses[course].Requests, + courses[course].Sem1, + courses[course].Sem2, + courses[course].Total, + courses[course].Seats, + courses[course].Occupied ) writer.writerow(courseData) diff --git a/main.py b/main.py index 5922533..636fc5b 100755 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ import json from app.util.convertRawData import putScheduleToWord, putMasterTimetable from app.generator import generateScheduleV3 from app.util.globals import Error -from app.util.courses import getCourses, writeCoursesToCSV +from app.util.courses import getCourses, writeCoursesToCSV, Course from app.util.students import getStudents, writeStudentsToCSV, Student from app.util.errorCalculator import writeErrorsToCSV from app.util.validator import validateInputData @@ -41,8 +41,7 @@ def start( # save raw file data to local file eel.post_data('Saving raw data to local file...') - with open(raw_data_dir, 'w') as raw_file: - raw_file.write(raw_file_data) + with open(raw_data_dir, 'w') as raw_file: raw_file.write(raw_file_data) # Ensure params are of correct type min_req = int(min_req) @@ -56,14 +55,14 @@ def start( # call pre-algorithm functions read raw data into a processable format eel.post_data('Collecting student information...') - students = getStudents( + students: list[Student] = getStudents( raw_data_dir, log = False, totalBlocks = total_blocks, log_dir = './output/raw/json/students.json' ) eel.post_data('Collecting course information...') - courses = getCourses( + courses: dict[str: Course] = getCourses( raw_data_dir, log = True, log_dir = f'{raw_json_dir}/courses.json' @@ -91,8 +90,10 @@ def start( eel.post_data('Gathering latest data...') with open(f'{raw_json_dir}/students.json', 'r') as studentFile: # Unpack dictionary to Student dataclass - students = [Student(**s) for s in json.load(studentFile)] - with open(f'{raw_json_dir}/courses.json', 'r') as cFile: courses = json.load(cFile) + students: list[Student] = [Student(**s) for s in json.load(studentFile)] + with open(f'{raw_json_dir}/courses.json', 'r') as cFile: + # Unpack dictionary to Course dataclass + courses: dict[str: Course] = {c["CrsNo"]: Course(**c) for c in json.load(cFile)} # call post-algorithm functions to present sorted data eel.post_data('Writing data to .csv files...')