From 7b909b770b162a911eed30b0c20b935c9e5664a1 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sun, 6 Nov 2022 11:50:13 -0800 Subject: [PATCH] initial commit --- app/__pycache__/generator.cpython-311.pyc | Bin 0 -> 23672 bytes app/generator.py | 540 ++++++++++++++++++ .../__pycache__/estimateGrade.cpython-311.pyc | Bin 0 -> 2221 bytes .../__pycache__/getCourses.cpython-311.pyc | Bin 0 -> 1588 bytes .../__pycache__/getStudents.cpython-311.pyc | Bin 0 -> 2988 bytes app/util/__pycache__/globals.cpython-311.pyc | Bin 0 -> 1057 bytes app/util/convertRawData.py | 45 ++ app/util/estimateGrade.py | 16 + app/util/getCourses.py | 33 ++ app/util/getStudents.py | 59 ++ app/util/globals.py | 19 + main.py | 50 ++ template/index.html | 94 +++ template/script.js | 38 ++ template/style.css | 117 ++++ 15 files changed, 1011 insertions(+) create mode 100644 app/__pycache__/generator.cpython-311.pyc create mode 100644 app/generator.py create mode 100644 app/util/__pycache__/estimateGrade.cpython-311.pyc create mode 100644 app/util/__pycache__/getCourses.cpython-311.pyc create mode 100644 app/util/__pycache__/getStudents.cpython-311.pyc create mode 100644 app/util/__pycache__/globals.cpython-311.pyc create mode 100644 app/util/convertRawData.py create mode 100644 app/util/estimateGrade.py create mode 100644 app/util/getCourses.py create mode 100644 app/util/getStudents.py create mode 100644 app/util/globals.py create mode 100755 main.py create mode 100644 template/index.html create mode 100644 template/script.js create mode 100644 template/style.css diff --git a/app/__pycache__/generator.cpython-311.pyc b/app/__pycache__/generator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03695220e29e4b8328d59eaf69f65d838c416ced GIT binary patch literal 23672 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 Xx5oyM+_7yHUJI?|+T06;eHH%~1HMsy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2eecaf0e214ee47fa4ac2766464cc7668e801ec8 GIT binary patch literal 2221 zcmcIl|4Um}6uZ}7w>%$ zwF!hF%z^_819e!MU*>cQ`VS2D<2D9MN?`I}P}ncsZ!+00{a?V*)O}?d+wKW z?!D)p^Eu~!>+zgIPzvf;?16;PA7XM)l*(Z5T^KAPf(X`75|3bvh%}2vBqCFpC{!6$ zh-+3EaS@fOmAM<{Zc;Oh{I!3IW7e^$jB0c$hAW4P&IL_|w)Y{BMPwY}8DhOsvS<}J z8&z%=-2+wavWZt=yiuLQh0>58oANlF_RFT6j82&{r-tc@>6w%P9ISD1Qp^6$(3qa2 zp;0ZGOoRp~A2;-L=p&}zqcMYrhGS!tWHfr6rYMUVI_pcPLs4iMBcX*THxkLHL9eqY zp~3X@eoYvTxl5uw>@*xMEXRF7^5{{=BVKrWd2(fP?c>c)i@wg1ue0py%%eSEBU6d# zgqkX&Muw#v#t~;&#+icwSRc=#EGoeGXd#l*Km%wMgf^;zIqWbec1ja@+%g}N2ujr( z!YP%KeL)h8s|-1m6vAhr?cD~lh~l8)ol7~K#RZrjEl#zznod&dp)}tEcAn?_TnC#iGevr$GTG8E7zv3ig()wqC(Lt(S&cqQ5Eut9~!`+(%pj-}Poz~dWV z-zf0qTPwFfk5XM%xvneU{=2tvA@b1om9K!Gcw39L;km}S#&2@lk<#UG`Es}@g)Lo8 zS2~kUXh$4pq3bWWMmR2qZG;i&6MMM#134Fo|z;2|b9gR9R&XJY1nWoT3WwQJkg%7L!m0)0;c zeLu970)yqiAiSj194R+Pb`f@gJdnBi9rg4V$WXS-B*#4-YWZAo4Hggq<- zYa&>A5(sF6@l(`Vbk?U0oyCUEyk}RDaChM>ydQV3*_|~$L~2yry>xpQL05=b-Mb?mAl@F_tkdUe#a@&X;5U0Lbzz!9czR~{Xy|?eZ zdGF2J{Ux1t5YUHTuM~$hg#K2-Ii=2+qkF*oj3}bGgqBWsyo7CJBWhEfC8)tPYQj#K z4lBWo9~0U0D5%RVThw}HESVH9qMY+aSuD(+WXJPc1kaIXj%cZdL|-cOONBY9Qx$p*q`tO zMjqa{StH`=MO$iuOuCU)5Vf{-IO#&fNhn}r9{L}zi&a77c?>puIT!bBjrnn`JiphB4!2#Kq2loLcVT~DcU zhSdEs^Q2Z3Poy*B7K8h&KpB^MJW)EWtd(;PS9NhifvF;rv#|q4JXv;2Ou~r8ZW{Ni z(q!u{oG#5%Gf7}zm6cE*&y5b9%;z8ZYb?K9Dy+GAsN-?qm-F|yzsib%$S)QjF(wg)9tAq3h8{clt?WafE>Cwh)*mEU9NyCYusf9g#jpeX!C_)L6jiGrf z%-(q3zdih7pq;(*+u|Pszkl;rc4{y4MXRsX_h+SPhRMvHHQ1VHO~4kWdn05L=dicG zRo$En`v=18)vbr^>{!@87}>@MoCMG$Ev+@TncBCm9Y<+(z5&_ADaPVfGzb6aF<;6-N79NRa?+U6KAkp^#L zlNjDA<97nh4<6=6ZqDDNL%=3f|NBx_?DS@E-B&04LVuny=>OGkyz1^-mH4 z0P4Z`5G5P&3X#)@SLC2nChYk%B11S6A%HN|6KSv?61WDXG!bax*A_CJ=4ey2rkm>< T<831w+2>xd>wkcM4Cnp}g?@Mc literal 0 HcmV?d00001 diff --git a/app/util/__pycache__/getStudents.cpython-311.pyc b/app/util/__pycache__/getStudents.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19d7af635df6643843eb06e25b39c2e9ea75f836 GIT binary patch literal 2988 zcmb7GU2NOd6}}WH{z{f*`A?P|#IVp>c5H2HoM3_NHj867MQa33ilJVUK@n+%F-<9# zwB@!^p@$9-1_RSWfT(B+_+^=g&mr^=GB|7<2g*wwl>10VGNqyAW=}0sFb-&e zHF}w*$Q-RPYr*9pg#`bS!!oC^GOuv5pzyK?x}*pS^osPmK>TnI)Y4W&IrLR0%b1G1fHSfJlgx^yY|cL?*xw9Hrt|A&VcksQOfhYk>$0Mo z#^fC%ue-dQF*CoFRqWDQKIcki-ppvf((>6=!{uPcf5l?OT*rErIRb@{1Ue1R%fExX zk8-HYm{jY131aa$@GyYTBY2u^3YCK<@i}OBj^31l55f1*k$;&jv2&DfGF|3N z%pAHgSQbix%(UWB;*O72{ncBqxEb;g+CVbn&R$^cu zzU9%^k9mSTV$sewa&;~^imc{>Vk8OX=mUmNYUmZqLYp)XI z$OF0^8-?0EeuU>7AU^vOc*3pK<&IS!0v`M+`^~`SvHX=IpNA33(%HZ?_-_3Ru(W@} zW05-PEICR=azyUTq4saoenvU^fGbNT>~z@VNGSk&!>~OIA(J}Gp&Tkna@Vnw#Y&-X z(6>#D(Gv4tA}_V?NtUl}s6+mBaIiNf!;RTeGxwC@CU6*i--weARqI(((D-21riAynO?qssHy=>NhC zeAB<^BH$HHSBjddEAwx-tLp_-J3Vs!bMc~PDp&{ldDmsGFMWL34IPlo6Gw+~6f_EaqKlPBF4rEtqOvPtiCAaB(*Xm(FOW8&Nh3N*1{6l9n+H#lWNpo)d1s zq{LGkCUL`EAY9(a-d5ywO~Jin$Yq-@ZWt?T8C6&H9AGmr2@dWj7QtJD+gve6cv4d~ z6wRfI&;4~NmsJ6;Zl*++$rlverL)F{E6%A|b4dYn!jX!O2@ASxrT`YQ8^jqsr(hCW zm+={uk=NG>sSZpC1(V9h;)w{I#aLOQ|jglui~f$dAzJw)W;q`gVRzNv~*`H8l;fUN!TD^i`a{17S7N*A7Cq zpy&$Dn0b7$P)NgZ((9(GrQyfqU>9Su@R=(e8{>CiD{>-Z6wX^gUI!W-tBn4WldA96 zZ&}=MgB!NFVGo_AliR=E4|i_etS>aeNjsdZ%sY`D51k4oUl38Hok(|O#pxOHP=Jg3 zqWZQIKfN8d?Zc44?Tp0#entk z%>yO7V&!x@oBwO^3oH9Ya}wkSfyDrHUs4MJhwrTQ)lb>cQJYWgjM@Cu-}vc0e%guk z){6J0o%mq=1^{=W10KiBiXH;-0x^R0iU=5j5K$;nEpAPJYg*EXhbUo=+PTu~zZ=~> z|HG_(Y1zK;8|%|G`+{!WHTEtT&9{!&RXbmwwPRzpIBuPpvBX)pZE<#YxFKF@jvC_Z zYmY^}@p}B;&1XIR^}c(bI*C*DvYnW4VuOIHKLjv*(H90ReQ{^#l+)AeB!>1Onzz56 zcrfAhg?a%yh^qMS0o2u3yIUXM&U`)5fOAJjEq)2^>P747($3hEWqa(s-NX;k-NbI9 zG4_!)_7Mc>50T$T>SBW*vH1~;9|6UifzRy)AtU^>@Wl&~Ag_pM&7GvZe*OBSX>XoF zT>}ss`n)eHx&6V3om^w^oIQB1vQYi7!Ne^l{w&BtUeOc#Lb#f%84V#}3kh%tPSlJ0 ztoW5$@v6VdCjF&-Eq63_?LsuTJ5)khOPD+@+{ot#Cn{_xF@_r zb$JMc6N-2=^v^iS3zA6|b;dl7Xgrii(@;dLSUp>d)v~pITT0l$Ax|O}WPy_eo&lu) E0j+Y+9smFU literal 0 HcmV?d00001 diff --git a/app/util/__pycache__/globals.cpython-311.pyc b/app/util/__pycache__/globals.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f6fb6b6cb97f8a71a65234ba3fd334d24e73df9 GIT binary patch literal 1057 zcmZ`%%}X0W6o0e3S@WT$id2LaQavRlE-gJ-5z@vAg@)27$X=FpXQJC~cGH(?ycGJ6MCd{GB=pogjAAji0! z3=d%HbgT}=^bhGNk~l-{&~`jqh@!HW-K?ya%lGbA%Jq@`s9cY=hR1dttZ^hVyNcSM zMmU6htnLlr3RQeURm`u|ib@Q%^(UX@Bg7Z-&xOlXV<%`bqv6?2*Qhe_Dhyi2a~`~8 zPAH7Jv%_fHV{6Q3+zta?X|)U+wH>;ivF!z0wkH-_Z!go!m|%u%_=IES4$+--$!;K!+O;G@5Nj$~>mBj)DOgUpeAu!eWAui$CK zGX;E%Ad`F&BX}O6sK%OLUW4C|o+$(&lNSmFE@_CArYKS z-HfW8r)RVCQT3ZP(@hr7;a#n$%M8BVWR?|YEvp&Gd7wRRS+Co+cXgwRkjHt;vVA`Y zZP~W4EG|dD$0W#HNj-c*S`x-l#WRHHkKcJ_JSs({ccruZbXSq za+2-8FnCGkui;d3qi>lBE@|O%);N)N%VT^_+B(~HMJNV!{aq`5RfBd{_-zc8QxG9S zPbGw$!=3K6)k{PEZlp!53ng5qQ0nQBeb~(`MLJqauBYf^p$FIDLi&D6xKJ^!Gxtwq G($_D1*!D*N literal 0 HcmV?d00001 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