add student dataclass

This commit is contained in:
SowinskiBraeden committed 2022-11-30 19:14:02 -08:00
1 parent be766eb962
commit 283c5e72ff
6 files changed
+124 -98

No files matched your search

+56 -55
View File
@@ -5,6 +5,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
# Takes in information to create or add a new conflict
# Returns if the particular student has a previous error
@@ -44,12 +45,12 @@ 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, # 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, # 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"
@@ -77,8 +78,8 @@ def generateScheduleV3(
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"]
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:
@@ -183,9 +184,9 @@ def generateScheduleV3(
# 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"]
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:
@@ -198,8 +199,8 @@ def generateScheduleV3(
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"]
"Pupil #": student.PupilNum,
"index": student.StudentIndex
})
getAvailableCourse = False
break
@@ -208,7 +209,7 @@ def generateScheduleV3(
if cname[len(cname)-1] == f"{len(emptyClasses[course])-1}":
if len(alternates) > 0:
# Use alternate
course = alternates[0]["CrsNo"]
course = alternates[0].CrsNo
alternates.remove(alternates[0])
isAlt = True
break
@@ -220,8 +221,8 @@ def generateScheduleV3(
elif cname not in selectedCourses:
selectedCourses[cname] = {
"students": [{
"Pupil #": student["Pupil #"],
"index": student["studentIndex"]
"Pupil #": student.PupilNum,
"index": student.StudentIndex
}],
"CrsNo": course,
"Description": courses[course]["Description"]
@@ -232,7 +233,7 @@ def generateScheduleV3(
elif course not in emptyClasses:
if len(alternates) > 0:
# Use alternate
course = alternates[0]["CrsNo"]
course = alternates[0].CrsNo
alternates.remove(alternates[0])
isAlt = True
else:
@@ -240,7 +241,7 @@ def generateScheduleV3(
# handle options to solve for missing class
getAvailableCourse = False
students[student["studentIndex"]]["remainingAlts"] = alternates
students[student.StudentIndex].RemainingAlts = alternates
tempStudents.remove(student)
@@ -323,8 +324,8 @@ def generateScheduleV3(
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
students[student["index"]].Schedule[block].append(cname)
students[student["index"]].Classes += 1
# Step 6 - Evaluate, move students to fix conflicts
@@ -334,7 +335,7 @@ def generateScheduleV3(
studentsCritical, studentsAcceptable = 0, 0
for student in students:
blocks = [student["schedule"][block] for block in student["schedule"]]
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
@@ -342,26 +343,26 @@ def generateScheduleV3(
# 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"]:
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
if not newConflict(student.PupilNum, "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1
continue
studentData = {
"Pupil #": student["Pupil #"],
"index": student["studentIndex"]
"Pupil #": student.PupilNum,
"index": student.StudentIndex
}
if hasConflicts:
student["classes"] = 0
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] = []
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 = [], []
@@ -382,9 +383,9 @@ def generateScheduleV3(
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)
student.Schedule[block].append(cname)
availableBlocks.remove(block)
student["classes"] += 1
student.Classes += 1
found = True
break
@@ -402,21 +403,21 @@ def generateScheduleV3(
if len(existsIn) > 0:
for i, existing in enumerate(existsIn):
if solution: break
classOut = student["schedule"][existing][0]
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
student.Classes += 1
# Move to existing class elsewhere
student["schedule"][block].append(cname)
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][student.Schedule[existing][0]]["students"].remove(studentData)
student.Schedule[existing][0] = existingClassNames[i]
running[existing][existingClassNames[i]]["students"].append(studentData)
solution = True
@@ -424,7 +425,7 @@ def generateScheduleV3(
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]
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
@@ -435,7 +436,7 @@ def generateScheduleV3(
})
if not newConflict(
student["Pupil #"],
student.PupilNum,
"", # Student Email
"Critical", # Err type
"C-CR", # Err code
@@ -453,9 +454,9 @@ def generateScheduleV3(
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)
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])
@@ -466,7 +467,7 @@ def generateScheduleV3(
if len(missing) > 0:
data = []
if student["gradelevel"] is None:
if student.Gradelevel is None:
for obj in missing:
data["missing"].append({
"CrsNo": obj["CrsNo"],
@@ -482,7 +483,7 @@ def generateScheduleV3(
courseInfo = running[obj['block']][cname]
courseGrade = getGrade(courseInfo['CrsNo'], courseInfo['Description'])
if courseGrade is None: continue
if (student["gradelevel"] == courseGrade) or (student["gradelevel"] == 12 and courseGrade == 11):
if (student.Gradelevel == courseGrade) or (student.Gradelevel == 12 and courseGrade == 11):
if len(running[obj['block']][cname]["students"]) < classCap:
blockSolution["solutions"].append({
"CrsNo": cname,
@@ -491,23 +492,23 @@ def generateScheduleV3(
data.append(blockSolution)
insertConflictSolutions(student["Pupil #"], conflictLogs, data)
insertConflictSolutions(student.PupilNum, conflictLogs, data)
metSelfRequirements = True if student["classes"] == student["expectedClasses"] else False
metSelfRequirements = True if student.Classes == student.ExpectedClasses else False
if not metSelfRequirements:
if (student["expectedClasses"] - 2) <= student["classes"] < student["expectedClasses"]:
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
if not newConflict(student.PupilNum, "", "Acceptable", "A-MC", "Missing 1-2 Classses", conflictLogs): studentsAcceptable += 1
elif student["classes"] < (student["expectedClasses"] - 2):
elif student.Classes < (student.ExpectedClasses - 2):
# Difference between classes inserted to and
# expected classes is too great
if student["Pupil #"] in conflictLogs:
if student.PupilNum in conflictLogs:
c_mc_count += 1
criticalCount += 1
if not newConflict(student["Pupil #"], "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1
if not newConflict(student.PupilNum, "", "Critical", "C-MC", "Missing too many Classses", conflictLogs): studentsCritical += 1
finalConflictLogs = {
"Conflicts": conflictLogs,
@@ -541,10 +542,10 @@ def generateScheduleV3(
# Insert flex (spare) course codes to empty blocks
for student in students:
for block in student["schedule"]:
if len(student["schedule"][block]) == 0:
for block in student.Schedule:
if len(student.Schedule[block]) == 0:
i = 0 if int(block[5:]) <= blockPerSem else 1
student["schedule"][block].append(flex[i])
student.Schedule[block].append(flex[i])
# Read timetable and collect data on courses
for block in running:
@@ -559,7 +560,7 @@ def generateScheduleV3(
# Update/log new student records
with open(studentsDir, "w") as outfile:
json.dump(students, outfile, indent=2)
json.dump(studentsToDict(students), outfile, indent=2)
# Update/log new course records
with open(coursesDir, "w") as outfile:
+4 -4
View File
@@ -44,7 +44,7 @@ def putScheduleToWord(
# create an instance of a word doc.
doc = docx.Document()
doc.add_heading(f'Schedule for Student {student["Pupil #"]}')
doc.add_heading(f'Schedule for Student {student.PupilNum}')
# Create a table object
table = doc.add_table(rows=1, cols=3)
@@ -62,12 +62,12 @@ def putScheduleToWord(
table.columns[2].width = Inches(0.75)
for block in student['schedule']:
courseCode = student["schedule"][block][0]
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)
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)
@@ -76,4 +76,4 @@ def putScheduleToWord(
for row in table.rows:
row.height = Inches(0.75)
doc.save(f'{output_dir}/{student["Pupil #"]}_schedule.docx')
doc.save(f'{output_dir}/{student.PupilNum}_schedule.docx')
+7
View File
@@ -1,8 +1,15 @@
#!/usr/bin/env python3.11
import json
import csv
from dataclasses import dataclass
from app.util.estimateGrade import getGrade
@dataclass
class Request:
CrsNo: str
Description: str
Alt: bool
# Get all requested courses from data
def getCourses(
data_dir: str,
+3 -3
View File
@@ -23,10 +23,10 @@ def extractGrade(string: str) -> int:
return grade if grade is not None and grade >= 8 else None
# Estimage students grade based off requests
def estimateStudentGrade(pupil: dict) -> int:
def estimateStudentGrade(pupil) -> int:
grades = [] # List of all possible grades
for request in (r for r in pupil["requests"] if r not in flex):
extractedGrade = getGrade(request["CrsNo"], request["Description"])
for request in (r for r in pupil.Requests if r not in flex):
extractedGrade = getGrade(request.CrsNo, request.Description)
if extractedGrade is not None: grades.append(extractedGrade)
return None if len(grades) == 0 else most_frequent(grades) # Final estimate of grade
+48 -30
View File
@@ -3,6 +3,26 @@ import json
import csv
from app.util.estimateGrade import estimateStudentGrade
from app.util.globals import flex
from app.util.courses import Request
from dataclasses import dataclass, asdict
@dataclass
class Student:
PupilNum: str
Requests: list[Request]
Schedule: dict[str: list]
ExpectedClasses: int
Classes: int
RemainingAlts: list[Request]
StudentIndex: int
Gradelevel: int = None # set to None since don't pass gradelevel
# This function converst Student.Requests to an array of dictionaries from an array of Requests
def RequestsToDict(self) -> None: self.Requests = [asdict(r) for r in self.Requests]
def studentsToDict(students: list[Student]) -> list[dict]:
[s.RequestsToDict for s in students]
return [asdict(s) for s in students]
# sort data into usable dictionary
def getStudents(
@@ -12,56 +32,54 @@ def getStudents(
log_dir: str = './output/raw/students.json'
) -> list[ dict[ str: any ] ]:
students: list[ dict[ str: any ] ] = []
students: list[Student] = []
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
exists = True if student.PupilNum == row["Pupil #"] else False
if exists: break
alternate = True if row["Alternate?"] == 'TRUE' else False
alternate = True if str(row["Alternate?"]).upper() == '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
if len(students[student.StudentIndex].Requests) >= totalBlocks and not alternate and row["CrsNo"] not in flex: alternate = True
students[student.StudentIndex].Requests.append(
Request(
row["CrsNo"],
row["Description"],
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)
}
newStudent["gradelevel"] = row.get("Grade") if row.get("Grade") is not None else row.get("grade")
for i in range(1, totalBlocks+1): newStudent["schedule"][f'block{i}'] = []
newStudent: Student = Student(
row["Pupil #"],
[Request(
row["CrsNo"],
row["Description"],
alternate
)],
{}, 1, 0, [],
len(students)
)
newStudent.Gradelevel = row.get("Grade") if row.get("Grade") is not None else row.get("grade")
for i in range(1, totalBlocks+1): newStudent.Schedule[f'block{i}'] = []
students.append(newStudent)
# Estimate student grades
for student in students:
if student["gradelevel"] is None: student["gradelevel"] = estimateStudentGrade(student)
if student.Gradelevel is None: student.Gradelevel = estimateStudentGrade(student)
if log:
with open(log_dir, "w") as outfile:
json.dump(students, outfile, indent=2)
json.dump(studentsToDict(students), outfile, indent=2)
return students
# Writes all students data to csv file
def writeStudentsToCSV(
students: dict,
students: list[Student],
output_dir: str = './output/raw/csv/students.csv'
) -> None:
with open(output_dir, 'w') as file:
@@ -71,5 +89,5 @@ def writeStudentsToCSV(
writer.writerow(data)
for student in students:
studentData = (student["Pupil #"], student["classes"], student["gradelevel"])
studentData = (student.PupilNum, student.Classes, student.Gradelevel)
writer.writerow(studentData)