1
0
Fork 0
mirror of https://codeberg.org/Mo8it/AdvLabDB.git synced 2024-09-19 18:31:16 +00:00
AdvLabDB/advlabdb/models.py

582 lines
21 KiB
Python

"""
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/
"""
from decimal import ROUND_HALF_UP, Decimal
from flask import flash
from flask_security import current_user
from flask_security.models.fsqla_v2 import FsRoleMixin, FsUserMixin
from . import db
from .configUtils import getConfig
from .exceptions import DataBaseException
MIN_MARK = 0
MAX_MARK = 15
MIN_YEAR = 22
MAX_YEAR = 99
MIN_STUDENT_NUMBER = 0
MIN_EXPERIMENT_NUMBER = 1
MIN_GROUP_NUMBER = 1
MIN_DURATION_IN_DAYS = 1
MIN_PART_NUMBER = 1
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(f"student_number >= {MIN_STUDENT_NUMBER}"), 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"<STD {self.repr()}>"
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(f"final_part_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
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(part, group):
if group is not None and group.program != part.program:
raise DataBaseException(
f"Group's program {group.program} and student part's program {part.program} do not match!"
)
def __init__(self, student, part, group=None):
PartStudent.check(part, group)
super().__init__(student=student, part=part, group=group)
def checkThenSetFinalPartMark(self):
finalWeightingSum = 0
finalMark = 0
groupExperiments = []
for experimentMark in self.experiment_marks:
if None in (experimentMark.oral_mark, experimentMark.protocol_mark):
# Not all marks are set!
return
groupExperiment = experimentMark.group_experiment
groupExperiments.append(groupExperiment)
semesterExperiment = groupExperiment.semester_experiment
finalWeighting = semesterExperiment.final_weighting
finalWeightingSum += finalWeighting
finalMark += finalWeighting * experimentMark.final_experiment_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")
db.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"<PARTSTD {self.repr()}>"
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.Integer, db.CheckConstraint(f"number >= {MIN_GROUP_NUMBER}"), 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()
)
if groupWithHighestNumber is not None:
number = groupWithHighestNumber.number + 1
else:
number = 1
return Group(
program=program,
number=number,
part_students=part_students,
semester=semester,
)
def repr(self):
return f"{self.number} {self.program.repr()} {self.semester.repr()}"
def __repr__(self):
return f"<GR {self.repr()}>"
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 check(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.group_experiment.semester_experiment.experiment == semester_experiment.experiment:
raise DataBaseException(
f"{student} has already done {semester_experiment.experiment} in {partStudent.part} and had {experimentMark}!"
)
def __init__(self, semester_experiment, group):
GroupExperiment.check(semester_experiment, group)
super().__init__(semester_experiment=semester_experiment, group=group)
for partStudent in group.part_students:
db.session.add(ExperimentMark(part_student=partStudent, group_experiment=self))
def repr(self):
return f"SemExp {self.semester_experiment.repr()}; Gr {self.group.repr()}"
def __repr__(self):
return f"<GREXP {self.repr()}>"
class Experiment(db.Model):
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.Integer, db.CheckConstraint(f"number >= {MIN_EXPERIMENT_NUMBER}"), 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)
building = db.Column(db.String(100), nullable=False)
room = db.Column(db.String(100), nullable=False)
responsibility = db.Column(db.String(200), nullable=True)
duration_in_days = db.Column(
db.Integer, db.CheckConstraint(f"duration_in_days >= {MIN_DURATION_IN_DAYS}"), nullable=False
)
active = db.Column(db.Boolean, default=True, 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 repr(self):
return f"{self.number} {self.program.repr()}"
def __repr__(self):
return f"<EXP {self.repr()}>"
# 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)
oral_weighting = db.Column(
db.Float,
db.CheckConstraint("oral_weighting BETWEEN 0 AND 1"),
default=0.5,
nullable=False,
)
protocol_weighting = db.Column(
db.Float,
db.CheckConstraint("protocol_weighting BETWEEN 0 AND 1"),
default=0.5,
nullable=False,
)
final_weighting = db.Column(
db.Float,
db.CheckConstraint("final_weighting BETWEEN 0 AND 1"),
default=1.0,
nullable=False,
)
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 checkAndRoundWeightings(self):
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 weightings (rounded to 2 decimal digits) sum to {weightingSum} and not 1.00!"
)
self.oral_weighting = roundedOralWeighting
self.protocol_weighting = roundedProtocolWeighting
def updateFinalExperimentAndPartMarks(self):
for groupExperiment in self.group_experiments:
for experimentMark in groupExperiment.experiment_marks:
experimentMark.updateFinalExperimentMark()
for groupExperiment in self.group_experiments:
for partStudent in groupExperiment.group.part_students:
partStudent.checkThenSetFinalPartMark()
def repr(self):
return f"{self.experiment.repr()} {self.semester.repr()}"
def __repr__(self):
return f"<SEMEXP {self.repr()}>"
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"<ASST: {self.repr()}>"
class Admin(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, unique=True)
experiment_marks = db.relationship("ExperimentMark", backref="admin", lazy=True)
def repr(self):
return f"{self.user.first_name} {self.user.last_name}"
def __repr__(self):
return f"<ADMIN: {self.repr()}>"
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(groupExperiment, assistant=None):
semesterExperiment = groupExperiment.semester_experiment
semesterExperimentAssistants = semesterExperiment.assistants
if not semesterExperimentAssistants:
raise DataBaseException(f"{semesterExperiment} does not have assistants yet!")
if assistant is not None:
if assistant not in semesterExperimentAssistants:
raise DataBaseException(f"{assistant} not responsible for {semesterExperiment}!")
else:
if len(semesterExperimentAssistants) == 1:
assistant = semesterExperimentAssistants[0]
else:
raise DataBaseException(
f"Experiment {semesterExperiment} has more than one assistant. You have to assign one of these assistants: {semesterExperimentAssistants}"
)
return assistant
def __init__(self, date, special, group_experiment, assistant=None):
assistant = Appointment.checkAndGetAssistant(group_experiment, assistant)
super().__init__(
date=date,
special=special,
group_experiment=group_experiment,
assistant=assistant,
)
def repr(self):
return f"{self.date} {self.group_experiment.repr()}"
def __repr__(self):
return f"<APPT {self.repr()}>"
class Part(db.Model):
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.Integer, db.CheckConstraint(f"number >= {MIN_PART_NUMBER}"), 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"<PART {self.repr()}>"
class Semester(db.Model):
id = db.Column(db.Integer, primary_key=True)
label = db.Column(db.String(10), db.CheckConstraint("label IN ('WS', 'SS')"), nullable=False)
year = db.Column(db.Integer, db.CheckConstraint(f"year BETWEEN {MIN_YEAR} AND {MAX_YEAR}"), 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 initFromOldSemester(label, year, oldSemester, transferParts, transferAssistants):
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"<SEM {self.repr()}>"
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"])
oral_mark = params.get("oral_mark")
if oral_mark is None:
oral_mark = experimentMark.oral_mark
if oral_mark is None:
return
protocol_mark = params.get("protocol_mark")
if protocol_mark is None:
protocol_mark = experimentMark.protocol_mark
if protocol_mark is None:
return
semesterExperiment = experimentMark.group_experiment.semester_experiment
return roundHalfUpToInt(
semesterExperiment.oral_weighting * oral_mark + semesterExperiment.protocol_weighting * protocol_mark
)
id = db.Column(db.Integer, primary_key=True)
oral_mark = db.Column(
db.Integer,
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
nullable=True,
)
protocol_mark = db.Column(
db.Integer,
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
nullable=True,
)
final_experiment_mark = db.Column(
db.Integer,
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
onupdate=final_experiment_mark_update,
nullable=True,
)
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 last assistant who edited the mark
admin_id = db.Column(db.Integer, db.ForeignKey("admin.id"), nullable=True) # The last admin who edited the mark
__table_args__ = (db.UniqueConstraint(part_student_id, group_experiment_id),)
def check(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!")
def __init__(self, part_student, group_experiment):
ExperimentMark(part_student, group_experiment)
super().__init__(part_student=part_student, group_experiment=group_experiment)
def updateFinalExperimentMark(self):
if None not in (self.oral_mark, self.protocol_mark):
semesterExperiment = self.group_experiment.semester_experiment
self.final_experiment_mark = roundHalfUpToInt(
semesterExperiment.oral_weighting * self.oral_mark
+ semesterExperiment.protocol_weighting * self.protocol_mark
)
def repr(self):
return f"Mark {self.final_experiment_mark}; Exp {self.group_experiment.semester_experiment.repr()}"
def __repr__(self):
return f"<EXPMARK {self.repr()}>"
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)
building = db.Column(db.String(100), nullable=True)
room = db.Column(db.String(100), nullable=True)
active_semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=True)
admin = db.relationship("Admin", backref="user", lazy=False, uselist=False)
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"<USR {self.repr()}>"
class Role(db.Model, FsRoleMixin):
def repr(self):
return f"{self.name}"
def __repr__(self):
return f"<ROLE {self.repr()}>"
class Program(db.Model):
id = db.Column(db.Integer, primary_key=True)
label = db.Column(db.String(25), nullable=False, unique=True)
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"<PROG {self.repr()}>"