mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-12-04 22:40:30 +00:00
Compare commits
30 commits
a2162363b9
...
0242b4389d
Author | SHA1 | Date | |
---|---|---|---|
0242b4389d | |||
a70e01114c | |||
b895d1f8f1 | |||
ffcbda905d | |||
4747f8a073 | |||
e530dceab4 | |||
8d7ea5cae3 | |||
e1f004de7b | |||
249f635772 | |||
83f4726296 | |||
d533db66ab | |||
623100fffc | |||
a562c2ee33 | |||
950823585e | |||
cf8279616e | |||
5d0f99a381 | |||
e5c266c481 | |||
4d8947b89e | |||
399ca4b198 | |||
429ea37c40 | |||
77385c0861 | |||
7fe8024202 | |||
cf433cb692 | |||
adbe8d0eb7 | |||
a5b5c4156c | |||
ce3de0f6de | |||
c35e2507e1 | |||
c0a4f215e8 | |||
8020762261 | |||
bc0da570cd |
51 changed files with 990 additions and 1971 deletions
|
@ -10,13 +10,13 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.37.3
|
||||
rev: v2.38.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.12.4
|
||||
rev: v1.12.7
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
|
|
43
advlabdb/actions.py
Normal file
43
advlabdb/actions.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from flask import flash
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import select
|
||||
|
||||
from .models import Assistant, Semester, User, db
|
||||
|
||||
|
||||
def update_final_experiment_and_part_marks():
|
||||
for semesterExperiment in current_user.active_semester.semester_experiments:
|
||||
semesterExperiment.updateFinalExperimentAndPartMarks()
|
||||
|
||||
flash("Manually updated all final experiment and part marks.", "success")
|
||||
|
||||
|
||||
def deactivate_assistants():
|
||||
user_ids_to_deactivate = db.session.scalars(
|
||||
select(Assistant.user_id)
|
||||
.join(User)
|
||||
.where(User.active == True)
|
||||
.except_(
|
||||
select(Assistant.user_id).join(Assistant.semester_experiments).join(Semester).where(Semester.done == False)
|
||||
)
|
||||
)
|
||||
|
||||
no_users_deactivated = True
|
||||
|
||||
try:
|
||||
for user_id in user_ids_to_deactivate:
|
||||
user = db.session.get(User, user_id)
|
||||
user.active = False
|
||||
|
||||
flash(f"User {user} deactivated!", "warning")
|
||||
no_users_deactivated = False
|
||||
|
||||
db.session.commit()
|
||||
except Exception as ex:
|
||||
flash(str(ex), "danger")
|
||||
|
||||
db.session.rollback()
|
||||
no_users_deactivated = True
|
||||
|
||||
if no_users_deactivated:
|
||||
flash("No users to deactivate.", "info")
|
|
@ -1,9 +1,6 @@
|
|||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from flask import flash, has_request_context, redirect, url_for
|
||||
from flask import flash, has_request_context, redirect
|
||||
from flask_admin import Admin as FlaskAdmin
|
||||
from flask_admin import expose
|
||||
from flask_admin.contrib.sqla.fields import QuerySelectField, QuerySelectMultipleField
|
||||
|
@ -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 (
|
||||
|
@ -32,6 +28,7 @@ from wtforms.validators import URL, DataRequired, Email, NumberRange, Optional
|
|||
from wtforms.widgets import NumberInput
|
||||
|
||||
from . import data_dir, user_datastore
|
||||
from .actions import deactivate_assistants, update_final_experiment_and_part_marks
|
||||
from .admin_link_formatters import (
|
||||
admin_formatter,
|
||||
appointment_date_formatter,
|
||||
|
@ -59,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,
|
||||
|
@ -128,7 +126,7 @@ class UserView(SecureAdminModelView):
|
|||
|
||||
@staticmethod
|
||||
def semesterQueryFactory():
|
||||
return Semester.query.order_by(Semester.id.desc()).where(Semester.done == False)
|
||||
return Semester.query.order_by(Semester.id.desc())
|
||||
|
||||
@staticmethod
|
||||
def default_roles():
|
||||
|
@ -255,8 +253,10 @@ class UserView(SecureAdminModelView):
|
|||
def on_model_change(self, form, model, is_created):
|
||||
if not is_created:
|
||||
if model == current_user:
|
||||
# Prevent locking out
|
||||
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!")
|
||||
|
||||
|
@ -268,16 +268,35 @@ class UserView(SecureAdminModelView):
|
|||
model, password, notify=False
|
||||
) # Password is automatically hashed with this function
|
||||
|
||||
if model.has_role("assistant") and model.assistant is None:
|
||||
semester_experiments = form.semester_experiments.data if form.semester_experiments else []
|
||||
user_assistant = model.assistant
|
||||
if model.has_role("assistant"):
|
||||
if user_assistant is None:
|
||||
# Create assistant instance after new role assignment
|
||||
semester_experiments = form.semester_experiments.data if form.semester_experiments else []
|
||||
|
||||
assistant = Assistant(user=model, semester_experiments=semester_experiments)
|
||||
self.session.add(assistant)
|
||||
assistant = Assistant(user=model, semester_experiments=semester_experiments)
|
||||
self.session.add(assistant)
|
||||
elif (
|
||||
user_assistant is not None
|
||||
and not user_assistant.semester_experiments
|
||||
and not user_assistant.appointments
|
||||
and not user_assistant.experiment_marks
|
||||
):
|
||||
# Delete assistant instance if there is no dependency
|
||||
# Useful for undoing an unwanted role assignment
|
||||
self.session.delete(user_assistant)
|
||||
|
||||
if model.has_role("admin") and model.admin is None:
|
||||
flash("Admin role was assigned!", "danger")
|
||||
admin = Admin(user=model)
|
||||
self.session.add(admin)
|
||||
user_admin = model.admin
|
||||
if model.has_role("admin"):
|
||||
if user_admin is None:
|
||||
# Create admin instance after new role assignment
|
||||
flash(f"Admin role was assigned to {model}!", "danger")
|
||||
admin = Admin(user=model)
|
||||
self.session.add(admin)
|
||||
elif user_admin is not None and not user_admin.experiment_marks:
|
||||
# Delete admin instance if there is no dependency
|
||||
# Useful for undoing an unwanted role assignment
|
||||
self.session.delete(user_admin)
|
||||
|
||||
# Lower email
|
||||
model.email = model.email.lower()
|
||||
|
@ -289,7 +308,8 @@ class UserView(SecureAdminModelView):
|
|||
category="success",
|
||||
)
|
||||
else:
|
||||
flash(f"Active semester is {model.active_semester}.", "warning")
|
||||
if model == current_user:
|
||||
flash(f"Active semester is {model.active_semester}.", "warning")
|
||||
|
||||
|
||||
class SemesterView(SecureAdminModelView):
|
||||
|
@ -332,6 +352,11 @@ class SemesterView(SecureAdminModelView):
|
|||
description="This option transfers assistants of your active semester to active experiments in the new semester. Make sure that your active semester is the last semester before creating a new one! Active experiments are transferred anyway. If you do not want an experiment to be transferred, set it to inactive before creating the new semester. Experiments which are switched to active before creating the new semester will be created in the new semester without assistants. It is recommended to check the assistants of all experiments after creating a new semester.",
|
||||
default=True,
|
||||
)
|
||||
set_old_semesters_done = BooleanField(
|
||||
"Set all old semesters as done",
|
||||
description="Setting semesters as done prevents assistants from changing or even seeing marks in these semesters. Choose this option only if all marks in all old semesters are already set.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
can_delete = False
|
||||
column_display_all_relations = True
|
||||
|
@ -368,7 +393,7 @@ class SemesterView(SecureAdminModelView):
|
|||
|
||||
form_args = {
|
||||
"done": {
|
||||
"description": "Marking a semester as done does not let assistants work in the semester anymore. This is useful if all marks are already set and prevents assistants from changing or even seeing them after the semester is marked as done. Setting a semester as done sets older semesters as done, too. Only set the semester as done if all marks in the semester and all previous semesters are already set."
|
||||
"description": "Setting a semester as done prevents assistants from changing or even seeing marks in this semester. Setting a semester as done sets older semesters as done, too. Only set the semester as done if all marks in the semester and all previous semesters are already set."
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -383,30 +408,16 @@ class SemesterView(SecureAdminModelView):
|
|||
|
||||
def on_model_change(self, form, model, is_created):
|
||||
if is_created:
|
||||
current_user.active_semester = model
|
||||
flash(f"Active semester changed to the new semester {model}!", "warning")
|
||||
if form.set_old_semesters_done.data:
|
||||
last_semester = db.session.get(Semester, model.id - 1)
|
||||
last_semester.set_done(model)
|
||||
else:
|
||||
current_user.active_semester = model
|
||||
flash(f"Active semester changed to the new semester {model}!", "warning")
|
||||
else:
|
||||
if model.done:
|
||||
next_semester = db.session.get(Semester, model.id + 1)
|
||||
set_next_semester = next_semester is not None
|
||||
|
||||
# Set all previous semesters as done
|
||||
for id in range(1, model.id + 1):
|
||||
semester = db.session.get(Semester, id)
|
||||
|
||||
if semester == model or not semester.done:
|
||||
semester.done = True
|
||||
|
||||
if set_next_semester:
|
||||
# Set active_semester to next_semester
|
||||
users_in_semester_done = db.session.execute(
|
||||
select(User).where(User.active_semester == semester)
|
||||
).scalars()
|
||||
|
||||
for user in users_in_semester_done:
|
||||
user.active_semester = next_semester
|
||||
if user == current_user:
|
||||
flash(f"Active semester changed to the next semester {next_semester}!", "warning")
|
||||
model.set_done(next_semester)
|
||||
|
||||
|
||||
def programQueryFactory():
|
||||
|
@ -529,7 +540,7 @@ class PartRowFilter(FilterEqual):
|
|||
if not has_request_context():
|
||||
return tuple()
|
||||
|
||||
parts = db.session.execute(select(Part).where(Part.semester == current_user.active_semester)).scalars()
|
||||
parts = db.session.scalars(select(Part).where(Part.semester == current_user.active_semester))
|
||||
return tuple((part.id, part.str_without_semester()) for part in parts)
|
||||
|
||||
|
||||
|
@ -607,7 +618,7 @@ class ProgramRowFilter(FilterEqual):
|
|||
if not has_request_context():
|
||||
return tuple()
|
||||
|
||||
programs = db.session.execute(select(Program)).scalars()
|
||||
programs = db.session.scalars(select(Program))
|
||||
return tuple((program.id, str(program)) for program in programs)
|
||||
|
||||
|
||||
|
@ -921,7 +932,7 @@ class ExperimentRowFilter(FilterEqual):
|
|||
if not has_request_context():
|
||||
return tuple()
|
||||
|
||||
activeExperiments = db.session.execute(select(Experiment).where(Experiment.active == True)).scalars()
|
||||
activeExperiments = db.session.scalars(select(Experiment).where(Experiment.active == True))
|
||||
return tuple(
|
||||
(
|
||||
f"{activeExperiment.number},{activeExperiment.program_id}",
|
||||
|
@ -1178,7 +1189,7 @@ class ExperimentMarkView(SecureAdminModelView):
|
|||
if not has_request_context():
|
||||
return tuple()
|
||||
|
||||
admins = db.session.execute(select(Admin).join(User).where(User.active == True)).scalars()
|
||||
admins = db.session.scalars(select(Admin).join(User).where(User.active == True))
|
||||
return tuple((admin.id, str(admin)) for admin in admins)
|
||||
|
||||
def apply(self, query, value, alias=None):
|
||||
|
@ -1330,8 +1341,15 @@ class ImportView(SecureAdminBaseView):
|
|||
|
||||
class ActionsView(SecureAdminBaseView):
|
||||
class ActionsForm(FlaskForm):
|
||||
manualUpdateFinalExperimentAndPartMarksSubmit = SubmitField(
|
||||
label="Manually update final experiment and part marks",
|
||||
update_final_experiment_and_part_marks = BooleanField(
|
||||
label="Manually update all final experiment and part marks in the active semester",
|
||||
)
|
||||
deactivate_assistants = BooleanField(
|
||||
label="Deactivate assistants that do not have experiments in a semester that is not set as done",
|
||||
)
|
||||
submit = SubmitField(
|
||||
label="Submit",
|
||||
render_kw={"class": "btn btn-primary btn-block"},
|
||||
)
|
||||
|
||||
@expose("/", methods=("GET", "POST"))
|
||||
|
@ -1339,167 +1357,34 @@ class ActionsView(SecureAdminBaseView):
|
|||
form = ActionsView.ActionsForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.manualUpdateFinalExperimentAndPartMarksSubmit.data:
|
||||
for semesterExperiment in current_user.active_semester.semester_experiments:
|
||||
semesterExperiment.updateFinalExperimentAndPartMarks()
|
||||
if form.update_final_experiment_and_part_marks.data:
|
||||
update_final_experiment_and_part_marks()
|
||||
if form.deactivate_assistants.data:
|
||||
deactivate_assistants()
|
||||
|
||||
flash("Manually updated all final experiment and part marks", "success")
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
return redirect(self.url)
|
||||
|
||||
return self.render("actions.jinja.html", form=form)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
@ -1511,8 +1396,6 @@ class DocsView(SecureAdminBaseView):
|
|||
|
||||
|
||||
def init_admin_model_views(app):
|
||||
adminSpace.init_app(app)
|
||||
|
||||
adminSpace.add_view(StudentView(Student, url="student"))
|
||||
adminSpace.add_view(PartStudentView(PartStudent, url="part_student"))
|
||||
adminSpace.add_view(GroupView(Group, url="group"))
|
||||
|
@ -1532,4 +1415,7 @@ def init_admin_model_views(app):
|
|||
adminSpace.add_view(AnalysisView(name="Analysis", url="analysis"))
|
||||
adminSpace.add_view(DocsView(name="Docs", url="docs"))
|
||||
|
||||
adminSpace.add_link(MenuLink(name="User settings", url="/user-settings"))
|
||||
adminSpace.add_link(MenuLink(name="Logout", url="/logout"))
|
||||
|
||||
adminSpace.init_app(app)
|
||||
|
|
142
advlabdb/analysis.py
Normal file
142
advlabdb/analysis.py
Normal file
|
@ -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,
|
||||
)
|
|
@ -7,6 +7,7 @@ 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 markupsafe import Markup
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from .advlabdb_independent_funs import (
|
||||
|
@ -39,6 +40,26 @@ assistantSpace = FlaskAdmin(
|
|||
|
||||
|
||||
class AssistantGroupExperimentView(SecureAssistantModelView):
|
||||
def is_accessible(self):
|
||||
if not super().is_accessible():
|
||||
return False
|
||||
|
||||
active_semester = current_user.active_semester
|
||||
|
||||
if active_semester.done:
|
||||
semester_changed = current_user.set_last_semester_as_active()
|
||||
|
||||
if not semester_changed:
|
||||
flash(
|
||||
Markup(
|
||||
f"Active semester {active_semester} is set as done. Therefore, you are not allowed to view or edit any marks in this semester. You should change your active semester in <a href='/user-settings'>user settings</a> if possible."
|
||||
),
|
||||
"danger",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
column_display_actions = True
|
||||
|
||||
column_list = [
|
||||
|
@ -109,7 +130,8 @@ class AssistantGroupExperimentView(SecureAssistantModelView):
|
|||
|
||||
if group_experiment not in self.get_query():
|
||||
reportBadAttempt("Assistant {current_user} tried to edit {group_experiment}")
|
||||
raise ModelViewException("Unauthorized action!")
|
||||
self.handle_view_exception(ModelViewException("Unauthorized action!"))
|
||||
return redirect(self.url)
|
||||
|
||||
form, appointments, experiment_marks = assistant_group_experiment_form_factory(current_user, group_experiment)
|
||||
|
||||
|
@ -172,7 +194,7 @@ class AssistantGroupExperimentView(SecureAssistantModelView):
|
|||
return self.render(
|
||||
"assistant_group_experiment_form.jinja.html",
|
||||
form=form,
|
||||
experiment_label=group_experiment.semester_experiment.experiment.str(),
|
||||
semester_experiment=group_experiment.semester_experiment,
|
||||
group_number=group_experiment.group.number,
|
||||
appointment_fields=appointment_fields,
|
||||
experiment_mark_zip=zip(
|
||||
|
@ -189,14 +211,14 @@ class AssistantUserView(SecureAssistantModelView):
|
|||
@staticmethod
|
||||
def semesterQueryFactory():
|
||||
# Show only last two semesters to assistants
|
||||
return Semester.query.order_by(Semester.id.desc()).limit(2).where(Semester.done == False)
|
||||
return Semester.query.order_by(Semester.id.desc()).where(Semester.done == False).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!",
|
||||
description="You should change the active semester to the last semester. Do not forget to click save! Only last two semesters are shown that are not set as done.",
|
||||
)
|
||||
|
||||
phone_number, mobile_phone_number, building, room = user_info_fields()
|
||||
|
@ -239,10 +261,14 @@ class AssistantDocsView(SecureAssistantBaseView):
|
|||
|
||||
|
||||
def init_assistant_model_views(app):
|
||||
assistantSpace.init_app(app)
|
||||
|
||||
assistantSpace.add_view(AssistantGroupExperimentView(GroupExperiment, url="group_experiment"))
|
||||
assistantSpace.add_view(AssistantUserView(User, url="user"))
|
||||
assistantSpace.add_view(AssistantDocsView(name="Docs", url="docs"))
|
||||
|
||||
# Don't add to menu
|
||||
# Has to be placed before assistantSpace.init_app
|
||||
assistantSpace._views.append(AssistantUserView(User, url="user"))
|
||||
|
||||
assistantSpace.add_link(MenuLink(name="User settings", url="/user-settings"))
|
||||
assistantSpace.add_link(MenuLink(name="Logout", url="/logout"))
|
||||
|
||||
assistantSpace.init_app(app)
|
||||
|
|
|
@ -82,4 +82,3 @@ def set_config(app, data_dir: Path):
|
|||
app.config["SECURITY_PASSWORD_SALT"] = secrets["SECURITY_PASSWORD_SALT"]
|
||||
app.config["SECURITY_PASSWORD_LENGTH_MIN"] = settings.getint("SECURITY_PASSWORD_LENGTH_MIN", 15)
|
||||
app.config["SECURITY_POST_LOGIN_VIEW"] = "/post-login"
|
||||
# TODO: app.config["SECURITY_LOGIN_USER_TEMPLATE"] =
|
||||
|
|
|
@ -8,14 +8,7 @@ from sqlalchemy import func, select
|
|||
|
||||
from .exceptions import DatabaseException, ModelViewException
|
||||
from .model_independent_funs import reportBadAttempt
|
||||
from .models import (
|
||||
Assistant,
|
||||
ExperimentMark,
|
||||
GroupExperiment,
|
||||
SemesterExperiment,
|
||||
db,
|
||||
get_count,
|
||||
)
|
||||
from .models import Assistant, ExperimentMark, GroupExperiment, SemesterExperiment, db
|
||||
|
||||
|
||||
def adminViewIsAccessible():
|
||||
|
@ -70,25 +63,20 @@ class SecureAssistantIndexView(CustomIndexView):
|
|||
|
||||
@expose("/")
|
||||
def index(self):
|
||||
active_semester_experiment_mark_ids_stmt = (
|
||||
select(ExperimentMark.final_experiment_mark)
|
||||
number_of_missing_final_experiment_marks = db.session.scalar(
|
||||
select(func.count())
|
||||
.select_from(ExperimentMark)
|
||||
.join(GroupExperiment)
|
||||
.join(SemesterExperiment)
|
||||
.where(SemesterExperiment.semester == current_user.active_semester)
|
||||
.join(SemesterExperiment.assistants)
|
||||
.where(Assistant.user == current_user)
|
||||
)
|
||||
|
||||
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)
|
||||
.where(ExperimentMark.final_experiment_mark == None)
|
||||
)
|
||||
|
||||
return self.render(
|
||||
"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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -289,7 +277,8 @@ class SecureAssistantModelView(CustomModelView):
|
|||
|
||||
if model not in self.get_query():
|
||||
reportBadAttempt("An assistant tried to change a model not in his filter!")
|
||||
raise ModelViewException("Unauthorized action!")
|
||||
self.handle_view_exception(ModelViewException("Unauthorized action!"))
|
||||
return redirect(self.url)
|
||||
|
||||
def on_model_delete(self, model):
|
||||
reportBadAttempt("An assistant tried to delete a model!")
|
||||
|
@ -308,7 +297,8 @@ class SecureAssistantModelView(CustomModelView):
|
|||
|
||||
if model not in self.get_query():
|
||||
reportBadAttempt("An assistant tried to edit a model not in his filter!")
|
||||
raise ModelViewException("Unauthorized action!")
|
||||
self.handle_view_exception(ModelViewException("Unauthorized action!"))
|
||||
return redirect(self.url)
|
||||
|
||||
return super().edit_view()
|
||||
|
||||
|
@ -325,7 +315,8 @@ class SecureAssistantModelView(CustomModelView):
|
|||
|
||||
if model not in self.get_query():
|
||||
reportBadAttempt("An assistant tried to see details of a model not in his filter!")
|
||||
raise ModelViewException("Unauthorized action!")
|
||||
self.handle_view_exception(ModelViewException("Unauthorized action!"))
|
||||
return redirect(self.url)
|
||||
|
||||
return super().details_view()
|
||||
|
||||
|
@ -339,7 +330,13 @@ class SecureAssistantModelView(CustomModelView):
|
|||
return super().get_details_columns()
|
||||
|
||||
|
||||
class SecureAdminBaseView(BaseView):
|
||||
class CustomBaseView(BaseView):
|
||||
def inaccessible_callback(self, name, **kwargs):
|
||||
# Redirect to login page if user doesn't have access
|
||||
return redirect(url_for("security.login", next=request.url))
|
||||
|
||||
|
||||
class SecureAdminBaseView(CustomBaseView):
|
||||
def __init__(self, **kwargs):
|
||||
url = get_url(kwargs)
|
||||
super().__init__(endpoint="admin_" + url, **kwargs)
|
||||
|
@ -348,7 +345,7 @@ class SecureAdminBaseView(BaseView):
|
|||
return adminViewIsAccessible()
|
||||
|
||||
|
||||
class SecureAssistantBaseView(BaseView):
|
||||
class SecureAssistantBaseView(CustomBaseView):
|
||||
def __init__(self, **kwargs):
|
||||
url = get_url(kwargs)
|
||||
super().__init__(endpoint="assistant_" + url, **kwargs)
|
||||
|
|
|
@ -6,7 +6,7 @@ from .model_dependent_funs import selection_mark_field
|
|||
|
||||
|
||||
class AssistantGroupExperimentFormBase(FlaskForm):
|
||||
submit = SubmitField(label="Save", render_kw={"class": "btn btn-primary btn-block"})
|
||||
submit = SubmitField(label="Save")
|
||||
|
||||
|
||||
def assistant_group_experiment_form_factory(current_user, group_experiment):
|
||||
|
@ -15,7 +15,6 @@ def assistant_group_experiment_form_factory(current_user, group_experiment):
|
|||
"Note",
|
||||
default=group_experiment.note,
|
||||
validators=[Optional()],
|
||||
render_kw={"class": "form-control"},
|
||||
)
|
||||
|
||||
appointments = group_experiment.appointments
|
||||
|
@ -35,7 +34,6 @@ def assistant_group_experiment_form_factory(current_user, group_experiment):
|
|||
default=appointment.date,
|
||||
validators=[DataRequired()],
|
||||
description=description,
|
||||
render_kw={"class": "form-control"},
|
||||
),
|
||||
)
|
||||
appointment_num += 1
|
||||
|
|
|
@ -4,7 +4,7 @@ Functions dependent on advlabdb.models.
|
|||
|
||||
from functools import cache
|
||||
|
||||
from flask import flash, url_for
|
||||
from flask import flash
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from wtforms.fields import BooleanField, IntegerField, SelectField, StringField
|
||||
|
@ -13,22 +13,13 @@ from wtforms.validators import DataRequired, NumberRange, Optional
|
|||
from .models import MAX_MARK, MIN_MARK, Semester
|
||||
|
||||
|
||||
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(
|
||||
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>."
|
||||
f"You are in the old semester {active_semester_str}! You should change your active semester in <a href='/user-settings'>user settings</a>."
|
||||
),
|
||||
"warning",
|
||||
)
|
||||
|
@ -63,7 +54,6 @@ def selection_mark_field(mark_type: str, default):
|
|||
default=default,
|
||||
choices=choices,
|
||||
validators=[DataRequired()],
|
||||
render_kw={"class": "form-control", "style": "width:auto;"},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -32,12 +32,8 @@ db = SQLAlchemy()
|
|||
FsModels.set_db_info(db)
|
||||
|
||||
|
||||
def get_count(table):
|
||||
return db.session.scalar(select(func.count()).select_from(table))
|
||||
|
||||
|
||||
def get_first(table):
|
||||
return db.session.execute(table.limit(1)).scalars().first()
|
||||
return db.session.scalars(table.limit(1)).first()
|
||||
|
||||
|
||||
def roundHalfUpToInt(number):
|
||||
|
@ -546,7 +542,7 @@ class Semester(db.Model):
|
|||
if transferParts:
|
||||
semester.transferPartsFrom(oldSemester)
|
||||
|
||||
for experiment in db.session.execute(select(Experiment).where(Experiment.active == True)).scalars():
|
||||
for experiment in db.session.scalars(select(Experiment).where(Experiment.active == True)):
|
||||
newSemesterExperiment = SemesterExperiment(experiment=experiment, semester=semester)
|
||||
|
||||
if transferAssistants:
|
||||
|
@ -571,12 +567,48 @@ class Semester(db.Model):
|
|||
if limit > 0:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
return db.session.execute(stmt).scalars()
|
||||
return db.session.scalars(stmt)
|
||||
|
||||
@staticmethod
|
||||
def lastSemester():
|
||||
return Semester.sortedSemestersStartingWithNewest(limit=1).first()
|
||||
|
||||
def num_missing_experiment_marks(self):
|
||||
return db.session.scalar(
|
||||
select(func.count())
|
||||
.select_from(ExperimentMark)
|
||||
.join(GroupExperiment)
|
||||
.join(SemesterExperiment)
|
||||
.where(SemesterExperiment.semester == self)
|
||||
.where(ExperimentMark.final_experiment_mark == None)
|
||||
)
|
||||
|
||||
def set_done(self, next_semester=None):
|
||||
set_next_semester = next_semester is not None
|
||||
|
||||
# Set also all previous semesters as done
|
||||
for id in range(1, self.id + 1):
|
||||
semester = db.session.get(Semester, id)
|
||||
|
||||
if semester == self or not semester.done:
|
||||
num_missing_experiment_marks = self.num_missing_experiment_marks()
|
||||
if num_missing_experiment_marks > 0:
|
||||
flash(
|
||||
f"Semester {semester} was set as done, but it has {num_missing_experiment_marks} missing experiment marks!",
|
||||
"danger",
|
||||
)
|
||||
|
||||
semester.done = True
|
||||
|
||||
if set_next_semester:
|
||||
# Set active_semester to next_semester
|
||||
users_in_semester_done = db.session.scalars(select(User).where(User.active_semester == semester))
|
||||
|
||||
for user in users_in_semester_done:
|
||||
user.active_semester = next_semester
|
||||
if user == current_user:
|
||||
flash(f"Active semester changed to the next semester {next_semester}!", "warning")
|
||||
|
||||
|
||||
class ExperimentMark(db.Model):
|
||||
# A mark for a student after a specific experiment
|
||||
|
@ -683,6 +715,32 @@ class User(db.Model, FsUserMixin):
|
|||
admin = db.relationship("Admin", back_populates="user", lazy=False, uselist=False)
|
||||
assistant = db.relationship("Assistant", back_populates="user", lazy=True, uselist=False)
|
||||
|
||||
def set_last_semester_as_active(self):
|
||||
"""
|
||||
Return True if changed, False otherwise.
|
||||
"""
|
||||
last_semester = Semester.lastSemester()
|
||||
|
||||
if last_semester.done:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.active_semester = last_semester
|
||||
|
||||
db.session.commit()
|
||||
except Exception as ex:
|
||||
flash(str(ex), "error")
|
||||
|
||||
db.session.rollback()
|
||||
return False
|
||||
else:
|
||||
flash(
|
||||
f"Active semester changed to {last_semester} because your last active semester was set as done!",
|
||||
"warning",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def str(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from flask import Blueprint, flash, redirect, request, url_for
|
||||
from flask import Blueprint, redirect, url_for
|
||||
from flask_login import current_user
|
||||
from flask_security.decorators import auth_required
|
||||
|
||||
from .model_dependent_funs import active_semester_str
|
||||
from .models import Semester, db
|
||||
|
||||
bp = Blueprint("main", __name__, root_path="/", template_folder="templates")
|
||||
|
||||
|
@ -36,22 +35,7 @@ def post_login():
|
|||
current_active_semester = current_user.active_semester
|
||||
|
||||
if current_active_semester.done:
|
||||
last_semester = Semester.lastSemester()
|
||||
|
||||
if not last_semester.done:
|
||||
try:
|
||||
current_user.active_semester = last_semester
|
||||
|
||||
db.session.commit()
|
||||
except Exception as ex:
|
||||
flash(str(ex), "error")
|
||||
|
||||
db.session.rollback()
|
||||
else:
|
||||
flash(
|
||||
f"Active semester changed to {last_semester} because the semester {current_active_semester} was marked as done!",
|
||||
"warning",
|
||||
)
|
||||
current_user.set_last_semester_as_active()
|
||||
|
||||
if current_user.has_role("admin"):
|
||||
endpoint_base = "admin"
|
||||
|
@ -64,3 +48,16 @@ def post_login():
|
|||
url = url_for(endpoint_base + ".index")
|
||||
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@bp.route("/user-settings")
|
||||
@auth_required()
|
||||
def user_settings():
|
||||
if current_user.has_role("admin"):
|
||||
role = "admin"
|
||||
else:
|
||||
role = "assistant"
|
||||
|
||||
url = url_for("main.index") + role + "/user/edit/?id=" + str(current_user.id)
|
||||
|
||||
return redirect(url)
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -7,8 +7,19 @@
|
|||
<hr>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.manualUpdateFinalExperimentAndPartMarksSubmit }}
|
||||
{% for field in form %}
|
||||
{% if field.widget.input_type == "checkbox" %}
|
||||
<div class="form-check">
|
||||
{{ field(class="form-check-input") }}
|
||||
|
||||
<label class="form-check-label" for="{{ field.id }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field() }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
|
||||
{{ footer|safe }}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context %}
|
||||
|
||||
{% macro dropdown(actions, btn_class='nav-link dropdown-toggle') -%}
|
||||
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)" role="button" aria-haspopup="true"
|
||||
aria-expanded="false">{{ _gettext('With selected') }}<b class="caret"></b></a>
|
||||
<div class="dropdown-menu">
|
||||
{% for p in actions %}
|
||||
<a class="dropdown-item" href="javascript:void(0)"
|
||||
onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form(actions, url) %}
|
||||
{% if actions %}
|
||||
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
|
||||
{% if action_form.csrf_token %}
|
||||
{{ action_form.csrf_token }}
|
||||
{% elif csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endif %}
|
||||
{{ action_form.url(value=return_url) }}
|
||||
{{ action_form.action() }}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro script(message, actions, actions_confirmation) %}
|
||||
{% if actions %}
|
||||
<div id="actions-confirmation-data" style="display:none;">{{ actions_confirmation|tojson|safe }}</div>
|
||||
<div id="message-data" style="display:none;">{{ message|tojson|safe }}</div>
|
||||
<script src="{{ admin_static.url(filename='admin/js/actions.js', v='1.0.0') }}"></script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
|
@ -1,101 +0,0 @@
|
|||
{% import 'admin/layout.html' as layout with context -%}
|
||||
{% import 'admin/static.html' as admin_static with context %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %}</title>
|
||||
{% block head_meta %}
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
{% endblock %}
|
||||
{% block head_css %}
|
||||
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/swatch/{swatch}/bootstrap.min.css'.format(swatch=config.get('FLASK_ADMIN_SWATCH', 'default')), v='4.2.1') }}"
|
||||
rel="stylesheet">
|
||||
{% if config.get('FLASK_ADMIN_SWATCH', 'default') == 'default' %}
|
||||
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/bootstrap.min.css', v='4.2.1') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/admin.css', v='1.1.1') }}" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/font-awesome.min.css', v='4.7.0') }}" rel="stylesheet">
|
||||
{% if admin_view.extra_css %}
|
||||
{% for css_url in admin_view.extra_css %}
|
||||
<link href="{{ css_url }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<style>
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block head_tail %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block page_body %}
|
||||
<div class="container{% if config.get('FLASK_ADMIN_FLUID_LAYOUT', False) %}-fluid{% endif %}">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-2" role="navigation">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#admin-navbar-collapse"
|
||||
aria-controls="admin-navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<!-- navbar content -->
|
||||
<div class="collapse navbar-collapse" id="admin-navbar-collapse">
|
||||
{% block brand %}
|
||||
<a class="navbar-brand" href="{{ admin_view.admin.url }}">{{ admin_view.admin.name }}</a>
|
||||
{% endblock %}
|
||||
{% block main_menu %}
|
||||
<ul class="nav navbar-nav mr-auto">
|
||||
{{ layout.menu() }}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block menu_links %}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{ layout.menu_links() }}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% block access_control %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% block messages %}
|
||||
{{ layout.messages() }}
|
||||
{% endblock %}
|
||||
|
||||
{# store the jinja2 context for form_rules rendering logic #}
|
||||
{% set render_ctx = h.resolve_ctx() %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
<script src="{{ admin_static.url(filename='vendor/jquery.min.js', v='3.5.1') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/popper.min.js') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/bootstrap.min.js', v='4.2.1') }}"
|
||||
type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/bootstrap4/util.js', v='4.3.1') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/bootstrap4/dropdown.js', v='4.3.1') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='4.2.1') }}"
|
||||
type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/multi-level-dropdowns-bootstrap/bootstrap4-dropdown-ml-hack.js') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='admin/js/helpers.js', v='1.0.0') }}" type="text/javascript"></script>
|
||||
{% if admin_view.extra_js %}
|
||||
{% for js_url in admin_view.extra_js %}
|
||||
<script src="{{ js_url }}" type="text/javascript"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{% block body %}
|
||||
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
|
||||
{% block fa_form %}
|
||||
{{ lib.render_form(form, dir_url) }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
|
@ -1,191 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% import 'admin/actions.html' as actionslib with context %}
|
||||
|
||||
{% block body %}
|
||||
{% block breadcrums %}
|
||||
<nav area-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ get_dir_url('.index_view', path=None) }}">{{ _gettext('Root') }}</a>
|
||||
</li>
|
||||
{% for name, path in breadcrumbs[:-1] %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ get_dir_url('.index_view', path=path) }}">{{ name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if breadcrumbs %}
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ get_dir_url('.index_view', path=breadcrumbs[-1][1]) }}">{{ breadcrumbs[-1][0] }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block file_list_table %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered model-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% block list_header scoped %}
|
||||
{% if actions %}
|
||||
<th class="list-checkbox-column">
|
||||
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" />
|
||||
</th>
|
||||
{% endif %}
|
||||
<th class=""> </th>
|
||||
{% for column in admin_view.column_list %}
|
||||
<th>
|
||||
{% if admin_view.is_column_sortable(column) %}
|
||||
{% if sort_column == column %}
|
||||
<a href="{{ sort_url(column, dir_path, True) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">
|
||||
{{ admin_view.column_label(column) }}
|
||||
{% if sort_desc %}
|
||||
<span class="fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ sort_url(column, dir_path) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">{{ admin_view.column_label(column) }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ _gettext(admin_view.column_label(column)) }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for name, path, is_dir, size, date in items %}
|
||||
<tr>
|
||||
{% block list_row scoped %}
|
||||
{% if actions %}
|
||||
<td>
|
||||
{% if not is_dir %}
|
||||
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ path }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% block list_row_actions scoped %}
|
||||
{% if admin_view.can_rename and path and name != '..' %}
|
||||
{%- if admin_view.rename_modal -%}
|
||||
{{ lib.add_modal_button(url=get_url('.rename', path=path, modal=True),
|
||||
title=_gettext('Rename File'),
|
||||
content='<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>') }}
|
||||
{% else %}
|
||||
<a class="icon" href="{{ get_url('.rename', path=path) }}" title="{{ _gettext('Rename File') }}">
|
||||
<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>
|
||||
</a>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- if admin_view.can_delete and path -%}
|
||||
{% if is_dir %}
|
||||
{% if name != '..' and admin_view.can_delete_dirs %}
|
||||
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
|
||||
{{ delete_form.path(value=path) }}
|
||||
{{ delete_form.csrf_token }}
|
||||
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
|
||||
<i class="fa fa-times glyphicon glyphicon-remove"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
|
||||
{{ delete_form.path(value=path) }}
|
||||
{{ delete_form.csrf_token }}
|
||||
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
|
||||
<i class="fa fa-trash glyphicon glyphicon-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
</td>
|
||||
{% if is_dir %}
|
||||
<td colspan="2">
|
||||
<a href="{{ get_dir_url('.index_view', path)|safe }}">
|
||||
<i class="fa fa-folder-o glyphicon glyphicon-folder-close"></i> <span>{{ name }}</span>
|
||||
</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
{% if admin_view.can_download %}
|
||||
{%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%}
|
||||
{{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe,
|
||||
btn_class='', content=name) }}
|
||||
{% else %}
|
||||
<a href="{{ get_file_url(path)|safe }}">{{ name }}</a>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
{{ name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if admin_view.is_column_visible('size') %}
|
||||
<td>
|
||||
{{ size|filesizeformat }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if admin_view.is_column_visible('date') %}
|
||||
<td>
|
||||
{{ timestamp_format(date) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block toolbar %}
|
||||
<div class="btn-toolbar">
|
||||
{% if admin_view.can_upload %}
|
||||
<div class="btn-group">
|
||||
{%- if admin_view.upload_modal -%}
|
||||
{{ lib.add_modal_button(url=get_dir_url('.upload', path=dir_path, modal=True),
|
||||
btn_class="btn btn-secondary",
|
||||
content=_gettext('Upload File')) }}
|
||||
{% else %}
|
||||
<a class="btn btn-secondary" href="{{ get_dir_url('.upload', path=dir_path) }}">{{ _gettext('Upload File') }}</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if admin_view.can_mkdir %}
|
||||
<div class="mx-1">
|
||||
{%- if admin_view.mkdir_modal -%}
|
||||
{{ lib.add_modal_button(url=get_dir_url('.mkdir', path=dir_path, modal=True),
|
||||
btn_class="btn btn-secondary",
|
||||
content=_gettext('Create Directory')) }}
|
||||
{% else %}
|
||||
<a class="btn btn-secondary" href="{{ get_dir_url('.mkdir', path=dir_path) }}">{{ _gettext('Create Directory') }}</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if actions %}
|
||||
<div class="mx-1">
|
||||
{{ actionslib.dropdown(actions, 'dropdown-toggle btn btn-secondary') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{ actionslib.form(actions, get_url('.action_view')) }}
|
||||
{% endblock %}
|
||||
|
||||
{%- if admin_view.rename_modal or admin_view.mkdir_modal
|
||||
or admin_view.upload_modal or admin_view.edit_modal -%}
|
||||
{{ lib.add_modal_window() }}
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{{ actionslib.script(_gettext('Please select at least one file.'),
|
||||
actions,
|
||||
actions_confirmation) }}
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{% block body %}
|
||||
{# content added to modal-content #}
|
||||
<div class="modal-header">
|
||||
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% block fa_form %}
|
||||
{{ lib.render_form(form, dir_url, action=request.url, is_modal=True) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,4 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
|
||||
{% block body %}
|
||||
{% endblock %}
|
|
@ -1,107 +0,0 @@
|
|||
{% macro menu_icon(item) -%}
|
||||
{% set icon_type = item.get_icon_type() %}
|
||||
{%- if icon_type %}
|
||||
{% set icon_value = item.get_icon_value() %}
|
||||
{% if icon_type == 'glyph' %}
|
||||
<i class="glyphicon {{ icon_value }}"></i>
|
||||
{% elif icon_type == 'fa' %}
|
||||
<i class="fa {{ icon_value }}"></i>
|
||||
{% elif icon_type == 'image' %}
|
||||
<img src="{{ url_for('static', filename=icon_value) }}" alt="menu image">
|
||||
{% elif icon_type == 'image-url' %}
|
||||
<img src="{{ icon_value }}" alt="menu image">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro menu(menu_root=None) %}
|
||||
{% set is_main_nav = menu_root == None %}
|
||||
{% if menu_root is none %}{% set menu_root = admin_view.admin.menu() %}{% endif %}
|
||||
{%- for item in menu_root %}
|
||||
{%- if item.is_category() -%}
|
||||
{% set children = item.get_children() %}
|
||||
{%- if children %}
|
||||
{% set class_name = item.get_class_name() or '' %}
|
||||
{%- if item.is_active(admin_view) %}
|
||||
<li class="active dropdown{% if class_name %} {{ class_name }}{% endif %}">
|
||||
{% else -%}
|
||||
<li class="dropdown{% if class_name %} {{ class_name }}{% endif %}">
|
||||
{%- endif %}
|
||||
<a class="dropdown-toggle {% if is_main_nav %}nav-link{% else %}dropdown-item{% endif %}" data-toggle="dropdown" href="javascript:void(0)">
|
||||
{% if item.class_name %}<span class="{{ item.class_name }}"></span> {% endif %}
|
||||
{{ menu_icon(item) }}{{ item.name }}
|
||||
{%- if 'dropdown-submenu' in class_name -%}
|
||||
<i class="glyphicon glyphicon-chevron-right small"></i>
|
||||
{%- else -%}
|
||||
<i class="glyphicon glyphicon-chevron-down small"></i>
|
||||
{%- endif -%}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{%- for child in children -%}
|
||||
{%- if child.is_category() -%}
|
||||
{{ menu(menu_root=[child]) }}
|
||||
{% else %}
|
||||
{% set class_name = child.get_class_name() %}
|
||||
<li{% if class_name %} class="{{ class_name }}"{% endif %}>
|
||||
{%- if child.is_active(admin_view) %}
|
||||
<a class="dropdown-item active" href="{{ child.get_url() }}"{% if child.target %}
|
||||
target="{{ child.target }}"{% endif %}>
|
||||
{{ menu_icon(child) }}{{ child.name }}</a>
|
||||
{% else %}
|
||||
<a class="dropdown-item" href="{{ child.get_url() }}"{% if child.target %}
|
||||
target="{{ child.target }}"{% endif %}>
|
||||
{{ menu_icon(child) }}{{ child.name }}</a>
|
||||
{%- endif %}
|
||||
</li>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{%- else %}
|
||||
{%- if item.is_accessible() and item.is_visible() -%}
|
||||
{% set class_name = item.get_class_name() %}
|
||||
{%- if item.is_active(admin_view) %}
|
||||
<li class="active{% if class_name %} {{ class_name }}{% endif %}">
|
||||
{%- else %}
|
||||
<li{% if class_name %} class="{{ class_name }}"{% endif %}>
|
||||
{%- endif %}
|
||||
<a class="nav-link" href="{{ item.get_url() }}"{% if item.target %} target="{{ item.target }}"{% endif %}>
|
||||
{{ menu_icon(item) }}{{ item.name }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro menu_links(links=None) %}
|
||||
{% if links is none %}{% set links = admin_view.admin.menu_links() %}{% endif %}
|
||||
{% for item in links %}
|
||||
{% set class_name = item.get_class_name() %}
|
||||
{% if item.is_accessible() and item.is_visible() %}
|
||||
<li{% if class_name %} class="{{ class_name }}"{% endif %}>
|
||||
<a class="nav-link" href="{{ item.get_url() }}"{% if item.target %} target="{{ item.target }}"{% endif %}>
|
||||
{{ menu_icon(item) }}{{ item.name }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro messages() %}
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, m in messages %}
|
||||
{% if category %}
|
||||
{# alert-error changed to alert-danger in bootstrap 3, mapping is for backwards compatibility #}
|
||||
{% set mapping = {'message': 'info', 'error': 'danger'} %}
|
||||
<div class="alert alert-{{ mapping.get(category, category) }} alert-dismissable">
|
||||
{% else %}
|
||||
<div class="alert alert-dismissable">
|
||||
{% endif %}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{ m }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endmacro %}
|
|
@ -1,292 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context %}
|
||||
|
||||
{# ---------------------- Pager -------------------------- #}
|
||||
{% macro pager(page, pages, generator) -%}
|
||||
{% if pages > 1 %}
|
||||
<ul class="pagination">
|
||||
{% set min = page - 3 %}
|
||||
{% set max = page + 3 + 1 %}
|
||||
|
||||
{% if min < 0 %}
|
||||
{% set max = max - min %}
|
||||
{% endif %}
|
||||
{% if max >= pages %}
|
||||
{% set min = min - max + pages %}
|
||||
{% endif %}
|
||||
|
||||
{% if min < 0 %}
|
||||
{% set min = 0 %}
|
||||
{% endif %}
|
||||
{% if max >= pages %}
|
||||
{% set max = pages %}
|
||||
{% endif %}
|
||||
|
||||
{% if min > 0 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ generator(0) }}">«</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="javascript:void(0)">«</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page > 0 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ generator(page-1) }}"><</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="javascript:void(0)"><</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(min, max) %}
|
||||
{% if page == p %}
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="javascript:void(0)">{{ p + 1 }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ generator(p) }}">{{ p + 1 }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page + 1 < pages %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ generator(page + 1) }}">></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="javascript:void(0)">></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if max < pages %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ generator(pages - 1) }}">»</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="javascript:void(0)">»</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro simple_pager(page, have_next, generator) -%}
|
||||
<ul class="pagination">
|
||||
{% if page > 0 %}
|
||||
<li class="page-item">
|
||||
<a href="{{ generator(page - 1) }}"><</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a href="{{ generator(0) }}"><</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if have_next %}
|
||||
<li class="page-item">
|
||||
<a href="{{ generator(page + 1) }}">></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a href="{{ generator(page) }}">></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{%- endmacro %}
|
||||
|
||||
{# ---------------------- Modal Window ------------------- #}
|
||||
{% macro add_modal_window(modal_window_id='fa_modal_window', modal_label_id='fa_modal_label') %}
|
||||
<div class="modal fade" id="{{ modal_window_id }}" tabindex="-1" role="dialog" aria-labelledby="{{ modal_label_id }}">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
{# bootstrap version > 3.1.0 required for this to work #}
|
||||
<div class="modal-content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro add_modal_button(url='', title='', content='', modal_window_id='fa_modal_window', btn_class='icon') %}
|
||||
<a class="{{ btn_class }}" data-target="#{{ modal_window_id }}" title="{{ title }}" href="{{ url }}" data-toggle="modal">
|
||||
{{ content|safe }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# ---------------------- Forms -------------------------- #}
|
||||
{% macro render_field(form, field, kwargs={}, caller=None) %}
|
||||
{% set direct_error = h.is_field_error(field.errors) %}
|
||||
{% set prepend = kwargs.pop('prepend', None) %}
|
||||
{% set append = kwargs.pop('append', None) %}
|
||||
<div class="form-group {{ kwargs.get('column_class', '') }}">
|
||||
<label for="{{ field.id }}" class="control-label" {% if field.widget.input_type == 'checkbox' %}style="display: block"{% endif %}>{{ field.label.text }}
|
||||
{% if h.is_required_form_field(field) %}
|
||||
<strong style="color: red">*</strong>
|
||||
{%- else -%}
|
||||
|
||||
{%- endif %}
|
||||
</label>
|
||||
{% if prepend or append %}
|
||||
<div class="input-group">
|
||||
{%- if prepend -%}
|
||||
<div class="input-group-prepend">
|
||||
{{ prepend }}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{% if field.widget.input_type == 'checkbox' %}
|
||||
{% set _class = kwargs.setdefault('class', 'form-control-lg') %}
|
||||
{% elif field.widget.input_type == 'file' %}
|
||||
{% set _class = kwargs.setdefault('class', 'form-control-file') %}
|
||||
{% else %}
|
||||
{% set _class = kwargs.setdefault('class', 'form-control') %}
|
||||
{% endif %}
|
||||
{%- if direct_error %} {% set _ = kwargs.update({'class': kwargs['class'] ~ ' is-invalid'}) %} {% endif -%}
|
||||
{{ field(**kwargs) | safe }}
|
||||
{%- if append -%}
|
||||
<div class="input-group-append">
|
||||
{{ append }}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{% if direct_error %}
|
||||
<div class="invalid-feedback">
|
||||
<ul class="help-block">
|
||||
{% for e in field.errors if e is string %}
|
||||
<li>{{ e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% elif field.description %}
|
||||
<div class="help-block">{{ field.description|safe }}</div>
|
||||
{% endif %}
|
||||
{% if prepend or append %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
{{ caller(form, field, direct_error, kwargs) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_header(form, text) %}
|
||||
<h3>{{ text }}</h3>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_form_fields(form, form_opts=None) %}
|
||||
{% if form.hidden_tag is defined %}
|
||||
{{ form.hidden_tag() }}
|
||||
{% else %}
|
||||
{% if csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endif %}
|
||||
{% for f in form if f.widget.input_type == 'hidden' %}
|
||||
{{ f }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if form_opts and form_opts.form_rules %}
|
||||
{% for r in form_opts.form_rules %}
|
||||
{{ r(form, form_opts=form_opts) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for f in form if f.widget.input_type != 'hidden' %}
|
||||
{% if form_opts %}
|
||||
{% set kwargs = form_opts.widget_args.get(f.short_name, {}) %}
|
||||
{% else %}
|
||||
{% set kwargs = {} %}
|
||||
{% endif %}
|
||||
{{ render_field(form, f, kwargs) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form_tag(form=None, action=None) %}
|
||||
<form action="{{ action or '' }}" method="POST" role="form" class="admin-form" enctype="multipart/form-data">
|
||||
<fieldset>
|
||||
{{ caller() }}
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_form_buttons(cancel_url, extra=None, is_modal=False) %}
|
||||
{% if is_modal %}
|
||||
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
|
||||
{% if extra %}
|
||||
{{ extra }}
|
||||
{% endif %}
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10 submit-row">
|
||||
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
|
||||
{% if extra %}
|
||||
{{ extra }}
|
||||
{% endif %}
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
|
||||
{% call form_tag(action=action) %}
|
||||
{{ render_form_fields(form, form_opts=form_opts) }}
|
||||
{{ render_form_buttons(cancel_url, extra, is_modal) }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form_css() %}
|
||||
<link href="{{ admin_static.url(filename='vendor/select2/select2.css', v='4.2.1') }}" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='vendor/select2/select2-bootstrap4.css', v='1.4.6') }}" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs4.css', v='1.3.22') }}" rel="stylesheet">
|
||||
{% if config.MAPBOX_MAP_ID %}
|
||||
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css', v='1.0.2') }}" rel="stylesheet">
|
||||
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css', v='0.4.6') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% if editable_columns %}
|
||||
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap4-editable.css', v='1.5.1.1') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form_js() %}
|
||||
{% if config.MAPBOX_MAP_ID %}
|
||||
<script>
|
||||
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
|
||||
{% if config.MAPBOX_ACCESS_TOKEN %}
|
||||
window.MAPBOX_ACCESS_TOKEN = "{{ config.MAPBOX_ACCESS_TOKEN }}";
|
||||
{% endif %}
|
||||
{% if config.DEFAULT_CENTER_LAT and config.DEFAULT_CENTER_LONG %}
|
||||
window.DEFAULT_CENTER_LAT = "{{ config.DEFAULT_CENTER_LAT }}";
|
||||
window.DEFAULT_CENTER_LONG = "{{ config.DEFAULT_CENTER_LONG }}";
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js', v='1.0.2') }}"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js', v='0.4.6') }}"></script>
|
||||
{% if config.MAPBOX_SEARCH %}
|
||||
<script>
|
||||
window.MAPBOX_SEARCH = "{{ config.MAPBOX_SEARCH }}";
|
||||
</script>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key={{ config.get('GOOGLE_MAPS_API_KEY') }}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
|
||||
{% if editable_columns %}
|
||||
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap4-editable.min.js', v='1.5.1.1') }}"></script>
|
||||
{% endif %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }}"></script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro extra() %}
|
||||
{% if admin_view.can_create %}
|
||||
<input name="_add_another" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Add Another') }}" />
|
||||
{% endif %}
|
||||
{% if admin_view.can_edit %}
|
||||
<input name="_continue_editing" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Continue Editing') }}" />
|
||||
{% endif %}
|
||||
{% endmacro %}
|
|
@ -1 +0,0 @@
|
|||
{% extends admin_base_template %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ lib.form_css() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% block navlinks %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a href="{{ return_url }}" class="nav-link">{{ _gettext('List') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('Create') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block create_form %}
|
||||
{{ lib.render_form(form, return_url, extra(), form_opts) }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{{ lib.form_js() }}
|
||||
{% endblock %}
|
|
@ -1,52 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{% block body %}
|
||||
{% block navlinks %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ return_url }}">{{ _gettext('List') }}</a>
|
||||
</li>
|
||||
{%- if admin_view.can_create -%}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ get_url('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{%- if admin_view.can_edit -%}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ get_url('.edit_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Edit') }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active disabled" href="javascript:void(0)">{{ _gettext('Details') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block details_search %}
|
||||
<div class="form-inline fa_filter_container col-lg-6">
|
||||
<label for="fa_filter">{{ _gettext('Filter') }}</label>
|
||||
<input id="fa_filter" type="text" class="ml-3 form-control">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block details_table %}
|
||||
<table class="table table-hover table-bordered searchable">
|
||||
{% for c, name in details_columns %}
|
||||
<tr>
|
||||
<td>
|
||||
<b>{{ name }}</b>
|
||||
</td>
|
||||
<td>
|
||||
{{ get_value(model, c) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ lib.form_css() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% block navlinks %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a href="{{ return_url }}" class="nav-link">{{ _gettext('List') }}</a>
|
||||
</li>
|
||||
{%- if admin_view.can_create -%}
|
||||
<li class="nav-item">
|
||||
<a href="{{ get_url('.create_view', url=return_url) }}" class="nav-link">{{ _gettext('Create') }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
<li class="nav-item">
|
||||
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('Edit') }}</a>
|
||||
</li>
|
||||
{%- if admin_view.can_view_details -%}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ get_url('.details_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Details') }}</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block edit_form %}
|
||||
{{ lib.render_form(form, return_url, extra(), form_opts) }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{{ lib.form_js() }}
|
||||
{% endblock %}
|
|
@ -1,15 +0,0 @@
|
|||
{% import 'admin/model/inline_list_base.html' as base with context %}
|
||||
|
||||
{% macro render_field(field) %}
|
||||
{{ field }}
|
||||
|
||||
{% if h.is_field_error(field.errors) %}
|
||||
<ul class="help-block input-errors">
|
||||
{% for e in field.errors if e is string %}
|
||||
<li>{{ e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{{ base.render_inline_fields(field, template, render_field, check) }}
|
|
@ -1,4 +0,0 @@
|
|||
{% import 'admin/lib.html' as lib with context %}
|
||||
<div class="inline-form-field">
|
||||
{{ lib.render_form_fields(field.form, form_opts=form_opts) }}
|
||||
</div>
|
|
@ -1,45 +0,0 @@
|
|||
{% macro render_inline_fields(field, template, render, check=None) %}
|
||||
<div class="inline-field" id="{{ field.id }}">
|
||||
{# existing inline form fields #}
|
||||
<div class="inline-field-list">
|
||||
{% for subfield in field %}
|
||||
<div id="{{ subfield.id }}" class="inline-field card card-body bg-light">
|
||||
{%- if not check or check(subfield) %}
|
||||
<legend>
|
||||
<small>
|
||||
{{ field.label.text }} #{{ loop.index }}
|
||||
<div class="pull-right">
|
||||
{% if subfield.get_pk and subfield.get_pk() %}
|
||||
<input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" />
|
||||
<label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label>
|
||||
{% else %}
|
||||
<a href="javascript:void(0)" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field"><i class="fa fa-times glyphicon glyphicon-remove"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</small>
|
||||
</legend>
|
||||
<div class='clearfix'></div>
|
||||
{%- endif -%}
|
||||
{{ render(subfield) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# template for new inline form fields #}
|
||||
<div class="inline-field-template hide">
|
||||
{% filter forceescape %}
|
||||
<div class="inline-field card card-body bg-light">
|
||||
<legend>
|
||||
<small>{{ _gettext('New') }} {{ field.label.text }}</small>
|
||||
<div class="pull-right">
|
||||
<a href="javascript:void(0)" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field"><span class="fa fa-times glyphicon glyphicon-remove"></span></a>
|
||||
</div>
|
||||
</legend>
|
||||
<div class='clearfix'></div>
|
||||
{{ render(template) }}
|
||||
</div>
|
||||
{% endfilter %}
|
||||
</div>
|
||||
<a id="{{ field.id }}-button" href="javascript:void(0)" class="btn btn-primary" role="button" onclick="faForm.addInlineField(this, '{{ field.id }}');">{{ _gettext('Add') }} {{ field.label.text }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
|
@ -1,106 +0,0 @@
|
|||
{% macro filter_options(btn_class='dropdown-toggle') %}
|
||||
<a class="nav-link {{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">{{ _gettext('Add Filter') }}<b class="caret"></b></a>
|
||||
<div class="dropdown-menu field-filters">
|
||||
{% for k in filter_groups %}
|
||||
<a href="javascript:void(0)" class="dropdown-item filter" onclick="return false;">{{ k }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro export_options(btn_class='dropdown-toggle') %}
|
||||
{% if admin_view.export_types|length > 1 %}
|
||||
<li class="dropdown">
|
||||
<a class="nav-link {{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">{{ _gettext('Export') }}<b class="caret"></b></a>
|
||||
<div class="dropdown-menu">
|
||||
{% for export_type in admin_view.export_types %}
|
||||
<a class="dropdown-item"
|
||||
href="{{ get_url('.export', export_type=export_type, **request.args) }}"
|
||||
title="{{ _gettext('Export') }}">{{ _gettext('Export') + ' ' + export_type|upper }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="nav-link" href="{{ get_url('.export', export_type=admin_view.export_types[0], **request.args) }}"
|
||||
title="{{ _gettext('Export') }}">{{ _gettext('Export') }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro filter_form() %}
|
||||
<form id="filter_form" method="GET" action="{{ return_url }}">
|
||||
{% if sort_column is not none %}
|
||||
<input type="hidden" name="sort" value="{{ sort_column }}">
|
||||
{% endif %}
|
||||
{% if sort_desc %}
|
||||
<input type="hidden" name="desc" value="{{ sort_desc }}">
|
||||
{% endif %}
|
||||
{% if search %}
|
||||
<input type="hidden" name="search" value="{{ search }}">
|
||||
{% endif %}
|
||||
{% if page_size != default_page_size %}
|
||||
<input type="hidden" name="page_size" value="{{ page_size }}">
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
|
||||
{% if active_filters %}
|
||||
<a href="{{ clear_search_url }}" class="btn btn-secondary">{{ _gettext('Reset Filters') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table class="filters"></table>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro search_form(input_class="col-auto") %}
|
||||
<form method="GET" action="{{ return_url }}" class="form-inline my-2 my-lg-0" role="search">
|
||||
{% for flt_name, flt_value in filter_args.items() %}
|
||||
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
|
||||
{% endfor %}
|
||||
{% if page_size != default_page_size %}
|
||||
<input type="hidden" name="page_size" value="{{ page_size }}">
|
||||
{% endif %}
|
||||
{% for arg_name, arg_value in extra_args.items() %}
|
||||
<input type="hidden" name="{{ arg_name }}" value="{{ arg_value }}">
|
||||
{% endfor %}
|
||||
{% if sort_column is not none %}
|
||||
<input type="hidden" name="sort" value="{{ sort_column }}">
|
||||
{% endif %}
|
||||
{% if sort_desc %}
|
||||
<input type="hidden" name="desc" value="{{ sort_desc }}">
|
||||
{% endif %}
|
||||
{% if search %}
|
||||
<div class="form-inline input-group">
|
||||
<input class="form-control {{ input_class }}" size="30" type="text" name="search" value="{{ search }}"
|
||||
placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">
|
||||
<a href="{{ clear_search_url }}" class="align-middle">
|
||||
<span class="fa fa-times glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary my-2 my-sm-0 ml-2" type="submit">{{ _gettext('Search') }}</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-inline">
|
||||
<input class="form-control {{ input_class }}" size="30" type="text" name="search" value=""
|
||||
placeholder="{{ _gettext('%(placeholder)s', placeholder=search_placeholder) }}">
|
||||
<button class="btn btn-secondary my-2 my-sm-0 ml-2" type="submit">{{ _gettext('Search') }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro page_size_form(generator, btn_class='nav-link dropdown-toggle') %}
|
||||
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">
|
||||
{{ page_size }} {{ _gettext('items') }}<b class="caret"></b>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item{% if page_size == 20 %} active{% endif %}" href="{{ generator(20) }}">20 {{ _gettext('items') }}</a>
|
||||
<a class="dropdown-item{% if page_size == 50 %} active{% endif %}" href="{{ generator(50) }}">50 {{ _gettext('items') }}</a>
|
||||
<a class="dropdown-item{% if page_size == 100 %} active{% endif %}" href="{{ generator(100) }}">100 {{ _gettext('items') }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
|
@ -1,198 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% import 'admin/static.html' as admin_static with context%}
|
||||
{% import 'admin/model/layout.html' as model_layout with context %}
|
||||
{% import 'admin/actions.html' as actionlib with context %}
|
||||
{% import 'admin/model/row_actions.html' as row_actions with context %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ lib.form_css() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% block model_menu_bar %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
|
||||
</li>
|
||||
|
||||
{% if admin_view.can_create %}
|
||||
<li class="nav-item">
|
||||
{%- if admin_view.create_modal -%}
|
||||
{{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), btn_class='nav-link', title=_gettext('Create New Record'), content=_gettext('Create')) }}
|
||||
{% else %}
|
||||
<a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create New Record') }}" class="nav-link">{{ _gettext('Create') }}</a>
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if admin_view.can_export %}
|
||||
{{ model_layout.export_options() }}
|
||||
{% endif %}
|
||||
|
||||
{% block model_menu_bar_before_filters %}{% endblock %}
|
||||
|
||||
{% if filters %}
|
||||
<li class="nav-item dropdown">
|
||||
{{ model_layout.filter_options() }}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if can_set_page_size %}
|
||||
<li class="nav-item dropdown">
|
||||
{{ model_layout.page_size_form(page_size_url) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if actions %}
|
||||
<li class="nav-item dropdown">
|
||||
{{ actionlib.dropdown(actions) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if search_supported %}
|
||||
<li class="nav-item ml-2">
|
||||
{{ model_layout.search_form() }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% block model_menu_bar_after_filters %}{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% if filters %}
|
||||
{{ model_layout.filter_form() }}
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
|
||||
{% block model_list_table %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-hover model-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% block list_header scoped %}
|
||||
{% if actions %}
|
||||
<th class="list-checkbox-column">
|
||||
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" title="{{ _gettext('Select all records') }}" />
|
||||
</th>
|
||||
{% endif %}
|
||||
{% block list_row_actions_header %}
|
||||
{% if admin_view.column_display_actions %}
|
||||
<th class=""> </th>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% for c, name in list_columns %}
|
||||
{% set column = loop.index0 %}
|
||||
<th class="column-header col-{{c}}">
|
||||
{% if admin_view.is_sortable(c) %}
|
||||
{% if sort_column == column %}
|
||||
<a href="{{ sort_url(column, True) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">
|
||||
{{ name }}
|
||||
{% if sort_desc %}
|
||||
<span class="fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ sort_url(column) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">{{ name }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ name }}
|
||||
{% endif %}
|
||||
{% if admin_view.column_descriptions.get(c) %}
|
||||
<a class="fa fa-question-circle glyphicon glyphicon-question-sign"
|
||||
title="{{ admin_view.column_descriptions[c] }}"
|
||||
href="javascript:void(0)" data-role="tooltip"
|
||||
></a>
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
{% block list_row scoped %}
|
||||
{% if actions %}
|
||||
<td>
|
||||
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
{% block list_row_actions_column scoped %}
|
||||
{% if admin_view.column_display_actions %}
|
||||
<td class="list-buttons-column">
|
||||
{% block list_row_actions scoped %}
|
||||
{% for action in list_row_actions %}
|
||||
{{ action.render_ctx(get_pk_value(row), row) }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</td>
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% for c, name in list_columns %}
|
||||
<td class="col-{{c}}">
|
||||
{% if admin_view.is_editable(c) %}
|
||||
{% set form = list_forms[get_pk_value(row)] %}
|
||||
{% if form.csrf_token %}
|
||||
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
|
||||
{% elif csrf_token %}
|
||||
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
|
||||
{% else %}
|
||||
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ get_value(row, c) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="999">
|
||||
{% block empty_list_message %}
|
||||
<div class="text-center">
|
||||
{{ admin_view.get_empty_list_message() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% block list_pager %}
|
||||
{% if num_pages is not none %}
|
||||
{{ lib.pager(page, num_pages, pager_url) }}
|
||||
{% else %}
|
||||
{{ lib.simple_pager(page, data|length == page_size, pager_url) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{ actionlib.form(actions, get_url('.action_view')) }}
|
||||
{% endblock %}
|
||||
|
||||
{%- if admin_view.edit_modal or admin_view.create_modal or admin_view.details_modal -%}
|
||||
{{ lib.add_modal_window() }}
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
|
||||
{% if filter_groups %}
|
||||
<div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
|
||||
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
|
||||
{% endif %}
|
||||
{{ lib.form_js() }}
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_filters.js', v='1.0.0') }}"></script>
|
||||
|
||||
|
||||
{{ actionlib.script(_gettext('Please select at least one record.'),
|
||||
actions,
|
||||
actions_confirmation) }}
|
||||
{% endblock %}
|
|
@ -1,36 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context%}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{# store the jinja2 context for form_rules rendering logic #}
|
||||
{% set render_ctx = h.resolve_ctx() %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="modal-header">
|
||||
{% block header_text %}<h5 class="modal-title">{{ _gettext('Create New Record') }}</h5>{% endblock %}
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
|
||||
<div class="modal-body">
|
||||
{{ lib.render_form_fields(form, form_opts=form_opts) }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{# "save and add" button is removed from modal (it won't function properly) #}
|
||||
{# % block create_form %}
|
||||
{{ lib.render_form(form, return_url, extra=None, form_opts=form_opts,
|
||||
action=url_for('.create_view', url=return_url),
|
||||
is_modal=True) }}
|
||||
{% endblock % #}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context%}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{% block body %}
|
||||
<div class="modal-header">
|
||||
{% block header_text %}
|
||||
<h3>{{ _gettext('View Record') + ' #' + request.args.get('id') }}</h3>
|
||||
{% endblock %}
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{% block details_search %}
|
||||
<div class="form-inline fa_filter_container col-lg-6">
|
||||
<label for="fa_filter">{{ _gettext('Filter') }}</label>
|
||||
<input id="fa_filter" type="text" class="ml-3 form-control">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block details_table %}
|
||||
<table class="table table-hover table-bordered searchable">
|
||||
{% for c, name in details_columns %}
|
||||
<tr>
|
||||
<td>
|
||||
<b>{{ name }}</b>
|
||||
</td>
|
||||
<td>
|
||||
{{ get_value(model, c) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,31 +0,0 @@
|
|||
{% import 'admin/static.html' as admin_static with context%}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{# store the jinja2 context for form_rules rendering logic #}
|
||||
{% set render_ctx = h.resolve_ctx() %}
|
||||
|
||||
{% block body %}
|
||||
<div class="modal-header">
|
||||
{% block header_text %}
|
||||
<h5 class="modal-title">{{ _gettext('Edit Record') + ' #' + request.args.get('id') }}</h5>
|
||||
{% endblock %}
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{% call lib.form_tag(action=url_for('.edit_view', id=request.args.get('id'), url=return_url)) %}
|
||||
<div class="modal-body">
|
||||
{{ lib.render_form_fields(form, form_opts=form_opts) }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,38 +0,0 @@
|
|||
{% import 'admin/lib.html' as lib with context %}
|
||||
|
||||
{% macro link(action, url, icon_class=None) %}
|
||||
<a class="icon" href="{{ url }}" title="{{ action.title or '' }}">
|
||||
<span class="{{ icon_class or action.icon_class }}"></span>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro view_row(action, row_id, row) %}
|
||||
{{ link(action, get_url('.details_view', id=row_id, url=return_url), 'fa fa-eye glyphicon glyphicon-eye-open') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro view_row_popup(action, row_id, row) %}
|
||||
{{ lib.add_modal_button(url=get_url('.details_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-eye glyphicon glyphicon-eye-open"></span>') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro edit_row(action, row_id, row) %}
|
||||
{{ link(action, get_url('.edit_view', id=row_id, url=return_url), 'fa fa-pencil glyphicon glyphicon-pencil') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro edit_row_popup(action, row_id, row) %}
|
||||
{{ lib.add_modal_button(url=get_url('.edit_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-pencil glyphicon glyphicon-pencil"></span>') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro delete_row(action, row_id, row) %}
|
||||
<form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
|
||||
{{ delete_form.id(value=get_pk_value(row)) }}
|
||||
{{ delete_form.url(value=return_url) }}
|
||||
{% if delete_form.csrf_token %}
|
||||
{{ delete_form.csrf_token }}
|
||||
{% elif csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endif %}
|
||||
<button onclick="return faHelpers.safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}">
|
||||
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endmacro %}
|
|
@ -1,27 +0,0 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% import 'admin/static.html' as admin_static with context%}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/rediscli.css', v='1.0.0') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="console">
|
||||
<div class="console-container">
|
||||
</div>
|
||||
<div class="console-line mb-4">
|
||||
<form action="#">
|
||||
<input type="text"></input>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
|
||||
<div id="execute-view-data" style="display:none;">{{ admin_view.get_url('.execute_view')|tojson|safe }}</div>
|
||||
<script src="{{ admin_static.url(filename='admin/js/rediscli.js', v='1.0.0') }}"></script>
|
||||
{% endblock %}
|
|
@ -1,32 +0,0 @@
|
|||
{% macro render(item, depth=0) %}
|
||||
{% set type = type_name(item) %}
|
||||
|
||||
{% if type == 'tuple' or type == 'list' %}
|
||||
{% if not item %}
|
||||
Empty {{ type }}.
|
||||
{% else %}
|
||||
{% for n in item %}
|
||||
{{ loop.index }}) {{ render(n, depth + 1) }}<br/>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% elif type == 'bool' %}
|
||||
{% if depth == 0 and item %}
|
||||
OK
|
||||
{% else %}
|
||||
<span class="type-bool">{{ item }}</span>
|
||||
{% endif %}
|
||||
{% elif type == 'str' or type == 'unicode' %}
|
||||
"{{ item }}"
|
||||
{% elif type == 'bytes' %}
|
||||
"{{ item.decode('utf-8') }}"
|
||||
{% elif type == 'TextWrapper' %}
|
||||
<pre>{{ item }}</pre>
|
||||
{% elif type == 'dict' %}
|
||||
{% for k, v in item.items() %}
|
||||
{{ loop.index }}) {{ k }} - {{ render(v, depth + 1) }}<br/>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ item }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{{ render(result) }}
|
|
@ -1,3 +0,0 @@
|
|||
{% macro url() -%}
|
||||
{{ get_url('{admin_endpoint}.static'.format(admin_endpoint=admin_view.admin.endpoint), *varargs, **kwargs) }}
|
||||
{%- endmacro %}
|
|
@ -7,9 +7,14 @@
|
|||
<hr>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.assistantMarksSubmit }}
|
||||
<hr>
|
||||
{{ form.finalPartMarksSubmit }}
|
||||
{% for field in form %}
|
||||
{% if field.widget.input_type == "submit" %}
|
||||
{{ field(class="btn btn-primary btn-block") }}
|
||||
|
||||
<br>
|
||||
{% else %}
|
||||
{{ field() }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -6,17 +6,36 @@
|
|||
|
||||
<hr>
|
||||
|
||||
<h2>Assistant's marks analysis</h2>
|
||||
|
||||
<p>
|
||||
This page shows an analysis of all marks of all assistants with an active user.
|
||||
<br>
|
||||
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 <em>individually</em>.
|
||||
</p>
|
||||
<p>
|
||||
The last two histograms show the oral and protocol marks of all active assistants <em>together</em>.
|
||||
</p>
|
||||
<p>
|
||||
An active assistant is an assistant with an active user. The marks are from all semesters, not only from the active semester.
|
||||
</p>
|
||||
|
||||
<h4>Export</h4>
|
||||
<p>
|
||||
You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally <code>Ctrl + p</code>. Select "Save as (PDF) file" afterwards instead of a printer!
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
{% for histInd in histIndices %}
|
||||
<img src="data:image/png;base64,{{ oralMarkHists[histInd]}}">
|
||||
<img src="data:image/png;base64,{{ protocolMarkHists[histInd]}}">
|
||||
{% for hist_ind in hist_indices %}
|
||||
<div class="row text-center">
|
||||
<div class="col-sm">
|
||||
<img class="img-fluid" src="data:image/png;base64,{{ oral_mark_hists[hist_ind]}}">
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<img class="img-fluid" src="data:image/png;base64,{{ protocol_mark_hists[hist_ind]}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{% endfor %}
|
||||
{% endblock body %}
|
||||
|
|
|
@ -6,10 +6,32 @@
|
|||
|
||||
<hr>
|
||||
|
||||
{% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %}
|
||||
<img src="data:image/png;base64,{{ activeSemesterFinalPartMarksHist }}">
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<h2>Final part marks analysis</h2>
|
||||
|
||||
<img src="data:image/png;base64,{{ meanFinalPartMarksPlot }}">
|
||||
<p>
|
||||
The histograms on this page show the final experiment marks of each part in the active semester.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h4>Export</h4>
|
||||
<p>
|
||||
You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally <code>Ctrl + p</code>. Select "Save as (PDF) file" afterwards instead of a printer!
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
{% for active_semester_final_part_marks_hist in active_semester_final_part_marks_hists %}
|
||||
<img class="img-fluid" src="data:image/png;base64,{{ active_semester_final_part_marks_hist }}">
|
||||
|
||||
<hr>
|
||||
{% endfor %}
|
||||
|
||||
<img class="img-fluid" src="data:image/png;base64,{{ mean_final_part_mark_plot }}">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -9,14 +9,6 @@
|
|||
<div class="d-inline-flex">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Experiment
|
||||
</th>
|
||||
<td>
|
||||
{{ experiment_label }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Group number
|
||||
|
@ -31,6 +23,47 @@
|
|||
|
||||
<br>
|
||||
|
||||
<div class="d-inline-flex">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Experiment
|
||||
</th>
|
||||
<td>
|
||||
{{ semester_experiment.experiment.str() }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Oral mark weighting
|
||||
</th>
|
||||
<td>
|
||||
{{ semester_experiment.oral_weighting }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Protocol mark weighting
|
||||
</th>
|
||||
<td>
|
||||
{{ semester_experiment.protocol_weighting }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Final experiment mark weighting
|
||||
</th>
|
||||
<td>
|
||||
{{ semester_experiment.final_weighting }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="d-inline-flex">
|
||||
|
@ -50,7 +83,7 @@
|
|||
{% for appointment_field in appointment_fields %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ appointment_field }}
|
||||
{{ appointment_field(class="form-control") }}
|
||||
</td>
|
||||
<td>
|
||||
{{ appointment_field.description }}
|
||||
|
@ -95,10 +128,10 @@
|
|||
{{ student }}
|
||||
</th>
|
||||
<td>
|
||||
{{ oral_experiment_mark }}
|
||||
{{ oral_experiment_mark(class="form-control", style="width: auto;") }}
|
||||
</td>
|
||||
<td>
|
||||
{{ protocol_experiment_mark }}
|
||||
{{ protocol_experiment_mark(class="form-control", style="width: auto;") }}
|
||||
</td>
|
||||
<td>
|
||||
{% if final_experiment_mark is none %}
|
||||
|
@ -126,13 +159,13 @@
|
|||
<div class="form-group form-row">
|
||||
<label for={{ form.note.id }} class="col-form-label">{{ form.note.label }}³</label>
|
||||
<div class="col">
|
||||
{{ form.note }}
|
||||
{{ form.note(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
{{ form.submit }}
|
||||
{{ form.submit(class="btn btn-primary btn-block") }}
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
<hr>
|
||||
|
||||
<p>
|
||||
Number of <strong>missing</strong> final experiment marks:
|
||||
{{ number_of_missing_final_experiment_marks }} / {{ number_of_all_experiment_marks }}
|
||||
Number of <strong>missing</strong> final experiment marks: {{ number_of_missing_final_experiment_marks }}
|
||||
</p>
|
||||
|
||||
{{ super() }}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
<h3>Home</h3>
|
||||
<p>
|
||||
This is your home page. Here, you currently only find the number of
|
||||
This is your home page. Here, you 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
|
||||
|
@ -21,7 +21,7 @@
|
|||
and protocol marks of an experiment are set.
|
||||
</p>
|
||||
|
||||
<h3 id="_group_experiment">Group Experiment</h3>
|
||||
<h3>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
|
||||
|
@ -48,38 +48,25 @@
|
|||
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.
|
||||
After editing appointment date(s) or experiment marks, click on
|
||||
<em>Save</em> 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>
|
||||
<h3>User settings</h3>
|
||||
<p>
|
||||
In the user settings, You can change your active semester, user information
|
||||
and password. Don't forget to click on <em>Save</em> after changing any
|
||||
option.
|
||||
</p>
|
||||
|
||||
<h4>Active semester</h4>
|
||||
<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
|
||||
|
@ -90,3 +77,25 @@
|
|||
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>
|
||||
|
||||
<h4>User information</h4>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h4>Password</h4>
|
||||
<p>
|
||||
You can generate a new random password by 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>
|
||||
<p>
|
||||
If you forget your password, you have to write an admin to reset it.
|
||||
</p>
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
{% 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 %}
|
||||
<div class="row" style="text-align: center;">
|
||||
<div class="col">
|
||||
User: {{ current_user }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
Active semester: {{ active_semester_str() }}
|
||||
<span class="fa fa-question-circle" title="The active semester can be changed in user settings"></span>
|
||||
</div>
|
||||
|
||||
{% if (role == "admin") and (current_user.has_role("assistant")) %}
|
||||
<div class="col">
|
||||
<a
|
||||
href="{{ url_for('main.index') }}assistant"
|
||||
>Assistant space</a>
|
||||
</div>
|
||||
{% elif (role == "assistant") and (current_user.has_role("admin")) %}
|
||||
<div class="col">
|
||||
<a
|
||||
href="{{ url_for('main.index') }}admin"
|
||||
>Admin space</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
103
advlabdb/templates/security/login_user.html
Normal file
103
advlabdb/templates/security/login_user.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
{% extends "security/base.html" %}
|
||||
{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors %}
|
||||
|
||||
{% block title %}AdvLabDB - Login{% endblock title %}
|
||||
|
||||
{% block body_attribs %}
|
||||
style="
|
||||
background-image: radial-gradient(#35393b, #181a1b);
|
||||
"
|
||||
{% endblock body_attribs %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.fs-error-msg {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<font color="white">
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 95vh;
|
||||
">
|
||||
<div style="
|
||||
text-align: center;
|
||||
">
|
||||
<h1>AdvLabDB</h1>
|
||||
<p>Database for labs</p>
|
||||
|
||||
<h2>Login</h2>
|
||||
|
||||
{% include "security/_messages.html" %}
|
||||
|
||||
<br>
|
||||
|
||||
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{{ render_form_errors(login_user_form) }}
|
||||
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Email
|
||||
</td>
|
||||
<td>
|
||||
{{ login_user_form.email() }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Password
|
||||
</td>
|
||||
<td>
|
||||
{{ login_user_form.password() }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if login_user_form.email.errors %}
|
||||
<ul>
|
||||
{% for error in login_user_form.email.errors %}
|
||||
<li class="fs-error-msg">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if login_user_form.password.errors %}
|
||||
<ul>
|
||||
{% for error in login_user_form.password.errors %}
|
||||
<li class="fs-error-msg">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
|
||||
Remember me {{ login_user_form.remember() }}
|
||||
|
||||
{% if login_user_form.remember.errors %}
|
||||
<ul>
|
||||
{% for error in login_user_form.remember.errors %}
|
||||
<li class="fs-error-msg">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{{ render_field_errors(login_user_form.csrf_token) }}
|
||||
|
||||
<br>
|
||||
|
||||
{{ render_field(login_user_form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</font>
|
||||
{% endblock body %}
|
|
@ -1,50 +0,0 @@
|
|||
from pathlib import Path
|
||||
from shutil import copytree, rmtree
|
||||
|
||||
import click
|
||||
from flask_admin import __file__ as flask_admin_path
|
||||
|
||||
|
||||
def _copy_admin_templates():
|
||||
src = Path(flask_admin_path).parent / "templates/bootstrap4/admin"
|
||||
if not src.is_dir():
|
||||
click.echo(click.style(f"Templates could not be found at {src}", fg="red"))
|
||||
return
|
||||
|
||||
dist = Path("advlabdb/templates/admin")
|
||||
if dist.is_dir():
|
||||
if not click.confirm(
|
||||
click.style(f"The directory {dist} already exists! Do you want to overwrite it?", fg="yellow")
|
||||
):
|
||||
return
|
||||
|
||||
rmtree(dist)
|
||||
click.echo(click.style("Old templates deleted!", fg="yellow"))
|
||||
|
||||
copytree(src, dist)
|
||||
click.echo(click.style(f"Copied {src} -> {dist}", fg="green"))
|
||||
|
||||
click.echo(
|
||||
click.style(
|
||||
f"""
|
||||
_________
|
||||
| WARNING
|
||||
| -------
|
||||
| You might have to edit the file {dist}/base.html
|
||||
| by adding nav in the following way:
|
||||
| This line:\t<ul class="navbar-nav mr-auto">
|
||||
| Becomes:\t<ul class="nav navbar-nav mr-auto">
|
||||
|
|
||||
| This will prevent the navigation bar from expanding
|
||||
| such that some elements can not be seen.
|
||||
| Refer to this pull request:
|
||||
| https://github.com/flask-admin/flask-admin/pull/2233
|
||||
|
|
||||
| If the above pull request is merged and flask-admin
|
||||
| is on a new release after the merge,
|
||||
| then this step is not needed.
|
||||
_________
|
||||
""",
|
||||
fg="yellow",
|
||||
)
|
||||
)
|
|
@ -14,12 +14,12 @@ def _reset_admin_password(manage):
|
|||
|
||||
with app.app_context():
|
||||
with db.session.begin():
|
||||
admins = db.session.execute(select(Admin).join(User).where(User.active == True)).scalars().all()
|
||||
admins = db.session.scalars(select(Admin).join(User).where(User.active == True)).all()
|
||||
activate_user = False
|
||||
|
||||
if len(admins) == 0:
|
||||
click.echo("There is no admin with an active user. The user of the chosen admin will be activated.")
|
||||
admins = db.session.execute(select(Admin)).scalars().all()
|
||||
admins = db.session.scalars(select(Admin)).all()
|
||||
activate_user = True
|
||||
|
||||
num_admins = len(admins)
|
||||
|
|
|
@ -4,7 +4,6 @@ import subprocess # nosec 404
|
|||
|
||||
import click
|
||||
|
||||
from cli.maintain.copy_admin_templates.main import _copy_admin_templates
|
||||
from cli.maintain.reset_admin_password.main import _reset_admin_password
|
||||
from cli.setup.generate_secrets.main import _generate_secrets
|
||||
from cli.setup.init_db.main import _init_db
|
||||
|
@ -70,14 +69,6 @@ def reset_admin_password():
|
|||
_reset_admin_password(Manage)
|
||||
|
||||
|
||||
@maintain.command(
|
||||
short_help="Copy admin templates",
|
||||
help="Copy the templates from the Flask-Admin package. This is only needed if the templates should be updated to a new version after a new release of Flask-Admin.",
|
||||
)
|
||||
def copy_admin_templates():
|
||||
_copy_admin_templates()
|
||||
|
||||
|
||||
@cli.group(
|
||||
short_help="Test commands.",
|
||||
help="Commands used to test AdvLabDB.",
|
||||
|
|
229
poetry.lock
generated
229
poetry.lock
generated
|
@ -40,6 +40,24 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.0.5"
|
||||
description = "Python library for calculating contours of 2D quadrilateral grids"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
numpy = ">=1.16"
|
||||
|
||||
[package.extras]
|
||||
bokeh = ["bokeh", "selenium"]
|
||||
docs = ["docutils (<0.18)", "sphinx", "sphinx-rtd-theme"]
|
||||
test = ["Pillow", "flake8", "isort", "matplotlib", "pytest"]
|
||||
test-minimal = ["pytest"]
|
||||
test-no-codebase = ["Pillow", "matplotlib", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.11.0"
|
||||
|
@ -66,8 +84,8 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "1.2.1"
|
||||
description = "A robust email syntax and deliverability validation library."
|
||||
version = "1.3.0"
|
||||
description = "A robust email address syntax and deliverability validation library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
@ -149,7 +167,7 @@ Flask = "*"
|
|||
|
||||
[[package]]
|
||||
name = "Flask-Security-Too"
|
||||
version = "5.0.1"
|
||||
version = "5.0.2"
|
||||
description = "Simple security for Flask apps."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -202,7 +220,7 @@ email = ["email-validator"]
|
|||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.37.1"
|
||||
version = "4.37.3"
|
||||
description = "Tools to manipulate font files"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -252,7 +270,7 @@ tornado = ["tornado (>=0.2)"]
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
version = "3.4"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -290,7 +308,7 @@ python-versions = ">=3.7"
|
|||
|
||||
[[package]]
|
||||
name = "Mako"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -314,22 +332,23 @@ python-versions = ">=3.7"
|
|||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.5.3"
|
||||
version = "3.6.0"
|
||||
description = "Python plotting package"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
|
||||
[package.dependencies]
|
||||
contourpy = ">=1.0.1"
|
||||
cycler = ">=0.10"
|
||||
fonttools = ">=4.22.0"
|
||||
kiwisolver = ">=1.0.1"
|
||||
numpy = ">=1.17"
|
||||
numpy = ">=1.19"
|
||||
packaging = ">=20.0"
|
||||
pillow = ">=6.2.0"
|
||||
pyparsing = ">=2.2.1"
|
||||
python-dateutil = ">=2.7"
|
||||
setuptools_scm = ">=4,<7"
|
||||
setuptools_scm = ">=7"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
|
@ -413,16 +432,17 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
|
|||
|
||||
[[package]]
|
||||
name = "setuptools-scm"
|
||||
version = "6.4.2"
|
||||
version = "7.0.5"
|
||||
description = "the blessed package to manage your versions by scm tags"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=20.0"
|
||||
setuptools = "*"
|
||||
tomli = ">=1.0.0"
|
||||
typing-extensions = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6.2)", "virtualenv (>20)"]
|
||||
|
@ -476,6 +496,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.3.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "Werkzeug"
|
||||
version = "2.2.2"
|
||||
|
@ -507,7 +535,7 @@ email = ["email-validator"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "7727fb4275266074afb431bd624d0e94b3896c220f736108b4b593a9207edf53"
|
||||
content-hash = "068b146f1e4f736e280ec42004d834ad249ab053a439796600d423f52d55b3d3"
|
||||
|
||||
[metadata.files]
|
||||
alembic = [
|
||||
|
@ -526,6 +554,77 @@ colorama = [
|
|||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
contourpy = [
|
||||
{file = "contourpy-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:87121b9428ac568fb84fae4af5e7852fc34f02eadc4e3e91f6c8989327692186"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fb782982c42cee667b892a0b0c52a9f6c7ecf1da5c5f4345845f04eaa862f93"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:689d7d2a840619915d0abd1ecc6e399fee202f8ad315acda2807f4ca420d0802"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d88814befbd1433152c5f6dd536905149ba028d795a22555b149ae0a36024d9e"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df65f4b2b4e74977f0336bef12a88051ab24e6a16873cd9249f34d67cb3e345d"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6b4c0c723664f65c2a47c8cb6ebbf660b0b2e2d936adf2e8503d4e93359465"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bcc98d397c3dea45d5b262029564b29cb8e945f2607a38bee6163694c0a8b4ef"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2bf5c846c257578b03d498b20f54f53551616a507d8e5463511c58bb58e9a9cf"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdacddb18d55ffec42d1907079cdc04ec4fa8a990cdf5b9d9fe67d281fc0d12e"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-win32.whl", hash = "sha256:434942fa2f9019b9ae525fb752dc523800c49a1a28fbd6d9240b0fa959573dcc"},
|
||||
{file = "contourpy-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:3b3082ade8849130203d461b98c2a061b382c46074b43b4edd5cefd81af92b8a"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:057114f698ffb9e54657e8fda6802e2f5c8fad609845cf6afaf31590ef6a33c0"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:218722a29c5c26677d37c44f5f8a372daf6f07870aad793a97d47eb6ad6b3290"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c02e22cf09996194bcb3a4784099975cf527d5c29caf759abadf29ebdb2fe27"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d5ee865b5fd16bf62d72122aadcc90aab296c30c1adb0a32b4b66bd843163e"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45822b0a2a452327ab4f95efe368d234d5294bbf89a99968be27c7938a21108"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dca5be83a6dfaf933a46e3bc2b9f2685e5ec61b22f6a38ad740aac9c16e9a0ff"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c3f2f6b898a40207843ae01970e57e33d22a26b22f23c6a5e07b4716751085f"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c2b4eab7c12f9cb460509bc34a3b086f9802f0dba27c89a63df4123819ad64af"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09ed9b63f4df8a7591b7a4a26c1ad066dcaafda1f846250fdcb534074a411692"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-win32.whl", hash = "sha256:f670686d99c867d0f24b28ce8c6f02429c6eef5e2674aab287850d0ee2d20437"},
|
||||
{file = "contourpy-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:c51568e94f7f232296de30002f2a50f77a7bd346673da3e4f2aaf9d2b833f2e5"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7c9e99aac7b430f6a9f15eebf058c742097cea3369f23a2bfc5e64d374b67e3a"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3210d93ad2af742b6a96cf39792f7181822edbb8fe11c3ef29d1583fe637a8d8"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128bd7acf569f8443ad5b2227f30ac909e4f5399ed221727eeacf0c6476187e6"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813c2944e940ef8dccea71305bacc942d4b193a021140874b3e58933ec44f5b6"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a74afd8d560eaafe0d9e3e1db8c06081282a05ca4de00ee416195085a79d7d3d"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d0ad9a85f208473b1f3613c45756c7aa6fcc288266a8c7b873f896aaf741b6b"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:60f37acd4e4227c5a29f737d9a85ca3145c529a8dd4bf70af7f0637c61b49222"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:b50e481a4317a8efcfffcfddcd4c9b36eacba440440e70cbe0256aeb6fd6abae"},
|
||||
{file = "contourpy-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:0395ae71164bfeb2dedd136e03c71a2718a5aa9873a46f518f4133be0d63e1d2"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3ca40d7844b391d90b864c6a6d1bb6b88b09035fb4d866d64d43c4d26fb0ab64"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3109fa601d2a448cec4643abd3a31f972bf05b7c2f2e83df9d3429878f8c10ae"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06c4d1dde5ee4f909a8a95ba1eb04040c6c26946b4f3b5beaf10d45f14e940ee"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f54dcc9bb9390fd0636301ead134d46d5229fe86da0db4d974c0fda349f560e"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46b8e24813e2fb5a3e598c1f8b9ae403e1438cb846a80cc2b33cddf19dddd7f2"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:061e1f066c419ffe25b615a1df031b4832ea1d7f2676937e69e8e00e24512005"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:19ea64fa0cf389d2ebc10974616acfa1fdecbd73d1fd9c72215b782f3c40f561"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfe924e5a63861c82332a12adeeab955dc8c8009ddbbd80cc2fcca049ff89a49"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bed3a2a823a041e8d249b1a7ec132933e1505299329b5cfe1b2b5ec689ec7675"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-win32.whl", hash = "sha256:0389349875424aa8c5e61f757e894687916bc4e9616cc6afcbd8051aa2428952"},
|
||||
{file = "contourpy-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:2b5e334330d82866923015b455260173cb3b9e3b4e297052d758abd262031289"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:def9a01b73c9e27d70ea03b381fb3e7aadfac1f398dbd63751313c3a46747ef5"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:59c827e536bb5e3ef58e06da0faba61fd89a14f30b68bcfeca41f43ca83a1942"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f05d311c937da03b0cd26ac3e14cb991f6ff8fc94f98b3df9713537817539795"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:970a4be7ec84ccda7c27cb4ae74930bbbd477bc8d849ed55ea798084dd5fca8c"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7672148f8fca48e4efc16aba24a7455b40c22d4f8abe42475dec6a12b0bb9a"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eba62b7c21a33e72dd8adab2b92dd5610d8527f0b2ac28a8e0770e71b21a13f9"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:dd084459ecdb224e617e4ab3f1d5ebe4d1c48facb41f24952b76aa6ba9712bb0"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5158616ab39d34b76c50f40c81552ee180598f7825dc7a66fd187d29958820f"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f856652f9b533c6cd2b9ad6836a7fc0e43917d7ff15be46c5baf1350f8cdc5d9"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-win32.whl", hash = "sha256:f1cc623fd6855b25da52b3275e0c9e51711b86a9dccc75f8c9ab4432fd8e42c7"},
|
||||
{file = "contourpy-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:e67dcaa34dcd908fcccbf49194211d847c731b6ebaac661c1c889f1bf6af1e44"},
|
||||
{file = "contourpy-1.0.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfd634cb9685161b2a51f73a7fc4736fd0d67a56632d52319317afaa27f08243"},
|
||||
{file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79908b9d02b1d6c1c71ff3b7ad127f3f82e14a8e091ab44b3c7e34b649fea733"},
|
||||
{file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4963cf08f4320d98ae72ec7694291b8ab85cb7da3b0cd824bc32701bc992edf"},
|
||||
{file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cfc067ddde78b76dcbc9684d82688b7d3c5158fa2254a085f9bcb9586c1e2d8"},
|
||||
{file = "contourpy-1.0.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9939796abcadb2810a63dfb26ff8ca4595fe7dd70a3ceae7f607a2639b714307"},
|
||||
{file = "contourpy-1.0.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d8150579bf30cdf896906baf256aa200cd50dbe6e565c17d6fd3d678e21ff5de"},
|
||||
{file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed9c91bf4ce614efed5388c3f989a7cfe08728ab871d995a486ea74ff88993db"},
|
||||
{file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46a04588ceb7cf132568e0e564a854627ef87a1ed3bf536234540a79ced44b0"},
|
||||
{file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b85553699862c09937a7a5ea14ee6229087971a7d51ae97d5f4b407f571a2c17"},
|
||||
{file = "contourpy-1.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99a8071e351b50827ad976b92ed91845fb614ac67a3c41109b24f3d8bd3afada"},
|
||||
{file = "contourpy-1.0.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fb0458d74726937ead9e2effc91144aea5a58ecee9754242f8539a782bed685a"},
|
||||
{file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f89f0608a5aa8142ed0e53957916623791a88c7f5e5f07ae530c328beeb888f"},
|
||||
{file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce763369e646e59e4ca2c09735cd1bdd3048d909ad5f2bc116e83166a9352f3c"},
|
||||
{file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c16fa267740d67883899e054cccb4279e002f3f4872873b752c1ba15045ff49"},
|
||||
{file = "contourpy-1.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a30e95274f5c0e007ccc759ec258aa5708c534ec058f153ee25ac700a2f1438b"},
|
||||
{file = "contourpy-1.0.5.tar.gz", hash = "sha256:896631cd40222aef3697e4e51177d14c3709fda49d30983269d584f034acc8a4"},
|
||||
]
|
||||
cycler = [
|
||||
{file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"},
|
||||
{file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
|
||||
|
@ -535,8 +634,8 @@ dnspython = [
|
|||
{file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
|
||||
]
|
||||
email-validator = [
|
||||
{file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"},
|
||||
{file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"},
|
||||
{file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"},
|
||||
{file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"},
|
||||
]
|
||||
Flask = [
|
||||
{file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"},
|
||||
|
@ -557,8 +656,8 @@ Flask-Principal = [
|
|||
{file = "Flask-Principal-0.4.0.tar.gz", hash = "sha256:f5d6134b5caebfdbb86f32d56d18ee44b080876a27269560a96ea35f75c99453"},
|
||||
]
|
||||
Flask-Security-Too = [
|
||||
{file = "Flask-Security-Too-5.0.1.tar.gz", hash = "sha256:436e3ba05984f010e27741055c69bc12cc45566946e4d805990a2628c3656594"},
|
||||
{file = "Flask_Security_Too-5.0.1-py2.py3-none-any.whl", hash = "sha256:2197fa7fceff6c485aa9774ccc79f653e2f66db25e831b892a002261ede5fc3a"},
|
||||
{file = "Flask-Security-Too-5.0.2.tar.gz", hash = "sha256:36fee0da5d1b3d211caf274553b7753478c208997c624abb84ebba4261de65c2"},
|
||||
{file = "Flask_Security_Too-5.0.2-py2.py3-none-any.whl", hash = "sha256:5f81e220c63f8f319bcd04327267328fd4b58ca05aa6f3ffc458756dfa78d579"},
|
||||
]
|
||||
Flask-SQLAlchemy = [
|
||||
{file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"},
|
||||
|
@ -569,8 +668,8 @@ Flask-WTF = [
|
|||
{file = "Flask_WTF-1.0.1-py3-none-any.whl", hash = "sha256:9d733658c80be551ce7d5bc13c7a7ac0d80df509be1e23827c847d9520f4359a"},
|
||||
]
|
||||
fonttools = [
|
||||
{file = "fonttools-4.37.1-py3-none-any.whl", hash = "sha256:fff6b752e326c15756c819fe2fe7ceab69f96a1dbcfe8911d0941cdb49905007"},
|
||||
{file = "fonttools-4.37.1.zip", hash = "sha256:4606e1a88ee1f6699d182fea9511bd9a8a915d913eab4584e5226da1180fcce7"},
|
||||
{file = "fonttools-4.37.3-py3-none-any.whl", hash = "sha256:a5bc5f5d48faa4085310b8ebd4c5d33bf27c6636c5f10a7de792510af2745a81"},
|
||||
{file = "fonttools-4.37.3.zip", hash = "sha256:f32ef6ec966cf0e7d2aa88601fed2e3a8f2851c26b5db2c80ccc8f82bee4eedc"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-1.1.3-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b"},
|
||||
|
@ -633,8 +732,8 @@ gunicorn = [
|
|||
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
|
@ -715,8 +814,8 @@ kiwisolver = [
|
|||
{file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"},
|
||||
]
|
||||
Mako = [
|
||||
{file = "Mako-1.2.2-py3-none-any.whl", hash = "sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534"},
|
||||
{file = "Mako-1.2.2.tar.gz", hash = "sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f"},
|
||||
{file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"},
|
||||
{file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"},
|
||||
]
|
||||
MarkupSafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
|
@ -761,41 +860,47 @@ MarkupSafe = [
|
|||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||
]
|
||||
matplotlib = [
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-win32.whl", hash = "sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2"},
|
||||
{file = "matplotlib-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-win32.whl", hash = "sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0"},
|
||||
{file = "matplotlib-3.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-win32.whl", hash = "sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a"},
|
||||
{file = "matplotlib-3.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-win32.whl", hash = "sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0"},
|
||||
{file = "matplotlib-3.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56"},
|
||||
{file = "matplotlib-3.5.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a"},
|
||||
{file = "matplotlib-3.5.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2"},
|
||||
{file = "matplotlib-3.5.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce"},
|
||||
{file = "matplotlib-3.5.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9"},
|
||||
{file = "matplotlib-3.5.3.tar.gz", hash = "sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:6b98e098549d3aea2bfb93f38f0b2ecadcb423fa1504bbff902c01efdd833fd8"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:798559837156b8e2e2df97cffca748c5c1432af6ec5004c2932e475d813f1743"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e572c67958f7d55eae77f5f64dc7bd31968cc9f24c233926833efe63c60545f2"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec2edf7f74829eae287aa53d64d83ad5d43ee51d29fb1d88e689d8b36028312"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51092d13499be72e47c15c3a1ae0209edaca6be42b65ffbbefbe0c85f6153c6f"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9295ca10a140c21e40d2ee43ef423213dc20767f6cea6b87c36973564bc51095"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-win32.whl", hash = "sha256:1a4835c177821f3729be27ae9be7b8ae209fe75e83db7d9b2bfd319a998f0a42"},
|
||||
{file = "matplotlib-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:2b60d4abcb6a405ca7d909c80791b00637d22c62aa3bb0ffff7e589f763867f5"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:66a0db13f77aa7806dba29273874cf862450c61c2e5158245d17ee85d983fe8e"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1739935d293d0348d7bf662e8cd0edb9c2aa8f20ccd646db755ce0f3456d24e4"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1559213b803959a2b8309122585b5226d1c2fb66c933b1a2094cf1e99cb4fb90"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5bd3b3ff191f81509d9a1afd62e1e3cda7a7889c35b5b6359a1241fe1511015"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1954d71cdf15c19e7f3bf2235a4fe1600ba42f34d472c9495bcf54d75a43e4e"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840712f4b4c7d2a119f993d7e43ca9bcaa73aeaa24c322fa2bdf4f689a3ee09"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-win32.whl", hash = "sha256:89e1978c3fbe4e3d4c6ad7db7e6f982607cb2546f982ccbe42708392437b1972"},
|
||||
{file = "matplotlib-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:9711ef291e184b5a73c9d3af3f2d5cfe25d571c8dd95aa498415f74ac7e221a8"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:fbbceb0a0dfe9213f6314510665a32ef25fe29b50657567cd00115fbfcb3b20d"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:62319d57dab5ad3e3494dd97a214e22079d3f72a0c8a2fd001829c2c6abbf8d1"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:140316427a7c384e3dd37efb3a73cd67e14b0b237a6d277def91227f43cdcec2"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ccea337fb9a44866c5300c594b13d4d87e827ebc3c353bff15d298bac976b654"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:16a899b958dd76606b571bc7eaa38f09160c27dfb262e493584644cfd4a77f0f"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd73a16a759865831be5a8fb6546f2a908c8d7d7f55c75f94ee7c2ca13cc95de"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-win32.whl", hash = "sha256:2ed779a896b70c8012fe301fb91ee37e713e1dda1eb8f37de04cdbf506706983"},
|
||||
{file = "matplotlib-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:eca6f59cd0729edaeaa7032d582dffce518a420d4961ef3e8c93dce86be352c3"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:408bbf968c15e9e38df9f25a588e372e28a43240cf5884c9bc6039a5021b7d5b"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7127e2b94571318531caf098dc9e8f60f5aba1704600f0b2483bf151d535674a"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0d5b9b14ccc7f539143ac9eb1c6b57d26d69ca52d30c3d719a7bc4123579e44"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa19508d8445f5648cd1ffe4fc6d4f7daf8b876f804e9a453df6c3708f6200b"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ae1b9b555212c1e242666af80e7ed796705869581e2d749971db4e682ccc1f3"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0958fc3fdc59c1b716ee1a5d14e73d03d541d873241a37c5c3a86f7ef6017923"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-win32.whl", hash = "sha256:efe9e8037b989b14bb1887089ae763385431cc06fe488406413079cfd2a3a089"},
|
||||
{file = "matplotlib-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b0320f882214f6ffde5992081520b57b55450510bdaa020e96aacff9b7ae10e6"},
|
||||
{file = "matplotlib-3.6.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:11c1987b803cc2b26725659cfe817478f0a9597878e5c4bf374cfe4e12cbbd79"},
|
||||
{file = "matplotlib-3.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:802feae98addb9f21707649a7f229c90a59fad34511881f20b906a5e8e6ea475"},
|
||||
{file = "matplotlib-3.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd2e12f8964f8fb4ba1984df71d85d02ef0531e687e59f78ec8fc07271a3857"},
|
||||
{file = "matplotlib-3.6.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4eba6972b796d97c8fcc5266b6dc42ef27c2dce4421b846cded0f3af851b81c9"},
|
||||
{file = "matplotlib-3.6.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:df26a09d955b3ab9b6bc18658b9403ed839096c97d7abe8806194e228a485a3c"},
|
||||
{file = "matplotlib-3.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e01382c06ac3710155a0ca923047c5abe03c676d08f03e146c6a240d0a910713"},
|
||||
{file = "matplotlib-3.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4699bb671dbc4afdb544eb893e4deb8a34e294b7734733f65b4fd2787ba5fbc6"},
|
||||
{file = "matplotlib-3.6.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:657fb7712185f82211170ac4debae0800ed4f5992b8f7ebba2a9eabaf133a857"},
|
||||
{file = "matplotlib-3.6.0.tar.gz", hash = "sha256:c5108ebe67da60a9204497d8d403316228deb52b550388190c53a57394d41531"},
|
||||
]
|
||||
numpy = [
|
||||
{file = "numpy-1.23.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee"},
|
||||
|
@ -908,8 +1013,8 @@ setuptools = [
|
|||
{file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"},
|
||||
]
|
||||
setuptools-scm = [
|
||||
{file = "setuptools_scm-6.4.2-py3-none-any.whl", hash = "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4"},
|
||||
{file = "setuptools_scm-6.4.2.tar.gz", hash = "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30"},
|
||||
{file = "setuptools_scm-7.0.5-py3-none-any.whl", hash = "sha256:7930f720905e03ccd1e1d821db521bff7ec2ac9cf0ceb6552dd73d24a45d3b02"},
|
||||
{file = "setuptools_scm-7.0.5.tar.gz", hash = "sha256:031e13af771d6f892b941adb6ea04545bbf91ebc5ce68c78aaf3fff6e1fb4844"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
|
@ -962,6 +1067,10 @@ tomli = [
|
|||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
]
|
||||
Werkzeug = [
|
||||
{file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
|
||||
{file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "advlabdb"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
description = "Database with a web interface for labs."
|
||||
authors = ["Mo Bitar <mo8it@proton.me>"]
|
||||
readme = "README.adoc"
|
||||
|
@ -9,17 +9,17 @@ readme = "README.adoc"
|
|||
# TODO: Use ^ instead of >=
|
||||
python = "^3.10"
|
||||
click = ">=8.1.3"
|
||||
email-validator = ">=1.2.1"
|
||||
email-validator = ">=1.3.0"
|
||||
flask = ">=2.2.2"
|
||||
flask-admin = ">=1.6.0"
|
||||
flask-login = ">=0.6.2"
|
||||
flask-migrate = ">=3.1.0"
|
||||
flask-security-Too = ">=5.0.1"
|
||||
flask-security-Too = ">=5.0.2"
|
||||
flask-sqlalchemy = ">=2.5.1"
|
||||
flask-wtf = ">=1.0.1"
|
||||
gunicorn = ">=20.1.0"
|
||||
markupsafe = ">=2.1.1"
|
||||
matplotlib = ">=3.5.3"
|
||||
matplotlib = ">=3.6.0"
|
||||
numpy = ">=1.23.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
|
187
requirements.txt
187
requirements.txt
|
@ -10,15 +10,85 @@ click==8.1.3 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
colorama==0.4.5 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" \
|
||||
--hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
|
||||
--hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
|
||||
contourpy==1.0.5 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0389349875424aa8c5e61f757e894687916bc4e9616cc6afcbd8051aa2428952 \
|
||||
--hash=sha256:0395ae71164bfeb2dedd136e03c71a2718a5aa9873a46f518f4133be0d63e1d2 \
|
||||
--hash=sha256:057114f698ffb9e54657e8fda6802e2f5c8fad609845cf6afaf31590ef6a33c0 \
|
||||
--hash=sha256:061e1f066c419ffe25b615a1df031b4832ea1d7f2676937e69e8e00e24512005 \
|
||||
--hash=sha256:06c4d1dde5ee4f909a8a95ba1eb04040c6c26946b4f3b5beaf10d45f14e940ee \
|
||||
--hash=sha256:09ed9b63f4df8a7591b7a4a26c1ad066dcaafda1f846250fdcb534074a411692 \
|
||||
--hash=sha256:0f7672148f8fca48e4efc16aba24a7455b40c22d4f8abe42475dec6a12b0bb9a \
|
||||
--hash=sha256:0f89f0608a5aa8142ed0e53957916623791a88c7f5e5f07ae530c328beeb888f \
|
||||
--hash=sha256:128bd7acf569f8443ad5b2227f30ac909e4f5399ed221727eeacf0c6476187e6 \
|
||||
--hash=sha256:19ea64fa0cf389d2ebc10974616acfa1fdecbd73d1fd9c72215b782f3c40f561 \
|
||||
--hash=sha256:1fb782982c42cee667b892a0b0c52a9f6c7ecf1da5c5f4345845f04eaa862f93 \
|
||||
--hash=sha256:218722a29c5c26677d37c44f5f8a372daf6f07870aad793a97d47eb6ad6b3290 \
|
||||
--hash=sha256:2b5e334330d82866923015b455260173cb3b9e3b4e297052d758abd262031289 \
|
||||
--hash=sha256:2bf5c846c257578b03d498b20f54f53551616a507d8e5463511c58bb58e9a9cf \
|
||||
--hash=sha256:2d0ad9a85f208473b1f3613c45756c7aa6fcc288266a8c7b873f896aaf741b6b \
|
||||
--hash=sha256:2f54dcc9bb9390fd0636301ead134d46d5229fe86da0db4d974c0fda349f560e \
|
||||
--hash=sha256:3109fa601d2a448cec4643abd3a31f972bf05b7c2f2e83df9d3429878f8c10ae \
|
||||
--hash=sha256:3210d93ad2af742b6a96cf39792f7181822edbb8fe11c3ef29d1583fe637a8d8 \
|
||||
--hash=sha256:3b3082ade8849130203d461b98c2a061b382c46074b43b4edd5cefd81af92b8a \
|
||||
--hash=sha256:3c3f2f6b898a40207843ae01970e57e33d22a26b22f23c6a5e07b4716751085f \
|
||||
--hash=sha256:3ca40d7844b391d90b864c6a6d1bb6b88b09035fb4d866d64d43c4d26fb0ab64 \
|
||||
--hash=sha256:3cfc067ddde78b76dcbc9684d82688b7d3c5158fa2254a085f9bcb9586c1e2d8 \
|
||||
--hash=sha256:434942fa2f9019b9ae525fb752dc523800c49a1a28fbd6d9240b0fa959573dcc \
|
||||
--hash=sha256:46b8e24813e2fb5a3e598c1f8b9ae403e1438cb846a80cc2b33cddf19dddd7f2 \
|
||||
--hash=sha256:59c827e536bb5e3ef58e06da0faba61fd89a14f30b68bcfeca41f43ca83a1942 \
|
||||
--hash=sha256:60f37acd4e4227c5a29f737d9a85ca3145c529a8dd4bf70af7f0637c61b49222 \
|
||||
--hash=sha256:689d7d2a840619915d0abd1ecc6e399fee202f8ad315acda2807f4ca420d0802 \
|
||||
--hash=sha256:6c02e22cf09996194bcb3a4784099975cf527d5c29caf759abadf29ebdb2fe27 \
|
||||
--hash=sha256:79908b9d02b1d6c1c71ff3b7ad127f3f82e14a8e091ab44b3c7e34b649fea733 \
|
||||
--hash=sha256:7c9e99aac7b430f6a9f15eebf058c742097cea3369f23a2bfc5e64d374b67e3a \
|
||||
--hash=sha256:813c2944e940ef8dccea71305bacc942d4b193a021140874b3e58933ec44f5b6 \
|
||||
--hash=sha256:87121b9428ac568fb84fae4af5e7852fc34f02eadc4e3e91f6c8989327692186 \
|
||||
--hash=sha256:896631cd40222aef3697e4e51177d14c3709fda49d30983269d584f034acc8a4 \
|
||||
--hash=sha256:970a4be7ec84ccda7c27cb4ae74930bbbd477bc8d849ed55ea798084dd5fca8c \
|
||||
--hash=sha256:9939796abcadb2810a63dfb26ff8ca4595fe7dd70a3ceae7f607a2639b714307 \
|
||||
--hash=sha256:99a8071e351b50827ad976b92ed91845fb614ac67a3c41109b24f3d8bd3afada \
|
||||
--hash=sha256:9c16fa267740d67883899e054cccb4279e002f3f4872873b752c1ba15045ff49 \
|
||||
--hash=sha256:a30e95274f5c0e007ccc759ec258aa5708c534ec058f153ee25ac700a2f1438b \
|
||||
--hash=sha256:a74afd8d560eaafe0d9e3e1db8c06081282a05ca4de00ee416195085a79d7d3d \
|
||||
--hash=sha256:b46a04588ceb7cf132568e0e564a854627ef87a1ed3bf536234540a79ced44b0 \
|
||||
--hash=sha256:b4963cf08f4320d98ae72ec7694291b8ab85cb7da3b0cd824bc32701bc992edf \
|
||||
--hash=sha256:b50e481a4317a8efcfffcfddcd4c9b36eacba440440e70cbe0256aeb6fd6abae \
|
||||
--hash=sha256:b85553699862c09937a7a5ea14ee6229087971a7d51ae97d5f4b407f571a2c17 \
|
||||
--hash=sha256:bcc98d397c3dea45d5b262029564b29cb8e945f2607a38bee6163694c0a8b4ef \
|
||||
--hash=sha256:bed3a2a823a041e8d249b1a7ec132933e1505299329b5cfe1b2b5ec689ec7675 \
|
||||
--hash=sha256:bf6b4c0c723664f65c2a47c8cb6ebbf660b0b2e2d936adf2e8503d4e93359465 \
|
||||
--hash=sha256:bfd634cb9685161b2a51f73a7fc4736fd0d67a56632d52319317afaa27f08243 \
|
||||
--hash=sha256:c0d5ee865b5fd16bf62d72122aadcc90aab296c30c1adb0a32b4b66bd843163e \
|
||||
--hash=sha256:c2b4eab7c12f9cb460509bc34a3b086f9802f0dba27c89a63df4123819ad64af \
|
||||
--hash=sha256:c51568e94f7f232296de30002f2a50f77a7bd346673da3e4f2aaf9d2b833f2e5 \
|
||||
--hash=sha256:c5158616ab39d34b76c50f40c81552ee180598f7825dc7a66fd187d29958820f \
|
||||
--hash=sha256:cdacddb18d55ffec42d1907079cdc04ec4fa8a990cdf5b9d9fe67d281fc0d12e \
|
||||
--hash=sha256:ce763369e646e59e4ca2c09735cd1bdd3048d909ad5f2bc116e83166a9352f3c \
|
||||
--hash=sha256:d45822b0a2a452327ab4f95efe368d234d5294bbf89a99968be27c7938a21108 \
|
||||
--hash=sha256:d8150579bf30cdf896906baf256aa200cd50dbe6e565c17d6fd3d678e21ff5de \
|
||||
--hash=sha256:d88814befbd1433152c5f6dd536905149ba028d795a22555b149ae0a36024d9e \
|
||||
--hash=sha256:dca5be83a6dfaf933a46e3bc2b9f2685e5ec61b22f6a38ad740aac9c16e9a0ff \
|
||||
--hash=sha256:dd084459ecdb224e617e4ab3f1d5ebe4d1c48facb41f24952b76aa6ba9712bb0 \
|
||||
--hash=sha256:def9a01b73c9e27d70ea03b381fb3e7aadfac1f398dbd63751313c3a46747ef5 \
|
||||
--hash=sha256:df65f4b2b4e74977f0336bef12a88051ab24e6a16873cd9249f34d67cb3e345d \
|
||||
--hash=sha256:dfe924e5a63861c82332a12adeeab955dc8c8009ddbbd80cc2fcca049ff89a49 \
|
||||
--hash=sha256:e67dcaa34dcd908fcccbf49194211d847c731b6ebaac661c1c889f1bf6af1e44 \
|
||||
--hash=sha256:eba62b7c21a33e72dd8adab2b92dd5610d8527f0b2ac28a8e0770e71b21a13f9 \
|
||||
--hash=sha256:ed9c91bf4ce614efed5388c3f989a7cfe08728ab871d995a486ea74ff88993db \
|
||||
--hash=sha256:f05d311c937da03b0cd26ac3e14cb991f6ff8fc94f98b3df9713537817539795 \
|
||||
--hash=sha256:f1cc623fd6855b25da52b3275e0c9e51711b86a9dccc75f8c9ab4432fd8e42c7 \
|
||||
--hash=sha256:f670686d99c867d0f24b28ce8c6f02429c6eef5e2674aab287850d0ee2d20437 \
|
||||
--hash=sha256:f856652f9b533c6cd2b9ad6836a7fc0e43917d7ff15be46c5baf1350f8cdc5d9 \
|
||||
--hash=sha256:fb0458d74726937ead9e2effc91144aea5a58ecee9754242f8539a782bed685a
|
||||
cycler==0.11.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \
|
||||
--hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f
|
||||
dnspython==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e \
|
||||
--hash=sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f
|
||||
email-validator==1.2.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8 \
|
||||
--hash=sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c
|
||||
email-validator==1.3.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769 \
|
||||
--hash=sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c
|
||||
flask-admin==1.6.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988
|
||||
flask-login==0.6.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
|
@ -29,9 +99,9 @@ flask-migrate==3.1.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
--hash=sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897
|
||||
flask-principal==0.4.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:f5d6134b5caebfdbb86f32d56d18ee44b080876a27269560a96ea35f75c99453
|
||||
flask-security-too==5.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:2197fa7fceff6c485aa9774ccc79f653e2f66db25e831b892a002261ede5fc3a \
|
||||
--hash=sha256:436e3ba05984f010e27741055c69bc12cc45566946e4d805990a2628c3656594
|
||||
flask-security-too==5.0.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:36fee0da5d1b3d211caf274553b7753478c208997c624abb84ebba4261de65c2 \
|
||||
--hash=sha256:5f81e220c63f8f319bcd04327267328fd4b58ca05aa6f3ffc458756dfa78d579
|
||||
flask-sqlalchemy==2.5.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912 \
|
||||
--hash=sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390
|
||||
|
@ -41,9 +111,9 @@ flask-wtf==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
flask==2.2.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b \
|
||||
--hash=sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526
|
||||
fonttools==4.37.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:4606e1a88ee1f6699d182fea9511bd9a8a915d913eab4584e5226da1180fcce7 \
|
||||
--hash=sha256:fff6b752e326c15756c819fe2fe7ceab69f96a1dbcfe8911d0941cdb49905007
|
||||
fonttools==4.37.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:a5bc5f5d48faa4085310b8ebd4c5d33bf27c6636c5f10a7de792510af2745a81 \
|
||||
--hash=sha256:f32ef6ec966cf0e7d2aa88601fed2e3a8f2851c26b5db2c80ccc8f82bee4eedc
|
||||
greenlet==1.1.3 ; python_version >= "3.10" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "4.0" \
|
||||
--hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \
|
||||
--hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \
|
||||
|
@ -102,9 +172,9 @@ greenlet==1.1.3 ; python_version >= "3.10" and (platform_machine == "aarch64" or
|
|||
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e \
|
||||
--hash=sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8
|
||||
idna==3.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
|
||||
--hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d
|
||||
idna==3.4 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
|
||||
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
|
||||
itsdangerous==2.1.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
|
||||
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
|
||||
|
@ -180,9 +250,9 @@ kiwisolver==1.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
--hash=sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2 \
|
||||
--hash=sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09 \
|
||||
--hash=sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c
|
||||
mako==1.2.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f \
|
||||
--hash=sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534
|
||||
mako==1.2.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd \
|
||||
--hash=sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f
|
||||
markupsafe==2.1.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \
|
||||
--hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \
|
||||
|
@ -224,42 +294,48 @@ markupsafe==2.1.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
--hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \
|
||||
--hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \
|
||||
--hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7
|
||||
matplotlib==3.5.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7 \
|
||||
--hash=sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a \
|
||||
--hash=sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc \
|
||||
--hash=sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5 \
|
||||
--hash=sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804 \
|
||||
--hash=sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1 \
|
||||
--hash=sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c \
|
||||
--hash=sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0 \
|
||||
--hash=sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f \
|
||||
--hash=sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9 \
|
||||
--hash=sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce \
|
||||
--hash=sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4 \
|
||||
--hash=sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2 \
|
||||
--hash=sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b \
|
||||
--hash=sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc \
|
||||
--hash=sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b \
|
||||
--hash=sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c \
|
||||
--hash=sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a \
|
||||
--hash=sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0 \
|
||||
--hash=sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7 \
|
||||
--hash=sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a \
|
||||
--hash=sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a \
|
||||
--hash=sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51 \
|
||||
--hash=sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693 \
|
||||
--hash=sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56 \
|
||||
--hash=sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094 \
|
||||
--hash=sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88 \
|
||||
--hash=sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511 \
|
||||
--hash=sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b \
|
||||
--hash=sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24 \
|
||||
--hash=sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9 \
|
||||
--hash=sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069 \
|
||||
--hash=sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be \
|
||||
--hash=sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8 \
|
||||
--hash=sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2
|
||||
matplotlib==3.6.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0958fc3fdc59c1b716ee1a5d14e73d03d541d873241a37c5c3a86f7ef6017923 \
|
||||
--hash=sha256:0ae1b9b555212c1e242666af80e7ed796705869581e2d749971db4e682ccc1f3 \
|
||||
--hash=sha256:11c1987b803cc2b26725659cfe817478f0a9597878e5c4bf374cfe4e12cbbd79 \
|
||||
--hash=sha256:140316427a7c384e3dd37efb3a73cd67e14b0b237a6d277def91227f43cdcec2 \
|
||||
--hash=sha256:1559213b803959a2b8309122585b5226d1c2fb66c933b1a2094cf1e99cb4fb90 \
|
||||
--hash=sha256:16a899b958dd76606b571bc7eaa38f09160c27dfb262e493584644cfd4a77f0f \
|
||||
--hash=sha256:1739935d293d0348d7bf662e8cd0edb9c2aa8f20ccd646db755ce0f3456d24e4 \
|
||||
--hash=sha256:1a4835c177821f3729be27ae9be7b8ae209fe75e83db7d9b2bfd319a998f0a42 \
|
||||
--hash=sha256:2b60d4abcb6a405ca7d909c80791b00637d22c62aa3bb0ffff7e589f763867f5 \
|
||||
--hash=sha256:2ed779a896b70c8012fe301fb91ee37e713e1dda1eb8f37de04cdbf506706983 \
|
||||
--hash=sha256:3ec2edf7f74829eae287aa53d64d83ad5d43ee51d29fb1d88e689d8b36028312 \
|
||||
--hash=sha256:408bbf968c15e9e38df9f25a588e372e28a43240cf5884c9bc6039a5021b7d5b \
|
||||
--hash=sha256:4699bb671dbc4afdb544eb893e4deb8a34e294b7734733f65b4fd2787ba5fbc6 \
|
||||
--hash=sha256:4eba6972b796d97c8fcc5266b6dc42ef27c2dce4421b846cded0f3af851b81c9 \
|
||||
--hash=sha256:51092d13499be72e47c15c3a1ae0209edaca6be42b65ffbbefbe0c85f6153c6f \
|
||||
--hash=sha256:62319d57dab5ad3e3494dd97a214e22079d3f72a0c8a2fd001829c2c6abbf8d1 \
|
||||
--hash=sha256:657fb7712185f82211170ac4debae0800ed4f5992b8f7ebba2a9eabaf133a857 \
|
||||
--hash=sha256:66a0db13f77aa7806dba29273874cf862450c61c2e5158245d17ee85d983fe8e \
|
||||
--hash=sha256:6b98e098549d3aea2bfb93f38f0b2ecadcb423fa1504bbff902c01efdd833fd8 \
|
||||
--hash=sha256:7127e2b94571318531caf098dc9e8f60f5aba1704600f0b2483bf151d535674a \
|
||||
--hash=sha256:798559837156b8e2e2df97cffca748c5c1432af6ec5004c2932e475d813f1743 \
|
||||
--hash=sha256:802feae98addb9f21707649a7f229c90a59fad34511881f20b906a5e8e6ea475 \
|
||||
--hash=sha256:89e1978c3fbe4e3d4c6ad7db7e6f982607cb2546f982ccbe42708392437b1972 \
|
||||
--hash=sha256:9295ca10a140c21e40d2ee43ef423213dc20767f6cea6b87c36973564bc51095 \
|
||||
--hash=sha256:9711ef291e184b5a73c9d3af3f2d5cfe25d571c8dd95aa498415f74ac7e221a8 \
|
||||
--hash=sha256:b0320f882214f6ffde5992081520b57b55450510bdaa020e96aacff9b7ae10e6 \
|
||||
--hash=sha256:b5bd3b3ff191f81509d9a1afd62e1e3cda7a7889c35b5b6359a1241fe1511015 \
|
||||
--hash=sha256:baa19508d8445f5648cd1ffe4fc6d4f7daf8b876f804e9a453df6c3708f6200b \
|
||||
--hash=sha256:c5108ebe67da60a9204497d8d403316228deb52b550388190c53a57394d41531 \
|
||||
--hash=sha256:ccea337fb9a44866c5300c594b13d4d87e827ebc3c353bff15d298bac976b654 \
|
||||
--hash=sha256:cd73a16a759865831be5a8fb6546f2a908c8d7d7f55c75f94ee7c2ca13cc95de \
|
||||
--hash=sha256:d840712f4b4c7d2a119f993d7e43ca9bcaa73aeaa24c322fa2bdf4f689a3ee09 \
|
||||
--hash=sha256:df26a09d955b3ab9b6bc18658b9403ed839096c97d7abe8806194e228a485a3c \
|
||||
--hash=sha256:e01382c06ac3710155a0ca923047c5abe03c676d08f03e146c6a240d0a910713 \
|
||||
--hash=sha256:e572c67958f7d55eae77f5f64dc7bd31968cc9f24c233926833efe63c60545f2 \
|
||||
--hash=sha256:eca6f59cd0729edaeaa7032d582dffce518a420d4961ef3e8c93dce86be352c3 \
|
||||
--hash=sha256:efd2e12f8964f8fb4ba1984df71d85d02ef0531e687e59f78ec8fc07271a3857 \
|
||||
--hash=sha256:efe9e8037b989b14bb1887089ae763385431cc06fe488406413079cfd2a3a089 \
|
||||
--hash=sha256:f0d5b9b14ccc7f539143ac9eb1c6b57d26d69ca52d30c3d719a7bc4123579e44 \
|
||||
--hash=sha256:f1954d71cdf15c19e7f3bf2235a4fe1600ba42f34d472c9495bcf54d75a43e4e \
|
||||
--hash=sha256:fbbceb0a0dfe9213f6314510665a32ef25fe29b50657567cd00115fbfcb3b20d
|
||||
numpy==1.23.3 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089 \
|
||||
--hash=sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164 \
|
||||
|
@ -360,9 +436,9 @@ pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
|
||||
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
|
||||
setuptools-scm==6.4.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30 \
|
||||
--hash=sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4
|
||||
setuptools-scm==7.0.5 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:031e13af771d6f892b941adb6ea04545bbf91ebc5ce68c78aaf3fff6e1fb4844 \
|
||||
--hash=sha256:7930f720905e03ccd1e1d821db521bff7ec2ac9cf0ceb6552dd73d24a45d3b02
|
||||
setuptools==65.3.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82 \
|
||||
--hash=sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57
|
||||
|
@ -414,6 +490,9 @@ sqlalchemy==1.4.41 ; python_version >= "3.10" and python_version < "4.0" \
|
|||
tomli==2.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
|
||||
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
|
||||
typing-extensions==4.3.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
|
||||
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
|
||||
werkzeug==2.2.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||
--hash=sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f \
|
||||
--hash=sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5
|
||||
|
|
Loading…
Reference in a new issue