mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-12-04 22:40:30 +00:00
Compare commits
13 commits
9bc1a334be
...
b02a6683f7
Author | SHA1 | Date | |
---|---|---|---|
b02a6683f7 | |||
a16e0bf33d | |||
0f7700b11a | |||
b6e54ba0c0 | |||
b1195fff7f | |||
19819d7e99 | |||
a66d95ffb5 | |||
0b4316a713 | |||
1d8c5db16c | |||
f212ddf181 | |||
8717f42bac | |||
8846a867f5 | |||
6edb47fed9 |
33 changed files with 326 additions and 296 deletions
|
@ -16,12 +16,12 @@ repos:
|
|||
args: [--py39-plus]
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.11.0
|
||||
rev: v1.12.4
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 22.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
|
@ -41,7 +41,7 @@ repos:
|
|||
- id: flake8
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 1.2.0b3
|
||||
rev: 1.2.0rc1
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-export
|
||||
|
|
|
@ -59,7 +59,6 @@ from .advlabdb_independent_funs import (
|
|||
flashRandomPassword,
|
||||
str_without_semester_formatter,
|
||||
)
|
||||
from .assistantModelViews import assistantSpace
|
||||
from .custom_classes import (
|
||||
SecureAdminBaseView,
|
||||
SecureAdminIndexView,
|
||||
|
@ -69,7 +68,6 @@ from .database_import import importFromFile
|
|||
from .exceptions import ModelViewException
|
||||
from .model_dependent_funs import (
|
||||
generate_new_password_field,
|
||||
initActiveSemesterMenuLinks,
|
||||
mark_field,
|
||||
user_info_fields,
|
||||
)
|
||||
|
@ -130,12 +128,20 @@ class UserView(SecureAdminModelView):
|
|||
|
||||
@staticmethod
|
||||
def semesterQueryFactory():
|
||||
return Semester.query
|
||||
return Semester.query.order_by(Semester.id.desc())
|
||||
|
||||
@staticmethod
|
||||
def default_roles():
|
||||
return [user_datastore.find_role("assistant")]
|
||||
|
||||
active_semester = QuerySelectField(
|
||||
"Active Semester",
|
||||
query_factory=semesterQueryFactory,
|
||||
validators=[DataRequired()],
|
||||
default=Semester.lastSemester,
|
||||
description="Not fixed and users (including assistants) can change it.",
|
||||
)
|
||||
|
||||
email = StringField(
|
||||
"Email",
|
||||
validators=[DataRequired(), Email()],
|
||||
|
@ -170,13 +176,6 @@ class UserView(SecureAdminModelView):
|
|||
"Active",
|
||||
default=True,
|
||||
)
|
||||
active_semester = QuerySelectField(
|
||||
"Active Semester",
|
||||
query_factory=semesterQueryFactory,
|
||||
validators=[DataRequired()],
|
||||
default=Semester.lastSemester,
|
||||
description="Not fixed and users (including assistants) can change it.",
|
||||
)
|
||||
|
||||
class EditForm(CreateForm):
|
||||
semester_experiments = None
|
||||
|
@ -280,6 +279,9 @@ class UserView(SecureAdminModelView):
|
|||
admin = Admin(user=model)
|
||||
self.session.add(admin)
|
||||
|
||||
# Lower email
|
||||
model.email = model.email.lower()
|
||||
|
||||
def after_model_change(self, form, model, is_created):
|
||||
if is_created:
|
||||
flash(
|
||||
|
@ -368,26 +370,9 @@ class SemesterView(SecureAdminModelView):
|
|||
transferAssistants=form.transfer_assistants.data,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def addMenuLink(space, newSemester):
|
||||
categoryText = "Active semester"
|
||||
link = MenuLink(
|
||||
name=str(newSemester),
|
||||
url=url_for("main.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):
|
||||
current_user.setActiveSemester(model)
|
||||
|
||||
SemesterView.addMenuLink(adminSpace, model)
|
||||
SemesterView.addMenuLink(assistantSpace, model)
|
||||
|
||||
|
||||
def programQueryFactory():
|
||||
return Program.query
|
||||
|
@ -483,10 +468,17 @@ class StudentView(SecureAdminModelView):
|
|||
}
|
||||
|
||||
def on_model_change(self, form, model, is_created):
|
||||
# Don't save contact_email if it is similar to uni_email
|
||||
# No need to strip strings because of email form validation
|
||||
if model.contact_email is not None and model.contact_email == model.uni_email:
|
||||
model.contact_email = None
|
||||
# Lower uni email
|
||||
model.uni_email = model.uni_email.lower()
|
||||
|
||||
if model.contact_email is not None:
|
||||
# Lower contact email
|
||||
model.contact_email = model.contact_email.lower()
|
||||
|
||||
# Don't save contact_email if it is similar to uni_email
|
||||
# No need to strip strings because of email form validation
|
||||
if model.contact_email == model.uni_email:
|
||||
model.contact_email = None
|
||||
|
||||
|
||||
def partQueryFactory():
|
||||
|
@ -1298,7 +1290,7 @@ class ImportView(SecureAdminBaseView):
|
|||
except Exception as ex:
|
||||
flash(str(ex), "error")
|
||||
|
||||
return self.render("import.html", form=form)
|
||||
return self.render("import.jinja.html", form=form)
|
||||
|
||||
|
||||
class ActionsView(SecureAdminBaseView):
|
||||
|
@ -1320,7 +1312,7 @@ class ActionsView(SecureAdminBaseView):
|
|||
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
return self.render("actions.html", form=form)
|
||||
return self.render("actions.jinja.html", form=form)
|
||||
|
||||
|
||||
class AnalysisView(SecureAdminBaseView):
|
||||
|
@ -1420,7 +1412,7 @@ class AnalysisView(SecureAdminBaseView):
|
|||
protocolMarkHists = AnalysisView.markHists("Protocol", activeAssistants)
|
||||
|
||||
return self.render(
|
||||
"analysis/assistant_marks.html",
|
||||
"analysis/assistant_marks.jinja.html",
|
||||
histIndices=range(len(oralMarkHists)),
|
||||
oralMarkHists=oralMarkHists,
|
||||
protocolMarkHists=protocolMarkHists,
|
||||
|
@ -1469,18 +1461,18 @@ class AnalysisView(SecureAdminBaseView):
|
|||
meanFinalPartMarksPlot = AnalysisView.htmlFig(fig)
|
||||
|
||||
return self.render(
|
||||
"analysis/final_part_marks.html",
|
||||
"analysis/final_part_marks.jinja.html",
|
||||
activeSemesterFinalPartMarksHists=activeSemesterFinalPartMarksHists,
|
||||
meanFinalPartMarksPlot=meanFinalPartMarksPlot,
|
||||
)
|
||||
|
||||
return self.render("analysis/analysis.html", form=form)
|
||||
return self.render("analysis/analysis.jinja.html", form=form)
|
||||
|
||||
|
||||
class DocsView(SecureAdminBaseView):
|
||||
@expose("/")
|
||||
def index(self):
|
||||
return self.render("docs/docs.html", role="admin")
|
||||
return self.render("docs/docs.jinja.html", role="admin")
|
||||
|
||||
|
||||
def init_admin_model_views(app):
|
||||
|
@ -1505,4 +1497,4 @@ def init_admin_model_views(app):
|
|||
adminSpace.add_view(AnalysisView(name="Analysis", url="analysis"))
|
||||
adminSpace.add_view(DocsView(name="Docs", url="docs"))
|
||||
|
||||
initActiveSemesterMenuLinks(adminSpace, app)
|
||||
adminSpace.add_link(MenuLink(name="Logout", url="/logout"))
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
from flask import flash, redirect, request, url_for
|
||||
from flask_admin import Admin as FlaskAdmin
|
||||
from flask_admin import expose
|
||||
from flask_admin.contrib.sqla.fields import QuerySelectField
|
||||
from flask_admin.menu import MenuLink
|
||||
from flask_admin.model.template import EndpointLinkRowAction
|
||||
from flask_login import current_user
|
||||
from flask_security.changeable import admin_change_password
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from .advlabdb_independent_funs import (
|
||||
deep_getattr,
|
||||
|
@ -21,12 +24,11 @@ from .exceptions import ModelViewException
|
|||
from .forms import assistant_group_experiment_form_factory
|
||||
from .model_dependent_funs import (
|
||||
generate_new_password_field,
|
||||
initActiveSemesterMenuLinks,
|
||||
parse_selection_mark_field,
|
||||
user_info_fields,
|
||||
)
|
||||
from .model_independent_funs import randomPassword, reportBadAttempt
|
||||
from .models import Assistant, GroupExperiment, SemesterExperiment, User, db
|
||||
from .models import Assistant, GroupExperiment, Semester, SemesterExperiment, User, db
|
||||
|
||||
assistantSpace = FlaskAdmin(
|
||||
name="Assistant@AdvLabDB",
|
||||
|
@ -168,7 +170,7 @@ class AssistantGroupExperimentView(SecureAssistantModelView):
|
|||
final_experiment_marks = [experiment_mark.final_experiment_mark for experiment_mark in experiment_marks]
|
||||
|
||||
return self.render(
|
||||
"assistant_group_experiment_form.html",
|
||||
"assistant_group_experiment_form.jinja.html",
|
||||
form=form,
|
||||
experiment_label=group_experiment.semester_experiment.experiment.str(),
|
||||
group_number=group_experiment.group.number,
|
||||
|
@ -184,6 +186,19 @@ class AssistantGroupExperimentView(SecureAssistantModelView):
|
|||
|
||||
class AssistantUserView(SecureAssistantModelView):
|
||||
class EditForm(FlaskForm):
|
||||
@staticmethod
|
||||
def semesterQueryFactory():
|
||||
# Show only last two semesters to assistants
|
||||
return Semester.query.order_by(Semester.id.desc()).limit(2)
|
||||
|
||||
active_semester = QuerySelectField(
|
||||
"Active Semester",
|
||||
query_factory=semesterQueryFactory,
|
||||
validators=[DataRequired()],
|
||||
default=Semester.lastSemester,
|
||||
description="You should change the active semester to the last semester. Do not forget to click save!",
|
||||
)
|
||||
|
||||
phone_number, mobile_phone_number, building, room = user_info_fields()
|
||||
|
||||
generate_new_password = generate_new_password_field()
|
||||
|
@ -220,7 +235,7 @@ class AssistantUserView(SecureAssistantModelView):
|
|||
class AssistantDocsView(SecureAssistantBaseView):
|
||||
@expose("/")
|
||||
def index(self):
|
||||
return self.render("docs/docs.html", role="assistant")
|
||||
return self.render("docs/docs.jinja.html", role="assistant")
|
||||
|
||||
|
||||
def init_assistant_model_views(app):
|
||||
|
@ -230,4 +245,4 @@ def init_assistant_model_views(app):
|
|||
assistantSpace.add_view(AssistantUserView(User, url="user"))
|
||||
assistantSpace.add_view(AssistantDocsView(name="Docs", url="docs"))
|
||||
|
||||
initActiveSemesterMenuLinks(assistantSpace, app)
|
||||
assistantSpace.add_link(MenuLink(name="Logout", url="/logout"))
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask_admin.contrib.sqla import ModelView
|
|||
from flask_admin.helpers import get_form_data
|
||||
from flask_admin.model.helpers import get_mdict_item_or_list
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from .exceptions import DatabaseException, ModelViewException
|
||||
from .model_independent_funs import reportBadAttempt
|
||||
|
@ -12,8 +12,6 @@ from .models import (
|
|||
Assistant,
|
||||
ExperimentMark,
|
||||
GroupExperiment,
|
||||
Part,
|
||||
PartStudent,
|
||||
SemesterExperiment,
|
||||
db,
|
||||
get_count,
|
||||
|
@ -48,22 +46,21 @@ class SecureAdminIndexView(CustomIndexView):
|
|||
|
||||
@expose("/")
|
||||
def index(self):
|
||||
active_semester_experiment_mark_ids_stmt = (
|
||||
select(ExperimentMark.final_experiment_mark)
|
||||
.join(PartStudent)
|
||||
.join(Part)
|
||||
.where(Part.semester == current_user.active_semester)
|
||||
)
|
||||
number_of_all_experiment_marks = get_count(active_semester_experiment_mark_ids_stmt)
|
||||
|
||||
number_of_missing_final_experiment_marks = get_count(
|
||||
active_semester_experiment_mark_ids_stmt.where(ExperimentMark.final_experiment_mark == None)
|
||||
assistants_num_missing = db.session.execute(
|
||||
select(Assistant, func.count())
|
||||
.join(Assistant.semester_experiments)
|
||||
.where(SemesterExperiment.semester == current_user.active_semester)
|
||||
.join(SemesterExperiment.group_experiments)
|
||||
.where(GroupExperiment.experiment_marks_missing == True)
|
||||
.join(GroupExperiment.experiment_marks)
|
||||
.where(ExperimentMark.final_experiment_mark == None)
|
||||
.group_by(Assistant.id)
|
||||
.order_by(func.count().desc())
|
||||
)
|
||||
|
||||
return self.render(
|
||||
"admin_index.html",
|
||||
number_of_missing_final_experiment_marks=number_of_missing_final_experiment_marks,
|
||||
number_of_all_experiment_marks=number_of_all_experiment_marks,
|
||||
"admin_index.jinja.html",
|
||||
assistants_num_missing=assistants_num_missing,
|
||||
)
|
||||
|
||||
|
||||
|
@ -89,7 +86,7 @@ class SecureAssistantIndexView(CustomIndexView):
|
|||
)
|
||||
|
||||
return self.render(
|
||||
"assistant_index.html",
|
||||
"assistant_index.jinja.html",
|
||||
number_of_missing_final_experiment_marks=number_of_missing_final_experiment_marks,
|
||||
number_of_all_experiment_marks=number_of_all_experiment_marks,
|
||||
)
|
||||
|
@ -230,14 +227,14 @@ class SecureAdminModelView(CustomModelView):
|
|||
column_display_actions = True
|
||||
can_view_details = True
|
||||
|
||||
list_template = "admin_list.html"
|
||||
create_template = "admin_create.html"
|
||||
edit_template = "admin_edit.html"
|
||||
details_template = "admin_details.html"
|
||||
list_template = "admin_list.jinja.html"
|
||||
create_template = "admin_create.jinja.html"
|
||||
edit_template = "admin_edit.jinja.html"
|
||||
details_template = "admin_details.jinja.html"
|
||||
|
||||
def __init__(self, model, **kwargs):
|
||||
url = get_url(kwargs)
|
||||
super().__init__(model, db.session, endpoint="admin_" + url, **kwargs)
|
||||
super().__init__(model, db.session, endpoint="admin_" + url, category="Tables", **kwargs)
|
||||
|
||||
def is_accessible(self):
|
||||
return adminViewIsAccessible()
|
||||
|
@ -250,10 +247,10 @@ class SecureAssistantModelView(CustomModelView):
|
|||
can_edit = False
|
||||
column_display_actions = False
|
||||
|
||||
list_template = "assistant_list.html"
|
||||
create_template = "assistant_create.html"
|
||||
edit_template = "assistant_edit.html"
|
||||
details_template = "assistant_details.html"
|
||||
list_template = "assistant_list.jinja.html"
|
||||
create_template = "assistant_create.jinja.html"
|
||||
edit_template = "assistant_edit.jinja.html"
|
||||
details_template = "assistant_details.jinja.html"
|
||||
|
||||
"""
|
||||
SECURITY NOTES:
|
||||
|
|
|
@ -6,7 +6,7 @@ from flask import flash
|
|||
from sqlalchemy import select
|
||||
|
||||
from . import data_dir
|
||||
from .exceptions import DataBaseImportException
|
||||
from .exceptions import DatabaseImportException
|
||||
from .models import (
|
||||
Appointment,
|
||||
Assistant,
|
||||
|
@ -37,14 +37,14 @@ def nullable(entry):
|
|||
if is_null(entry):
|
||||
return None
|
||||
|
||||
return entry
|
||||
return entry.strip()
|
||||
|
||||
|
||||
def not_nullable(entry):
|
||||
if is_null(entry):
|
||||
raise DataBaseImportException("Unnullable entry is NULL!")
|
||||
raise DatabaseImportException("Unnullable entry is NULL!")
|
||||
|
||||
return entry
|
||||
return entry.strip()
|
||||
|
||||
|
||||
def importFromFile(filePath: Path):
|
||||
|
@ -54,7 +54,7 @@ def importFromFile(filePath: Path):
|
|||
db_bk_dir.mkdir(exist_ok=True)
|
||||
|
||||
if filePath.name[-4:] != ".txt":
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
"The import file has to be a text file with txt extension (.txt at the end of the filename)!"
|
||||
)
|
||||
|
||||
|
@ -82,7 +82,7 @@ def importFromFile(filePath: Path):
|
|||
|
||||
if expectingTable:
|
||||
if line[0] != "#":
|
||||
raise DataBaseImportException(f"Expected a Table name starting with # but got this line: {line}")
|
||||
raise DatabaseImportException(f"Expected a Table name starting with # but got this line: {line}")
|
||||
|
||||
expectingTable = False
|
||||
tableName = line[1:]
|
||||
|
@ -104,7 +104,7 @@ def importFromFile(filePath: Path):
|
|||
elif tableName == "Appointment":
|
||||
activeDict = appointments
|
||||
else:
|
||||
raise DataBaseImportException(f"{tableName} is not a valid table name!")
|
||||
raise DatabaseImportException(f"{tableName} is not a valid table name!")
|
||||
|
||||
readHeader = True
|
||||
continue
|
||||
|
@ -122,7 +122,7 @@ def importFromFile(filePath: Path):
|
|||
|
||||
cellsLen = len(cells)
|
||||
if cellsLen != len(activeDict["_header"]):
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"The number of header cells is not equal to the number of row cells in row {cells}!"
|
||||
)
|
||||
|
||||
|
@ -134,14 +134,14 @@ def importFromFile(filePath: Path):
|
|||
flash("Semester...")
|
||||
|
||||
if len(semesters["label"]) != 1:
|
||||
raise DataBaseImportException("Only one semester is allowed in an import file!")
|
||||
raise DatabaseImportException("Only one semester is allowed in an import file!")
|
||||
|
||||
semesterLabel = not_nullable(semesters["label"][0])
|
||||
semesterYear = int(not_nullable(semesters["year"][0]))
|
||||
dbSemester = get_first(select(Semester).where(Semester.label == semesterLabel, Semester.year == semesterYear))
|
||||
|
||||
if dbSemester is None:
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"{semesterLabel}{semesterYear} does not exist in the database! Please make sure that you create the semester from the web interface first."
|
||||
)
|
||||
|
||||
|
@ -164,7 +164,7 @@ def importFromFile(filePath: Path):
|
|||
)
|
||||
|
||||
if dbPart is None:
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"Part with number {partNumber} and program label {partProgramLabel} does not exist in the database! Please make sure that you create parts from the web interface first."
|
||||
)
|
||||
|
||||
|
@ -178,8 +178,10 @@ def importFromFile(filePath: Path):
|
|||
student_number = int(not_nullable(student_number))
|
||||
first_name = not_nullable(students["first_name"][i])
|
||||
last_name = not_nullable(students["last_name"][i])
|
||||
uni_email = not_nullable(students["uni_email"][i])
|
||||
uni_email = not_nullable(students["uni_email"][i]).lower()
|
||||
contact_email = nullable(students["contact_email"][i])
|
||||
if contact_email is not None:
|
||||
contact_email = contact_email.lower()
|
||||
bachelor_thesis = nullable(students["bachelor_thesis"][i])
|
||||
bachelor_thesis_work_group = nullable(students["bachelor_thesis_work_group"][i])
|
||||
note = nullable(students["note"][i])
|
||||
|
@ -277,7 +279,7 @@ def importFromFile(filePath: Path):
|
|||
|
||||
if dbExperiment is None:
|
||||
# TODO: Check if experimentProgram is None
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"Experiment with number {experimentNumber} and program {experimentProgram} does not exist in the database. Please make sure that experiments are created from the web interface."
|
||||
)
|
||||
|
||||
|
@ -288,7 +290,7 @@ def importFromFile(filePath: Path):
|
|||
)
|
||||
|
||||
if dbSemesterExperiment is None:
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"No semester experiment exists in the database to the experiment with number {experimentNumber} and program {experimentProgram}. Please make sure that semester experiments are created in the web interface. The problem might be that the experiment was not active while creating a new semester"
|
||||
)
|
||||
|
||||
|
@ -312,11 +314,11 @@ def importFromFile(filePath: Path):
|
|||
|
||||
for i, date in enumerate(appointments["date"]):
|
||||
date = not_nullable(date)
|
||||
assistantEmail = not_nullable(appointments["assistant_email"][i])
|
||||
assistantEmail = not_nullable(appointments["assistant_email"][i]).lower()
|
||||
assistant = get_first(select(Assistant).join(User).where(User.email == assistantEmail))
|
||||
|
||||
if assistant is None:
|
||||
raise DataBaseImportException(
|
||||
raise DatabaseImportException(
|
||||
f"Assistant with email {assistantEmail} does not exist in the database! Please make sure that you create assistants in the web interface."
|
||||
)
|
||||
|
||||
|
@ -345,5 +347,5 @@ def importFromFile(filePath: Path):
|
|||
|
||||
flash(f"Made a backup of the database after the import at {dest}")
|
||||
|
||||
flash("\nImport done!")
|
||||
flash("Please check that everything worked as expected. Restore the database backup otherwise!")
|
||||
flash("\nImport done!", "success")
|
||||
flash("Please check that everything worked as expected. Restore the database backup otherwise!", "warning")
|
||||
|
|
|
@ -6,5 +6,5 @@ class DatabaseException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class DataBaseImportException(Exception):
|
||||
class DatabaseImportException(Exception):
|
||||
pass
|
||||
|
|
|
@ -4,40 +4,34 @@ Functions dependent on advlabdb.models.
|
|||
|
||||
from functools import cache
|
||||
|
||||
from flask import flash
|
||||
from flask_admin.menu import MenuLink
|
||||
from flask import flash, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from wtforms.fields import BooleanField, IntegerField, SelectField, StringField
|
||||
from wtforms.validators import DataRequired, NumberRange, Optional
|
||||
|
||||
from .models import MAX_MARK, MIN_MARK, Semester
|
||||
|
||||
|
||||
def initActiveSemesterMenuLinks(space, app):
|
||||
with app.app_context():
|
||||
try:
|
||||
semesters = Semester.sortedSemestersStartingWithNewest()
|
||||
for semester in semesters:
|
||||
space.add_link(
|
||||
MenuLink(
|
||||
name=str(semester),
|
||||
url="/set_semester" + "?semester_id=" + str(semester.id),
|
||||
category="Active semester",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
print(
|
||||
"ERROR: The Semester table does not exist yet! Therefore, menu links could not be generated. You can ignore this error if you are just initializing the database."
|
||||
)
|
||||
else:
|
||||
space.add_link(MenuLink(name="Logout", url="/logout"))
|
||||
def user_settings_url():
|
||||
if current_user.has_role("admin"):
|
||||
role = "admin"
|
||||
else:
|
||||
role = "assistant"
|
||||
|
||||
return url_for("main.index") + role + "/user/edit/?id=" + str(current_user.id)
|
||||
|
||||
|
||||
def active_semester_str():
|
||||
active_semester = current_user.active_semester
|
||||
active_semester_str = str(active_semester)
|
||||
if active_semester != Semester.lastSemester():
|
||||
flash(f"You are in the old semester {active_semester_str}!", "warning")
|
||||
flash(
|
||||
Markup(
|
||||
f"You are in the old semester {active_semester_str}! You should change your active semester in <a href='{ user_settings_url() }'>user settings</a>."
|
||||
),
|
||||
"warning",
|
||||
)
|
||||
|
||||
return active_semester_str
|
||||
|
||||
|
|
|
@ -61,9 +61,16 @@ class Student(db.Model):
|
|||
part_students = db.relationship("PartStudent", back_populates="student", lazy=True)
|
||||
|
||||
def __init__(self, uni_email, contact_email=None, **kwargs):
|
||||
# Don't save contact_email if it is similar to uni_email
|
||||
if contact_email is not None and contact_email.strip() == uni_email.strip():
|
||||
contact_email = None
|
||||
# Lower and strip uni email
|
||||
uni_email = uni_email.strip().lower()
|
||||
|
||||
if contact_email is not None:
|
||||
# Lower and strip contact email
|
||||
contact_email = contact_email.strip().lower()
|
||||
|
||||
# Don't save contact_email if it is similar to uni_email
|
||||
if contact_email == uni_email:
|
||||
contact_email = None
|
||||
|
||||
super().__init__(uni_email=uni_email, contact_email=contact_email, **kwargs)
|
||||
|
||||
|
@ -527,7 +534,7 @@ class Semester(db.Model):
|
|||
|
||||
if last_semester is not None:
|
||||
if year < last_semester.year or (year == last_semester.year and label == "SS"):
|
||||
raise DatabaseException(f"You can only create semesters older than the last semester {last_semester}!")
|
||||
raise DatabaseException(f"You can only create semesters later than the last semester {last_semester}!")
|
||||
|
||||
super().__init__(label=label, year=year, **kwargs)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/create.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/details.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/edit.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,14 +0,0 @@
|
|||
{% from "macros.html" import information, missing_final_experiment_marks %}
|
||||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{ information(current_user, active_semester_str, role="admin") }}
|
||||
|
||||
<hr>
|
||||
|
||||
{{ missing_final_experiment_marks(number_of_missing_final_experiment_marks, number_of_all_experiment_marks) }}
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{{ footer|safe }}
|
||||
{% endblock %}
|
42
advlabdb/templates/admin_index.jinja.html
Normal file
42
advlabdb/templates/admin_index.jinja.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{ information(current_user, active_semester_str, role="admin") }}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-inline-flex">
|
||||
<div class="table">
|
||||
<table class="table table-bordered">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Assistant
|
||||
</th>
|
||||
<th scope="col">
|
||||
Missing experiment marks
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for assistant_num_missing in assistants_num_missing %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ assistant_num_missing[0] }}
|
||||
</td>
|
||||
<td>
|
||||
{{ assistant_num_missing[1] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{{ footer|safe }}
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/list.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,8 +1,8 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{information(current_user, active_semester_str, role="admin")}}
|
||||
{{ information(current_user, active_semester_str, role="admin")}}
|
||||
|
||||
<hr>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{information(current_user, active_semester_str, role="admin")}}
|
||||
{{ information(current_user, active_semester_str, role="admin")}}
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -15,8 +15,8 @@
|
|||
<hr>
|
||||
|
||||
{% for histInd in histIndices %}
|
||||
<img src="data:image/png;base64,{{oralMarkHists[histInd]}}">
|
||||
<img src="data:image/png;base64,{{protocolMarkHists[histInd]}}">
|
||||
<img src="data:image/png;base64,{{ oralMarkHists[histInd]}}">
|
||||
<img src="data:image/png;base64,{{ protocolMarkHists[histInd]}}">
|
||||
<hr>
|
||||
{% endfor %}
|
||||
{% endblock body %}
|
|
@ -1,15 +0,0 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{information(current_user, active_semester_str, role="admin")}}
|
||||
|
||||
<hr>
|
||||
|
||||
{% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %}
|
||||
<img src="data:image/png;base64,{{activeSemesterFinalPartMarksHist}}">
|
||||
<hr>
|
||||
{% endfor %}
|
||||
|
||||
<img src="data:image/png;base64,{{meanFinalPartMarksPlot}}">
|
||||
{% endblock body %}
|
15
advlabdb/templates/analysis/final_part_marks.jinja.html
Normal file
15
advlabdb/templates/analysis/final_part_marks.jinja.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{ information(current_user, active_semester_str, role="admin")}}
|
||||
|
||||
<hr>
|
||||
|
||||
{% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %}
|
||||
<img src="data:image/png;base64,{{ activeSemesterFinalPartMarksHist }}">
|
||||
<hr>
|
||||
{% endfor %}
|
||||
|
||||
<img src="data:image/png;base64,{{ meanFinalPartMarksPlot }}">
|
||||
{% endblock body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/create.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/details.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/edit.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,14 +0,0 @@
|
|||
{% from "macros.html" import information, missing_final_experiment_marks %}
|
||||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{ information(current_user, active_semester_str, role="assistant") }}
|
||||
|
||||
<hr>
|
||||
|
||||
{{missing_final_experiment_marks(number_of_missing_final_experiment_marks, number_of_all_experiment_marks)}}
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{{ footer|safe }}
|
||||
{% endblock %}
|
17
advlabdb/templates/assistant_index.jinja.html
Normal file
17
advlabdb/templates/assistant_index.jinja.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block body %}
|
||||
{{ information(current_user, active_semester_str, role="assistant") }}
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
Number of <strong>missing</strong> final experiment marks:
|
||||
{{ number_of_missing_final_experiment_marks }} / {{ number_of_all_experiment_marks }}
|
||||
</p>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{{ footer|safe }}
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/model/list.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,98 +0,0 @@
|
|||
<div class="alert alert-warning" role="alert">
|
||||
Changes have been done to the web interface. Read the section
|
||||
<a href="#_group_experiment">Group Experiment</a> for more information.
|
||||
</div>
|
||||
|
||||
<h2>Welcome</h2>
|
||||
<p>
|
||||
Welcome to this website, AdvLabDB, where you will be able to manage your lab
|
||||
assistance. Please, <strong>read this documentation</strong> to familiarize
|
||||
yourself with this website.
|
||||
</p>
|
||||
|
||||
<h2>Menu items</h2>
|
||||
<p>
|
||||
In this section, you will find explanations to each menu item on the upper
|
||||
navigation bar.
|
||||
</p>
|
||||
|
||||
<h3>Home</h3>
|
||||
<p>
|
||||
This is your home page. Here, you currently only find the number of
|
||||
<strong>missing</strong> final experiment marks for the experiments that you
|
||||
are assigned to. At the latest at the end of the semester, this number should
|
||||
be 0. A number higher than 0 means that some oral and/or protocol marks are
|
||||
missing. The final experiment mark is calculated automatically after the oral
|
||||
and protocol marks of an experiment are set.
|
||||
</p>
|
||||
|
||||
<h3 id="_group_experiment">Group Experiment</h3>
|
||||
<p>
|
||||
This is the most important menu item. Here, you see all pairs of experiments
|
||||
and groups that you are responsible for. If a value in the column
|
||||
<em>Experiment Marks Missing</em> is <span style="color: red">Yes</span>, then
|
||||
you did not set all the experiment marks for the pair in the related row. If
|
||||
you click on the edit button on the left side of a row, then you will get a
|
||||
form.
|
||||
</p>
|
||||
<p>
|
||||
In the form, you can edit the <em>date</em> of an appointment by clicking on
|
||||
it. You should do so after writing with the students doing the experiment
|
||||
corresponding to the appointment. The initial date you find before editing it
|
||||
is only a suggestion in the week where the appointment should take place.
|
||||
Please take take into account if the appointment should be in the semester
|
||||
break.
|
||||
</p>
|
||||
<p>
|
||||
Below the appointments in the form, you can set the students' oral and
|
||||
protocol marks. Students in a group have separate marks. The oral mark has to
|
||||
be set after the oral exam. The protocol mark has to be set after correcting
|
||||
the group's protocol.
|
||||
</p>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Oral and protocol marks are between 0 and 15!
|
||||
</div>
|
||||
<p>
|
||||
After editing appointment date(s) or experiment marks, click on the
|
||||
<em>Save</em> button to save the changes.
|
||||
</p>
|
||||
<p>
|
||||
The changes are lost if you don't click on <em>Save</em>. Therefore, if you
|
||||
want to discard the changes, just click on the back button of your browser.
|
||||
</p>
|
||||
|
||||
<h3>User</h3>
|
||||
<p>
|
||||
Here, you find a table with only one row which is you as a user. Make sure
|
||||
that the fields <em>Phone Number</em>, <em>Mobile Phone Number</em>,
|
||||
<em>Building</em> and <em>Room</em> are filled and up to date, especially if
|
||||
you are a new assistant. To edit these fields, click on the pen icon at the
|
||||
left of the single entry in the table. After editing, click on <em>Save</em>.
|
||||
</p>
|
||||
<p>
|
||||
You can generate a new random password by clicking on the pen icon, checking
|
||||
the corresponding checkbox and then clicking on
|
||||
<em>Save</em>. You will be then logged out. Your new password is displayed
|
||||
above the login fields. Make sure that you save the password in a safe place.
|
||||
Using a free open source password manager like
|
||||
<a href="https://bitwarden.com/" target="_blank" rel="noopener">Bitwarden</a>
|
||||
or
|
||||
<a href="https://keepassxc.org/" target="_blank" rel="noopener">KeepassXC</a>
|
||||
is recommended.
|
||||
</p>
|
||||
|
||||
<h3>Docs</h3>
|
||||
<p>This is a link which leads you to this page.</p>
|
||||
|
||||
<h3>Active semester</h3>
|
||||
<p>
|
||||
An active semester is the semester you are working in. All shown experiment
|
||||
marks and appointments are in your active semester. By default as a new
|
||||
assistant, your active semester should be the latest semester.
|
||||
</p>
|
||||
<p>
|
||||
If you see a warning that you are in an old semester, click on
|
||||
<em>Active semester</em> and choose the latest semester. You should only work
|
||||
in an old semester if there are still experiment marks to be set in the old
|
||||
semester.
|
||||
</p>
|
92
advlabdb/templates/docs/assistant.jinja.html
Normal file
92
advlabdb/templates/docs/assistant.jinja.html
Normal file
|
@ -0,0 +1,92 @@
|
|||
<h2>Welcome</h2>
|
||||
<p>
|
||||
Welcome to this website, AdvLabDB, where you will be able to manage your lab
|
||||
assistance. Please, <strong>read this documentation</strong> to familiarize
|
||||
yourself with this website.
|
||||
</p>
|
||||
|
||||
<h2>Menu items</h2>
|
||||
<p>
|
||||
In this section, you will find explanations to each menu item on the upper
|
||||
navigation bar.
|
||||
</p>
|
||||
|
||||
<h3>Home</h3>
|
||||
<p>
|
||||
This is your home page. Here, you currently only find the number of
|
||||
<strong>missing</strong> final experiment marks for the experiments that you
|
||||
are assigned to. At the latest at the end of the semester, this number should
|
||||
be 0. A number higher than 0 means that some oral and/or protocol marks are
|
||||
missing. The final experiment mark is calculated automatically after the oral
|
||||
and protocol marks of an experiment are set.
|
||||
</p>
|
||||
|
||||
<h3 id="_group_experiment">Group Experiment</h3>
|
||||
<p>
|
||||
This is the most important menu item. Here, you see all pairs of experiments
|
||||
and groups that you are responsible for. If a value in the column
|
||||
<em>Experiment Marks Missing</em> is <span style="color: red">Yes</span>, then
|
||||
you did not set all the experiment marks for the pair in the related row. If
|
||||
you click on the edit button on the left side of a row, then you will get a
|
||||
form.
|
||||
</p>
|
||||
<p>
|
||||
In the form, you can edit the <em>date</em> of an appointment by clicking on
|
||||
it. You should do so after writing with the students doing the experiment
|
||||
corresponding to the appointment. The initial date you find before editing it
|
||||
is only a suggestion in the week where the appointment should take place.
|
||||
Please take take into account if the appointment should be in the semester
|
||||
break.
|
||||
</p>
|
||||
<p>
|
||||
Below the appointments in the form, you can set the students' oral and
|
||||
protocol marks. Students in a group have separate marks. The oral mark has to
|
||||
be set after the oral exam. The protocol mark has to be set after correcting
|
||||
the group's protocol.
|
||||
</p>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Oral and protocol marks are between 0 and 15!
|
||||
</div>
|
||||
<p>
|
||||
After editing appointment date(s) or experiment marks, click on the
|
||||
<em>Save</em> button to save the changes.
|
||||
</p>
|
||||
<p>
|
||||
The changes are lost if you don't click on <em>Save</em>. Therefore, if you
|
||||
want to discard the changes, just click on the back button of your browser.
|
||||
</p>
|
||||
|
||||
<h3>User</h3>
|
||||
<p>
|
||||
Here, you find a table with only one row which is you as a user. Make sure
|
||||
that the fields <em>Phone Number</em>, <em>Mobile Phone Number</em>,
|
||||
<em>Building</em> and <em>Room</em> are filled and up to date, especially if
|
||||
you are a new assistant. To edit these fields, click on the pen icon at the
|
||||
left of the single entry in the table. After editing, click on <em>Save</em>.
|
||||
</p>
|
||||
<p>
|
||||
You can generate a new random password by clicking on the pen icon, checking
|
||||
the corresponding checkbox and then clicking on
|
||||
<em>Save</em>. You will be then logged out. Your new password is displayed
|
||||
above the login fields. Make sure that you save the password in a safe place.
|
||||
Using a free open source password manager like
|
||||
<a href="https://bitwarden.com/" target="_blank" rel="noopener">Bitwarden</a>
|
||||
or
|
||||
<a href="https://keepassxc.org/" target="_blank" rel="noopener">KeepassXC</a>
|
||||
is recommended.
|
||||
</p>
|
||||
|
||||
<h3>Docs</h3>
|
||||
<p>This is a link which leads you to this page.</p>
|
||||
|
||||
<h3>Active semester</h3>
|
||||
<p>
|
||||
An active semester is the semester you are working in. All shown experiment
|
||||
marks and appointments are in your active semester. By default as a new
|
||||
assistant, your active semester should be the latest semester.
|
||||
</p>
|
||||
<p>
|
||||
If you see a warning that you are in an old semester, then you should change
|
||||
your active semester in the user settings. You should only work in an old
|
||||
semester if there are still experiment marks to be set in the old semester.
|
||||
</p>
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
@ -6,7 +6,7 @@
|
|||
|
||||
<hr>
|
||||
|
||||
{% include "docs/" + role + ".html" %}
|
||||
{% include "docs/" + role + ".jinja.html" %}
|
||||
|
||||
{{ footer|safe }}
|
||||
{% endblock body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% from "macros.html" import information %}
|
||||
{% from "macros.jinja.html" import information %}
|
||||
{% extends "admin/master.html" %}
|
||||
|
||||
{% block body %}
|
|
@ -1,20 +0,0 @@
|
|||
{% macro information(current_user, active_semester_str, role) %} User:
|
||||
<a
|
||||
href="{{ url_for('main.index') }}{{ role }}/user/details/?id={{ current_user.id }}"
|
||||
>{{ current_user }}</a
|
||||
>
|
||||
|
||||
| Active semester: {{ active_semester_str() }} {% if (role == "admin") and
|
||||
(current_user.has_role("assistant")) %} |
|
||||
<a href="{{ url_for('main.index') }}assistant">Assistant space</a>. {% elif
|
||||
(role == "assistant") and (current_user.has_role("admin")) %} |
|
||||
<a href="{{ url_for('main.index') }}admin">Admin space</a>. {% endif %} {%
|
||||
endmacro %} {% macro
|
||||
missing_final_experiment_marks(number_of_missing_final_experiment_marks,
|
||||
number_of_all_experiment_marks) %}
|
||||
<p>
|
||||
Number of <strong>missing</strong> final experiment marks: {{
|
||||
number_of_missing_final_experiment_marks }} / {{
|
||||
number_of_all_experiment_marks }}
|
||||
</p>
|
||||
{% endmacro %}
|
18
advlabdb/templates/macros.jinja.html
Normal file
18
advlabdb/templates/macros.jinja.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% macro information(current_user, active_semester_str, role) %}
|
||||
User:
|
||||
<a
|
||||
href="{{ url_for('main.index') }}{{ role }}/user/edit/?id={{ current_user.id }}"
|
||||
>{{ current_user }}</a>
|
||||
| Active semester: {{ active_semester_str() }}
|
||||
{% if (role == "admin") and (current_user.has_role("assistant")) %}
|
||||
|
|
||||
<a
|
||||
href="{{ url_for('main.index') }}assistant"
|
||||
>Assistant space</a>.
|
||||
{% elif (role == "assistant") and (current_user.has_role("admin")) %}
|
||||
|
|
||||
<a
|
||||
href="{{ url_for('main.index') }}admin"
|
||||
>Admin space</a>.
|
||||
{% endif %}
|
||||
{% endmacro %}
|
Loading…
Reference in a new issue