commit 7b909b770b162a911eed30b0c20b935c9e5664a1 Author: SowinskiBraeden Date: Sun Nov 6 11:50:13 2022 -0800 initial commit diff --git a/app/__pycache__/generator.cpython-311.pyc b/app/__pycache__/generator.cpython-311.pyc new file mode 100644 index 0000000..0369522 Binary files /dev/null and b/app/__pycache__/generator.cpython-311.pyc differ diff --git a/app/generator.py b/app/generator.py new file mode 100644 index 0000000..56cb42f --- /dev/null +++ b/app/generator.py @@ -0,0 +1,540 @@ +#!/usr/bin/env python3.11 +import json +import random +from string import hexdigits + +from app.util.globals import flex, Error +from app.util.estimateGrade import getGradeFromCourseCode + +# Takes in information to create or add a new conflict +# Returns if the particular student has a previous error +def newConflict(pupilNum: str, email: str, conflictType: str, code: str, description: str, logs: dict) -> bool: + exists = True if pupilNum in logs else False + log = { + "Pupil #": pupilNum, + "Email": email, + "Type": conflictType, + "Code": code, + "Conflict": description + } + if exists: logs[pupilNum].append(log) + else: logs[pupilNum] = [log] + return exists if conflictType == "Critical" else False + +def insertConflictSolutions(pupilNum: str, logs: dict, data: dict) -> None: + log = logs[pupilNum] + for conflict in log: + if conflict["Type"] == "Critical": + conflict["Missing"] = data + break + +# V3 differs a lot by V1/2 as it does not focus on fitting the classes +# into the time table first. +# It starts by trying to get all classes full and give all students a full class list. +# 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, # Refer to /util/mockStudents.py to see the students list structure + courses: dict, # Reger to /util/generateCourses.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" +) -> tuple[dict, Error]: # Returns the completed 'running' dictionary from above + + # 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') + return (None, totalBlockError) # return none an signal failure + + # First we need to setup some values + median = (minReq + classCap) // 2 + blockPerSem = int(totalBlocks / 2) + running = {} + for i in range(1, totalBlocks + 1): + running[f'block{i}'] = {} + + + def equal(l: list) -> list: # Used to equalize list of numbers + q,r = divmod(sum(l),len(l)) + return [q+1]*r + [q]*(len(l)-r) + + + # Step 1 - Calculate which classes can run + activeCourses = {} + for student in students: + # 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 + # Add course to active list if enough requests + if courses[code]["Requests"] > minReq and courses[code]["CrsNo"] not in activeCourses: + activeCourses[code] = courses[code] + + + # Step 2 - Generate empty classes + allClassRunCounts = [] + courseRunInfo = {} # Generated now, used in step 4 + emptyClasses = {} # List of all classes with how many students should be entered during step 3 + # calculate number of times to run class + 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 + + # Put number of classRunCount classes in emptyClasses + for j in range(classRunCount): + emptyClasses[index][f"{index}-{hexdigits[j]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": median # Number of students expected in this class / may be altered later + } + + # If remaining fit in open slots in existing classes + if remaining <= classRunCount * (classCap - median): + # Equally disperse remaining into existing classes + for j in range(classRunCount): + if remaining == 0: break + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1 + remaining -= 1 + + # If we can create a class using remaining, create class + elif remaining >= minReq: + # Create a class using remaining + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + # If a class previously existed + if classRunCount >= 2: + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] = newExpectedLens[j] + + # Else if we can't fit remaining into available slots in existing classes, + # and it's unable to create its own class, + # and the required amount (minReq - remaining) to make a class is less than + # the number that existing classes can provide (classRunCount * (median - minReq)) + elif minReq - remaining < classRunCount * (median - minReq): + # Take 1 from each class till min requirment met + for j in range(classRunCount): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] -= 1 + remaining += 1 + if remaining == minReq: break + + # Create a class using remaining + required amount from existing classes + emptyClasses[index][f"{index}-{hexdigits[classRunCount]}"] = { + "CrsNo": index, + "Description": activeCourses[index]["Description"], + "expectedLen": remaining + } + + classRunCount += 1 + + # Equalize (level) class expectedLen's + expectedLengths = [emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] for j in range(classRunCount)] + newExpectedLens = equal(expectedLengths) + for j in range(len(newExpectedLens)): + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] = newExpectedLens[j] + + else: + # In the case that the remaining requests are unable to be resolved + # Fill as many requests into existing classes. Any left that can't fit, + # Will need to be ignored so later we can fold them into their alternative + # requests + for j in range(classRunCount): + if remaining == 0: break + if emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] < classCap: + emptyClasses[index][f"{index}-{hexdigits[j]}"]["expectedLen"] += 1 + remaining -= 1 + + courseRunInfo[index] = { + "Total": classRunCount, + "CrsNo": index + } + allClassRunCounts.append(classRunCount) + + + # Step 3 - Fill 'emptyClasses' with Students + selectedCourses = {} + tempStudents = list(students) + + while len(tempStudents) > 0: + # Choose random student to prevent any success + # bias to students at the top of the list + student = tempStudents[random.randint(0, len(tempStudents)-1)] + + alternates = [request for request in student["requests"] if request["alt"]] + for request in (request for request in student["requests"] if not request["alt"] and request["CrsNo"] not in flex): + course = request["CrsNo"] + getAvailableCourse = True + isAlt = False + while getAvailableCourse: + if course in emptyClasses: + # if course exists, get first available class + for cname in emptyClasses[course]: + if cname in selectedCourses: + if isAlt and emptyClasses[course][cname]["expectedLen"] < classCap: + emptyClasses[course][cname]["expectedLen"] += 1 + if len(selectedCourses[cname]["students"]) < emptyClasses[course][cname]["expectedLen"]: + # Class exists with room for student + selectedCourses[cname]["students"].append({ + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + }) + getAvailableCourse = False + break + elif len(selectedCourses[cname]["students"]) == emptyClasses[course][cname]["expectedLen"]: + # If class is full, and there's no more classes available for that course + if cname[len(cname)-1] == f"{len(emptyClasses[course])-1}": + if len(alternates) > 0: + # Use alternate + course = alternates[0]["CrsNo"] + alternates.remove(alternates[0]) + isAlt = True + break + else: + # Force break loop, ignore and let an admin + # handle options to solve for missing class + getAvailableCourse = False + break + elif cname not in selectedCourses: + selectedCourses[cname] = { + "students": [{ + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + }], + "CrsNo": course, + "Description": courses[course]["Description"] + } + getAvailableCourse = False + break + + elif course not in emptyClasses: + if len(alternates) > 0: + # Use alternate + course = alternates[0]["CrsNo"] + alternates.remove(alternates[0]) + isAlt = True + else: + # Force break loop, ignore and let an admin + # handle options to solve for missing class + getAvailableCourse = False + + students[student["studentIndex"]]["remainingAlts"] = alternates + tempStudents.remove(student) + + + # Step 4 - Attempt to fit classes into timetable + def stepIndex(offset: int, stepType: int) -> int: + # ALl this logic handles taking the # of blocks per semester, 4 or 5 and calculating + # the number used to step between the index of the first or second semester + + # stepType 0 is for stepping between first and second semester + if stepType == 0: return blockPerSem if offset in (0, (-1 * (blockPerSem - 1))) else (-1 * (blockPerSem - 1)) + + # stepType 1 is for stepping between second and first semester + elif stepType == 1: return (-1 * blockPerSem) if offset in (0, (blockPerSem + 1)) else (blockPerSem + 1) + + # Return Error if code is altered to cause error + else: + invalidStepTypeError = Error('Invalid stepType', 'An invalid \'stepType\' was passed to func \'stepIndex\'') + return (None, invalidStepTypeError) + + # Create copy for step 6 + courseRunInfoCopy = dict(courseRunInfo) + + while len(allClassRunCounts) > 0: + # Get highest resource class (most times run) + index = allClassRunCounts.index(max(allClassRunCounts)) + course = list(courseRunInfo)[index] + + # Tally first and second semester + allSemBlockLens = [len(running[f'block{i}']) for i in range(1, totalBlocks + 1)] + + # If there is more than one class Running + if allClassRunCounts[index] > 1: + blockIndex = allSemBlockLens.index(min(allSemBlockLens)) + stepType = 0 if blockIndex < blockPerSem else 1 + offset = 0 + + # Spread classes throughout both semesters + for i in range(courseRunInfo[course]["Total"]): + cname = f"{course}-{hexdigits[i]}" + classInserted = False + while not classInserted: + blockIndex += offset + if len(running[f'block{blockIndex+1}']) < blockClassLimit: + running[list(running)[blockIndex]][cname] = { + "CrsNo": course, + "Description": emptyClasses[course][cname]["Description"], + "students": selectedCourses[cname]["students"] + } + allClassRunCounts[index] -= 1 + classInserted = True + + offset = stepIndex(offset, stepType) + + if blockIndex >= (totalBlocks - 1): + blockIndex = 0 if stepType == 0 else blockPerSem + offset = 0 + + # If the class only runs once, place in semester with least classes + elif allClassRunCounts[index] == 1: + # Equally disperse into semesters classes + + # Get block with least classes + leastBlock = allSemBlockLens.index(min(allSemBlockLens)) + 1 + cname = f"{course}-0" + + running[f"block{leastBlock}"][cname] = { + "CrsNo": course, + "Description": emptyClasses[course][cname]["Description"], + "students": selectedCourses[cname]["students"], + } + + allClassRunCounts[index] -= 1 + + # Remove course when fully inserted + if allClassRunCounts[index] == 0: + allClassRunCounts.remove(allClassRunCounts[index]) + courseRunInfo.pop(list(courseRunInfo)[index]) + + # Step 5 - Fill student schedule + for block in running: + for cname in running[block]: + for student in running[block][cname]["students"]: + students[student["index"]]["schedule"][block].append(cname) + students[student["index"]]["classes"] += 1 + + + # Step 6 - Evaluate, move students to fix conflicts + conflictLogs = {} + criticalCount, acceptableCount = 0, 0 + c_mc_count, c_cr_count, a_mc_count = 0, 0, 0 + studentsCritical, studentsAcceptable = 0, 0 + + for student in students: + blocks = [student["schedule"][block] for block in student["schedule"]] + hasConflicts = True if sum(1 for b in blocks if len(b)>1) > 0 else False + + # If there is no conflicts + # and classes inserted to is equal to expectedClasses + # or classes the student is inserted to is missing + # no more than two classes: + # continue to next student + if not hasConflicts and student["classes"] == student["expectedClasses"]: continue + elif not hasConflicts and (student["expectedClasses"]-2) <= student["classes"] < student["expectedClasses"]: + # TODO: Insert alternates if available? + a_mc_count += 1 + acceptableCount += 1 + if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1 + continue + + studentData = { + "Pupil #": student["Pupil #"], + "index": student["studentIndex"] + } + + if hasConflicts: + student["classes"] = 0 + missing = [] + # Clear student schedule to restructure + for block in student["schedule"]: + [running[block][cname]["students"].remove(studentData) for cname in student["schedule"][block]] + student["schedule"][block] = [] + + # Find class in student schedule that's run the least + classes, runCounts = [], [] + for block in blocks: + for cname in block: + classes.append(cname[:-2]) + runCounts.append(courseRunInfoCopy[cname[:-2]]["Total"]) + + # Rebuild student schedule + availableBlocks = [f'block{i}' for i in range(1, totalBlocks + 1)] + while len(classes) > 0: + index = runCounts.index(min(runCounts)) # Get class least run + found = False + + # find slot for class + for block in availableBlocks: + if found: break + for cname in running[block]: + if cname[:-2] == classes[index] and len(running[block][cname]["students"]) < classCap: + running[block][cname]["students"].append(studentData) + student["schedule"][block].append(cname) + availableBlocks.remove(block) + student["classes"] += 1 + found = True + break + + if not found: + # Determine all places class exists + existsIn, existingClassNames = [], [] + for block in running: + for cname in running[block]: + if cname[:-2] == classes[index] and len(running[block][cname]["students"]) < classCap: + existsIn.append(block) + existingClassNames.append(cname) + + # Attempt to fix + solution = False + if len(existsIn) > 0: + for i, existing in enumerate(existsIn): + if solution: break + classOut = student["schedule"][existing][0] + for block in running: + if solution: break + if block == existing or block not in availableBlocks: continue + for cname in running[block]: + if cname[:-2] == classOut[:-2] and len(running[block][cname]["students"]) < classCap: + student["classes"] += 1 + + # Move to existing class elsewhere + student["schedule"][block].append(cname) + running[block][cname]["students"].append(studentData) + + # Overwrite old class + running[existing][student["schedule"][existing][0]]["students"].remove(studentData) + student["schedule"][existing][0] = existingClassNames[i] + running[existing][existingClassNames[i]]["students"].append(studentData) + + solution = True + break + + if not solution: + # Try alternate + alternates = [alt["CrsNo"] for alt in students[student["studentIndex"]]["remainingAlts"] if alt["CrsNo"] not in flex and alt["CrsNo"] in courseRunInfoCopy] + if len(alternates) == 0: # If no alternates, create critical error + c_cr_count += 1 + criticalCount += 1 + + missing.append({ + "block": f'block{index + 1}', + "CrsNo": classes[index] + }) + + if not newConflict( + student["Pupil #"], + "", # Student Email + "Critical", # Err type + "C-CR", # Err code + "Couldn't Resolve", # Err msg + conflictLogs): # logs dir + studentsCritical += 1 + + else: + # Get alternate least run + altRunCounts = [courseRunInfoCopy[alt]["Total"] for alt in alternates] + altIndex = altRunCounts.index(min(altRunCounts)) + + # add alternate + classes.append(alternates[altIndex]) + runCounts.append(altRunCounts[altIndex]) + + # Remove alternate from remaining alternates + for remaining in students[student["studentIndex"]]["remainingAlts"]: + if remaining["CrsNo"] == alternates[altIndex]: + students[student["studentIndex"]]["remainingAlts"].remove(remaining) + + # Remove class after inserted or failed to insert + classes.remove(classes[index]) + runCounts.remove(runCounts[index]) + + # Collect missing course data and solutions + + if len(missing) > 0: + data = [] + + if student["gradelevel"] is None: + for obj in missing: + data["missing"].append({ + "CrsNo": obj["CrsNo"], + "block": obj['block'], + "solutions": None, + "error": "Unable to find solutions, err no grade" + }) + + else: + for obj in missing: + blockSolution = { "CrsNo": obj["CrsNo"], "block": obj['block'], "solutions": [] } + for cname in running[obj['block']]: + courseGrade = getGradeFromCourseCode(cname[:-2]) + if courseGrade is None: continue + if (student["gradelevel"] == courseGrade) or (student["gradelevel"] == 12 and courseGrade == 11): + if len(running[obj['block']][cname]["students"]) < classCap: + blockSolution["solutions"].append({ + "CrsNo": cname, + "Description": running[obj['block']][cname]["Description"] + }) + + data.append(blockSolution) + + insertConflictSolutions(student["Pupil #"], conflictLogs, data) + + metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False + if not metSelfRequirements: + + if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]: + a_mc_count += 1 + acceptableCount += 1 + if not newConflict(student["Pupil #"], "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1 + + elif student["classes"] < (student["expectedClasses"] - 2): + # Difference between classes inserted to and + # expected classes is too great + if student["Pupil #"] in conflictLogs: + c_mc_count += 1 + criticalCount += 1 + if not newConflict(student["Pupil #"], "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1 + + finalConflictLogs = { + "Conflicts": conflictLogs, + "Critical": { + "Total": criticalCount, + "Students": studentsCritical, + "Errors": [{ + "Total": c_mc_count, + "Description": "Missing too many Classes", + "Code": "C-MC" + }, { + "Total": c_cr_count, + "Description": "Couldn't Resolve", + "Code": "C-CR" + }] + }, + "Acceptable": { + "Total": acceptableCount, + "Students": studentsAcceptable, + "Errors": [{ + "Total": a_mc_count, + "Description": "Missing 1-2 Classes", + "Code": "A-MC" + }] + } + } + + # Log Conflict to records + with open(conflictsDir, "w") as outfile: + json.dump(finalConflictLogs, outfile, indent=2) + + # Insert flex (spare) course codes to empty blocks + for student in students: + for block in student["schedule"]: + if len(student["schedule"][block]) == 0: + student["schedule"][block].append(flex[0]) if int(block[5:]) <= 5 else student["schedule"][block].append(flex[1]) + + # Update/log Student records + with open(studentsDir, "w") as outfile: + json.dump(students, outfile, indent=2) + + return (running, None) diff --git a/app/util/__pycache__/estimateGrade.cpython-311.pyc b/app/util/__pycache__/estimateGrade.cpython-311.pyc new file mode 100644 index 0000000..2eecaf0 Binary files /dev/null and b/app/util/__pycache__/estimateGrade.cpython-311.pyc differ diff --git a/app/util/__pycache__/getCourses.cpython-311.pyc b/app/util/__pycache__/getCourses.cpython-311.pyc new file mode 100644 index 0000000..5f043e9 Binary files /dev/null and b/app/util/__pycache__/getCourses.cpython-311.pyc differ diff --git a/app/util/__pycache__/getStudents.cpython-311.pyc b/app/util/__pycache__/getStudents.cpython-311.pyc new file mode 100644 index 0000000..19d7af6 Binary files /dev/null and b/app/util/__pycache__/getStudents.cpython-311.pyc differ diff --git a/app/util/__pycache__/globals.cpython-311.pyc b/app/util/__pycache__/globals.cpython-311.pyc new file mode 100644 index 0000000..4f6fb6b Binary files /dev/null and b/app/util/__pycache__/globals.cpython-311.pyc differ diff --git a/app/util/convertRawData.py b/app/util/convertRawData.py new file mode 100644 index 0000000..ba312d5 --- /dev/null +++ b/app/util/convertRawData.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python 3.11 +from docx.shared import Inches +from app.util.globals import flex +import docx + +def putMasterTimetable(timetable: dict) -> None: + pass + +def putScheduleToWord(courses: dict, student: dict, output_dir: str='../../output/final/student_schedules') -> None: + # create an instance of a word doc. + doc = docx.Document() + + doc.add_heading(f'Schedule for Student {student["Pupil #"]}') + + # Create a table object + table = doc.add_table(rows=1, cols=3) + table.autofit = True + table.style = 'Colorful List' + + # course name + (code) | block | semester + row = table.rows[0].cells + row[0].text = 'Course' + row[1].text = 'Block' + row[2].text = 'Sem.' + + table.columns[0].width = Inches(3.5) + table.columns[1].width = Inches(0.75) + table.columns[2].width = Inches(0.75) + + for block in student['schedule']: + courseCode = student["schedule"][block][0] + if courseCode in flex: courseName = 'Study' + 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) + row = table.add_row().cells + row[0].text = f'{courseName}\n({courseCode})' + row[1].text = str(blockNum) + row[2].text = str(semester) + + for row in table.rows: + row.height = Inches(0.75) + + doc.save(f'{output_dir}/{student["Pupil #"]}_schedule.docx') diff --git a/app/util/estimateGrade.py b/app/util/estimateGrade.py new file mode 100644 index 0000000..1772847 --- /dev/null +++ b/app/util/estimateGrade.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3.11 +from app.util.globals import flex + +most_frequent = lambda l : max(set(l), key = l.count) + +def getGradeFromCourseCode(code: str) -> int: + grades = [int(s) for s in code.split("-") if s.isdigit()] + return None if len(grades) == 0 else most_frequent(grades) + +def getEstimatedGrade(pupil: dict) -> int: + grades = [] # List of all possible grades + for request in (r for r in pupil["requests"] if r not in flex): + for extractedGrade in [int(s) for s in request["CrsNo"].split("-") if s.isdigit()]: + grades.append(extractedGrade) + + return None if len(grades) == 0 else most_frequent(grades) # Final estimate of grade diff --git a/app/util/getCourses.py b/app/util/getCourses.py new file mode 100644 index 0000000..2f18f78 --- /dev/null +++ b/app/util/getCourses.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3.11 +import json +import csv + +# 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]]: + + courses = {} + + 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 + if exists: break + if not exists: + courses[row["CrsNo"]] = { + "CrsNo": row["CrsNo"], + "Requests": 0, + "Description": row["Description"], + "students": [] + } + + if log: + with open(log_dir, "w") as outfile: + json.dump(courses, outfile, indent=2) + + return courses diff --git a/app/util/getStudents.py b/app/util/getStudents.py new file mode 100644 index 0000000..e870c7c --- /dev/null +++ b/app/util/getStudents.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3.11 +import json +import csv +from app.util.estimateGrade import getEstimatedGrade +from app.util.globals import flex + +# sort data into usable dictionary +def getStudents( + data_dir: str, + log: bool=False, + totalBlocks: int=10, + log_dir: str='./output/raw/students.json' +) -> list[ dict[ str: any ] ]: + + students: list[ dict[ str: any ] ] = [] + + with open(data_dir, newline='') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + exists = False + for student in students: + exists = True if student["Pupil #"] == row["Pupil #"] else False + if exists: break + alternate = True if row["Alternate?"] == 'TRUE' else False + if exists: + if len(students[student["studentIndex"]]["requests"]) >= totalBlocks and not alternate and row["CrsNo"] not in flex: alternate = True + students[student["studentIndex"]]["requests"].append({ + "CrsNo": row["CrsNo"], + "Description": row["Description"], + "alt": alternate + }) + if row["CrsNo"] not in flex and not alternate and students[student["studentIndex"]]["expectedClasses"] < 10: + students[student["studentIndex"]]["expectedClasses"] += 1 + else: + newStudent = { + "Pupil #": row["Pupil #"], + "requests": [{ + "CrsNo": row["CrsNo"], + "Description": row["Description"], + "alt": alternate + }], + "schedule": {}, + "expectedClasses": 1, + "classes": 0, + "remainingAlts": [], + "studentIndex": len(students) + } + for i in range(1, totalBlocks+1): newStudent["schedule"][f'block{i}'] = [] + students.append(newStudent) + + # Estimate student grades + for student in students: + student["gradelevel"] = getEstimatedGrade(student) + + if log: + with open(log_dir, "w") as outfile: + json.dump(students, outfile, indent=2) + + return students diff --git a/app/util/globals.py b/app/util/globals.py new file mode 100644 index 0000000..ad7c1e8 --- /dev/null +++ b/app/util/globals.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3.11 +from dataclasses import dataclass + +# These are the codes for Flex (spare) blocks +# Semester 1 and 2 +flex: tuple = ("XAT--12A-S", "XAT--12B-S") + +# Global function +exists = lambda n : True if n not in ('', None) else False + +# Global data class +@dataclass +class Error: + Title: str + Description: str + + def __init__(self, title: str, description: str): + self.Title = title + self.Description = description diff --git a/main.py b/main.py new file mode 100755 index 0000000..9bad127 --- /dev/null +++ b/main.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3.11 +import eel +from app.generator import generateScheduleV3 +from app.util.globals import Error +from app.util.getCourses import getCourses +from app.util.getStudents import getStudents + +eel.init('template') + +@eel.expose +def start( + input_file_dir: str, + min_req: int, + class_cap: int, + block_class_limit: int, + total_blocks: int, +) -> Error: + + # Ensure params are of correct type + input_file_dir = str(input_file_dir) + min_req = int(min_req) + class_cap = int(class_cap) + block_class_limit = int(block_class_limit) + total_blocks = int(total_blocks) + + err = None + + # call pre-algorithm functions read raw data into a processable format + students = getStudents(input_file_dir, log=True, totalBlocks=total_blocks) + courses = getCourses(input_file_dir, log=True) + + # call algorithm to sort data + master_timetable, err = generateScheduleV3( + students, + courses, + minReq=min_req, + classCap=class_cap, + blockClassLimit=block_class_limit, + totalBlocks=total_blocks, + studentsDir='./output/raw/students.json', + conflictsDir='./output/raw/conflicts.json' + ) + + # TODO: log master_timetable + + # TODO: call post-algorithm functions to present sorted data + + return err + +eel.start('index.html', size=(800, 1000)) diff --git a/template/index.html b/template/index.html new file mode 100644 index 0000000..2990933 --- /dev/null +++ b/template/index.html @@ -0,0 +1,94 @@ + + + + Test App + + + + + + + + + + + +
+
+
+
+
+

