diff --git a/advlabdb/adminModelViews.py b/advlabdb/adminModelViews.py index 45df2d1..bd14b20 100644 --- a/advlabdb/adminModelViews.py +++ b/advlabdb/adminModelViews.py @@ -1,8 +1,5 @@ -from base64 import b64encode -from io import BytesIO from pathlib import Path -import numpy as np from flask import flash, has_request_context, redirect from flask_admin import Admin as FlaskAdmin from flask_admin import expose @@ -15,7 +12,6 @@ from flask_security.changeable import admin_change_password from flask_security.utils import hash_password from flask_wtf import FlaskForm from flask_wtf.file import FileAllowed, FileField, FileRequired -from matplotlib.figure import Figure from sqlalchemy import and_, not_, or_, select from werkzeug.utils import secure_filename from wtforms.fields import ( @@ -60,6 +56,7 @@ from .advlabdb_independent_funs import ( flashRandomPassword, str_without_semester_formatter, ) +from .analysis import assistant_marks_analysis, final_part_marks_analysis from .custom_classes import ( SecureAdminBaseView, SecureAdminIndexView, @@ -1372,154 +1369,22 @@ class ActionsView(SecureAdminBaseView): class AnalysisView(SecureAdminBaseView): class AnalysisForm(FlaskForm): - assistantMarksSubmit = SubmitField( - label="Assistant's marks", + assistant_marks_submit = SubmitField( + label="Active assistant's marks in all semesters", ) - finalPartMarksSubmit = SubmitField( - label="Final part marks", + final_part_marks_submit = SubmitField( + label="Final part marks in active semester", ) - @staticmethod - def htmlFig(fig): - buf = BytesIO() - fig.savefig(buf, format="png") - - return b64encode(buf.getbuffer()).decode("ascii") - - @staticmethod - 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: - 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) - - @staticmethod - 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) - - @staticmethod - def markHists(markType, activeAssistants): - attr = markType.lower() + "_mark" - markTypeTitleAddition = f" | {markType} marks" - marks = [AnalysisView.get_experiment_marks(assistant, attr) for assistant in activeAssistants] - - hists = [ - AnalysisView.markHist( - data=marks[i], - title=str(activeAssistants[i]) + markTypeTitleAddition, - ) - for i in range(len(marks)) - ] - - hists.append( - AnalysisView.markHist( - data=np.hstack(marks), - title="All" + markTypeTitleAddition, - ) - ) - - return hists - - @staticmethod - 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) - @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.jinja.html", - histIndices=range(len(oralMarkHists)), - oralMarkHists=oralMarkHists, - protocolMarkHists=protocolMarkHists, - ) - - if form.finalPartMarksSubmit.data: - parts = current_user.active_semester.parts - activeSemesterFinalPartMarksHists = [ - AnalysisView.markHist( - data=AnalysisView.get_final_part_marks(part), - title=str(part), - ) - for part in parts - ] - - semesters = Semester.sortedSemestersStartingWithNewest() - 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() - 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) - - meanFinalPartMarksPlot = AnalysisView.htmlFig(fig) - - return self.render( - "analysis/final_part_marks.jinja.html", - activeSemesterFinalPartMarksHists=activeSemesterFinalPartMarksHists, - meanFinalPartMarksPlot=meanFinalPartMarksPlot, - ) + if form.assistant_marks_submit.data: + return assistant_marks_analysis(self) + elif form.final_part_marks_submit.data: + return final_part_marks_analysis(self) return self.render("analysis/analysis.jinja.html", form=form) diff --git a/advlabdb/analysis.py b/advlabdb/analysis.py new file mode 100644 index 0000000..d8ab58b --- /dev/null +++ b/advlabdb/analysis.py @@ -0,0 +1,142 @@ +from base64 import b64encode +from io import BytesIO + +import numpy as np +from flask_login import current_user +from matplotlib.figure import Figure +from matplotlib.ticker import MaxNLocator +from sqlalchemy import select + +from .models import MAX_MARK, MIN_MARK, Assistant, Semester, User, db + + +def html_fig(fig): + buf = BytesIO() + fig.savefig(buf, format="png") + + return b64encode(buf.getbuffer()).decode("ascii") + + +def mark_hist(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)) + # Only integer ticks + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + ax.set_xlabel("Mark") + + N = data.size + title += f"\nN = {N}" + + if N > 0: + ax.hist( + data, + bins=np.arange(MAX_MARK + 2) - 0.5, + ) + title += f" | mean = {round(np.mean(data), 1)}" + + ax.set_title(title) + + return html_fig(fig) + + +def get_experiment_marks(assistant, attr): + data = [] + + for experiment_mark in assistant.experiment_marks: + mark = getattr(experiment_mark, attr) + if mark is not None: + data.append(mark) + + return np.array(data) + + +def mark_hists(markType, active_assistants): + attr = markType.lower() + "_mark" + mark_type_title_addition = f" | {markType} marks" + marks = [get_experiment_marks(assistant, attr) for assistant in active_assistants] + + hists = [ + mark_hist( + data=marks[i], + title=str(active_assistants[i]) + mark_type_title_addition, + ) + for i in range(len(marks)) + ] + + hists.append( + mark_hist( + data=np.hstack(marks), + title="All" + mark_type_title_addition, + ) + ) + + return hists + + +def assistant_marks_analysis(cls): + active_assistants = db.session.scalars(select(Assistant).join(User).where(User.active == True)).all() + + oral_mark_hists = mark_hists("Oral", active_assistants) + protocol_mark_hists = mark_hists("Protocol", active_assistants) + + return cls.render( + "analysis/assistant_marks.jinja.html", + hist_indices=range(len(oral_mark_hists)), + oral_mark_hists=oral_mark_hists, + protocol_mark_hists=protocol_mark_hists, + ) + + +def get_final_part_marks(part): + data = [] + + for part_student in part.part_students: + mark = part_student.final_part_mark + if mark is not None: + data.append(mark) + + return np.array(data) + + +def final_part_marks_analysis(cls): + parts = current_user.active_semester.parts + + active_semester_final_part_marks_hists = [ + mark_hist( + data=get_final_part_marks(part), + title=part.str(), + ) + for part in parts + ] + + semesters = db.session.scalars(select(Semester)).all() + mean_final_part_marks = np.array( + [np.mean(np.hstack([get_final_part_marks(part) for part in semester.parts])) for semester in semesters] + ) + + fig = Figure() + len_mean_final_part_marks = mean_final_part_marks.size + + ax = fig.subplots() + x = range(len_mean_final_part_marks) + ax.plot( + x, + mean_final_part_marks, + marker="d", + ) + ax.set_xticks(x, [semester.str() for semester in semesters]) + + ax.set_xlabel("Semester") + ax.set_ylabel("Mean final experiment mark") + + ax.set_title("Mean final experiment mark over all semesters") + + mean_final_part_mark_plot = html_fig(fig) + + return cls.render( + "analysis/final_part_marks.jinja.html", + active_semester_final_part_marks_hists=active_semester_final_part_marks_hists, + mean_final_part_mark_plot=mean_final_part_mark_plot, + ) diff --git a/advlabdb/templates/analysis/analysis.jinja.html b/advlabdb/templates/analysis/analysis.jinja.html index 3b05596..73e7097 100644 --- a/advlabdb/templates/analysis/analysis.jinja.html +++ b/advlabdb/templates/analysis/analysis.jinja.html @@ -7,9 +7,14 @@
- {{ form.csrf_token }} - {{ form.assistantMarksSubmit }} -
- {{ form.finalPartMarksSubmit }} + {% for field in form %} + {% if field.widget.input_type == "submit" %} + {{ field(class="btn btn-primary btn-block") }} + +
+ {% else %} + {{ field() }} + {% endif %} + {% endfor %}
{% endblock body %} diff --git a/advlabdb/templates/analysis/assistant_marks.jinja.html b/advlabdb/templates/analysis/assistant_marks.jinja.html index 2206c5b..fb5bbf1 100644 --- a/advlabdb/templates/analysis/assistant_marks.jinja.html +++ b/advlabdb/templates/analysis/assistant_marks.jinja.html @@ -6,17 +6,36 @@
+

Assistant's marks analysis

+

- This page shows an analysis of all marks of all assistants with an active user. -
- The marks are from all semesters, not only the active semester. + The histograms on this page (except the last two histograms) show the oral and protocol marks of each active assistant individually. +

+

+ The last two histograms show the oral and protocol marks of all active assistants together. +

+

+ An active assistant is an assistant with an active user. The marks are from all semesters, not only from the active semester.

+

Export

+

+ You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally Ctrl + p. Select "Save as (PDF) file" afterwards instead of a printer! +

+ +

- {% for histInd in histIndices %} - - + {% for hist_ind in hist_indices %} +
+
+ +
+
+ +
+
+
{% endfor %} {% endblock body %} diff --git a/advlabdb/templates/analysis/final_part_marks.jinja.html b/advlabdb/templates/analysis/final_part_marks.jinja.html index 4dcf8a0..e74d571 100644 --- a/advlabdb/templates/analysis/final_part_marks.jinja.html +++ b/advlabdb/templates/analysis/final_part_marks.jinja.html @@ -6,10 +6,32 @@
- {% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %} - -
- {% endfor %} +

Final part marks analysis

- +

+ The histograms on this page show the final experiment marks of each part in the active semester. +

+

+ The plot at the end of the page shows the course of the mean value of the final experiment marks in all parts of each semester over all semesters. +

+ +

Export

+

+ You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally Ctrl + p. Select "Save as (PDF) file" afterwards instead of a printer! +

+ +
+
+ +
+ {% for active_semester_final_part_marks_hist in active_semester_final_part_marks_hists %} + + +
+ {% endfor %} + + +
+ +
{% endblock body %}