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

767 lines
28 KiB
Python
Raw Normal View History

2021-03-18 13:53:55 +00:00
"""
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/
"""
2022-02-13 18:58:05 +00:00
from decimal import ROUND_HALF_UP, Decimal
2021-09-11 18:47:48 +00:00
from flask import flash
2022-08-15 20:22:36 +00:00
from flask_login import current_user
2022-09-16 18:04:42 +00:00
from flask_security.models.fsqla_v3 import FsModels, FsRoleMixin, FsUserMixin
2022-08-09 12:46:48 +00:00
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func, select
2021-04-03 00:11:26 +00:00
2022-08-09 23:14:47 +00:00
from .exceptions import DatabaseException
2021-03-18 13:53:55 +00:00
2022-04-11 23:44:53 +00:00
MIN_MARK = 0
MAX_MARK = 15
2022-04-18 16:15:52 +00:00
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
2022-04-11 23:44:53 +00:00
2022-08-09 12:46:48 +00:00
db = SQLAlchemy()
# For Flask-Security-Too
2022-09-16 18:04:42 +00:00
FsModels.set_db_info(db)
2022-08-09 12:46:48 +00:00
def get_first(table):
return db.session.scalars(table.limit(1)).first()
2022-08-09 12:46:48 +00:00
2021-03-18 13:53:55 +00:00
2021-08-29 16:15:14 +00:00
def roundHalfUpToInt(number):
return int(Decimal(number).quantize(Decimal(0), rounding=ROUND_HALF_UP))
2021-03-18 13:53:55 +00:00
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-04-18 16:15:52 +00:00
student_number = db.Column(
db.Integer, db.CheckConstraint(f"student_number >= {MIN_STUDENT_NUMBER}"), nullable=False, unique=True
)
2021-03-18 13:53:55 +00:00
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)
2021-03-18 13:53:55 +00:00
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)
2022-05-16 15:16:42 +00:00
part_students = db.relationship("PartStudent", back_populates="student", lazy=True)
2021-03-18 13:53:55 +00:00
def __init__(self, uni_email, contact_email=None, **kwargs):
2022-09-12 17:08:02 +00:00
# Lower and strip uni email
uni_email = uni_email.strip().lower()
if contact_email is not None:
# Lower and strip contact email
contact_email = contact_email.strip().lower()
# Don't save contact_email if it is similar to uni_email
if contact_email == uni_email:
contact_email = None
super().__init__(uni_email=uni_email, contact_email=contact_email, **kwargs)
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.first_name} {self.last_name}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return self.str()
2021-03-18 13:53:55 +00:00
class PartStudent(db.Model):
# A student doing a specific part
id = db.Column(db.Integer, primary_key=True)
2021-07-11 12:27:00 +00:00
final_part_mark = db.Column(
db.Integer,
2022-05-15 17:06:28 +00:00
db.CheckConstraint(f"final_part_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
2021-07-11 12:27:00 +00:00
nullable=True,
)
2021-03-18 13:53:55 +00:00
student_id = db.Column(db.Integer, db.ForeignKey("student.id"), nullable=False)
2022-05-16 15:16:42 +00:00
student = db.relationship("Student", back_populates="part_students")
2021-03-18 13:53:55 +00:00
part_id = db.Column(db.Integer, db.ForeignKey("part.id"), nullable=False)
2022-05-16 15:16:42 +00:00
part = db.relationship("Part", back_populates="part_students")
2021-03-18 13:53:55 +00:00
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=True)
2022-05-16 15:16:42 +00:00
group = db.relationship("Group", back_populates="part_students")
2022-05-16 15:16:42 +00:00
experiment_marks = db.relationship("ExperimentMark", back_populates="part_student", lazy=True)
2021-03-18 13:53:55 +00:00
2021-07-11 01:21:54 +00:00
__table_args__ = (
db.UniqueConstraint(student_id, part_id),
db.UniqueConstraint(student_id, group_id),
)
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.student.str()} {self.part}"
def __str__(self):
return f"<{self.str()}>"
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-15 18:59:57 +00:00
def check(part, group):
2022-05-15 18:05:00 +00:00
if group is not None and group.program != part.program:
2022-08-09 23:14:47 +00:00
raise DatabaseException(
2021-07-14 02:24:04 +00:00
f"Group's program {group.program} and student part's program {part.program} do not match!"
)
2022-09-07 22:56:47 +00:00
def __init__(self, part, group=None, **kwargs):
2022-05-15 18:59:57 +00:00
PartStudent.check(part, group)
2021-07-14 02:24:04 +00:00
2022-09-07 22:56:47 +00:00
super().__init__(part=part, group=group, **kwargs)
2021-07-14 02:24:04 +00:00
def checkThenSetFinalPartMark(self):
2022-06-30 15:29:52 +00:00
"""
Return True if final_part_mark changed, False otherwise.
"""
finalWeightingSum = 0
2022-08-18 15:39:40 +00:00
finalMarkSum = 0
groupExperiments = []
for experimentMark in self.experiment_marks:
if None in (experimentMark.oral_mark, experimentMark.protocol_mark):
# Not all marks are set!
2022-06-30 15:29:52 +00:00
try:
self.final_part_mark = None
db.session.commit()
return True
except Exception as ex:
flash(str(ex), "error")
db.session.rollback()
return False
groupExperiment = experimentMark.group_experiment
groupExperiments.append(groupExperiment)
semesterExperiment = groupExperiment.semester_experiment
finalWeighting = semesterExperiment.final_weighting
finalWeightingSum += finalWeighting
2022-08-18 15:39:40 +00:00
# Not using final_experiment_mark to avoid rounding two times
finalMarkSum += finalWeighting * (
semesterExperiment.protocol_weighting * experimentMark.protocol_mark
+ semesterExperiment.oral_weighting * experimentMark.oral_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")
2022-06-30 15:29:52 +00:00
return False
oldFinalPartMark = self.final_part_mark
try:
2022-08-18 15:39:40 +00:00
self.final_part_mark = roundHalfUpToInt(finalMarkSum / finalWeightingSum)
db.session.commit()
except Exception as ex:
flash(str(ex), "error")
db.session.rollback()
2022-06-30 15:29:52 +00:00
return False
# Inform admin about what changed
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")
return True
2021-03-18 13:53:55 +00:00
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-04-18 16:43:55 +00:00
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)
2022-05-16 15:16:42 +00:00
semester = db.relationship("Semester", back_populates="groups")
program_id = db.Column(db.Integer, db.ForeignKey("program.id"), nullable=False)
2022-05-16 15:16:42 +00:00
program = db.relationship("Program", back_populates="groups")
2022-05-16 15:16:42 +00:00
part_students = db.relationship("PartStudent", back_populates="group", lazy=True)
group_experiments = db.relationship("GroupExperiment", back_populates="group", lazy=True)
2021-03-18 13:53:55 +00:00
__table_args__ = (db.UniqueConstraint(number, semester_id, program_id),)
2021-07-11 01:21:54 +00:00
2022-06-01 20:54:17 +00:00
def str_without_semester(self):
return f"{self.number} {self.program.str()}"
def str(self):
return f"{self.str_without_semester()} {self.semester.str()}"
def __str__(self):
return f"<{self.str()}>"
2022-07-02 22:48:05 +00:00
@staticmethod
2021-07-14 02:24:04 +00:00
def check(part_students, program=None):
commonProgram = part_students[0].part.program
2021-08-18 18:06:18 +00:00
2021-07-14 02:24:04 +00:00
if program and program != commonProgram:
2022-08-09 23:14:47 +00:00
raise DatabaseException("Group's program and students' program mismatch!")
2021-07-14 02:24:04 +00:00
for partStudent in part_students[1:]:
if partStudent.part.program != commonProgram:
2022-08-09 23:14:47 +00:00
raise DatabaseException(f"Part Students {part_students} are not in the same program!")
2022-07-02 22:48:05 +00:00
@staticmethod
2021-07-14 02:24:04 +00:00
def customInit(part_students):
Group.check(part_students)
semester = part_students[0].part.semester
program = part_students[0].part.program
2022-05-16 20:23:15 +00:00
highestGroupNumber = get_first(
select(Group.number)
.where(Group.semester == semester, Group.program == program)
.order_by(Group.number.desc())
2021-07-14 02:24:04 +00:00
)
2022-05-15 18:59:57 +00:00
2023-11-02 18:38:09 +00:00
number = highestGroupNumber + 1 if highestGroupNumber is not None else 1
2021-07-12 12:48:29 +00:00
return Group(
program=program,
2022-05-15 18:59:57 +00:00
number=number,
2021-07-14 02:24:04 +00:00
part_students=part_students,
semester=semester,
2021-07-12 12:48:29 +00:00
)
2021-03-18 13:53:55 +00:00
class GroupExperiment(db.Model):
# An experiment specified to a group
id = db.Column(db.Integer, primary_key=True)
2022-06-17 17:15:38 +00:00
note = db.Column(db.Text, nullable=True)
2022-06-27 23:17:22 +00:00
experiment_marks_missing = db.Column(db.Boolean, default=True, nullable=False)
semester_experiment_id = db.Column(db.Integer, db.ForeignKey("semester_experiment.id"), nullable=False)
2022-05-16 15:16:42 +00:00
semester_experiment = db.relationship("SemesterExperiment", back_populates="group_experiments")
2021-03-18 13:53:55 +00:00
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
2022-05-16 15:16:42 +00:00
group = db.relationship("Group", back_populates="group_experiments")
2022-05-16 15:16:42 +00:00
appointments = db.relationship("Appointment", back_populates="group_experiment", lazy=True)
experiment_marks = db.relationship("ExperimentMark", back_populates="group_experiment", lazy=True)
2021-03-18 13:53:55 +00:00
2021-07-11 01:21:54 +00:00
__table_args__ = (db.UniqueConstraint(semester_experiment_id, group_id),)
2022-06-01 20:54:17 +00:00
def str(self):
return f"<SemExp: {self.semester_experiment.str()} | Gr: {self.group.str()}>"
def __str__(self):
return self.str()
2022-08-15 20:22:36 +00:00
@staticmethod
2022-05-15 20:24:49 +00:00
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:
2022-08-09 23:14:47 +00:00
raise DatabaseException(
2021-07-11 01:21:54 +00:00
f"{student} has already done {semester_experiment.experiment} in {partStudent.part} and had {experimentMark}!"
)
2022-09-07 22:56:47 +00:00
def __init__(self, semester_experiment, group, **kwargs):
2022-05-15 20:24:49 +00:00
GroupExperiment.check(semester_experiment, group)
2022-09-07 22:56:47 +00:00
super().__init__(semester_experiment=semester_experiment, group=group, **kwargs)
2021-07-13 00:50:15 +00:00
for partStudent in group.part_students:
2022-05-15 19:07:00 +00:00
db.session.add(ExperimentMark(part_student=partStudent, group_experiment=self))
2022-06-27 22:57:59 +00:00
def update_experiment_marks_missing(self):
for experiment_mark in self.experiment_marks:
2022-06-27 23:13:35 +00:00
if experiment_mark.final_experiment_mark is None:
2022-06-27 23:17:22 +00:00
self.experiment_marks_missing = True
2022-06-27 22:57:59 +00:00
return
2022-06-27 23:17:22 +00:00
self.experiment_marks_missing = False
2022-06-27 22:57:59 +00:00
2021-03-18 13:53:55 +00:00
class Experiment(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-04-18 16:43:55 +00:00
number = db.Column(db.Integer, db.CheckConstraint(f"number >= {MIN_EXPERIMENT_NUMBER}"), nullable=False)
title = db.Column(db.String(200), nullable=False)
2021-03-18 13:53:55 +00:00
description = db.Column(db.Text, nullable=True)
wiki_link = db.Column(db.String(300), nullable=True)
2021-03-18 13:53:55 +00:00
building = db.Column(db.String(100), nullable=False)
2022-02-23 19:36:29 +00:00
room = db.Column(db.String(100), nullable=False)
2021-03-18 13:53:55 +00:00
responsibility = db.Column(db.String(200), nullable=True)
2022-04-18 16:15:52 +00:00
duration_in_days = db.Column(
db.Integer, db.CheckConstraint(f"duration_in_days >= {MIN_DURATION_IN_DAYS}"), nullable=False
)
2021-07-13 00:50:15 +00:00
active = db.Column(db.Boolean, default=True, nullable=False)
program_id = db.Column(db.Integer, db.ForeignKey("program.id"), nullable=False)
2022-05-16 15:16:42 +00:00
program = db.relationship("Program", back_populates="experiments")
2022-05-16 15:16:42 +00:00
semester_experiments = db.relationship("SemesterExperiment", back_populates="experiment", lazy=True)
2021-03-18 13:53:55 +00:00
__table_args__ = (db.UniqueConstraint(number, program_id),)
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.number} {self.program.str()}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return f"<{self.str()}>"
2021-03-18 13:53:55 +00:00
# Helper table for the many to many relationship between Assistant and SemesterExperiment
2021-06-02 21:43:41 +00:00
experiment_assistant = db.Table(
"experiment_assistant",
db.Column("semester_experiment_id", db.Integer, db.ForeignKey("semester_experiment.id"), primary_key=True),
2021-06-02 21:43:41 +00:00
db.Column("assistant_id", db.Integer, db.ForeignKey("assistant.id"), primary_key=True),
)
2021-03-18 13:53:55 +00:00
class SemesterExperiment(db.Model):
2021-07-11 01:21:54 +00:00
# An experiment in a specific semester
2021-03-18 13:53:55 +00:00
id = db.Column(db.Integer, primary_key=True)
oral_weighting = db.Column(
db.Float,
2022-05-15 17:06:28 +00:00
db.CheckConstraint("oral_weighting BETWEEN 0 AND 1"),
default=0.5,
nullable=False,
)
protocol_weighting = db.Column(
db.Float,
2022-05-15 17:06:28 +00:00
db.CheckConstraint("protocol_weighting BETWEEN 0 AND 1"),
default=0.5,
nullable=False,
)
final_weighting = db.Column(
db.Float,
2022-05-15 17:06:28 +00:00
db.CheckConstraint("final_weighting BETWEEN 0 AND 1"),
default=1.0,
nullable=False,
)
2021-03-18 13:53:55 +00:00
experiment_id = db.Column(db.Integer, db.ForeignKey("experiment.id"), nullable=False)
2022-05-16 15:16:42 +00:00
experiment = db.relationship("Experiment", back_populates="semester_experiments")
semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False)
2022-05-16 15:16:42 +00:00
semester = db.relationship("Semester", back_populates="semester_experiments")
2021-06-02 21:43:41 +00:00
assistants = db.relationship(
2022-05-16 15:16:42 +00:00
"Assistant", secondary=experiment_assistant, back_populates="semester_experiments", lazy=True
2021-06-02 21:43:41 +00:00
)
2022-05-16 15:16:42 +00:00
group_experiments = db.relationship("GroupExperiment", back_populates="semester_experiment", lazy=True)
2021-03-18 13:53:55 +00:00
2021-07-11 01:21:54 +00:00
__table_args__ = (db.UniqueConstraint(experiment_id, semester_id),)
2022-06-01 20:54:17 +00:00
def str_without_semester(self):
return f"{self.experiment.str()}"
def str(self):
return f"{self.str_without_semester()} {self.semester.str()}"
def __str__(self):
return f"<{self.str()}>"
def checkAndRoundWeightings(self):
roundedOralWeighting = round(self.oral_weighting, 2)
roundedProtocolWeighting = round(self.protocol_weighting, 2)
weightingSum = round(roundedOralWeighting + roundedProtocolWeighting, 2)
if weightingSum != 1:
2022-08-09 23:14:47 +00:00
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.update_final_experiment_mark()
for groupExperiment in self.group_experiments:
for partStudent in groupExperiment.group.part_students:
partStudent.checkThenSetFinalPartMark()
2021-03-18 13:53:55 +00:00
class Assistant(db.Model):
id = db.Column(db.Integer, primary_key=True)
2021-07-11 01:21:54 +00:00
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, unique=True)
2022-05-16 15:16:42 +00:00
user = db.relationship("User", back_populates="assistant")
2022-05-16 15:16:42 +00:00
semester_experiments = db.relationship(
"SemesterExperiment", secondary=experiment_assistant, back_populates="assistants", lazy=True
)
appointments = db.relationship("Appointment", back_populates="assistant", lazy=True)
experiment_marks = db.relationship("ExperimentMark", back_populates="assistant", lazy=True)
2021-03-18 13:53:55 +00:00
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.user}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return self.str()
2021-03-18 13:53:55 +00:00
2022-02-27 18:30:28 +00:00
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)
2022-05-16 15:16:42 +00:00
user = db.relationship("User", back_populates="admin")
2022-02-27 18:30:28 +00:00
2022-05-16 15:16:42 +00:00
experiment_marks = db.relationship("ExperimentMark", back_populates="admin", lazy=True)
2022-02-27 18:30:28 +00:00
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.user}"
2022-02-27 18:30:28 +00:00
2022-06-01 20:54:17 +00:00
def __str__(self):
return self.str()
2022-02-27 18:30:28 +00:00
2021-03-18 13:53:55 +00:00
class Appointment(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-06-20 01:06:33 +00:00
date = db.Column(
db.Date, db.CheckConstraint(f"date BETWEEN '20{MIN_YEAR}-01-01' AND '20{MAX_YEAR}-01-01'"), nullable=False
) # To be specified with the python package "datetime"
2021-07-13 00:50:15 +00:00
special = db.Column(db.Boolean, default=False, nullable=False) # In the break or not
2021-03-18 13:53:55 +00:00
group_experiment_id = db.Column(db.Integer, db.ForeignKey("group_experiment.id"), nullable=False)
2022-05-16 15:16:42 +00:00
group_experiment = db.relationship("GroupExperiment", back_populates="appointments")
2021-03-18 13:53:55 +00:00
assistant_id = db.Column(db.Integer, db.ForeignKey("assistant.id"), nullable=False)
2022-05-16 15:16:42 +00:00
assistant = db.relationship("Assistant", back_populates="appointments")
2021-03-18 13:53:55 +00:00
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.date} {self.group_experiment.str()}"
def __str__(self):
return f"<{self.str()}>"
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-15 20:24:49 +00:00
def checkAndGetAssistant(groupExperiment, assistant=None):
2021-07-12 12:48:29 +00:00
semesterExperiment = groupExperiment.semester_experiment
semesterExperimentAssistants = semesterExperiment.assistants
2022-05-17 00:31:09 +00:00
if semesterExperimentAssistants is None:
2022-08-09 23:14:47 +00:00
raise DatabaseException(f"{semesterExperiment} does not have assistants yet!")
2021-07-12 12:48:29 +00:00
2022-05-15 20:24:49 +00:00
if assistant is not None:
2021-07-12 12:48:29 +00:00
if assistant not in semesterExperimentAssistants:
2022-08-09 23:14:47 +00:00
raise DatabaseException(f"{assistant} is not responsible for {semesterExperiment}!")
2023-11-02 19:04:09 +00:00
elif len(semesterExperimentAssistants) == 1:
assistant = semesterExperimentAssistants[0]
2021-07-12 12:48:29 +00:00
else:
2023-11-02 19:04:09 +00:00
raise DatabaseException(
f"Experiment {semesterExperiment} has more than one assistant. You have to assign one of these assistants: {semesterExperimentAssistants}"
)
2021-07-12 12:48:29 +00:00
2021-07-12 14:42:11 +00:00
return assistant
2022-09-07 22:56:47 +00:00
def __init__(self, group_experiment, assistant=None, **kwargs):
2022-05-15 20:24:49 +00:00
assistant = Appointment.checkAndGetAssistant(group_experiment, assistant)
2021-07-12 14:42:11 +00:00
2022-09-07 22:56:47 +00:00
super().__init__(group_experiment=group_experiment, assistant=assistant, **kwargs)
2021-07-12 12:48:29 +00:00
2022-05-17 00:31:09 +00:00
def custom_update(self, date, special, group_experiment, assistant=None):
self.assistant = Appointment.checkAndGetAssistant(group_experiment, assistant)
self.date = date
self.special = special
self.group_experiment = group_experiment
2021-03-18 13:53:55 +00:00
class Part(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-04-18 16:43:55 +00:00
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)
2022-05-16 15:16:42 +00:00
program = db.relationship("Program", back_populates="parts")
2021-03-18 13:53:55 +00:00
semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False)
2022-05-16 15:16:42 +00:00
semester = db.relationship("Semester", back_populates="parts")
2022-05-16 15:16:42 +00:00
part_students = db.relationship("PartStudent", back_populates="part", lazy=True)
2021-03-18 13:53:55 +00:00
__table_args__ = (db.UniqueConstraint(program_id, number, semester_id),)
2021-07-11 01:21:54 +00:00
2022-06-01 20:54:17 +00:00
def str_without_semester(self):
return f"{self.program.str()}{self.number}"
def str(self):
return f"{self.str_without_semester()} {self.semester.str()}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return f"<{self.str()}>"
2021-04-24 11:38:03 +00:00
2021-03-18 13:53:55 +00:00
class Semester(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-05-15 17:06:28 +00:00
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)
2022-09-16 18:02:55 +00:00
# Assistants can not work in semesters that are done
done = db.Column(db.Boolean, default=False, nullable=False)
2022-05-16 15:16:42 +00:00
parts = db.relationship("Part", back_populates="semester", lazy=True)
semester_experiments = db.relationship("SemesterExperiment", back_populates="semester", lazy=True)
active_users = db.relationship("User", back_populates="active_semester", lazy=True)
groups = db.relationship("Group", back_populates="semester", lazy=True)
__table_args__ = (db.UniqueConstraint(label, year),)
2021-03-18 13:53:55 +00:00
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.label}{self.year}"
def __str__(self):
return self.str()
2022-09-07 22:56:47 +00:00
def __init__(self, label, year, **kwargs):
last_semester = Semester.lastSemester()
2023-11-02 18:38:09 +00:00
if last_semester is not None and (year < last_semester.year or (year == last_semester.year and label == "SS")):
raise DatabaseException(f"You can only create semesters later than the last semester {last_semester}!")
2022-09-07 22:56:47 +00:00
super().__init__(label=label, year=year, **kwargs)
2022-05-16 15:17:11 +00:00
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-15 18:05:00 +00:00
def initFromOldSemester(label, year, oldSemester, transferParts, transferAssistants):
2022-05-15 17:06:28 +00:00
semester = Semester(label=label, year=year)
if transferParts:
semester.transferPartsFrom(oldSemester)
for experiment in db.session.scalars(select(Experiment).where(Experiment.active == True)):
2021-07-15 22:44:26 +00:00
newSemesterExperiment = SemesterExperiment(experiment=experiment, semester=semester)
if transferAssistants:
for oldSemesterExperiment in oldSemester.semester_experiments:
if oldSemesterExperiment.experiment == experiment:
newSemesterExperiment.assistants = oldSemesterExperiment.assistants
2021-07-15 22:44:26 +00:00
db.session.add(newSemesterExperiment)
return semester
2021-07-01 17:43:59 +00:00
def transferPartsFrom(self, oldSemester):
for part in oldSemester.parts:
db.session.add(Part(program=part.program, number=part.number, semester=self))
2021-06-01 23:56:49 +00:00
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-29 19:47:23 +00:00
def sortedSemestersStartingWithNewest(limit=0):
# Inserting an older semester is not allowed!
# Therefore, the id is enough.
stmt = select(Semester).order_by(Semester.id.desc())
if limit > 0:
stmt = stmt.limit(limit)
return db.session.scalars(stmt)
2022-05-29 19:47:23 +00:00
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-29 19:47:23 +00:00
def lastSemester():
return Semester.sortedSemestersStartingWithNewest(limit=1).first()
def num_missing_experiment_marks(self):
return db.session.scalar(
select(func.count())
.select_from(ExperimentMark)
.join(GroupExperiment)
.join(SemesterExperiment)
.where(SemesterExperiment.semester == self)
.where(ExperimentMark.final_experiment_mark == None)
)
def set_done(self, next_semester=None):
set_next_semester = next_semester is not None
# Set also all previous semesters as done
for id in range(1, self.id + 1):
semester = db.session.get(Semester, id)
if semester == self or not semester.done:
num_missing_experiment_marks = self.num_missing_experiment_marks()
if num_missing_experiment_marks > 0:
flash(
f"Semester {semester} was set as done, but it has {num_missing_experiment_marks} missing experiment marks!",
"danger",
)
semester.done = True
if set_next_semester:
# Set active_semester to next_semester
users_in_semester_done = db.session.scalars(select(User).where(User.active_semester == semester))
for user in users_in_semester_done:
user.active_semester = next_semester
if user == current_user:
flash(f"Active semester changed to the next semester {next_semester}!", "warning")
2021-03-18 13:53:55 +00:00
class ExperimentMark(db.Model):
# A mark for a student after a specific experiment
2021-08-29 16:15:14 +00:00
2021-03-18 13:53:55 +00:00
id = db.Column(db.Integer, primary_key=True)
2021-07-11 12:27:00 +00:00
oral_mark = db.Column(
2021-08-16 21:30:30 +00:00
db.Integer,
2022-05-15 17:06:28 +00:00
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
2021-08-16 21:30:30 +00:00
nullable=True,
2021-07-11 12:27:00 +00:00
)
protocol_mark = db.Column(
2021-08-16 21:30:30 +00:00
db.Integer,
2022-05-15 17:06:28 +00:00
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
2021-08-16 21:30:30 +00:00
nullable=True,
2021-07-11 12:27:00 +00:00
)
2021-08-29 16:15:14 +00:00
final_experiment_mark = db.Column(
db.Integer,
2022-05-15 17:06:28 +00:00
db.CheckConstraint(f"oral_mark BETWEEN {MIN_MARK} AND {MAX_MARK}"),
2021-08-29 16:15:14 +00:00
nullable=True,
)
2021-03-18 13:53:55 +00:00
part_student_id = db.Column(db.Integer, db.ForeignKey("part_student.id"), nullable=False)
2022-05-16 15:16:42 +00:00
part_student = db.relationship("PartStudent", back_populates="experiment_marks")
2021-03-18 13:53:55 +00:00
group_experiment_id = db.Column(db.Integer, db.ForeignKey("group_experiment.id"), nullable=False)
2022-05-16 15:16:42 +00:00
group_experiment = db.relationship("GroupExperiment", back_populates="experiment_marks")
2021-06-02 21:43:41 +00:00
assistant_id = db.Column(
db.Integer, db.ForeignKey("assistant.id"), nullable=True
2022-02-27 18:30:28 +00:00
) # The last assistant who edited the mark
2022-05-16 15:16:42 +00:00
assistant = db.relationship("Assistant", back_populates="experiment_marks")
2022-02-27 18:30:28 +00:00
admin_id = db.Column(db.Integer, db.ForeignKey("admin.id"), nullable=True) # The last admin who edited the mark
2022-05-16 15:16:42 +00:00
admin = db.relationship("Admin", back_populates="experiment_marks")
2021-07-11 01:21:54 +00:00
__table_args__ = (db.UniqueConstraint(part_student_id, group_experiment_id),)
2022-06-01 20:54:17 +00:00
def str(self):
return (
f"<Final Exp Mark: {self.final_experiment_mark} | Exp: {self.group_experiment.semester_experiment.str()}>"
)
def __str__(self):
return self.str()
2022-07-02 22:48:05 +00:00
@staticmethod
2022-05-15 20:24:49 +00:00
def check(part_student, group_experiment):
2021-08-18 17:39:21 +00:00
if not part_student.group:
2022-08-09 23:14:47 +00:00
raise DatabaseException("The part student does not have a group yet!")
2023-11-02 18:24:14 +00:00
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!")
2021-08-18 17:39:21 +00:00
2022-09-07 22:56:47 +00:00
def __init__(self, part_student, group_experiment, **kwargs):
2022-05-15 20:38:49 +00:00
ExperimentMark.check(part_student, group_experiment)
2022-05-15 20:24:49 +00:00
2022-09-07 22:56:47 +00:00
super().__init__(part_student=part_student, group_experiment=group_experiment, **kwargs)
2021-08-18 17:39:21 +00:00
def update_final_experiment_mark(self):
"""
Return True if final_experiment_mark changed, False otherwise.
"""
old_final_experiment_mark = self.final_experiment_mark
oral_mark = self.oral_mark
protocol_mark = self.protocol_mark
if None in (oral_mark, protocol_mark):
self.final_experiment_mark = None
else:
2022-04-10 19:01:56 +00:00
semesterExperiment = self.group_experiment.semester_experiment
self.final_experiment_mark = roundHalfUpToInt(
semesterExperiment.oral_weighting * oral_mark + semesterExperiment.protocol_weighting * protocol_mark
2022-04-10 19:01:56 +00:00
)
final_experiment_mark_changed = self.final_experiment_mark != old_final_experiment_mark
return final_experiment_mark_changed
def set_oral_protocol_mark(self, oral_mark: int, protocol_mark: int, call_update_experiment_marks_missing=True):
"""
Return True if final_experiment_mark changed, False otherwise.
"""
self.oral_mark = oral_mark
self.protocol_mark = protocol_mark
final_experiment_mark_changed = self.update_final_experiment_mark()
if final_experiment_mark_changed:
self.part_student.checkThenSetFinalPartMark()
if call_update_experiment_marks_missing:
self.group_experiment.update_experiment_marks_missing()
return final_experiment_mark_changed
2021-03-18 13:53:55 +00:00
2021-04-03 00:11:26 +00:00
class User(db.Model, FsUserMixin):
2021-08-15 23:15:19 +00:00
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)
2022-02-23 19:36:29 +00:00
room = db.Column(db.String(100), nullable=True)
2021-08-15 23:15:19 +00:00
2022-05-29 16:44:30 +00:00
active_semester_id = db.Column(db.Integer, db.ForeignKey("semester.id"), nullable=False)
2022-05-16 15:16:42 +00:00
active_semester = db.relationship("Semester", back_populates="active_users")
2021-04-03 00:11:26 +00:00
2022-05-16 15:16:42 +00:00
admin = db.relationship("Admin", back_populates="user", lazy=False, uselist=False)
assistant = db.relationship("Assistant", back_populates="user", lazy=True, uselist=False)
2022-09-19 13:19:25 +00:00
def set_last_semester_as_active(self):
"""
Return True if changed, False otherwise.
"""
last_semester = Semester.lastSemester()
if last_semester.done:
return False
try:
self.active_semester = last_semester
db.session.commit()
except Exception as ex:
flash(str(ex), "error")
db.session.rollback()
return False
else:
flash(
f"Active semester changed to {last_semester} because your last active semester was set as done!",
2022-09-19 13:19:25 +00:00
"warning",
)
return True
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.first_name} {self.last_name}"
def __str__(self):
return self.str()
2021-04-03 00:11:26 +00:00
class Role(db.Model, FsRoleMixin):
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.name}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return self.str()
class Program(db.Model):
id = db.Column(db.Integer, primary_key=True)
2022-03-01 21:23:30 +00:00
label = db.Column(db.String(25), nullable=False, unique=True)
2022-05-16 15:16:42 +00:00
parts = db.relationship("Part", back_populates="program", lazy=True)
experiments = db.relationship("Experiment", back_populates="program", lazy=True)
groups = db.relationship("Group", back_populates="program", lazy=True)
2022-06-01 20:54:17 +00:00
def str(self):
return f"{self.label}"
2022-06-01 20:54:17 +00:00
def __str__(self):
return self.str()