diff --git a/advlabdb/adminModelViews.py b/advlabdb/adminModelViews.py index 80cf234..6475150 100644 --- a/advlabdb/adminModelViews.py +++ b/advlabdb/adminModelViews.py @@ -1,5 +1,8 @@ +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 @@ -10,6 +13,7 @@ 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 @@ -36,6 +40,8 @@ from advlabdb.customClasses import ( from advlabdb.database_import import importFromFile from advlabdb.exceptions import DataBaseException, ModelViewException from advlabdb.models import ( + MAX_MARK, + MIN_MARK, Admin, Appointment, Assistant, @@ -1270,20 +1276,20 @@ class ExperimentMarkView(SecureAdminModelView): class EditForm(Form): oral_mark = IntegerField( "Oral Mark", - validators=[NumberRange(min=0, max=15), Optional()], - description="Between 0 and 15", + validators=[NumberRange(min=MIN_MARK, max=MAX_MARK), Optional()], + description=f"Between {MIN_MARK} and {MAX_MARK}", ) protocol_mark = IntegerField( "Protocol Mark", - validators=[NumberRange(min=0, max=15), Optional()], - description="Between 0 and 15", + validators=[NumberRange(min=MIN_MARK, max=MAX_MARK), Optional()], + description=f"Between {MIN_MARK} and {MAX_MARK}", ) form = EditForm column_descriptions = { - "oral_mark": "Between 0 and 15", - "protocol_mark": "Between 0 and 15", + "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", @@ -1429,6 +1435,65 @@ class ActionsView(SecureAdminBaseView): return self.render("actions.html", form=form) +class AnalysisView(SecureAdminBaseView): + class AnalysisForm(FlaskForm): + assistantMarksSubmit = SubmitField( + label="Assistant's marks", + ) + + def markHist(data, title): + fig = Figure() + ax = fig.subplots() + ax.set_title(title) + ax.set_xlim(MIN_MARK - 0.5, MAX_MARK + 0.5) + ax.set_xticks(np.arange(MAX_MARK + 1)) + ax.set_xlabel("Mark") + + if data: + hist = ax.hist( + data, + bins=np.arange(MAX_MARK) - 0.5, + ) + ax.set_yticks(np.arange(len(data) + 1)) + else: + ax.set_yticks(np.arange(2)) + + buf = BytesIO() + fig.savefig(buf, format="png") + + return b64encode(buf.getbuffer()).decode("ascii") + + def markHists(markType, activeAssistants): + attr = markType.lower() + "_mark" + return [ + AnalysisView.markHist( + data=[getattr(experimentMark, attr) for experimentMark in assistant.experiment_marks], + title=f"{assistant.repr()} - {markType} Marks", + ) + for assistant in activeAssistants + ] + + @expose(methods=("GET", "POST")) + def index(self): + form = AnalysisView.AnalysisForm() + + if form.validate_on_submit(): + if form.assistantMarksSubmit.data: + activeAssistants = Assistant.query.filter(Assistant.user.has(User.active == True)) + + 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, + ) + + return self.render("analysis/analysis.html", form=form) + + class DocsView(SecureAdminBaseView): @expose("/") def index(self): @@ -1452,6 +1517,7 @@ 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) diff --git a/advlabdb/templates/analysis/analysis.html b/advlabdb/templates/analysis/analysis.html new file mode 100644 index 0000000..2cf06dd --- /dev/null +++ b/advlabdb/templates/analysis/analysis.html @@ -0,0 +1,13 @@ +{% from "macros.html" import information %} +{% extends "admin/master.html" %} + +{% block body %} + {{information(current_user, userActiveSemester, role="admin")}} + +
+ +
+ {{ form.csrf_token }} + {{ form.assistantMarksSubmit }} +
+{% endblock body %} diff --git a/advlabdb/templates/analysis/assistant_marks.html b/advlabdb/templates/analysis/assistant_marks.html new file mode 100644 index 0000000..b32d475 --- /dev/null +++ b/advlabdb/templates/analysis/assistant_marks.html @@ -0,0 +1,5 @@ +{% for histInd in histIndices %} + + +
+{% endfor %}