1
0
Fork 0
mirror of https://codeberg.org/Mo8it/AdvLabDB.git synced 2024-11-12 21:40:41 +00:00
AdvLabDB/advlabdb/adminModelViews.py

1562 lines
48 KiB
Python
Raw Normal View History

2022-04-11 23:52:01 +00:00
from base64 import b64encode
from io import BytesIO
2022-02-13 18:58:05 +00:00
from pathlib import Path
2022-04-11 23:52:01 +00:00
import numpy as np
2022-03-04 02:49:02 +00:00
from flask import flash, has_request_context, redirect, request, url_for
2022-02-13 18:58:05 +00:00
from flask_admin import expose
2021-07-30 12:20:54 +00:00
from flask_admin.contrib.sqla.fields import QuerySelectField, QuerySelectMultipleField
2022-05-09 00:13:18 +00:00
from flask_admin.contrib.sqla.filters import BooleanEqualFilter, FilterEqual
2021-07-30 12:20:54 +00:00
from flask_admin.helpers import get_form_data
2021-06-02 21:43:41 +00:00
from flask_admin.menu import MenuLink
2021-07-01 14:38:37 +00:00
from flask_admin.model.template import EndpointLinkRowAction
2021-07-30 12:20:54 +00:00
from flask_security import admin_change_password, current_user, hash_password
2022-02-13 18:58:05 +00:00
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField, FileRequired
2022-04-11 23:52:01 +00:00
from matplotlib.figure import Figure
2022-05-21 19:14:10 +00:00
from sqlalchemy import and_, not_, or_, select
2022-02-13 18:58:05 +00:00
from werkzeug.utils import secure_filename
from wtforms import Form
from wtforms.fields import (
BooleanField,
DateField,
2022-04-04 16:56:16 +00:00
DecimalField,
IntegerField,
RadioField,
StringField,
2022-04-10 19:01:42 +00:00
SubmitField,
)
from wtforms.validators import URL, DataRequired, Email, NumberRange, Optional
2022-04-04 16:56:16 +00:00
from wtforms.widgets import NumberInput
2022-05-08 19:26:25 +00:00
from . import adminSpace, app, assistantSpace, db, user_datastore
2022-05-29 17:03:54 +00:00
from .advlabdb_independent_funs import flashRandomPassword
2022-05-08 19:26:25 +00:00
from .customClasses import (
2022-03-04 02:49:02 +00:00
CustomIdEndpointLinkRowAction,
SecureAdminBaseView,
SecureAdminModelView,
)
2022-05-08 19:26:25 +00:00
from .database_import import importFromFile
2022-05-16 20:20:36 +00:00
from .exceptions import DataBaseException, ModelViewException
2022-05-29 19:47:23 +00:00
from .model_dependent_funs import initActiveSemesterMenuLinks
2022-05-29 17:03:54 +00:00
from .model_independent_funs import randomPassword
2022-05-08 19:26:25 +00:00
from .models import (
2022-04-11 23:52:01 +00:00
MAX_MARK,
2022-04-18 16:37:21 +00:00
MAX_YEAR,
2022-04-11 23:52:01 +00:00
MIN_MARK,
2022-04-18 16:37:21 +00:00
MIN_YEAR,
2022-03-04 02:49:02 +00:00
Admin,
2021-06-02 21:43:41 +00:00
Appointment,
Assistant,
Experiment,
ExperimentMark,
Group,
GroupExperiment,
Part,
PartStudent,
2021-07-30 12:20:54 +00:00
Program,
2021-06-02 21:43:41 +00:00
Role,
Semester,
2021-07-30 12:20:54 +00:00
SemesterExperiment,
2021-06-02 21:43:41 +00:00
Student,
User,
)
2021-08-15 23:15:19 +00:00
def semesterExperimentQueryFactory():
2022-05-21 19:04:06 +00:00
return SemesterExperiment.query.where(SemesterExperiment.semester == current_user.active_semester)
2021-08-15 23:15:19 +00:00
2022-05-07 00:09:37 +00:00
class SemesterRowFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-29 19:47:23 +00:00
semesters = Semester.sortedSemestersStartingWithNewest()
2022-05-07 00:09:37 +00:00
return tuple((semester.id, semester.repr()) for semester in semesters)
2022-03-03 03:26:05 +00:00
2021-07-30 00:03:44 +00:00
class UserView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class SemesterFilter(SemesterRowFilter):
2022-03-03 03:26:05 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(User.active_semester_id == int(value))
2022-03-03 03:26:05 +00:00
2021-07-29 18:55:22 +00:00
class CreateForm(Form):
def roleQueryFactory():
return Role.query
def semesterQueryFactory():
return Semester.query
2022-05-07 19:05:16 +00:00
def default_roles():
return [user_datastore.find_role("assistant")]
email = StringField(
"Email",
validators=[DataRequired(), Email()],
)
2021-07-29 18:55:22 +00:00
roles = QuerySelectMultipleField(
"Roles",
query_factory=roleQueryFactory,
validators=[DataRequired()],
2022-05-07 19:05:16 +00:00
default=default_roles,
2021-07-29 18:55:22 +00:00
)
2021-08-15 23:15:19 +00:00
2022-05-07 19:05:16 +00:00
first_name = StringField(
"First Name",
validators=[DataRequired()],
)
last_name = StringField(
"Last Name",
validators=[DataRequired()],
)
phone_number = StringField(
"Phone Number",
2022-05-08 20:04:08 +00:00
validators=[Optional()],
2022-05-07 19:05:16 +00:00
)
mobile_phone_number = StringField(
"Mobile Phone Number",
2022-05-08 20:04:08 +00:00
validators=[Optional()],
2022-05-07 19:05:16 +00:00
)
building = StringField(
"Building",
2022-05-08 20:04:08 +00:00
validators=[Optional()],
2022-05-07 19:05:16 +00:00
)
room = StringField(
"Room",
2022-05-08 20:04:08 +00:00
validators=[Optional()],
2022-05-07 19:05:16 +00:00
)
2021-08-15 23:15:19 +00:00
semester_experiments = QuerySelectMultipleField(
"Semester Experiments",
query_factory=semesterExperimentQueryFactory,
allow_blank=True,
blank_text="-",
description="Only needed if the user has the assistant role",
)
2022-05-07 19:05:16 +00:00
active = BooleanField(
"Active",
default=True,
)
2021-07-29 18:55:22 +00:00
active_semester = QuerySelectField(
"Active Semester",
query_factory=semesterQueryFactory,
2022-05-16 23:11:47 +00:00
validators=[DataRequired()],
2022-05-29 19:47:23 +00:00
default=Semester.lastSemester,
2021-08-16 21:47:04 +00:00
description="Not fixed and users (including assistants) can change it.",
2021-07-29 18:55:22 +00:00
)
class EditForm(CreateForm):
2021-08-15 23:15:19 +00:00
semester_experiments = None
2022-05-07 19:05:16 +00:00
generate_new_password = BooleanField(
2022-05-08 20:04:08 +00:00
"Generate new random password. For security reasons, it is not possible to manually enter a password. Please use a password manager like Bitwarden or KeepassXC to save the randomly generated password.",
2022-05-07 19:05:16 +00:00
default=False,
)
2021-07-29 18:55:22 +00:00
2021-07-29 18:03:41 +00:00
can_view_details = True
2021-11-30 00:41:18 +00:00
column_list = [
"first_name",
"last_name",
"email",
"active",
"roles",
"active_semester",
2022-05-29 17:42:36 +00:00
"current_login_at",
2021-11-30 00:41:18 +00:00
]
2021-08-15 23:15:19 +00:00
column_details_list = column_list + [
"phone_number",
"mobile_phone_number",
"building",
2022-02-23 19:36:29 +00:00
"room",
2021-08-15 23:15:19 +00:00
"create_datetime",
"update_datetime",
]
2021-11-30 00:41:18 +00:00
column_searchable_list = [
"first_name",
"last_name",
"email",
]
2022-05-06 23:44:39 +00:00
2022-03-02 00:56:42 +00:00
column_filters = (
2022-03-03 03:26:05 +00:00
SemesterFilter(User, "Active Semester"),
2021-11-30 00:41:18 +00:00
"active",
2022-03-02 00:56:42 +00:00
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2021-11-30 00:41:18 +00:00
column_editable_list = [
"active",
]
2021-04-27 21:28:47 +00:00
2022-05-17 00:31:09 +00:00
_skip_session_addition_on_model_creation = True
2022-05-17 00:31:09 +00:00
def customCreateModel(self, form):
password = randomPassword()
hashedPassword = hash_password(password)
email = form.email.data.lower()
roles = [role.name for role in form.roles.data]
model = user_datastore.create_user(
email=email,
password=hashedPassword,
roles=roles,
first_name=form.first_name.data,
last_name=form.last_name.data,
phone_number=form.phone_number.data,
mobile_phone_number=form.mobile_phone_number.data,
building=form.building.data,
room=form.room.data,
active=form.active.data,
active_semester=form.active_semester.data,
)
2021-07-29 18:55:22 +00:00
2022-05-17 00:31:09 +00:00
flashRandomPassword(password)
2021-07-29 18:55:22 +00:00
2022-05-17 00:31:09 +00:00
return model
2021-06-09 00:22:37 +00:00
def on_model_delete(self, model):
if model == current_user:
raise ModelViewException("Tried to delete yourself as user!")
2021-06-09 00:22:37 +00:00
def on_model_change(self, form, model, is_created):
2021-07-29 22:24:10 +00:00
if not is_created:
2021-11-30 00:41:18 +00:00
if model == current_user:
if not form.active.data:
2022-02-13 18:58:05 +00:00
raise ModelViewException("Tried to deactivate yourself as user!")
2021-11-30 00:41:18 +00:00
if not model.has_role("admin"):
raise ModelViewException("Tried to remove your admin role!")
2021-06-09 00:22:37 +00:00
2021-07-30 13:14:35 +00:00
if hasattr(form, "generate_new_password") and form.generate_new_password.data:
2021-07-29 22:24:10 +00:00
password = randomPassword()
2022-05-17 00:36:37 +00:00
flashRandomPassword(password)
2021-07-29 18:55:22 +00:00
2021-07-29 22:24:10 +00:00
admin_change_password(
model, password, notify=False
2022-02-23 19:36:29 +00:00
) # Password is automatically hashed with this function
2021-07-29 18:55:22 +00:00
2022-05-09 00:13:18 +00:00
if model.has_role("assistant") and model.assistant is None:
semester_experiments = form.semester_experiments.data if form.semester_experiments else []
2021-08-21 21:18:55 +00:00
assistant = Assistant(user=model, semester_experiments=semester_experiments)
self.session.add(assistant)
2022-05-09 00:13:18 +00:00
if model.has_role("admin") and model.admin is None:
flash("Admin role was assigned!", "danger")
2022-02-27 18:30:28 +00:00
admin = Admin(user=model)
self.session.add(admin)
2022-05-17 00:31:09 +00:00
def after_model_change(self, form, model, is_created):
if is_created:
flash(
2022-05-17 00:36:37 +00:00
f"{model.email} registered with role(s): {', '.join([role.name for role in model.roles])}.",
2022-05-17 00:31:09 +00:00
category="success",
)
2021-07-30 00:03:44 +00:00
class SemesterView(SecureAdminModelView):
2021-07-29 14:27:15 +00:00
class CreateForm(Form):
2022-04-18 16:37:21 +00:00
def defaultFormLabel():
last_semester = Semester.lastSemester()
if last_semester.label == "WS":
return "SS"
else:
return "WS"
2022-04-18 16:37:21 +00:00
def defaultFormYear():
last_semester = Semester.lastSemester()
if last_semester.label == "WS":
return last_semester.year + 1
else:
return last_semester.year
2022-05-07 19:05:16 +00:00
label = RadioField(
"Semester",
choices=["WS", "SS"],
validators=[DataRequired()],
default=defaultFormLabel,
)
2022-04-18 16:37:21 +00:00
year = IntegerField(
"Year",
validators=[DataRequired(), NumberRange(MIN_YEAR, MAX_YEAR)],
default=defaultFormYear,
description=f"Between {MIN_YEAR} and {MAX_YEAR}",
)
2021-07-29 14:27:15 +00:00
transfer_parts = BooleanField(
2021-07-01 14:38:37 +00:00
"Transfer parts",
2022-05-16 01:37:08 +00:00
description="This option transfers the parts you have in your active semester. Make sure that your active semester is the last semester before creating a new one!",
default=True,
2021-07-29 14:27:15 +00:00
)
transfer_assistants = BooleanField(
2021-07-01 14:38:37 +00:00
"Transfer Assistants",
2022-05-16 01:37:08 +00:00
description="This option transfers assistants of your active semester to active experiments in the new semester. Make sure that your active semester is the last semester before creating a new one! Active experiments are transferred anyway. If you do not want an experiment to be transferred, set it to inactive before creating the new semester. Experiments which are switched to active before creating the new semester will be created in the new semester without assistants. It is recommended to check the assistants of all experiments after creating a new semester.",
2021-07-01 14:38:37 +00:00
default=True,
2021-07-29 14:27:15 +00:00
)
can_edit = False
can_delete = False
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-07-29 14:27:15 +00:00
2021-11-30 00:41:18 +00:00
column_list = [
"label",
"year",
"parts",
]
column_searchable_list = [
"label",
"year",
]
2022-03-02 17:59:14 +00:00
column_default_sort = [
("year", True),
("label", True),
]
2021-04-24 11:38:03 +00:00
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
2022-05-15 18:05:00 +00:00
return Semester.initFromOldSemester(
2021-07-29 22:24:10 +00:00
label=form.label.data,
year=form.year.data,
oldSemester=current_user.active_semester,
2021-07-29 22:24:10 +00:00
transferParts=form.transfer_parts.data,
transferAssistants=form.transfer_assistants.data,
)
def addMenuLink(space, newSemester):
2022-03-02 00:56:42 +00:00
categoryText = "Active semester"
link = MenuLink(
name=newSemester.repr(),
url=url_for("set_semester") + "?semester_id=" + str(newSemester.id),
category=categoryText,
)
category = space._menu_categories.get(categoryText)
2022-03-02 00:56:42 +00:00
link.parent = category
category._children.insert(0, link)
def after_model_change(self, form, model, is_created):
2022-05-29 19:41:01 +00:00
current_user.setActiveSemester(model)
SemesterView.addMenuLink(adminSpace, model)
SemesterView.addMenuLink(assistantSpace, model)
2021-04-24 11:38:03 +00:00
2021-07-29 22:24:10 +00:00
def programQueryFactory():
return Program.query
2021-07-30 00:03:44 +00:00
class PartView(SecureAdminModelView):
2021-04-24 11:38:03 +00:00
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-07-28 22:58:50 +00:00
2021-07-29 14:27:15 +00:00
column_sortable_list = []
2021-11-30 00:41:18 +00:00
column_list = [
"program",
"number",
]
2022-05-30 00:53:17 +00:00
column_details_exclude_list = [
"semester",
2021-11-30 00:41:18 +00:00
]
2022-05-30 00:53:17 +00:00
form_columns = column_list
2021-07-29 22:24:10 +00:00
form_extra_fields = {
"program": QuerySelectField(
2022-05-07 19:05:16 +00:00
"Program",
query_factory=programQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
2021-07-29 22:24:10 +00:00
)
}
2022-04-04 17:08:19 +00:00
form_args = {
"number": {"widget": NumberInput(min=1)},
}
2021-04-24 11:38:03 +00:00
column_searchable_list = [
"program.label",
"number",
]
2022-03-03 03:26:05 +00:00
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return query.where(Part.semester == current_user.active_semester)
2021-06-10 01:14:30 +00:00
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
return Part(program=form.program.data, number=form.number.data, semester=current_user.active_semester)
2021-07-29 22:24:10 +00:00
2021-07-30 00:03:44 +00:00
class StudentView(SecureAdminModelView):
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-11-30 00:41:18 +00:00
column_list = [
"student_number",
"first_name",
"last_name",
"uni_email",
"contact_email",
"part_students",
]
column_descriptions = {
"contact_email": "The preferred contact email address if entered by the student",
}
2021-11-30 00:41:18 +00:00
column_sortable_list = [
"student_number",
"first_name",
"last_name",
]
column_searchable_list = column_sortable_list + [
"uni_email",
"contact_email",
]
2021-11-30 00:41:18 +00:00
form_excluded_columns = [
"part_students",
]
2021-04-26 22:26:11 +00:00
2021-04-27 21:28:47 +00:00
form_args = {
2022-04-04 17:08:19 +00:00
"student_number": {"widget": NumberInput(min=0)},
"uni_email": {"validators": [Email()]},
"contact_email": {"validators": [Email()]},
2021-04-27 21:28:47 +00:00
}
2021-07-01 14:38:37 +00:00
column_extra_row_actions = [
EndpointLinkRowAction(
2022-03-03 03:26:05 +00:00
icon_class="fa fa-history",
endpoint="experimentmark.index_view",
2021-07-01 14:38:37 +00:00
id_arg="flt1_0",
title="Experiments history",
)
]
2021-06-10 01:14:30 +00:00
2021-07-12 14:42:11 +00:00
def partQueryFactory():
2022-05-21 19:04:06 +00:00
return Part.query.where(Part.semester == current_user.active_semester)
2021-06-10 01:14:30 +00:00
2021-07-12 14:42:11 +00:00
def groupQueryFactory():
2022-05-21 19:04:06 +00:00
return Group.query.where(Group.semester == current_user.active_semester)
2021-07-01 11:12:43 +00:00
2022-05-07 00:09:37 +00:00
class PartRowFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-21 19:12:15 +00:00
parts = db.session.execute(select(Part).where(Part.semester == current_user.active_semester)).scalars()
2022-05-07 21:53:47 +00:00
return tuple((part.id, f"{part.program.repr()}{part.number}") for part in parts)
2022-03-02 00:56:42 +00:00
2021-07-30 00:03:44 +00:00
class PartStudentView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class PartFilter(PartRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(PartStudent.part_id == int(value))
2022-03-02 00:56:42 +00:00
2022-03-03 03:26:05 +00:00
class StudentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.student_id
class GroupEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.group_id
class PartEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.part_id
class CreateForm(Form):
2021-07-12 14:42:11 +00:00
def studentQueryFactory():
return Student.query
2021-04-26 22:26:11 +00:00
student = QuerySelectField(
2021-07-13 01:37:00 +00:00
"Student",
query_factory=studentQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
part = QuerySelectField(
2021-07-13 01:37:00 +00:00
"Part",
query_factory=partQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
group = QuerySelectField(
"Group",
query_factory=groupQueryFactory,
allow_blank=True,
blank_text="-",
)
2021-04-26 22:26:11 +00:00
class EditForm(CreateForm):
student = None
part = None
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2022-03-02 00:56:42 +00:00
column_filters = (
PartFilter(PartStudent, "Part"),
"student.student_number",
"student.first_name",
"student.last_name",
"group.number",
"experiment_marks",
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
StudentEndpointLinkRowAction(
icon_class="fa fa-user",
endpoint="student.details_view",
title="Student",
),
GroupEndpointLinkRowAction(
icon_class="fa fa-users",
endpoint="group.details_view",
title="Group",
),
PartEndpointLinkRowAction(
icon_class="fa fa-puzzle-piece",
endpoint="part.details_view",
title="Part",
),
]
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return query.join(Part).where(Part.semester == current_user.active_semester)
2021-07-12 14:42:11 +00:00
def on_model_change(self, form, model, is_created):
2022-05-15 18:59:57 +00:00
PartStudent.check(model.part, model.group)
2021-04-27 21:28:47 +00:00
2021-07-29 21:18:24 +00:00
def partStudentQueryFactory():
2022-05-21 16:44:53 +00:00
return PartStudent.query.join(Part).where(Part.semester == current_user.active_semester)
2021-07-13 01:37:00 +00:00
2022-05-07 00:09:37 +00:00
class ProgramRowFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-21 19:12:15 +00:00
programs = db.session.execute(select(Program)).scalars()
2022-05-07 00:09:37 +00:00
return tuple((program.id, program.repr()) for program in programs)
2022-03-02 00:56:42 +00:00
2022-05-06 23:44:39 +00:00
class GroupView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class ProgramFilter(ProgramRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(Group.program_id == int(value))
2022-03-02 00:56:42 +00:00
2021-08-18 18:11:40 +00:00
def formFactory(is_created, group):
if is_created:
2021-07-29 21:18:24 +00:00
def query_factory():
2022-05-21 16:44:53 +00:00
return partStudentQueryFactory().where(PartStudent.group == None)
2021-07-29 21:18:24 +00:00
else:
def query_factory():
2022-05-21 16:44:53 +00:00
return partStudentQueryFactory().where(
2021-08-18 18:06:18 +00:00
or_(
2022-05-21 18:56:02 +00:00
and_(PartStudent.group == None, Part.program == group.program),
2021-08-18 18:06:18 +00:00
PartStudent.group == group,
)
)
2021-07-29 21:18:24 +00:00
class CustomForm(Form):
part_students = QuerySelectMultipleField(
"Part Students",
query_factory=query_factory,
validators=[DataRequired()],
description="The part students have to be in the same program!",
2021-07-29 21:18:24 +00:00
)
return CustomForm
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2022-03-03 03:26:05 +00:00
2022-05-30 00:53:17 +00:00
column_exclude_list = [
"semester",
2021-11-30 00:41:18 +00:00
]
2022-05-06 23:44:39 +00:00
2022-03-02 00:56:42 +00:00
column_filters = (
ProgramFilter(Group, "Program"),
2021-11-30 00:41:18 +00:00
"number",
2022-03-02 00:56:42 +00:00
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
2022-05-21 16:44:40 +00:00
return query.where(Group.semester == current_user.active_semester)
2021-07-12 14:42:11 +00:00
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
return Group.customInit(form.part_students.data)
2021-07-29 21:18:24 +00:00
def create_form(self, obj=None):
2022-05-06 23:06:08 +00:00
formClass = GroupView.formFactory(is_created=True, group=None)
return formClass(get_form_data(), obj=obj)
2021-07-29 21:18:24 +00:00
def edit_form(self, obj=None):
2022-05-06 23:06:08 +00:00
formClass = GroupView.formFactory(is_created=False, group=obj)
return formClass(get_form_data(), obj=obj)
2021-07-29 21:18:24 +00:00
2021-06-07 15:15:10 +00:00
2021-07-30 00:03:44 +00:00
class ExperimentView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class ProgramFilter(ProgramRowFilter):
2022-03-02 17:59:14 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(Experiment.program_id == int(value))
2022-03-02 17:59:14 +00:00
2021-06-21 16:07:18 +00:00
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-07-28 22:58:50 +00:00
2022-03-02 17:59:14 +00:00
column_filters = (
ProgramFilter(Experiment, "Program"),
"active",
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2021-11-30 00:41:18 +00:00
column_list = [
"number",
"program",
"title",
"active",
]
column_descriptions = {
"active": "Active experiments are present in new semesters",
}
2022-05-30 00:53:17 +00:00
column_searchable_list = [
"number",
"title",
]
form_columns = column_list + [
2021-07-28 11:59:41 +00:00
"description",
"wiki_link",
"building",
2022-02-23 19:36:29 +00:00
"room",
"responsibility",
"duration_in_days",
2022-03-02 17:59:14 +00:00
]
2021-06-21 16:26:38 +00:00
2021-11-30 00:41:18 +00:00
column_editable_list = [
"active",
]
2021-07-13 16:41:00 +00:00
form_args = {
2022-04-04 17:08:19 +00:00
"number": {"widget": NumberInput(min=1)},
"wiki_link": {"validators": [URL()]},
2022-04-04 17:08:19 +00:00
"duration_in_days": {"widget": NumberInput(min=1)},
}
form_extra_fields = {
"program": QuerySelectField(
2022-05-07 19:05:16 +00:00
"Program",
query_factory=programQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
}
2021-07-13 16:41:00 +00:00
2021-06-21 16:26:38 +00:00
2021-07-28 22:31:43 +00:00
def assistantQueryFactory():
2022-05-21 18:56:02 +00:00
return Assistant.query.join(User).where(User.active == True)
2021-07-28 22:31:43 +00:00
2021-07-30 00:03:44 +00:00
class SemesterExperimentView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class ProgramFilter(ProgramRowFilter):
2022-03-02 17:59:14 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
return query.join(Experiment).where(Experiment.program_id == int(value))
2022-03-02 17:59:14 +00:00
2022-03-03 03:26:05 +00:00
class ExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.experiment_id
2021-07-28 22:31:43 +00:00
class CreateForm(Form):
def experimentQueryFactory():
2022-05-21 19:04:06 +00:00
return Experiment.query.where(Experiment.active == True)
2021-07-28 22:31:43 +00:00
experiment = QuerySelectField(
"Experiment",
query_factory=experimentQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
2022-04-04 16:56:16 +00:00
oral_weighting = DecimalField(
"Oral weighting",
2022-04-18 16:37:21 +00:00
validators=[DataRequired(), NumberRange(0, 1)],
default=0.5,
description="Between 0 and 1",
places=2,
2022-04-04 16:56:16 +00:00
widget=NumberInput(step=0.01),
)
2022-04-04 16:56:16 +00:00
protocol_weighting = DecimalField(
"Protocol weighting",
2022-04-18 16:37:21 +00:00
validators=[DataRequired(), NumberRange(0, 1)],
default=0.5,
description="Between 0 and 1. Oral and protocol weightings have to add to 1! Both are rounded to 2 decimal digits.",
places=2,
2022-04-04 16:56:16 +00:00
widget=NumberInput(step=0.01),
)
2022-04-04 16:56:16 +00:00
final_weighting = DecimalField(
"Final weighting",
2022-04-18 16:37:21 +00:00
validators=[DataRequired(), NumberRange(0, 1)],
default=1.0,
description="Between 0 and 1",
places=2,
2022-04-04 16:56:16 +00:00
widget=NumberInput(step=0.01),
)
2022-05-07 19:05:16 +00:00
assistants = QuerySelectMultipleField(
"Assistants",
query_factory=assistantQueryFactory,
)
2021-07-28 22:31:43 +00:00
class EditForm(CreateForm):
experiment = None
2021-07-28 22:31:43 +00:00
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-07-28 22:31:43 +00:00
2021-11-30 00:41:18 +00:00
column_list = [
"experiment",
"assistants",
]
2022-05-30 00:53:17 +00:00
column_details_exclude_list = [
"semester",
2021-11-30 00:41:18 +00:00
]
2022-05-06 23:44:39 +00:00
2022-03-02 17:59:14 +00:00
column_filters = (ProgramFilter(SemesterExperiment, "Program"),)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2022-03-02 17:59:14 +00:00
column_searchable_list = [
"experiment.number",
"experiment.title",
]
2021-06-21 16:26:38 +00:00
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
ExperimentEndpointLinkRowAction(
icon_class="fa fa-flask",
endpoint="experiment.details_view",
title="Experiment",
),
]
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return query.where(SemesterExperiment.semester == current_user.active_semester)
2021-06-21 16:07:18 +00:00
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
return SemesterExperiment(
semester=current_user.active_semester,
oral_weighting=form.oral_weighting.data,
protocol_weighting=form.protocol_weighting.data,
final_weighting=form.final_weighting.data,
experiment=form.experiment.data,
assistants=form.assistants.data,
2021-07-29 22:24:10 +00:00
)
2021-07-28 22:31:43 +00:00
def on_model_change(self, form, model, is_created):
model.checkAndRoundWeightings()
def update_model(self, form, model):
2022-04-04 16:47:53 +00:00
weightingsChanged = (
form.oral_weighting.data != model.oral_weighting
or form.protocol_weighting.data != model.protocol_weighting
or form.final_weighting.data != model.final_weighting
)
updateSuccessful = super().update_model(form, model)
if updateSuccessful and weightingsChanged:
# Custom after_model_change
model.updateFinalExperimentAndPartMarks()
return updateSuccessful
2021-06-21 16:07:18 +00:00
2022-05-09 00:13:18 +00:00
def userHasRoleFilterFactory(column, role_name):
class UserHasRoleFilter(BooleanEqualFilter):
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
query = query.join(User)
2022-05-09 00:13:18 +00:00
if bool(int(value)): # value is string "0" or "1"
2022-05-21 18:56:02 +00:00
return query.join(User.roles).where(Role.name == role_name)
2022-05-09 00:13:18 +00:00
else:
2022-05-21 18:56:02 +00:00
return query.where(not_(User.roles.any(Role.name == role_name)))
2022-05-09 00:13:18 +00:00
return UserHasRoleFilter(column, f"Currently has {role_name} role")
2021-07-30 00:03:44 +00:00
class AssistantView(SecureAdminModelView):
2022-03-03 03:26:05 +00:00
class UserEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.user_id
def assistantUserQueryFactory():
2022-05-21 19:04:06 +00:00
return User.query.join(User.roles).where(Role.name == "assistant")
2021-06-24 17:24:14 +00:00
can_view_details = True
2021-07-28 22:58:50 +00:00
2021-11-30 00:41:18 +00:00
column_list = [
"user.first_name",
"user.last_name",
"semester_experiments",
]
2021-07-01 11:12:43 +00:00
column_details_list = column_list + [
2021-08-15 23:15:19 +00:00
"user.phone_number",
"user.mobile_phone_number",
"user.building",
2022-02-23 19:36:29 +00:00
"user.room",
2021-07-01 11:12:43 +00:00
"appointments",
"experiment_marks",
]
2021-11-30 00:41:18 +00:00
column_searchable_list = [
"user.first_name",
"user.last_name",
"user.email",
]
2022-05-09 00:13:18 +00:00
column_filters = (
userHasRoleFilterFactory(Assistant, "assistant"),
"user.active",
)
2021-11-30 00:41:18 +00:00
form_excluded_columns = [
"experiment_marks",
"appointments",
]
form_extra_fields = {
"user": QuerySelectField(
"User",
query_factory=assistantUserQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
2021-08-15 23:15:19 +00:00
),
"semester_experiments": QuerySelectMultipleField(
"Semester Experiments",
query_factory=semesterExperimentQueryFactory,
allow_blank=True,
blank_text="-",
),
}
2021-06-24 17:24:14 +00:00
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
UserEndpointLinkRowAction(
icon_class="fa fa-user",
endpoint="user.details_view",
title="User",
),
]
2021-06-24 17:24:14 +00:00
2022-03-03 03:26:05 +00:00
class AdminView(SecureAdminModelView):
class UserEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.user_id
can_export = False
can_set_page_size = False
can_create = False
can_edit = False
can_delete = False
column_list = [
"user.first_name",
"user.last_name",
]
2022-05-09 00:13:18 +00:00
column_filters = (
userHasRoleFilterFactory(Admin, "admin"),
"user.active",
)
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
UserEndpointLinkRowAction(
icon_class="fa fa-user",
endpoint="user.details_view",
title="User",
),
]
2022-03-01 21:23:30 +00:00
2021-07-12 14:42:11 +00:00
assistantBlankText = "Auto assign if experiment has only one assistant"
2022-05-07 00:09:37 +00:00
class ExperimentRowFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-21 19:12:15 +00:00
activeExperiments = db.session.execute(select(Experiment).where(Experiment.active == True)).scalars()
2022-05-07 00:09:37 +00:00
return tuple(
(
f"{activeExperiment.number},{activeExperiment.program_id}",
f"{activeExperiment.number} {activeExperiment.program.repr()}",
)
for activeExperiment in activeExperiments
2022-03-02 00:56:42 +00:00
)
2022-05-21 18:56:02 +00:00
def get_values(value):
values = value.split(",")
experimentNumber = int(values[0])
programId = int(values[1])
return (experimentNumber, programId)
2022-03-02 00:56:42 +00:00
2021-07-30 00:03:44 +00:00
class GroupExperimentView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class ExperimentFilter(ExperimentRowFilter):
2022-03-01 21:23:30 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
experimentNumber, programId = ExperimentRowFilter.get_values(value)
return (
query.join(SemesterExperiment)
.join(Experiment)
.where(Experiment.program_id == programId, Experiment.number == experimentNumber)
2022-03-01 21:23:30 +00:00
)
2022-03-03 03:26:05 +00:00
class GroupEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.group_id
class SemesterExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.semester_experiment_id
2021-07-01 11:12:43 +00:00
class CreateForm(Form):
group = QuerySelectField(
2021-07-13 01:37:00 +00:00
"Group",
query_factory=groupQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
2021-07-01 11:12:43 +00:00
)
semester_experiment = QuerySelectField(
"Semester Experiment",
query_factory=semesterExperimentQueryFactory,
2021-07-01 11:12:43 +00:00
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
appointment1_date = DateField(
"Appointment-1 Date",
validators=[Optional()],
description="Set if you already want to add an appointment. Otherwise, leave it blank and you can do it later under the Appointment tab.",
)
2022-05-07 19:05:16 +00:00
appointment1_special = BooleanField(
"Appointment-1 Special",
default=False,
)
2021-07-01 11:12:43 +00:00
appointment1_assistant = QuerySelectField(
2021-07-01 14:38:37 +00:00
"Appointment-1 Assistant",
2021-07-01 11:12:43 +00:00
query_factory=assistantQueryFactory,
allow_blank=True,
blank_text=assistantBlankText,
)
appointment2_date = DateField(
2022-05-07 19:05:16 +00:00
"Appointment-2 Date",
validators=[Optional()],
description="Add a second appointment (see above).",
)
appointment2_special = BooleanField(
"Appointment-2 Special",
default=False,
)
2021-07-01 11:12:43 +00:00
appointment2_assistant = QuerySelectField(
2021-07-01 14:38:37 +00:00
"Appointment-2 Assistant",
2021-07-01 11:12:43 +00:00
query_factory=assistantQueryFactory,
allow_blank=True,
blank_text=assistantBlankText,
)
2021-07-01 14:38:37 +00:00
can_edit = False
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2022-05-06 23:44:39 +00:00
2022-03-02 00:56:42 +00:00
column_filters = (
2022-03-01 21:23:30 +00:00
ExperimentFilter(GroupExperiment, "Experiment"),
2022-03-02 00:56:42 +00:00
"group.number",
2021-11-30 00:41:18 +00:00
"appointments",
2022-03-02 00:56:42 +00:00
"experiment_marks",
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2021-06-24 17:39:26 +00:00
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
GroupEndpointLinkRowAction(
icon_class="fa fa-users",
endpoint="group.details_view",
title="Group",
),
SemesterExperimentEndpointLinkRowAction(
icon_class="fa fa-flask",
endpoint="semesterexperiment.details_view",
title="SemesterExperiment",
),
]
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return query.join(Group).where(Group.semester == current_user.active_semester)
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
2022-05-15 18:59:57 +00:00
return GroupExperiment(semester_experiment=form.semester_experiment.data, group=form.group.data)
2021-07-01 17:43:59 +00:00
2021-07-29 22:24:10 +00:00
def on_model_change(self, form, model, is_created):
if is_created:
2022-05-15 20:24:49 +00:00
for date, special, assistant in (
(form.appointment1_date.data, form.appointment1_special.data, form.appointment1_assistant.data),
(form.appointment2_date.data, form.appointment2_special.data, form.appointment2_assistant.data),
2021-07-01 17:43:59 +00:00
):
2022-05-15 20:24:49 +00:00
if date is not None:
appointment = Appointment(date=date, special=special, group_experiment=model, assistant=assistant)
2021-07-01 17:43:59 +00:00
self.session.add(appointment)
2021-07-29 22:24:10 +00:00
def after_model_change(self, form, model, is_created):
if is_created:
if model.appointments:
flash(f"Appointments {model.appointments} added.", "success")
2021-07-01 17:43:59 +00:00
2021-06-24 17:39:26 +00:00
2021-07-12 14:42:11 +00:00
def groupExperimentQueryFactory():
2022-05-21 18:56:02 +00:00
return GroupExperiment.query.join(SemesterExperiment).where(
SemesterExperiment.semester == current_user.active_semester
2021-07-12 14:42:11 +00:00
)
2022-05-07 00:09:37 +00:00
class AssistantRowFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-07 00:09:37 +00:00
activeAssistants = assistantQueryFactory()
return tuple((assistant.id, assistant.repr()) for assistant in activeAssistants)
2022-03-02 00:56:42 +00:00
2021-07-30 00:03:44 +00:00
class AppointmentView(SecureAdminModelView):
2022-05-07 00:09:37 +00:00
class ExperimentFilter(ExperimentRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
experimentNumber, programId = ExperimentRowFilter.get_values(value)
return (
query.join(GroupExperiment)
.join(SemesterExperiment)
.join(Experiment)
.where(Experiment.program_id == programId, Experiment.number == experimentNumber)
2022-03-02 00:56:42 +00:00
)
2022-05-07 00:09:37 +00:00
class AssistantFilter(AssistantRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(Appointment.assistant_id == int(value))
2022-03-02 00:56:42 +00:00
2022-03-03 03:26:05 +00:00
class GroupExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.group_experiment_id
class AssistantEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.assistant_id
2022-05-06 23:06:08 +00:00
class CreateAndEditForm(Form):
2021-07-12 14:42:11 +00:00
group_experiment = QuerySelectField(
2021-07-13 01:37:00 +00:00
"Group Experiment",
query_factory=groupExperimentQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
2021-07-12 14:42:11 +00:00
)
2022-05-07 19:05:16 +00:00
date = DateField(
"Date",
validators=[DataRequired()],
)
special = BooleanField(
"Special",
default=False,
2022-05-30 02:18:19 +00:00
description="A special appointment should take place in the semester break",
2022-05-07 19:05:16 +00:00
)
2021-07-12 14:42:11 +00:00
assistant = QuerySelectField(
"Assistant",
query_factory=assistantQueryFactory,
allow_blank=True,
blank_text=assistantBlankText,
)
2022-05-30 02:18:19 +00:00
column_descriptions = {
"special": "A special appointment should take place in the semester break",
}
2022-03-02 00:56:42 +00:00
column_filters = (
ExperimentFilter(Appointment, "Experiment"),
AssistantFilter(Appointment, "Assistant"),
"group_experiment.group",
"date",
"special",
2022-03-02 00:56:42 +00:00
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2021-11-30 00:41:18 +00:00
column_editable_list = [
"date",
"special",
]
2021-07-12 14:42:11 +00:00
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
GroupExperimentEndpointLinkRowAction(
icon_class="fa fa-flask",
endpoint="groupexperiment.details_view",
title="GroupExperiment",
),
AssistantEndpointLinkRowAction(
icon_class="fa fa-user-secret",
endpoint="assistant.details_view",
title="Assistant",
),
]
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return (
query.join(GroupExperiment)
.join(SemesterExperiment)
.where(SemesterExperiment.semester == current_user.active_semester)
2021-07-12 14:42:11 +00:00
)
2021-07-29 22:24:10 +00:00
def customCreateModel(self, form):
2022-05-15 20:24:49 +00:00
return Appointment(
date=form.date.data,
special=form.special.data,
group_experiment=form.group_experiment.data,
assistant=form.assistant.data,
2021-07-29 22:24:10 +00:00
)
2021-07-12 14:42:11 +00:00
2022-05-17 00:31:09 +00:00
def customUpdateModel(self, form, model):
if form.date is None:
# For editable
model.special = form.special.data
elif form.special is None:
# For editable
model.date = form.date.data
else:
# Not editable, full form
model.custom_update(
date=form.date.data,
special=form.special.data,
group_experiment=form.group_experiment.data,
assistant=form.assistant.data,
)
2021-07-01 11:12:43 +00:00
2021-07-30 00:03:44 +00:00
class ExperimentMarkView(SecureAdminModelView):
2022-02-27 18:30:28 +00:00
class StudentFilter(FilterEqual):
2021-07-01 14:38:37 +00:00
def validate(self, value):
2022-05-16 14:56:24 +00:00
if db.session.get(Student, value) is not None:
2021-07-01 14:38:37 +00:00
return True
else:
return False
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
return query.join(PartStudent).where(PartStudent.student_id == int(value))
2022-03-02 00:56:42 +00:00
2022-05-07 00:09:37 +00:00
class AssistantFilter(AssistantRowFilter):
2022-03-01 21:23:30 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(ExperimentMark.assistant_id == int(value))
2022-03-01 21:23:30 +00:00
class AdminFilter(FilterEqual):
def get_options(self, view):
2022-05-07 21:53:47 +00:00
if not has_request_context():
return tuple()
2022-05-21 19:12:15 +00:00
admins = db.session.execute(select(Admin).join(User).where(User.active == True)).scalars()
return tuple((admin.id, admin.repr()) for admin in admins)
2022-03-01 21:23:30 +00:00
def apply(self, query, value, alias=None):
2022-05-21 19:19:26 +00:00
return query.where(ExperimentMark.admin_id == int(value))
2022-03-02 00:56:42 +00:00
2022-05-07 00:09:37 +00:00
class ExperimentFilter(ExperimentRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
experimentNumber, programId = ExperimentRowFilter.get_values(value)
return (
query.join(GroupExperiment)
.join(SemesterExperiment)
.join(Experiment)
.where(Experiment.program_id == programId, Experiment.number == experimentNumber)
2022-03-02 00:56:42 +00:00
)
2022-05-07 00:09:37 +00:00
class ProgramFilter(ProgramRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
return query.join(PartStudent).join(Part).where(Part.program_id == int(value))
2022-03-02 00:56:42 +00:00
2022-05-07 00:09:37 +00:00
class PartFilter(PartRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
return query.join(PartStudent).where(PartStudent.part_id == int(value))
2022-03-02 00:56:42 +00:00
2022-05-07 00:09:37 +00:00
class SemesterFilter(SemesterRowFilter):
2022-03-02 00:56:42 +00:00
def apply(self, query, value, alias=None):
2022-05-21 18:56:02 +00:00
return (
query.join(GroupExperiment).join(SemesterExperiment).where(SemesterExperiment.semester_id == int(value))
2022-03-02 00:56:42 +00:00
)
2022-03-01 21:23:30 +00:00
2022-03-03 03:26:05 +00:00
class PartStudentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.part_student_id
class GroupExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.group_experiment_id
class AssistantEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
def customId(self, row):
return row.assistant_id
2021-07-13 00:50:15 +00:00
class CreateForm(Form):
part_student = QuerySelectField(
"Part Student",
2021-07-29 21:18:24 +00:00
query_factory=partStudentQueryFactory,
2021-07-13 00:50:15 +00:00
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
group_experiment = QuerySelectField(
"Group Experiment",
query_factory=groupExperimentQueryFactory,
validators=[DataRequired()],
allow_blank=True,
blank_text="-",
)
class EditForm(Form):
oral_mark = IntegerField(
"Oral Mark",
2022-04-18 16:37:21 +00:00
validators=[Optional(), NumberRange(MIN_MARK, MAX_MARK)],
2022-04-11 23:52:01 +00:00
description=f"Between {MIN_MARK} and {MAX_MARK}",
)
protocol_mark = IntegerField(
"Protocol Mark",
2022-04-18 16:37:21 +00:00
validators=[Optional(), NumberRange(MIN_MARK, MAX_MARK)],
2022-04-11 23:52:01 +00:00
description=f"Between {MIN_MARK} and {MAX_MARK}",
)
2021-07-13 00:50:15 +00:00
column_descriptions = {
2022-04-11 23:52:01 +00:00
"oral_mark": f"Between {MIN_MARK} and {MAX_MARK}",
"protocol_mark": f"Between {MIN_MARK} and {MAX_MARK}",
2021-08-29 16:15:14 +00:00
"final_experiment_mark": "Calculated automatically with oral and protocol marks and weightings",
2022-02-27 18:30:28 +00:00
"assistant": "The last assistant who edited the mark",
"admin": "The last admin who edited the mark",
}
2022-03-02 00:56:42 +00:00
column_filters = (
StudentFilter(ExperimentMark, "Student / ID"),
SemesterFilter(ExperimentMark, "Semester"),
AssistantFilter(ExperimentMark, "Assistant"),
AdminFilter(ExperimentMark, "Admin"),
ExperimentFilter(ExperimentMark, "Experiment"),
ProgramFilter(ExperimentMark, "Program"),
PartFilter(ExperimentMark, "Part"),
2021-07-30 21:42:20 +00:00
"group_experiment.group",
2021-08-29 16:15:14 +00:00
"oral_mark",
"protocol_mark",
"final_experiment_mark",
2022-03-02 00:56:42 +00:00
)
2022-05-06 23:44:39 +00:00
refreshFiltersCache = True
2021-07-30 21:42:20 +00:00
column_default_sort = [("oral_mark", False), ("protocol_mark", False)]
2021-07-13 00:50:15 +00:00
2022-03-03 03:26:05 +00:00
column_extra_row_actions = [
PartStudentEndpointLinkRowAction(
icon_class="fa fa-user",
endpoint="partstudent.details_view",
title="PartStudent",
),
GroupExperimentEndpointLinkRowAction(
icon_class="fa fa-flask",
endpoint="groupexperiment.details_view",
title="GroupExperiment",
),
AssistantEndpointLinkRowAction(
icon_class="fa fa-user-secret",
endpoint="assistant.details_view",
title="Assistant",
),
]
2022-05-30 00:53:17 +00:00
# TODO: Find solution
"""
2022-02-13 18:58:05 +00:00
# Deactivated for the experiments history of a student.
2022-05-21 16:32:34 +00:00
def query_modifier(self, query):
return (
query.join(GroupExperiment)
.join(SemesterExperiment)
.where(SemesterExperiment.semester == current_user.active_semester)
2021-07-13 00:50:15 +00:00
)
"""
2021-07-13 00:50:15 +00:00
2021-08-18 17:39:21 +00:00
def customCreateModel(self, form):
2022-05-15 19:07:00 +00:00
return ExperimentMark(part_student=form.part_student.data, group_experiment=form.group_experiment.data)
def update_model(self, form, model):
2022-05-15 20:24:49 +00:00
if (form.oral_mark is not None and form.oral_mark.data != model.oral_mark) or (
form.protocol_mark is not None and form.protocol_mark.data != model.protocol_mark
):
2022-02-27 18:30:28 +00:00
model.admin = current_user.admin
2022-05-15 20:24:49 +00:00
updateSuccessful = super().update_model(form, model)
model.part_student.checkThenSetFinalPartMark()
2022-05-15 20:24:49 +00:00
return updateSuccessful
2022-05-07 00:09:37 +00:00
# Nothing changed
return True
2021-07-01 14:38:37 +00:00
2021-07-30 00:03:44 +00:00
class ProgramView(SecureAdminModelView):
2022-03-03 03:26:05 +00:00
can_export = False
can_set_page_size = False
2021-07-28 23:04:58 +00:00
can_view_details = True
2022-05-30 00:53:17 +00:00
column_display_all_relations = True
2021-07-28 23:04:58 +00:00
2021-11-30 00:41:18 +00:00
column_list = [
"label",
]
2022-05-30 00:53:17 +00:00
column_sortable_list = []
2021-11-30 00:41:18 +00:00
form_excluded_columns = [
"parts",
"experiments",
"groups",
]
2021-07-14 02:27:17 +00:00
2021-09-11 18:53:11 +00:00
class ImportView(SecureAdminBaseView):
class FileForm(FlaskForm):
file = FileField(
label="Import file",
validators=[FileRequired(), FileAllowed(["txt"], "Only txt files are allowed!")],
description="The import file has to be a text file (with .txt at the end) encoded in UTF-8. It has to strictly follow the required format.",
)
2022-04-10 19:01:42 +00:00
submit = SubmitField(
label="Upload and import",
)
2021-09-11 18:53:11 +00:00
@expose(methods=("GET", "POST"))
2021-09-11 18:53:11 +00:00
def index(self):
form = ImportView.FileForm()
if form.validate_on_submit():
f = form.file.data
filename = secure_filename(f.filename)
2022-01-13 01:25:31 +00:00
directory = "db/import_files"
2021-09-11 18:53:11 +00:00
Path(directory).mkdir(exist_ok=True)
filePath = directory + f"/{filename}"
f.save(filePath)
try:
importFromFile(filePath)
2022-05-15 20:24:49 +00:00
except Exception as ex:
2022-05-17 23:07:27 +00:00
flash(str(ex), "error")
2021-09-11 18:53:11 +00:00
return redirect(url_for("index"))
return self.render("import.html", form=form)
2022-04-10 19:01:42 +00:00
class ActionsView(SecureAdminBaseView):
class ActionsForm(FlaskForm):
manualUpdateFinalExperimentAndPartMarksSubmit = SubmitField(
label="Manually update final experiment and part marks",
)
@expose(methods=("GET", "POST"))
def index(self):
form = ActionsView.ActionsForm()
if form.validate_on_submit():
if form.manualUpdateFinalExperimentAndPartMarksSubmit.data:
for semesterExperiment in current_user.active_semester.semester_experiments:
2022-04-10 19:01:42 +00:00
semesterExperiment.updateFinalExperimentAndPartMarks()
flash("Manually updated all final experiment and part marks", "success")
return redirect(url_for("index"))
return self.render("actions.html", form=form)
2022-04-11 23:52:01 +00:00
class AnalysisView(SecureAdminBaseView):
class AnalysisForm(FlaskForm):
assistantMarksSubmit = SubmitField(
label="Assistant's marks",
)
2022-04-12 13:02:05 +00:00
finalPartMarksSubmit = SubmitField(
label="Final part marks",
)
def htmlFig(fig):
buf = BytesIO()
fig.savefig(buf, format="png")
return b64encode(buf.getbuffer()).decode("ascii")
2022-04-11 23:52:01 +00:00
def markHist(data, title):
fig = Figure()
ax = fig.subplots()
ax.set_xlim(MIN_MARK - 0.5, MAX_MARK + 0.5)
ax.set_xticks(np.arange(MAX_MARK + 1))
ax.set_xlabel("Mark")
2022-04-12 11:29:08 +00:00
N = data.size
title += f"\nN = {N}"
if N > 0:
2022-04-11 23:52:01 +00:00
hist = ax.hist(
data,
bins=np.arange(MAX_MARK) - 0.5,
)
2022-04-12 11:29:08 +00:00
ax.set_yticks(np.arange(N + 1))
title += f" | mean = {round(np.mean(data), 1)}"
2022-04-11 23:52:01 +00:00
else:
ax.set_yticks(np.arange(2))
2022-04-12 11:29:08 +00:00
ax.set_title(title)
2022-04-12 13:02:05 +00:00
return AnalysisView.htmlFig(fig)
2022-04-11 23:52:01 +00:00
2022-05-22 22:56:10 +00:00
def get_experiment_marks(assistant, attr):
data = []
for experimentMark in assistant.experiment_marks:
mark = getattr(experimentMark, attr)
if mark is not None:
data.append(mark)
return np.array(data)
2022-04-11 23:52:01 +00:00
def markHists(markType, activeAssistants):
attr = markType.lower() + "_mark"
2022-04-12 11:29:08 +00:00
markTypeTitleAddition = f" | {markType} marks"
2022-05-22 22:56:10 +00:00
marks = [AnalysisView.get_experiment_marks(assistant, attr) for assistant in activeAssistants]
2022-04-12 11:29:08 +00:00
hists = [
2022-04-11 23:52:01 +00:00
AnalysisView.markHist(
2022-05-22 22:56:10 +00:00
data=marks[i],
title=activeAssistants[i].repr() + markTypeTitleAddition,
2022-04-11 23:52:01 +00:00
)
2022-05-22 22:56:10 +00:00
for i in range(len(marks))
2022-04-11 23:52:01 +00:00
]
2022-04-12 11:29:08 +00:00
hists.append(
AnalysisView.markHist(
2022-05-22 22:56:10 +00:00
data=np.hstack(marks),
2022-04-12 11:29:08 +00:00
title="All" + markTypeTitleAddition,
)
)
return hists
2022-05-22 22:56:10 +00:00
def get_final_part_marks(part):
data = []
for partStudent in part.part_students:
mark = partStudent.final_part_mark
if mark is not None:
data.append(mark)
return np.array(data)
2022-04-11 23:52:01 +00:00
@expose(methods=("GET", "POST"))
def index(self):
form = AnalysisView.AnalysisForm()
if form.validate_on_submit():
if form.assistantMarksSubmit.data:
2022-04-12 11:29:08 +00:00
activeAssistants = assistantQueryFactory()
2022-04-11 23:52:01 +00:00
oralMarkHists = AnalysisView.markHists("Oral", activeAssistants)
protocolMarkHists = AnalysisView.markHists("Protocol", activeAssistants)
return self.render(
"analysis/assistant_marks.html",
histIndices=range(len(oralMarkHists)),
oralMarkHists=oralMarkHists,
protocolMarkHists=protocolMarkHists,
)
2022-04-12 13:02:05 +00:00
if form.finalPartMarksSubmit.data:
parts = current_user.active_semester.parts
2022-04-12 13:02:05 +00:00
activeSemesterFinalPartMarksHists = [
AnalysisView.markHist(
2022-05-22 22:56:10 +00:00
data=AnalysisView.get_final_part_marks(part),
2022-04-12 13:02:05 +00:00
title=part.repr(),
)
for part in parts
]
2022-05-29 19:47:23 +00:00
semesters = Semester.sortedSemestersStartingWithNewest()
2022-04-12 13:02:05 +00:00
semesterLabels = [semester.repr() for semester in semesters]
meanFinalPartMarks = np.flip(
[
np.mean(
np.hstack(
[
[partStudent.final_part_mark for partStudent in part.part_students]
for part in semester.parts
]
)
)
for semester in semesters
]
)
fig = Figure()
2022-05-22 22:56:10 +00:00
lenMeanFinalPartMarks = len(meanFinalPartMarks)
if lenMeanFinalPartMarks > 0:
ax = fig.subplots()
x = range(1, lenMeanFinalPartMarks + 1)
ax.plot(
x,
meanFinalPartMarks,
marker="d",
)
# TODO: Change ticks to semester labels
# TODO: Check linestyle
ax.set_xticks(x)
ax.set_xlim(0.5, x[-1] + 0.5)
2022-04-12 13:02:05 +00:00
meanFinalPartMarksPlot = AnalysisView.htmlFig(fig)
return self.render(
"analysis/final_part_marks.html",
activeSemesterFinalPartMarksHists=activeSemesterFinalPartMarksHists,
meanFinalPartMarksPlot=meanFinalPartMarksPlot,
)
2022-04-11 23:52:01 +00:00
return self.render("analysis/analysis.html", form=form)
2022-02-23 18:37:09 +00:00
class DocsView(SecureAdminBaseView):
2022-03-04 02:49:02 +00:00
@expose("/")
2022-02-23 18:37:09 +00:00
def index(self):
return self.render("docs/admin.html")
2021-07-30 00:03:44 +00:00
adminSpace.add_view(StudentView(Student, db.session))
adminSpace.add_view(PartStudentView(PartStudent, db.session))
adminSpace.add_view(GroupView(Group, db.session))
adminSpace.add_view(GroupExperimentView(GroupExperiment, db.session))
adminSpace.add_view(AppointmentView(Appointment, db.session))
adminSpace.add_view(ExperimentMarkView(ExperimentMark, db.session))
adminSpace.add_view(ExperimentView(Experiment, db.session))
adminSpace.add_view(SemesterExperimentView(SemesterExperiment, db.session))
adminSpace.add_view(SemesterView(Semester, db.session))
2021-08-15 23:15:19 +00:00
adminSpace.add_view(PartView(Part, db.session))
adminSpace.add_view(AssistantView(Assistant, db.session))
2022-03-03 03:26:05 +00:00
adminSpace.add_view(AdminView(Admin, db.session))
2021-07-30 00:03:44 +00:00
adminSpace.add_view(UserView(User, db.session))
2021-08-15 23:15:19 +00:00
adminSpace.add_view(ProgramView(Program, db.session))
2021-09-11 18:53:11 +00:00
adminSpace.add_view(ImportView(name="Import"))
2022-04-10 19:01:42 +00:00
adminSpace.add_view(ActionsView(name="Actions"))
2022-04-11 23:52:01 +00:00
adminSpace.add_view(AnalysisView(name="Analysis"))
2022-02-23 18:37:09 +00:00
adminSpace.add_view(DocsView(name="Docs"))
initActiveSemesterMenuLinks(adminSpace)