From 98cef5e9a772602d42acfcf233838c760424db9a Mon Sep 17 00:00:00 2001 From: Nicolas James Date: Thu, 13 Feb 2025 18:00:17 +1100 Subject: initial commit --- comp3311/2/prog | 328 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ comp3311/2/rules | 107 ++++++++++++++++++ comp3311/2/trans | 63 +++++++++++ 3 files changed, 498 insertions(+) create mode 100755 comp3311/2/prog create mode 100755 comp3311/2/rules create mode 100755 comp3311/2/trans (limited to 'comp3311/2') diff --git a/comp3311/2/prog b/comp3311/2/prog new file mode 100755 index 0000000..130b82b --- /dev/null +++ b/comp3311/2/prog @@ -0,0 +1,328 @@ +#!/usr/bin/python3 +# COMP3311 21T3 Ass2 ... progression check for a given student + +import sys +import psycopg2 +import re +from helpers import * + +# define any local helper functions here + +### set up some globals + +def getMostRecentProgStrm(zid, db): + cur = db.cursor(); + cur.execute(""" + SELECT program, ST.code + FROM Students AS S, + Program_enrolments AS PE, + Stream_Enrolments AS SE, + Programs AS P, + Streams AS ST + WHERE S.id = %s + AND PE.student = S.id + AND SE.partof = PE.id + AND P.id = PE.program + AND ST.id = SE.stream + ORDER BY term DESC + """, [zid]); + info = cur.fetchone(); + cur.close(); + if not info: + return None + else: + return info + +def getRequirements(prog_code, strm_code, db): + cur = db.cursor(); + cur.execute(""" + SELECT R.name, R.min_req, R.max_req, R.type, AOG.definition, AOG.defby, 1 AS IS_PROG, R.id + FROM Streams AS S, Stream_Rules AS SR, Rules AS R, Academic_Object_groups AS AOG + WHERE S.code = %s + AND SR.stream = S.id + AND SR.rule = R.id + AND AOG.id = R.ao_group + UNION + SELECT R.name, R.min_req, R.max_req, R.type, AOG.definition, AOG.defby, 0 AS IS_PROG, R.id + FROM Program_Rules as PR, Rules as R, Academic_Object_Groups AS AOG + WHERE PR.program = %s + AND R.id = PR.rule + AND AOG.id = R.ao_group + AND AOG.type != 'stream' + ORDER BY IS_PROG DESC, id ASC + """, [strm_code, prog_code]); + info = cur.fetchall(); + cur.close(); + if not info: + return None + else: + return list(info) + + +def maybeRemovePatternRequirement(requirement, course_code, UOC): + (name, min_req, max_req, type, definition, defby, stream, aogtype) = requirement + + if type == "FE": + if requirement[1] != None: + requirement[1] = requirement[1] - UOC + if requirement[2] != None: + requirement[2] = requirement[2] - UOC + return True + + for subject in definition.split(","): + match = True + + if len(subject) < 2: + continue + + #print(f"testing {subject} against {course_code}") + + for i in range(0, len(subject)): + if subject[i] != '#' and course_code[i] != subject[i]: + match = False + break + + if match == True: + if requirement[1] != None: + requirement[1] = requirement[1] - UOC + if requirement[2] != None: + requirement[2] = requirement[2] - UOC + + if not '#' in subject: + requirement[4] = requirement[4].replace(subject, "") + return True; + + return False + + +def maybeRemoveEnumRequirement(requirement, course_code): + removeStr = None; + (name, min_req, max_req, type, definition, defby, stream, aogtype) = requirement + for subject in definition.split(","): + subject = subject.replace("{", "") + subject = subject.replace("}", "") + for code in subject.split(";"): + if code == course_code: + removeStr = subject + break + if removeStr != None: + break + + if removeStr == None: + return False + + requirement[4] = requirement[4].replace(removeStr, "") + return True + + +def maybeRemoveInnerRequirement(requirement, course_code, UOC): + (name, min_req, max_req, type, definition, defby, stream, aogtype) = requirement + if defby == "pattern": + return maybeRemovePatternRequirement(requirement, course_code, UOC) + elif defby == "enumerated": + return maybeRemoveEnumRequirement(requirement, course_code) + + +def shouldPopPattern(requirement): + (name, min_req, max_req, type, definition, defby, stream, aogtype) = requirement + if min_req != None and min_req <= 0: + return True + if max_req != None and max_req <= 0: + return True + return any(l.isalnum() for l in definition) == False + + +def maybeRemoveRequirement(requirements, course_code, UOC): + removeIndex = None + + if removeIndex == None: + for count, requirement in enumerate(requirements): + (name, min_req, max_req, type, definition, defby, stream, _) = requirement + + if stream == 1: + continue + + if type == "FE": + continue # handled in next loop + + #print(f"checking against {name} : { maybeRemoveInnerRequirement(requirement, course_code, UOC)}") + if maybeRemoveInnerRequirement(requirement, course_code, UOC): + removeIndex = count + break + + if removeIndex == None: + for count, requirement in enumerate(requirements): + (name, min_req, max_req, type, definition, defby, stream, _) = requirement + + if stream == 0: + continue + + if type == "FE": + continue # handled in next loop + + + if maybeRemoveInnerRequirement(requirement, course_code, UOC) == True: + removeIndex = count + break + + + # Special FE case, they should be prioritised last. + isFreeElective = False # For some reason we only return Free Electives here + if removeIndex == None: + for count, requirement in enumerate(requirements): + (name, min_req, max_req, type, definition, defby, stream, _) = requirement + + if type != "FE": + continue # handled in next loop + + if maybeRemoveInnerRequirement(requirement, course_code, UOC) == True: + removeIndex = count + isFreeElective = True + break + + if removeIndex == None: + return None + + # Remove if our min_req is zero for pattern, or if we have nothing else to + # enumerate in requirements. + (name, min_req, max_req, type, definition, defby, stream, _) = requirements[removeIndex]; + if defby == "pattern": + if shouldPopPattern(requirement): + requirements.pop(removeIndex) + elif defby == "enumerated": + if any(l.isalnum() for l in definition) == False: + requirements.pop(removeIndex) + + return name if isFreeElective == False else "Free Electives" + + +def printAcademicRequirementsType(requirements, expected_type, db): + for requirement in requirements: + (name, min_req, max_req, type, definition, defby, stream, aogtype) = requirement + + if type != expected_type: + continue + + def_count = str.count(definition, ",") + if type == "DS": + print(f"{min_req} stream(s) from {name}") + printStreams(definition, db) + + elif type == "CC" or type == "PE" and min_req: + if defby == "pattern": + print(getCountString(def_count, min_req, max_req, name)) + continue + printSubjects(definition, defby, db) + + elif type == "GE" and min_req: + print(f"{min_req} UOC of {name}") + + elif type == "FE" and min_req: + print(f"at least {min_req} UOC of Free Electives") + + #else: + # print(f"UNEXPECTED {name}, {min_req}, {max_req}, {type}, {defby}") + + +def printAcademicRequirements(requirements, db): + printAcademicRequirementsType(requirements, "CC", db) + printAcademicRequirementsType(requirements, "PE", db) + printAcademicRequirementsType(requirements, "GE", db) + printAcademicRequirementsType(requirements, "FE", db) + + +usage = f"Usage: {sys.argv[0]} zID [Program Stream]" +db = None + +### process command-line args + +argc = len(sys.argv) +if argc < 2: + print(usage) + exit(1) +zid = sys.argv[1] +if zid[0] == 'z': + zid = zid[1:8] +digits = re.compile("^\d{7}$") +if not digits.match(zid): + print("Invalid student ID") + exit(1) + +progCode = None +strmCode = None + +if argc == 4: + progCode = sys.argv[2] + strmCode = sys.argv[3] + +# manipulate database + +try: + db = psycopg2.connect("dbname=mymyunsw") + stuInfo = getStudent(db,zid) + if not stuInfo: + print(f"Invalid student id {zid}") + exit() + + if progCode == None or strmCode == None: + (pc, sc) = getMostRecentProgStrm(zid, db) + progCode = str(pc) + strmCode = str(sc) + + progInfo = getProgram(db,progCode) + if progCode: + if not progInfo: + print(f"Invalid program code {progCode}") + exit() + + strmInfo = getStream(db,strmCode) + if strmCode: + if not strmInfo: + print(f"Invalid program code {strmCode}") + exit() + + print(f"{stuInfo[0]} {stuInfo[1]}, {stuInfo[2]}") + print(f" {progInfo[0]} {progInfo[1]}") + print(f" {strmCode} {strmInfo[0]}"); + + # Our general strategy is threefold: + # 1. Generate a data structure containing all the requirements of the degree + # 2. Iterate and print all completed subjects, while removing requirements + # from (1) if fulfilled. + # 3. Print the remainder of (1), or ready to graduate if empty. + requirements = list(getRequirements(progCode, strmCode, db)) + requirements = [list(a) for a in requirements] + + print("\nCompleted:") + + total_uoc = 0 + for (CourseCode, Term, SubjectTitle, Mark, Grade, UOC) in getStudentCourses(db, zid): + + remove_requirement_str = maybeRemoveRequirement(requirements, CourseCode, UOC) if doesGradeAddUOC(Grade) else None + mark_str = Mark if Mark != None else "-" + + total_uoc += UOC if doesGradeAddUOC(Grade) and remove_requirement_str != None else 0 + + uoc_str = " " + getGradeString(Grade) if getGradeString(Grade) != "Xuoc" else f"{UOC:2d}uoc" + if remove_requirement_str != None: + uoc_str = uoc_str + f" towards {remove_requirement_str}" + elif doesGradeAddUOC(Grade) == False: + uoc_str = uoc_str + " does not count" + else: + uoc_str = " 0uoc does not satisfy any rule" + + print(f"{CourseCode} {Term} {SubjectTitle :<32s}{mark_str :>3} {Grade :>2s} {uoc_str}") + + print(f"UOC = {total_uoc} so far") + + if len(requirements) > 0: + print("\nRemaining to complete degree:") + printAcademicRequirements(requirements, db) + else: + print("Eligible to graduate") + +except Exception as err: + print("DB error: ", err) +finally: + if db: + db.close() diff --git a/comp3311/2/rules b/comp3311/2/rules new file mode 100755 index 0000000..7f32a99 --- /dev/null +++ b/comp3311/2/rules @@ -0,0 +1,107 @@ +#!/usr/bin/python3 +# COMP3311 21T3 Ass2 ... print list of rules for a program or stream + +import sys +import psycopg2 +import re +from helpers import * + +# define any local helper functions here + +# Requires an array with with: name, min_req, max_req, type, definition, defby +def printAcademicRequirements(rules, db): + print("Academic Requirements:") + for (name, min_req, max_req, type, definition, defby) in rules: + def_count = str.count(definition, ",") + + # There is some weirdly specific behaviour to emulate when printing. While + # this might pass the tests, it is unlikely to continue doing so. + # According to our lecturer we won't be tested on edge cases, so I can only + # hope this is enough. + if type == "DS": + print(f"{min_req} stream(s) from {name}") + printStreams(definition, db) + + elif type == "CC" or type == "PE": + print(getCountString(def_count, min_req, max_req, name)) + printSubjects(definition, defby, db) + + elif type == "GE": + print(f"{min_req} UOC of {name}") + + elif type == "FE": + print(f"at least {min_req} UOC of Free Electives") + + else: + print(f"UNEXPECTED {name}, {min_req}, {max_req}, {type}, {defby}") + + +### set up some globals + +usage = f"Usage: {sys.argv[0]} (ProgramCode|StreamCode)" +db = None + +### process command-line args + +argc = len(sys.argv) +if argc < 2: + print(usage) + exit(1) +code = sys.argv[1] +if len(code) == 4: + codeOf = "program" +elif len(code) == 6: + codeOf = "stream" + +try: + db = psycopg2.connect("dbname=mymyunsw") + if codeOf == "program": + progInfo = getProgram(db,code) + if not progInfo: + print(f"Invalid program code {code}") + exit() + + print(f"{progInfo[0]} {progInfo[1]}, {progInfo[2]} UOC, {progInfo[3] / 12.0} years") + print(f"- offered by {progInfo[4]}"); + + cur = db.cursor() + query = """ + SELECT R.name, R.min_req, R.max_req, R.type, AOG.definition, AOG.defby + FROM Program_Rules as PR, Rules as R, Academic_Object_Groups AS AOG + WHERE PR.program = %s + AND R.id = PR.rule + AND AOG.id = R.ao_group + """ + cur.execute(query, [code]) + printAcademicRequirements(cur.fetchall(), db) + cur.close() + + + elif codeOf == "stream": + strmInfo = getStream(db,code) + if not strmInfo: + print(f"Invalid stream code {code}") + exit() + + print(f"{code} {strmInfo[0]}"); + print(f"- offered by {strmInfo[1]}") + + cur = db.cursor() + query = """ + SELECT R.name, R.min_req, R.max_req, R.type, AOG.definition, AOG.defby + FROM Streams AS S, Stream_Rules AS SR, Rules AS R, Academic_Object_groups AS AOG + WHERE S.code = %s + AND SR.stream = S.id + AND SR.rule = R.id + AND AOG.id = R.ao_group + """ + + cur.execute(query, [code]) + printAcademicRequirements(cur.fetchall(), db) + cur.close(); + +except Exception as err: + print(err) +finally: + if db: + db.close() diff --git a/comp3311/2/trans b/comp3311/2/trans new file mode 100755 index 0000000..f530676 --- /dev/null +++ b/comp3311/2/trans @@ -0,0 +1,63 @@ +#!/usr/bin/python3 +# COMP3311 21T3 Ass2 ... print a transcript for a given student + +import sys +import psycopg2 +import re +from helpers import getStudent, getStudentCourses, doesGradeAddRule +from helpers import doesGradeAddUOC, doesGradeAddWAM, getGradeString + +# define any local helper functions here + +### set up some globals + +usage = f"Usage: {sys.argv[0]} zID" +db = None + +### process command-line args + +argc = len(sys.argv) +if argc < 2: + print(usage) + exit(1) +zid = sys.argv[1] +if zid[0] == 'z': + zid = zid[1:8] +digits = re.compile("^\d{7}$") +if not digits.match(zid): + print(f"Invalid student ID {zid}") + exit(1) + +# manipulate database + +try: + db = psycopg2.connect("dbname=mymyunsw") + stuInfo = getStudent(db,zid) + if not stuInfo: + print(f"Invalid student ID {zid}") + exit() + + student = getStudent(db, zid) + print(f"{zid} {student[1]}, {student[2]}") + + total_uoc = 0; + total_wam_num = 0; + total_wam_den = 0; + for (CourseCode, Term, SubjectTitle, Mark, Grade, UOC) in getStudentCourses(db, zid): + total_uoc += UOC if doesGradeAddUOC(Grade) else 0 + total_wam_num += UOC * (Mark if Mark != None else 0) + total_wam_den += UOC if doesGradeAddWAM(Grade) else 0 + + mark_str = Mark if Mark != None else "-" + uoc_str = " " + getGradeString(Grade) if getGradeString(Grade) != "Xuoc" else f"{UOC:2d}uoc" + print(f"{CourseCode} {Term} {SubjectTitle :<32s}{mark_str :>3} {Grade :>2s} {uoc_str}") + + # Avoid division by zero. + print(f"UOC = {total_uoc}, WAM = {total_wam_num / total_wam_den if total_wam_den != 0 else 0 :.1f}"); + +except Exception as err: + print("DB error: ", err) +finally: + if db: + db.close() + -- cgit v1.2.3