mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-11-08 21:21:06 +00:00
560 lines
20 KiB
Python
560 lines
20 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/
|
|
"""
|
|
|
|
# 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"<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("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"<PARTSTD {self.repr()}>"
|
|
|
|
|
|
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"<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 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.customInit(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"<GREXP {self.repr()}>"
|
|
|
|
|
|
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"<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)
|
|
|
|
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"<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 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, group_experiment, assistant):
|
|
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"<APPT {self.repr()}>"
|
|
|
|
|
|
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"<PART {self.repr()}>"
|
|
|
|
|
|
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):
|
|
if label not in ("WS", "SS"):
|
|
raise DataBaseException("The semester label has to be WS or SS!")
|
|
|
|
return Semester(label, year)
|
|
|
|
def customInit(label, year, oldSemester, transferParts, transferAssistants):
|
|
semester = Semester.customInit(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"])
|
|
|
|
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"<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)
|
|
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"<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)
|
|
|
|
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()}>"
|