""" See the file DB.drawio for the design of the database. It can be opened in the internet browser with the website: https://app.diagrams.net For more information about the implementation, see the part to Models in the documentation of Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/ """ # Imports from flask import flash, has_request_context from flask_security import current_user from flask_security.models.fsqla_v2 import FsRoleMixin, FsUserMixin from decimal import Decimal, ROUND_HALF_UP # Importing the database instance from advlabdb import db from advlabdb.configUtils import getConfig from advlabdb.exceptions import DataBaseException def roundHalfUpToInt(number): return int(Decimal(number).quantize(Decimal(0), rounding=ROUND_HALF_UP)) class Student(db.Model): id = db.Column(db.Integer, primary_key=True) student_number = db.Column(db.Integer, db.CheckConstraint("student_number > -1"), nullable=False, unique=True) first_name = db.Column(db.String(100), nullable=False) last_name = db.Column(db.String(100), nullable=False) uni_email = db.Column(db.String(200), nullable=False, unique=True) contact_email = db.Column(db.String(200), nullable=True, unique=True) bachelor_thesis = db.Column(db.String, nullable=True) bachelor_thesis_work_group = db.Column(db.String, nullable=True) note = db.Column(db.Text, nullable=True) part_students = db.relationship("PartStudent", backref="student", lazy=True) def repr(self): return f"{self.first_name} {self.last_name}" def __repr__(self): return f"" class PartStudent(db.Model): # A student doing a specific part id = db.Column(db.Integer, primary_key=True) final_part_mark = db.Column( db.Integer, db.CheckConstraint("final_part_mark > -1"), db.CheckConstraint("final_part_mark < 16"), nullable=True, ) student_id = db.Column(db.Integer, db.ForeignKey("student.id"), nullable=False) part_id = db.Column(db.Integer, db.ForeignKey("part.id"), nullable=False) group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=True) experiment_marks = db.relationship("ExperimentMark", backref="part_student", lazy=True) __table_args__ = ( db.UniqueConstraint(student_id, part_id), db.UniqueConstraint(student_id, group_id), ) def check(group, part): if group and group.program != part.program: raise DataBaseException( f"Group's program {group.program} and student part's program {part.program} do not match!" ) def customInit(student, part, group=None): PartStudent.check(group, part) return PartStudent(student=student, part=part, group=group) def customUpdate(self, group, final_part_mark): Part.check(group, self.part) self.group = group self.final_part_mark = final_part_mark def checkThenSetFinalPartMark(self): finalWeightingSum = 0 finalMark = 0 groupExperiments = [] for experimentMark in self.experiment_marks: if not (experimentMark.oral_mark and experimentMark.protocol_mark): return groupExperiment = experimentMark.group_experiment groupExperiments.append(groupExperiment) experiment = groupExperiment.semester_experiment.experiment finalWeighting = experiment.final_weighting finalWeightingSum += finalWeighting finalMark += finalWeighting * ( experiment.oral_weighting * experimentMark.oral_mark + experiment.protocol_weighting * experimentMark.protocol_mark ) if set(groupExperiments) != set(self.group.group_experiments): flash(f"{self} does not have an experiment mark for every group experiment in his group!", "warning") return oldFinalPartMark = self.final_part_mark try: self.final_part_mark = roundHalfUpToInt(finalMark / finalWeightingSum) db.session.commit() except Exception as ex: flash(str(ex), "error") self.session.rollback() else: if current_user.has_role("admin"): if oldFinalPartMark != self.final_part_mark: category = "danger" if oldFinalPartMark and oldFinalPartMark > self.final_part_mark else "info" flash( f"Final part mark changed for {self} from {oldFinalPartMark} to {self.final_part_mark}.", category, ) else: flash(f"Final part mark did not change for {self} from {oldFinalPartMark}.", "warning") def repr(self): return f"{self.student.repr()} {self.part.repr()}" def __repr__(self): return f"" class Group(db.Model): id = db.Column(db.Integer, primary_key=True) number = db.Column(db.Integer, db.CheckConstraint("number > 0"), nullable=False) semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False) program_id = db.Column(db.Integer, db.ForeignKey("program.id"), nullable=False) part_students = db.relationship("PartStudent", backref="group", lazy=True) group_experiments = db.relationship("GroupExperiment", backref="group", lazy=True) __table_args__ = (db.UniqueConstraint(number, semester_id, program_id),) def check(part_students, program=None): commonProgram = part_students[0].part.program if program and program != commonProgram: raise DataBaseException("Group's program and students' program mismatch!") for partStudent in part_students[1:]: if partStudent.part.program != commonProgram: raise DataBaseException(f"Part Students {part_students} are not in the same program!") def customInit(part_students): Group.check(part_students) semester = part_students[0].part.semester program = part_students[0].part.program groupWithHighestNumber = ( Group.query.filter(Group.semester == semester, Group.program == program) .order_by(Group.number.desc()) .first() ) lastTakenGroupNumber = (groupWithHighestNumber and groupWithHighestNumber.number) or 0 return Group( program=program, number=lastTakenGroupNumber + 1, part_students=part_students, semester=semester, ) def customUpdate(self, part_students): Group.check(part_students, self.program) self.part_students = part_students def repr(self): return f"{self.number} {self.program.repr()} {self.semester.repr()}" def __repr__(self): return f"" class GroupExperiment(db.Model): # An experiment specified to a group id = db.Column(db.Integer, primary_key=True) semester_experiment_id = db.Column(db.Integer, db.ForeignKey("semester_experiment.id"), nullable=False) group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False) appointments = db.relationship("Appointment", backref="group_experiment", lazy=True) experiment_marks = db.relationship("ExperimentMark", backref="group_experiment", lazy=True) __table_args__ = (db.UniqueConstraint(semester_experiment_id, group_id),) def customInit(semester_experiment, group): for partStudent in group.part_students: student = partStudent.student for partStudent in student.part_students: for experimentMark in partStudent.experiment_marks: if ( (experimentMark.oral_mark or experimentMark.protocol_mark) and experimentMark.group_experiment.semester_experiment.experiment == semester_experiment.experiment ): raise Exception( f"{student} has already done {semester_experiment.experiment} in {partStudent.part} and had {experimentMark}!" ) groupExperiment = GroupExperiment(semester_experiment=semester_experiment, group=group) for partStudent in group.part_students: db.session.add(ExperimentMark(part_student=partStudent, group_experiment=groupExperiment)) return groupExperiment def repr(self): return f"SemExp {self.semester_experiment.repr()}; Gr {self.group.repr()}" def __repr__(self): return f"" class Experiment(db.Model): id = db.Column(db.Integer, primary_key=True) number = db.Column(db.Integer, db.CheckConstraint("number > 0"), nullable=False) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text, nullable=True) wiki_link = db.Column(db.String(300), nullable=True) room = db.Column(db.String(100), nullable=False) building = db.Column(db.String(100), nullable=False) responsibility = db.Column(db.String(200), nullable=True) duration_in_days = db.Column(db.Integer, db.CheckConstraint("duration_in_days > -1"), nullable=False) active = db.Column(db.Boolean, default=True, nullable=False) oral_weighting = db.Column( db.Float, db.CheckConstraint("oral_weighting >= 0"), db.CheckConstraint("oral_weighting <= 1"), default=0.5, nullable=False, ) protocol_weighting = db.Column( db.Float, db.CheckConstraint("protocol_weighting >= 0"), db.CheckConstraint("protocol_weighting <= 1"), default=0.5, nullable=False, ) final_weighting = db.Column( db.Float, db.CheckConstraint("final_weighting >= 0"), db.CheckConstraint("final_weighting <= 1"), nullable=False ) program_id = db.Column(db.Integer, db.ForeignKey("program.id"), nullable=False) semester_experiments = db.relationship("SemesterExperiment", backref="experiment", lazy=True) __table_args__ = (db.UniqueConstraint(number, program_id),) def checkWeightings(self, roundWeightings=False): roundedOralWeighting = round(self.oral_weighting, 2) roundedProtocolWeighting = round(self.protocol_weighting, 2) weightingSum = round(roundedOralWeighting + roundedProtocolWeighting, 2) if weightingSum != 1: raise DataBaseException( f"Oral and protocol weighting (rounded to 2 decimal digits) sum to {weightingSum} and not 1.00!" ) if roundWeightings: self.oral_weighting = roundedOralWeighting self.protocol_weighting = roundedProtocolWeighting def repr(self): return f"{self.number} {self.program.repr()}" def __repr__(self): return f"" # Helper table for the many to many relationship between Assistant and SemesterExperiment experiment_assistant = db.Table( "experiment_assistant", db.Column("semester_experiment_id", db.Integer, db.ForeignKey("semester_experiment.id"), primary_key=True), db.Column("assistant_id", db.Integer, db.ForeignKey("assistant.id"), primary_key=True), ) class SemesterExperiment(db.Model): # An experiment in a specific semester id = db.Column(db.Integer, primary_key=True) experiment_id = db.Column(db.Integer, db.ForeignKey("experiment.id"), nullable=False) semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False) assistants = db.relationship( "Assistant", secondary=experiment_assistant, lazy=True, backref=db.backref("semester_experiments", lazy=True) ) group_experiments = db.relationship("GroupExperiment", backref="semester_experiment", lazy=True) __table_args__ = (db.UniqueConstraint(experiment_id, semester_id),) def repr(self): return f"{self.experiment.repr()} {self.semester.repr()}" def __repr__(self): return f"" class Assistant(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, unique=True) appointments = db.relationship("Appointment", backref="assistant", lazy=True) experiment_marks = db.relationship("ExperimentMark", backref="assistant", lazy=True) def repr(self): return f"{self.user.first_name} {self.user.last_name}" def __repr__(self): return f"" class Appointment(db.Model): id = db.Column(db.Integer, primary_key=True) date = db.Column(db.Date, nullable=False) # To be specified with the python package "datetime" special = db.Column(db.Boolean, default=False, nullable=False) # In the break or not group_experiment_id = db.Column(db.Integer, db.ForeignKey("group_experiment.id"), nullable=False) assistant_id = db.Column(db.Integer, db.ForeignKey("assistant.id"), nullable=False) def checkAndGetAssistant(assistant, groupExperiment): semesterExperiment = groupExperiment.semester_experiment semesterExperimentAssistants = semesterExperiment.assistants if not semesterExperimentAssistants: raise Exception(f"{semesterExperiment} does not have assistants yet!") if assistant: if assistant not in semesterExperimentAssistants: raise Exception(f"{assistant} not responsible for {semesterExperiment}!") else: if len(semesterExperimentAssistants) == 1: assistant = semesterExperimentAssistants[0] else: raise Exception( f"Experiment {semesterExperiment} has more than one assistant. You have to assign one of these assistants: {semesterExperimentAssistants}" ) return assistant def customInit(date, special, assistant, group_experiment): assistant = Appointment.checkAndGetAssistant(assistant, group_experiment) return Appointment( date=date, special=special, group_experiment=group_experiment, assistant=assistant, ) def customUpdate(self, date, special, assistant, group_experiment): assistant = Appointment.checkAndGetAssistant(assistant, group_experiment) self.date = date self.special = special self.assistant = assistant self.group_experiment = group_experiment def repr(self): return f"{self.date} {self.group_experiment.repr()}" def __repr__(self): return f"" class Part(db.Model): id = db.Column(db.Integer, primary_key=True) number = db.Column(db.Integer, db.CheckConstraint("number > 0"), nullable=False) program_id = db.Column(db.Integer, db.ForeignKey("program.id"), nullable=False) semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False) part_students = db.relationship("PartStudent", backref="part", lazy=True) __table_args__ = (db.UniqueConstraint(program_id, number, semester_id),) def repr(self): return f"{self.program.repr()}{self.number} {self.semester.repr()}" def __repr__(self): return f"" class Semester(db.Model): id = db.Column(db.Integer, primary_key=True) label = db.Column(db.String(10), nullable=False) year = db.Column(db.Integer, db.CheckConstraint("year > 0"), db.CheckConstraint("year < 100"), nullable=False) parts = db.relationship("Part", backref="semester", lazy=True) semester_experiments = db.relationship("SemesterExperiment", backref="semester", lazy=True) active_users = db.relationship("User", backref="active_semester", lazy=True) groups = db.relationship("Group", backref="semester", lazy=True) __table_args__ = (db.UniqueConstraint(label, year),) def customInit(label, year, oldSemester, transferParts, transferAssistants): if label not in ("WS", "SS"): raise DataBaseException("The semester label has to be WS or SS!") semester = Semester(label=label, year=year) if transferParts: semester.transferPartsFrom(oldSemester) for experiment in Experiment.query.filter(Experiment.active == True): newSemesterExperiment = SemesterExperiment(experiment=experiment, semester=semester) if transferAssistants: for oldSemesterExperiment in oldSemester.semester_experiments: if oldSemesterExperiment.experiment == experiment: newSemesterExperiment.assistants = oldSemesterExperiment.assistants db.session.add(newSemesterExperiment) return semester def transferPartsFrom(self, oldSemester): for part in oldSemester.parts: db.session.add(Part(program=part.program, number=part.number, semester=self)) def repr(self): return f"{self.label}{self.year}" def __repr__(self): return f"" class ExperimentMark(db.Model): # A mark for a student after a specific experiment def final_experiment_mark_update(context): params = context.get_current_parameters() experimentMark = ExperimentMark.query.get(params["experiment_mark_id"]) experiment = experimentMark.group_experiment.semester_experiment.experiment oral_mark = params.get("oral_mark") or experimentMark.oral_mark protocol_mark = params.get("protocol_mark") or experimentMark.protocol_mark if not (oral_mark and protocol_mark): return else: return roundHalfUpToInt( experiment.oral_weighting * oral_mark + experiment.protocol_weighting * protocol_mark ) id = db.Column(db.Integer, primary_key=True) oral_mark = db.Column( db.Integer, db.CheckConstraint("oral_mark > -1"), db.CheckConstraint("oral_mark < 16"), nullable=True, ) protocol_mark = db.Column( db.Integer, db.CheckConstraint("protocol_mark > -1"), db.CheckConstraint("protocol_mark < 16"), nullable=True, ) final_experiment_mark = db.Column( db.Integer, db.CheckConstraint("protocol_mark > -1"), db.CheckConstraint("protocol_mark < 16"), onupdate=final_experiment_mark_update, nullable=True, ) edited_by_admin = db.Column(db.Boolean, default=False, nullable=False) part_student_id = db.Column(db.Integer, db.ForeignKey("part_student.id"), nullable=False) group_experiment_id = db.Column(db.Integer, db.ForeignKey("group_experiment.id"), nullable=False) assistant_id = db.Column( db.Integer, db.ForeignKey("assistant.id"), nullable=True ) # The assistant who gave the mark __table_args__ = (db.UniqueConstraint(part_student_id, group_experiment_id),) def customInit(part_student, group_experiment): if not part_student.group: raise DataBaseException("The part student does not have a group yet!") else: if group_experiment not in part_student.group.group_experiments: raise DataBaseException("The group of the part student does not have the given group experiment!") return ExperimentMark(part_student=part_student, group_experiment=group_experiment) def repr(self): return f"Mark {self.final_experiment_mark}; Exp {self.group_experiment.semester_experiment.repr()}" def __repr__(self): return f"" class User(db.Model, FsUserMixin): first_name = db.Column(db.String(100), nullable=False) last_name = db.Column(db.String(100), nullable=False) phone_number = db.Column(db.String(50), nullable=True) mobile_phone_number = db.Column(db.String(50), nullable=True) room = db.Column(db.String(100), nullable=True) building = db.Column(db.String(100), nullable=True) active_semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=True) assistant = db.relationship("Assistant", backref="user", lazy=True, uselist=False) def repr(self): return f"{self.first_name} {self.last_name}" def __repr__(self): return f"" class Role(db.Model, FsRoleMixin): def repr(self): return f"{self.name}" def __repr__(self): return f"" class Program(db.Model): id = db.Column(db.Integer, primary_key=True) label = db.Column(db.String(25), nullable=False) parts = db.relationship("Part", backref="program", lazy=True) experiments = db.relationship("Experiment", backref="program", lazy=True) groups = db.relationship("Group", backref="program", lazy=True) def repr(self): return f"{self.label}" def __repr__(self): return f""