Schedule Generator

+ + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/template/script.js b/template/script.js new file mode 100644 index 0000000..2c4bf31 --- /dev/null +++ b/template/script.js @@ -0,0 +1,38 @@ +function start() { + let file = document.getElementById("input_file"); + let blockClassLimit = document.getElementById("classrooms").value; + let minReq = document.getElementById("min_req").value; + let classCap = document.getElementById("class_cap").value; + let totalBlocks = document.getElementById("total_blocks").value; + + console.log(file); +} + +function summation() { + var data_1 = document.getElementById("int1").value; + var data_2 = document.getElementById("int2").value; + eel.add(data_1, data_2)(call_Back); +} + +function call_Back(output){ + document.getElementById("res").value = output; +} + +var i = 0; +function move() { + if (i == 0) { + i = 1; + var elem = document.getElementById("myBar"); + var width = 1; + var id = setInterval(frame, 10); + function frame() { + if (width >= 100) { + clearInterval(id); + i = 0; + } else { + width++; + elem.style.width = width + "%"; + } + } + } +} \ No newline at end of file diff --git a/template/style.css b/template/style.css new file mode 100644 index 0000000..209fb60 --- /dev/null +++ b/template/style.css @@ -0,0 +1,117 @@ +*, +*:before, +*:after{ + padding: 0; + margin: 0; + box-sizing: border-box; +} +body{ + min-width: 800; + min-height: 600; + background-color: #080710; +} +.background{ + width: 430px; + height: 520px; + position: absolute; + transform: translate(-50%,-50%); + left: 50%; + top: 50%; +} +.background .shape{ + height: 200px; + width: 200px; + position: absolute; + border-radius: 50%; +} +.shape:first-child{ + background: linear-gradient( + #1845ad, + #23a2f6 + ); + left: -80px; + top: -80px; +} +.shape:last-child{ + background: linear-gradient( + to right, + #ff512f, + #f09819 + ); + right: -30px; + bottom: -80px; +} +.main{ + height: 600px; + width: 400px; + background-color: rgba(255,255,255,0.13); + position: absolute; + transform: translate(-50%,-50%); + top: 50%; + left: 50%; + border-radius: 10px; + backdrop-filter: blur(10px); + border: 2px solid rgba(255,255,255,0.1); + box-shadow: 0 0 40px rgba(8,7,16,0.6); + padding: 50px 35px; +} +.main *{ + font-family: 'Poppins',sans-serif; + color: #ffffff; + letter-spacing: 0.5px; + outline: none; + border: none; +} +.main h3{ + font-size: 32px; + font-weight: 500; + line-height: 42px; + text-align: center; +} + +label{ + display: block; + margin-top: 30px; + font-size: 16px; + font-weight: 500; +} +.form-control{ + display: block; + height: 50px; + width: 100%; + background-color: rgba(255,255,255,0.07); + border-radius: 3px; + padding: 0 10px; + margin-top: 8px; + font-size: 14px; + font-weight: 300; +} +::placeholder{ + color: #e5e5e5; +} +.main button{ + transition: 0.05s; + margin-top: 50px; + width: 100%; + background-color: #ffffff; + color: #080710; + padding: 15px 0; + font-size: 18px; + font-weight: 600; + border-radius: 5px; + cursor: pointer; +} +.processing { + display: none; +} + +#myProgress { + width: 100%; + background-color: #ddd; +} + +#myBar { + width: 1%; + height: 30px; + background-color: #04AA6D; +} \ No newline at end of file