mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-12-20 23:41:20 +00:00
1602 lines
49 KiB
Python
1602 lines
49 KiB
Python
from base64 import b64encode
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from flask import flash, has_request_context, redirect, request, url_for
|
|
from flask_admin import expose
|
|
from flask_admin.contrib.sqla.fields import QuerySelectField, QuerySelectMultipleField
|
|
from flask_admin.contrib.sqla.filters import FilterEqual
|
|
from flask_admin.helpers import get_form_data
|
|
from flask_admin.menu import MenuLink
|
|
from flask_admin.model.template import EndpointLinkRowAction
|
|
from flask_security import admin_change_password, current_user, hash_password
|
|
from flask_wtf import FlaskForm
|
|
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
|
from matplotlib.figure import Figure
|
|
from sqlalchemy import and_, func, or_
|
|
from werkzeug.utils import secure_filename
|
|
from wtforms import Form
|
|
from wtforms.fields import (
|
|
BooleanField,
|
|
DateField,
|
|
DecimalField,
|
|
IntegerField,
|
|
RadioField,
|
|
SelectField,
|
|
StringField,
|
|
SubmitField,
|
|
)
|
|
from wtforms.validators import URL, DataRequired, Email, NumberRange, Optional
|
|
from wtforms.widgets import NumberInput
|
|
|
|
from advlabdb import adminSpace, app, assistantSpace, db, user_datastore
|
|
from advlabdb.configUtils import getConfig
|
|
from advlabdb.customClasses import (
|
|
CustomIdEndpointLinkRowAction,
|
|
SecureAdminBaseView,
|
|
SecureAdminModelView,
|
|
)
|
|
from advlabdb.database_import import importFromFile
|
|
from advlabdb.dependent_funs import (
|
|
flashRandomPassword,
|
|
initActiveSemesterMenuLinks,
|
|
setUserActiveSemester,
|
|
sortedSemestersStartingWithNewest,
|
|
userActiveSemester,
|
|
)
|
|
from advlabdb.exceptions import DataBaseException, ModelViewException
|
|
from advlabdb.independent_funs import randomPassword
|
|
from advlabdb.models import (
|
|
MAX_MARK,
|
|
MAX_YEAR,
|
|
MIN_MARK,
|
|
MIN_YEAR,
|
|
Admin,
|
|
Appointment,
|
|
Assistant,
|
|
Experiment,
|
|
ExperimentMark,
|
|
Group,
|
|
GroupExperiment,
|
|
Part,
|
|
PartStudent,
|
|
Program,
|
|
Role,
|
|
Semester,
|
|
SemesterExperiment,
|
|
Student,
|
|
User,
|
|
)
|
|
|
|
|
|
def semesterExperimentQueryFactory():
|
|
return SemesterExperiment.query.filter(SemesterExperiment.semester == userActiveSemester())
|
|
|
|
|
|
def semesterFilterOptions():
|
|
semesters = Semester.query.order_by(Semester.id.desc())
|
|
return tuple((semester.id, semester.repr()) for semester in semesters)
|
|
|
|
|
|
class UserView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class SemesterFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return semesterFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.active_semester_id == int(value))
|
|
|
|
class CreateForm(Form):
|
|
def roleQueryFactory():
|
|
return Role.query
|
|
|
|
def semesterQueryFactory():
|
|
return Semester.query
|
|
|
|
email = StringField("Email", validators=[DataRequired(), Email()])
|
|
roles = QuerySelectMultipleField(
|
|
"Roles",
|
|
query_factory=roleQueryFactory,
|
|
validators=[DataRequired()],
|
|
default=[user_datastore.find_role("assistant")],
|
|
)
|
|
|
|
first_name = StringField("First Name", validators=[DataRequired()])
|
|
last_name = StringField("Last Name", validators=[DataRequired()])
|
|
phone_number = StringField("Phone Number")
|
|
mobile_phone_number = StringField("Mobile Phone Number")
|
|
building = StringField("Building")
|
|
room = StringField("Room")
|
|
|
|
semester_experiments = QuerySelectMultipleField(
|
|
"Semester Experiments",
|
|
query_factory=semesterExperimentQueryFactory,
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
description="Only needed if the user has the assistant role",
|
|
)
|
|
|
|
active = BooleanField("Active", default=True)
|
|
active_semester = QuerySelectField(
|
|
"Active Semester",
|
|
query_factory=semesterQueryFactory,
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
default=userActiveSemester,
|
|
description="Not fixed and users (including assistants) can change it.",
|
|
)
|
|
|
|
class EditForm(CreateForm):
|
|
semester_experiments = None
|
|
|
|
generate_new_password = BooleanField("Generate new random password", default=False)
|
|
|
|
form = EditForm
|
|
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"active",
|
|
"roles",
|
|
"assistant",
|
|
"active_semester",
|
|
]
|
|
column_details_list = column_list + [
|
|
"phone_number",
|
|
"mobile_phone_number",
|
|
"building",
|
|
"room",
|
|
"create_datetime",
|
|
"update_datetime",
|
|
]
|
|
column_searchable_list = [
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
]
|
|
column_filters = (
|
|
SemesterFilter(User, "Active Semester"),
|
|
"active",
|
|
)
|
|
column_editable_list = [
|
|
"active",
|
|
]
|
|
|
|
def create_form(self, obj=None):
|
|
form = self.CreateForm
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
def create_model(self, form):
|
|
try:
|
|
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,
|
|
)
|
|
|
|
self.on_model_change(form, model, True)
|
|
self.session.commit()
|
|
except Exception as ex:
|
|
flash(str(ex), "error")
|
|
|
|
self.session.rollback()
|
|
else:
|
|
flash(
|
|
f"{email} registered with roles: {', '.join([role.name for role in model.roles])}.",
|
|
category="success",
|
|
)
|
|
|
|
flashRandomPassword(password)
|
|
|
|
self.after_model_change(form, model, True)
|
|
return model
|
|
|
|
def on_model_delete(self, model):
|
|
if model == current_user:
|
|
raise ModelViewException("Tried to delete yourself as user!")
|
|
|
|
def on_model_change(self, form, model, is_created):
|
|
if not is_created:
|
|
if model == current_user:
|
|
if not form.active.data:
|
|
raise ModelViewException("Tried to deactivate yourself as user!")
|
|
if not model.has_role("admin"):
|
|
raise ModelViewException("Tried to remove your admin role!")
|
|
|
|
if hasattr(form, "generate_new_password") and form.generate_new_password.data:
|
|
password = randomPassword()
|
|
UserView.flashPassword(password)
|
|
|
|
admin_change_password(
|
|
model, password, notify=False
|
|
) # Password is automatically hashed with this function
|
|
|
|
if model.has_role("assistant") and not model.assistant:
|
|
semester_experiments = form.semester_experiments.data if form.semester_experiments else []
|
|
|
|
assistant = Assistant(user=model, semester_experiments=semester_experiments)
|
|
self.session.add(assistant)
|
|
|
|
if model.has_role("admin") and not model.admin:
|
|
admin = Admin(user=model)
|
|
self.session.add(admin)
|
|
|
|
|
|
class RoleView(SecureAdminModelView):
|
|
can_export = False
|
|
can_set_page_size = False
|
|
can_create = False
|
|
can_edit = False
|
|
can_delete = False
|
|
column_display_actions = False
|
|
|
|
column_list = [
|
|
"name",
|
|
"description",
|
|
"permissions",
|
|
"update_datetime",
|
|
]
|
|
|
|
|
|
class SemesterView(SecureAdminModelView):
|
|
class CreateForm(Form):
|
|
def defaultFormLabel():
|
|
if userActiveSemester().label == "WS":
|
|
return "SS"
|
|
else:
|
|
return "WS"
|
|
|
|
def defaultFormYear():
|
|
activeSemester = userActiveSemester()
|
|
if activeSemester.label == "WS":
|
|
return activeSemester.year + 1
|
|
else:
|
|
return activeSemester.year
|
|
|
|
label = RadioField("Semester", choices=["WS", "SS"], validators=[DataRequired()], default=defaultFormLabel)
|
|
year = IntegerField(
|
|
"Year",
|
|
validators=[DataRequired(), NumberRange(MIN_YEAR, MAX_YEAR)],
|
|
default=defaultFormYear,
|
|
description=f"Between {MIN_YEAR} and {MAX_YEAR}",
|
|
)
|
|
transfer_parts = BooleanField(
|
|
"Transfer parts",
|
|
description="This option transfers the parts you have in your current active semester. Make sure that your semester is the last semester before creating a new one (recommended)!",
|
|
default=True,
|
|
)
|
|
transfer_assistants = BooleanField(
|
|
"Transfer Assistants",
|
|
description="This option transfers assistants of your active semester to active experiments in the new semester. Make sure that your semester is the last semester before creating a new one (recommended)! 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 important to check the assistants of all experiments after creating a new semester.",
|
|
default=True,
|
|
)
|
|
|
|
form = CreateForm
|
|
|
|
can_edit = False
|
|
can_delete = False
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"label",
|
|
"year",
|
|
"parts",
|
|
]
|
|
column_details_list = column_list + [
|
|
"semester_experiments",
|
|
"active_users",
|
|
"groups",
|
|
]
|
|
column_searchable_list = [
|
|
"label",
|
|
"year",
|
|
]
|
|
column_default_sort = [
|
|
("year", True),
|
|
("label", True),
|
|
]
|
|
|
|
def customCreateModel(self, form):
|
|
return Semester.customInitFromOldSemester(
|
|
label=form.label.data,
|
|
year=form.year.data,
|
|
oldSemester=userActiveSemester(),
|
|
transferParts=form.transfer_parts.data,
|
|
transferAssistants=form.transfer_assistants.data,
|
|
)
|
|
|
|
def addMenuLink(space, newSemester):
|
|
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)
|
|
|
|
link.parent = category
|
|
category._children.insert(0, link)
|
|
|
|
def after_model_change(self, form, model, is_created):
|
|
setUserActiveSemester(model.id)
|
|
|
|
SemesterView.addMenuLink(adminSpace, model)
|
|
SemesterView.addMenuLink(assistantSpace, model)
|
|
|
|
|
|
def programQueryFactory():
|
|
return Program.query
|
|
|
|
|
|
class PartView(SecureAdminModelView):
|
|
can_view_details = True
|
|
|
|
column_sortable_list = []
|
|
column_list = [
|
|
"program",
|
|
"number",
|
|
]
|
|
column_details_list = column_list + [
|
|
"part_students",
|
|
]
|
|
form_columns = [
|
|
"program",
|
|
"number",
|
|
]
|
|
|
|
form_extra_fields = {
|
|
"program": QuerySelectField(
|
|
"Program", query_factory=programQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-"
|
|
)
|
|
}
|
|
form_args = {
|
|
"number": {"widget": NumberInput(min=1)},
|
|
}
|
|
|
|
column_searchable_list = ["program.label", "number"]
|
|
|
|
def queryFilter(self):
|
|
return Part.semester == userActiveSemester()
|
|
|
|
def customCreateModel(self, form):
|
|
return Part(program=form.program.data, number=form.number.data, semester=userActiveSemester())
|
|
|
|
|
|
class StudentView(SecureAdminModelView):
|
|
can_view_details = True
|
|
|
|
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",
|
|
}
|
|
column_details_list = column_list + [
|
|
"bachelor_thesis",
|
|
"bachelor_thesis_work_group",
|
|
"note",
|
|
]
|
|
|
|
column_sortable_list = [
|
|
"student_number",
|
|
"first_name",
|
|
"last_name",
|
|
]
|
|
column_searchable_list = column_sortable_list + [
|
|
"uni_email",
|
|
"contact_email",
|
|
]
|
|
|
|
form_excluded_columns = [
|
|
"part_students",
|
|
]
|
|
|
|
form_args = {
|
|
"student_number": {"widget": NumberInput(min=0)},
|
|
"uni_email": {"validators": [Email()]},
|
|
"contact_email": {"validators": [Email()]},
|
|
}
|
|
|
|
column_extra_row_actions = [
|
|
EndpointLinkRowAction(
|
|
icon_class="fa fa-history",
|
|
endpoint="experimentmark.index_view",
|
|
id_arg="flt1_0",
|
|
title="Experiments history",
|
|
)
|
|
]
|
|
|
|
|
|
def partQueryFactory():
|
|
return Part.query.filter(Part.semester == userActiveSemester())
|
|
|
|
|
|
def groupQueryFactory():
|
|
return Group.query.filter(Group.semester == userActiveSemester())
|
|
|
|
|
|
def partFilterOptions():
|
|
if has_request_context():
|
|
parts = Part.query.filter(Part.semester == userActiveSemester())
|
|
return tuple((part.id, f"{part.program.repr()}{part.number}") for part in parts)
|
|
|
|
|
|
class PartStudentView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class PartFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return partFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.part_id == int(value))
|
|
|
|
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):
|
|
def studentQueryFactory():
|
|
return Student.query
|
|
|
|
student = QuerySelectField(
|
|
"Student",
|
|
query_factory=studentQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
part = QuerySelectField(
|
|
"Part",
|
|
query_factory=partQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
group = QuerySelectField(
|
|
"Group",
|
|
query_factory=groupQueryFactory,
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
|
|
class EditForm(CreateForm):
|
|
student = None
|
|
part = None
|
|
|
|
form = EditForm
|
|
|
|
column_list = [
|
|
"student",
|
|
"part",
|
|
"group",
|
|
"final_part_mark",
|
|
"experiment_marks",
|
|
]
|
|
|
|
column_filters = (
|
|
PartFilter(PartStudent, "Part"),
|
|
"student.student_number",
|
|
"student.first_name",
|
|
"student.last_name",
|
|
"group.number",
|
|
"experiment_marks",
|
|
)
|
|
|
|
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",
|
|
),
|
|
]
|
|
|
|
def queryFilter(self):
|
|
return PartStudent.part.has(Part.semester == userActiveSemester())
|
|
|
|
def create_form(self, obj=None):
|
|
form = self.CreateForm
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
def on_model_change(self, form, model, is_created):
|
|
PartStudent.check(model.group, model.part)
|
|
|
|
|
|
def partStudentQueryFactory():
|
|
return PartStudent.query.filter(PartStudent.part.has(Part.semester == userActiveSemester()))
|
|
|
|
|
|
class GroupView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class ProgramFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
programs = Program.query
|
|
return tuple((program.id, program.repr()) for program in programs)
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.program_id == int(value))
|
|
|
|
def formFactory(is_created, group):
|
|
if is_created:
|
|
|
|
def query_factory():
|
|
return partStudentQueryFactory().filter(PartStudent.group == None)
|
|
|
|
else:
|
|
|
|
def query_factory():
|
|
return partStudentQueryFactory().filter(
|
|
or_(
|
|
and_(PartStudent.group == None, PartStudent.part.has(Part.program == group.program)),
|
|
PartStudent.group == group,
|
|
)
|
|
)
|
|
|
|
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!",
|
|
)
|
|
|
|
return CustomForm
|
|
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"number",
|
|
"program",
|
|
"part_students",
|
|
"group_experiments",
|
|
]
|
|
column_details_list = column_list
|
|
column_filters = (
|
|
ProgramFilter(Group, "Program"),
|
|
"number",
|
|
)
|
|
|
|
def queryFilter(self):
|
|
return Group.semester == userActiveSemester()
|
|
|
|
def customCreateModel(self, form):
|
|
return Group.customInit(form.part_students.data)
|
|
|
|
def create_form(self, obj=None):
|
|
form = GroupView.formFactory(is_created=True, group=None)
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
def edit_form(self, obj=None):
|
|
form = GroupView.formFactory(is_created=False, group=obj)
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
|
|
class ExperimentView(SecureAdminModelView):
|
|
class ProgramFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
programs = Program.query
|
|
return tuple((program.id, program.repr()) for program in programs)
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.program_id == int(value))
|
|
|
|
can_view_details = True
|
|
|
|
column_filters = (
|
|
ProgramFilter(Experiment, "Program"),
|
|
"active",
|
|
)
|
|
column_list = [
|
|
"number",
|
|
"program",
|
|
"title",
|
|
"active",
|
|
]
|
|
column_descriptions = {
|
|
"active": "Active experiments are present in new semesters",
|
|
}
|
|
column_details_list = column_list + [
|
|
"description",
|
|
"wiki_link",
|
|
"building",
|
|
"room",
|
|
"responsibility",
|
|
"duration_in_days",
|
|
"semester_experiments",
|
|
]
|
|
column_searchable_list = [
|
|
"number",
|
|
"title",
|
|
]
|
|
|
|
form_columns = column_details_list.copy()
|
|
form_columns.remove("semester_experiments")
|
|
|
|
column_editable_list = [
|
|
"active",
|
|
]
|
|
|
|
form_args = {
|
|
"number": {"widget": NumberInput(min=1)},
|
|
"wiki_link": {"validators": [URL()]},
|
|
"duration_in_days": {"widget": NumberInput(min=1)},
|
|
}
|
|
form_extra_fields = {
|
|
"program": QuerySelectField(
|
|
"Program", query_factory=programQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-"
|
|
)
|
|
}
|
|
|
|
|
|
def assistantQueryFactory():
|
|
return Assistant.query.filter(Assistant.user.has(User.active == True))
|
|
|
|
|
|
class SemesterExperimentView(SecureAdminModelView):
|
|
class ProgramFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
programs = Program.query
|
|
return tuple((program.id, program.repr()) for program in programs)
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.experiment.has(Experiment.program_id == int(value)))
|
|
|
|
class ExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.experiment_id
|
|
|
|
class CreateForm(Form):
|
|
def experimentQueryFactory():
|
|
return Experiment.query.filter(Experiment.active == True)
|
|
|
|
experiment = QuerySelectField(
|
|
"Experiment",
|
|
query_factory=experimentQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
|
|
oral_weighting = DecimalField(
|
|
"Oral weighting",
|
|
validators=[DataRequired(), NumberRange(0, 1)],
|
|
default=0.5,
|
|
description="Between 0 and 1",
|
|
places=2,
|
|
widget=NumberInput(step=0.01),
|
|
)
|
|
protocol_weighting = DecimalField(
|
|
"Protocol weighting",
|
|
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,
|
|
widget=NumberInput(step=0.01),
|
|
)
|
|
final_weighting = DecimalField(
|
|
"Final weighting",
|
|
validators=[DataRequired(), NumberRange(0, 1)],
|
|
default=1.0,
|
|
description="Between 0 and 1",
|
|
places=2,
|
|
widget=NumberInput(step=0.01),
|
|
)
|
|
|
|
assistants = QuerySelectMultipleField("Assistants", query_factory=assistantQueryFactory)
|
|
|
|
class EditForm(CreateForm):
|
|
experiment = None
|
|
|
|
form = EditForm
|
|
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"experiment",
|
|
"assistants",
|
|
]
|
|
column_details_list = column_list + [
|
|
"oral_weighting",
|
|
"protocol_weighting",
|
|
"final_weighting",
|
|
"group_experiments",
|
|
]
|
|
column_filters = (ProgramFilter(SemesterExperiment, "Program"),)
|
|
column_searchable_list = [
|
|
"experiment.number",
|
|
"experiment.title",
|
|
]
|
|
|
|
column_extra_row_actions = [
|
|
ExperimentEndpointLinkRowAction(
|
|
icon_class="fa fa-flask",
|
|
endpoint="experiment.details_view",
|
|
title="Experiment",
|
|
),
|
|
]
|
|
|
|
def create_form(self, obj=None):
|
|
form = self.CreateForm
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
def queryFilter(self):
|
|
return SemesterExperiment.semester == userActiveSemester()
|
|
|
|
def customCreateModel(self, form):
|
|
return SemesterExperiment(
|
|
semester=userActiveSemester(),
|
|
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,
|
|
)
|
|
|
|
def on_model_change(self, form, model, is_created):
|
|
model.checkAndRoundWeightings()
|
|
|
|
def update_model(self, form, model):
|
|
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
|
|
|
|
|
|
class AssistantView(SecureAdminModelView):
|
|
class UserEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.user_id
|
|
|
|
def assistantUserQueryFactory():
|
|
return User.query.filter(User.roles.any(Role.name == "assistant"))
|
|
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"user.first_name",
|
|
"user.last_name",
|
|
"semester_experiments",
|
|
]
|
|
column_details_list = column_list + [
|
|
"user.phone_number",
|
|
"user.mobile_phone_number",
|
|
"user.building",
|
|
"user.room",
|
|
"appointments",
|
|
"experiment_marks",
|
|
]
|
|
column_searchable_list = [
|
|
"user.first_name",
|
|
"user.last_name",
|
|
"user.email",
|
|
]
|
|
column_filters = ("user.active",)
|
|
form_excluded_columns = [
|
|
"experiment_marks",
|
|
"appointments",
|
|
]
|
|
|
|
form_extra_fields = {
|
|
"user": QuerySelectField(
|
|
"User",
|
|
query_factory=assistantUserQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
),
|
|
"semester_experiments": QuerySelectMultipleField(
|
|
"Semester Experiments",
|
|
query_factory=semesterExperimentQueryFactory,
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
),
|
|
}
|
|
|
|
column_extra_row_actions = [
|
|
UserEndpointLinkRowAction(
|
|
icon_class="fa fa-user",
|
|
endpoint="user.details_view",
|
|
title="User",
|
|
),
|
|
]
|
|
|
|
|
|
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",
|
|
]
|
|
column_filters = ("user.active",)
|
|
|
|
column_extra_row_actions = [
|
|
UserEndpointLinkRowAction(
|
|
icon_class="fa fa-user",
|
|
endpoint="user.details_view",
|
|
title="User",
|
|
),
|
|
]
|
|
|
|
|
|
assistantBlankText = "Auto assign if experiment has only one assistant"
|
|
|
|
|
|
def experimentFilterOptions():
|
|
activeExperiments = Experiment.query.filter(Experiment.active == True)
|
|
return tuple(
|
|
(
|
|
f"{activeExperiment.number},{activeExperiment.program_id}",
|
|
f"{activeExperiment.number} {activeExperiment.program.repr()}",
|
|
)
|
|
for activeExperiment in activeExperiments
|
|
)
|
|
|
|
|
|
class GroupExperimentView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class ExperimentFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return experimentFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
values = value.split(",")
|
|
experimentNumber = int(values[0])
|
|
programId = int(values[1])
|
|
|
|
return query.filter(
|
|
self.column.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.program_id == programId)
|
|
),
|
|
self.column.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.number == experimentNumber)
|
|
),
|
|
)
|
|
|
|
class GroupEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.group_id
|
|
|
|
class SemesterExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.semester_experiment_id
|
|
|
|
class CreateForm(Form):
|
|
group = QuerySelectField(
|
|
"Group",
|
|
query_factory=groupQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
semester_experiment = QuerySelectField(
|
|
"Semester Experiment",
|
|
query_factory=semesterExperimentQueryFactory,
|
|
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.",
|
|
)
|
|
appointment1_special = BooleanField("Appointment-1 Special", default=False)
|
|
appointment1_assistant = QuerySelectField(
|
|
"Appointment-1 Assistant",
|
|
query_factory=assistantQueryFactory,
|
|
allow_blank=True,
|
|
blank_text=assistantBlankText,
|
|
)
|
|
|
|
appointment2_date = DateField(
|
|
"Appointment-2 Date", validators=[Optional()], description="Add a second appointment (see above)."
|
|
)
|
|
appointment2_special = BooleanField("Appointment-2 Special", default=False)
|
|
appointment2_assistant = QuerySelectField(
|
|
"Appointment-2 Assistant",
|
|
query_factory=assistantQueryFactory,
|
|
allow_blank=True,
|
|
blank_text=assistantBlankText,
|
|
)
|
|
|
|
form = CreateForm
|
|
|
|
can_edit = False
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"group",
|
|
"semester_experiment",
|
|
"appointments",
|
|
"experiment_marks",
|
|
]
|
|
column_details_list = column_list
|
|
column_filters = (
|
|
ExperimentFilter(GroupExperiment, "Experiment"),
|
|
"group.number",
|
|
"appointments",
|
|
"experiment_marks",
|
|
)
|
|
|
|
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",
|
|
),
|
|
]
|
|
|
|
def queryFilter(self):
|
|
return GroupExperiment.group.has(Group.semester == userActiveSemester())
|
|
|
|
def customCreateModel(self, form):
|
|
return GroupExperiment.customInit(semester_experiment=form.semester_experiment.data, group=form.group.data)
|
|
|
|
def on_model_change(self, form, model, is_created):
|
|
if is_created:
|
|
for date, special, assistant in zip(
|
|
(form.appointment1_date.data, form.appointment2_date.data),
|
|
(form.appointment1_special.data, form.appointment2_special.data),
|
|
(form.appointment1_assistant.data, form.appointment2_assistant.data),
|
|
):
|
|
if date:
|
|
appointment = Appointment.customInit(
|
|
date=date, special=special, group_experiment=model, assistant=assistant
|
|
)
|
|
|
|
self.session.add(appointment)
|
|
|
|
def after_model_change(self, form, model, is_created):
|
|
if is_created:
|
|
if model.appointments:
|
|
flash(f"Appointments {model.appointments} added.", "success")
|
|
|
|
|
|
def groupExperimentQueryFactory():
|
|
return GroupExperiment.query.filter(
|
|
GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester())
|
|
)
|
|
|
|
|
|
def assistantFilterOptions():
|
|
activeAssistants = assistantQueryFactory()
|
|
return tuple((assistant.id, assistant.repr()) for assistant in activeAssistants)
|
|
|
|
|
|
class AppointmentView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class ExperimentFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return experimentFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
values = value.split(",")
|
|
experimentNumber = int(values[0])
|
|
programId = int(values[1])
|
|
|
|
return query.filter(
|
|
self.column.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.program_id == programId)
|
|
)
|
|
),
|
|
self.column.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.number == experimentNumber)
|
|
)
|
|
),
|
|
)
|
|
|
|
class AssistantFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return assistantFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.assistant_id == int(value))
|
|
|
|
class GroupExperimentEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.group_experiment_id
|
|
|
|
class AssistantEndpointLinkRowAction(CustomIdEndpointLinkRowAction):
|
|
def customId(self, row):
|
|
return row.assistant_id
|
|
|
|
class CreateForm(Form):
|
|
group_experiment = QuerySelectField(
|
|
"Group Experiment",
|
|
query_factory=groupExperimentQueryFactory,
|
|
validators=[DataRequired()],
|
|
allow_blank=True,
|
|
blank_text="-",
|
|
)
|
|
date = DateField("Date", validators=[DataRequired()])
|
|
special = BooleanField("Special", default=False)
|
|
assistant = QuerySelectField(
|
|
"Assistant",
|
|
query_factory=assistantQueryFactory,
|
|
allow_blank=True,
|
|
blank_text=assistantBlankText,
|
|
)
|
|
|
|
form = CreateForm
|
|
|
|
column_filters = (
|
|
ExperimentFilter(Appointment, "Experiment"),
|
|
AssistantFilter(Appointment, "Assistant"),
|
|
"group_experiment.group",
|
|
"date",
|
|
"special",
|
|
)
|
|
column_editable_list = [
|
|
"date",
|
|
"special",
|
|
]
|
|
|
|
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",
|
|
),
|
|
]
|
|
|
|
def queryFilter(self):
|
|
return Appointment.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester())
|
|
)
|
|
|
|
def customCreateModel(self, form):
|
|
return Appointment.customInit(
|
|
date=form.date.data,
|
|
special=form.special.data,
|
|
group_experiment=form.group_experiment.data,
|
|
assistant=form.assistant.data,
|
|
)
|
|
|
|
def update_model(self, form, model):
|
|
if None in (form.date, form.special): # For editables
|
|
return super().update_model(form, model)
|
|
|
|
try:
|
|
model.customUpdate(form.date.data, form.special.data, form.assistant.data, form.group_experiment.data)
|
|
|
|
self.on_model_change(form, model, False)
|
|
self.session.commit()
|
|
except Exception as ex:
|
|
flash(str(ex), "error")
|
|
|
|
self.session.rollback()
|
|
return False
|
|
else:
|
|
self.after_model_change(form, model, False)
|
|
return True
|
|
|
|
|
|
class ExperimentMarkView(SecureAdminModelView):
|
|
@expose("/")
|
|
def index_view(self):
|
|
# To update filter options
|
|
self._refresh_filters_cache()
|
|
return super().index_view()
|
|
|
|
class StudentFilter(FilterEqual):
|
|
def validate(self, value):
|
|
if Student.query.get(value):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.part_student.has(PartStudent.student_id == int(value)))
|
|
|
|
class AssistantFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return assistantFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.assistant_id == int(value))
|
|
|
|
class AdminFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
admins = Admin.query.filter(Admin.user.has(User.active == True))
|
|
return tuple((admin.id, admin.repr()) for admin in admins)
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.admin_id == int(value))
|
|
|
|
class ExperimentFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return experimentFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
values = value.split(",")
|
|
experimentNumber = int(values[0])
|
|
programId = int(values[1])
|
|
|
|
return query.filter(
|
|
self.column.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.program_id == programId)
|
|
)
|
|
),
|
|
self.column.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(
|
|
SemesterExperiment.experiment.has(Experiment.number == experimentNumber)
|
|
)
|
|
),
|
|
)
|
|
|
|
class ProgramFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
programs = Program.query
|
|
return tuple((program.id, program.repr()) for program in programs)
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.part_student.has(PartStudent.part.has(Part.program_id == int(value))))
|
|
|
|
class PartFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return partFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(self.column.part_student.has(PartStudent.part_id == int(value)))
|
|
|
|
class SemesterFilter(FilterEqual):
|
|
def get_options(self, view):
|
|
return semesterFilterOptions()
|
|
|
|
def apply(self, query, value, alias=None):
|
|
return query.filter(
|
|
self.column.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(SemesterExperiment.semester_id == int(value))
|
|
)
|
|
)
|
|
|
|
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
|
|
|
|
class CreateForm(Form):
|
|
part_student = QuerySelectField(
|
|
"Part Student",
|
|
query_factory=partStudentQueryFactory,
|
|
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",
|
|
validators=[Optional(), NumberRange(MIN_MARK, MAX_MARK)],
|
|
description=f"Between {MIN_MARK} and {MAX_MARK}",
|
|
)
|
|
protocol_mark = IntegerField(
|
|
"Protocol Mark",
|
|
validators=[Optional(), NumberRange(MIN_MARK, MAX_MARK)],
|
|
description=f"Between {MIN_MARK} and {MAX_MARK}",
|
|
)
|
|
|
|
form = EditForm
|
|
|
|
column_descriptions = {
|
|
"oral_mark": f"Between {MIN_MARK} and {MAX_MARK}",
|
|
"protocol_mark": f"Between {MIN_MARK} and {MAX_MARK}",
|
|
"final_experiment_mark": "Calculated automatically with oral and protocol marks and weightings",
|
|
"assistant": "The last assistant who edited the mark",
|
|
"admin": "The last admin who edited the mark",
|
|
}
|
|
|
|
column_filters = (
|
|
StudentFilter(ExperimentMark, "Student / ID"),
|
|
SemesterFilter(ExperimentMark, "Semester"),
|
|
AssistantFilter(ExperimentMark, "Assistant"),
|
|
AdminFilter(ExperimentMark, "Admin"),
|
|
ExperimentFilter(ExperimentMark, "Experiment"),
|
|
ProgramFilter(ExperimentMark, "Program"),
|
|
PartFilter(ExperimentMark, "Part"),
|
|
"group_experiment.group",
|
|
"oral_mark",
|
|
"protocol_mark",
|
|
"final_experiment_mark",
|
|
)
|
|
column_default_sort = [("oral_mark", False), ("protocol_mark", False)]
|
|
|
|
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",
|
|
),
|
|
]
|
|
|
|
"""
|
|
# Deactivated for the experiments history of a student.
|
|
def queryFilter(self):
|
|
return ExperimentMark.group_experiment.has(
|
|
GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester())
|
|
)
|
|
"""
|
|
|
|
def create_form(self, obj=None):
|
|
form = self.CreateForm
|
|
return form(get_form_data(), obj=obj)
|
|
|
|
def customCreateModel(self, form):
|
|
return ExperimentMark.customInit(
|
|
part_student=form.part_student.data, group_experiment=form.group_experiment.data
|
|
)
|
|
|
|
def update_model(self, form, model):
|
|
if (form.oral_mark and form.oral_mark.data != model.oral_mark) or (
|
|
form.protocol_mark and form.protocol_mark.data != model.protocol_mark
|
|
):
|
|
model.admin = current_user.admin
|
|
|
|
ret = super().update_model(form, model)
|
|
|
|
model.part_student.checkThenSetFinalPartMark()
|
|
|
|
return ret
|
|
else:
|
|
# Nothing changed
|
|
return True
|
|
|
|
|
|
class ProgramView(SecureAdminModelView):
|
|
can_export = False
|
|
can_set_page_size = False
|
|
|
|
can_view_details = True
|
|
|
|
column_list = [
|
|
"label",
|
|
]
|
|
form_excluded_columns = [
|
|
"parts",
|
|
"experiments",
|
|
"groups",
|
|
]
|
|
column_details_list = column_list + form_excluded_columns
|
|
|
|
|
|
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.",
|
|
)
|
|
submit = SubmitField(
|
|
label="Upload and import",
|
|
)
|
|
|
|
@expose(methods=("GET", "POST"))
|
|
def index(self):
|
|
form = ImportView.FileForm()
|
|
|
|
if form.validate_on_submit():
|
|
f = form.file.data
|
|
filename = secure_filename(f.filename)
|
|
|
|
directory = "db/import_files"
|
|
Path(directory).mkdir(exist_ok=True)
|
|
|
|
filePath = directory + f"/{filename}"
|
|
f.save(filePath)
|
|
|
|
try:
|
|
importFromFile(filePath)
|
|
except Exception as exc:
|
|
flash(str(exc), "error")
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
return self.render("import.html", form=form)
|
|
|
|
|
|
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 userActiveSemester().semester_experiments:
|
|
semesterExperiment.updateFinalExperimentAndPartMarks()
|
|
|
|
flash("Manually updated all final experiment and part marks", "success")
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
return self.render("actions.html", form=form)
|
|
|
|
|
|
class AnalysisView(SecureAdminBaseView):
|
|
class AnalysisForm(FlaskForm):
|
|
assistantMarksSubmit = SubmitField(
|
|
label="Assistant's marks",
|
|
)
|
|
finalPartMarksSubmit = SubmitField(
|
|
label="Final part marks",
|
|
)
|
|
|
|
def htmlFig(fig):
|
|
buf = BytesIO()
|
|
fig.savefig(buf, format="png")
|
|
|
|
return b64encode(buf.getbuffer()).decode("ascii")
|
|
|
|
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")
|
|
|
|
N = data.size
|
|
title += f"\nN = {N}"
|
|
|
|
if N > 0:
|
|
hist = ax.hist(
|
|
data,
|
|
bins=np.arange(MAX_MARK) - 0.5,
|
|
)
|
|
ax.set_yticks(np.arange(N + 1))
|
|
title += f" | mean = {round(np.mean(data), 1)}"
|
|
else:
|
|
ax.set_yticks(np.arange(2))
|
|
|
|
ax.set_title(title)
|
|
|
|
return AnalysisView.htmlFig(fig)
|
|
|
|
def markHists(markType, activeAssistants):
|
|
attr = markType.lower() + "_mark"
|
|
markTypeTitleAddition = f" | {markType} marks"
|
|
|
|
hists = [
|
|
AnalysisView.markHist(
|
|
data=np.array([getattr(experimentMark, attr) for experimentMark in assistant.experiment_marks]),
|
|
title=assistant.repr() + markTypeTitleAddition,
|
|
)
|
|
for assistant in activeAssistants
|
|
]
|
|
|
|
hists.append(
|
|
AnalysisView.markHist(
|
|
data=np.hstack(
|
|
[
|
|
[getattr(experimentMark, attr) for experimentMark in assistant.experiment_marks]
|
|
for assistant in activeAssistants
|
|
]
|
|
),
|
|
title="All" + markTypeTitleAddition,
|
|
)
|
|
)
|
|
|
|
return hists
|
|
|
|
@expose(methods=("GET", "POST"))
|
|
def index(self):
|
|
form = AnalysisView.AnalysisForm()
|
|
|
|
if form.validate_on_submit():
|
|
if form.assistantMarksSubmit.data:
|
|
activeAssistants = assistantQueryFactory()
|
|
|
|
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,
|
|
)
|
|
|
|
if form.finalPartMarksSubmit.data:
|
|
parts = userActiveSemester().parts
|
|
activeSemesterFinalPartMarksHists = [
|
|
AnalysisView.markHist(
|
|
data=np.array([partStudent.final_part_mark for partStudent in part.part_students]),
|
|
title=part.repr(),
|
|
)
|
|
for part in parts
|
|
]
|
|
|
|
semesters = sortedSemestersStartingWithNewest()
|
|
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()
|
|
ax = fig.subplots()
|
|
x = range(1, len(meanFinalPartMarks) + 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)
|
|
|
|
meanFinalPartMarksPlot = AnalysisView.htmlFig(fig)
|
|
|
|
return self.render(
|
|
"analysis/final_part_marks.html",
|
|
activeSemesterFinalPartMarksHists=activeSemesterFinalPartMarksHists,
|
|
meanFinalPartMarksPlot=meanFinalPartMarksPlot,
|
|
)
|
|
|
|
return self.render("analysis/analysis.html", form=form)
|
|
|
|
|
|
class DocsView(SecureAdminBaseView):
|
|
@expose("/")
|
|
def index(self):
|
|
return self.render("docs/admin.html")
|
|
|
|
|
|
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))
|
|
adminSpace.add_view(PartView(Part, db.session))
|
|
adminSpace.add_view(AssistantView(Assistant, db.session))
|
|
adminSpace.add_view(AdminView(Admin, db.session))
|
|
adminSpace.add_view(UserView(User, db.session))
|
|
adminSpace.add_view(RoleView(Role, db.session))
|
|
adminSpace.add_view(ProgramView(Program, db.session))
|
|
adminSpace.add_view(ImportView(name="Import"))
|
|
adminSpace.add_view(ActionsView(name="Actions"))
|
|
adminSpace.add_view(AnalysisView(name="Analysis"))
|
|
adminSpace.add_view(DocsView(name="Docs"))
|
|
|
|
initActiveSemesterMenuLinks(adminSpace)
|