forked from ScoDoc/ScoDoc
629 lines
18 KiB
Python
629 lines
18 KiB
Python
import datetime
|
|
|
|
from flask import g, request, render_template
|
|
|
|
from flask import abort, url_for
|
|
|
|
from app.comp import res_sem
|
|
from app.comp.res_compat import NotesTableCompat
|
|
from app.decorators import (
|
|
scodoc,
|
|
permission_required,
|
|
)
|
|
from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite
|
|
from app.views import assiduites_bp as bp
|
|
from app.views import ScoData
|
|
|
|
# ---------------
|
|
from app.scodoc.sco_permissions import Permission
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_moduleimpl
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_groups_view
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc import sco_find_etud
|
|
from flask_login import current_user
|
|
from app.scodoc import sco_utils as scu
|
|
from app.scodoc import sco_assiduites as scass
|
|
|
|
|
|
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
|
|
|
# --- UTILS ---
|
|
|
|
|
|
class HTMLElement:
|
|
""""""
|
|
|
|
|
|
class HTMLElement:
|
|
"""Représentation d'un HTMLElement version Python"""
|
|
|
|
def __init__(self, tag: str, *attr, **kattr) -> None:
|
|
self.tag: str = tag
|
|
self.children: list[HTMLElement] = []
|
|
self.self_close: bool = kattr.get("self_close", False)
|
|
self.text_content: str = kattr.get("text_content", "")
|
|
self.key_attributes: dict[str, any] = kattr
|
|
self.attributes: list[str] = list(attr)
|
|
|
|
def add(self, *child: HTMLElement) -> None:
|
|
"""add child element to self"""
|
|
for kid in child:
|
|
self.children.append(kid)
|
|
|
|
def remove(self, child: HTMLElement) -> None:
|
|
"""Remove child element from self"""
|
|
if child in self.children:
|
|
self.children.remove(child)
|
|
|
|
def __str__(self) -> str:
|
|
attr: list[str] = self.attributes
|
|
|
|
for att, val in self.key_attributes.items():
|
|
if att in ("self_close", "text_content"):
|
|
continue
|
|
|
|
if att != "cls":
|
|
attr.append(f'{att}="{val}"')
|
|
else:
|
|
attr.append(f'class="{val}"')
|
|
|
|
if not self.self_close:
|
|
head: str = f"<{self.tag} {' '.join(attr)}>{self.text_content}"
|
|
body: str = "\n".join(map(str, self.children))
|
|
foot: str = f"</{self.tag}>"
|
|
return head + body + foot
|
|
return f"<{self.tag} {' '.join(attr)}/>"
|
|
|
|
def __add__(self, other: str):
|
|
return str(self) + other
|
|
|
|
def __radd__(self, other: str):
|
|
return other + str(self)
|
|
|
|
|
|
class HTMLStringElement(HTMLElement):
|
|
"""Utilisation d'une chaine de caracètres pour représenter un element"""
|
|
|
|
def __init__(self, text: str) -> None:
|
|
self.text: str = text
|
|
HTMLElement.__init__(self, "textnode")
|
|
|
|
def __str__(self) -> str:
|
|
return self.text
|
|
|
|
|
|
class HTMLBuilder:
|
|
def __init__(self, *content: HTMLElement or str) -> None:
|
|
self.content: list[HTMLElement or str] = list(content)
|
|
|
|
def add(self, *element: HTMLElement or str):
|
|
self.content.extend(element)
|
|
|
|
def remove(self, element: HTMLElement or str):
|
|
if element in self.content:
|
|
self.content.remove(element)
|
|
|
|
def __str__(self) -> str:
|
|
return "\n".join(map(str, self.content))
|
|
|
|
def build(self) -> str:
|
|
return self.__str__()
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
#
|
|
# Assiduités (/ScoDoc/<dept>/Scolarite/Assiduites/...)
|
|
#
|
|
# --------------------------------------------------------------------
|
|
|
|
|
|
@bp.route("/")
|
|
@bp.route("/index_html")
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
def index_html():
|
|
"""Gestionnaire assiduités, page principale"""
|
|
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title="Saisie des assiduités",
|
|
cssstyles=["css/calabs.css"],
|
|
javascripts=["js/calabs.js"],
|
|
),
|
|
"""<h2>Traitement des assiduités</h2>
|
|
<p class="help">
|
|
Pour saisir des assiduités ou consulter les états, il est recommandé par passer par
|
|
le semestre concerné (saisie par jours nommés ou par semaines).
|
|
</p>
|
|
""",
|
|
]
|
|
H.append(
|
|
"""<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant,
|
|
choisissez d'abord concerné:</p>"""
|
|
)
|
|
H.append(sco_find_etud.form_search_etud())
|
|
if current_user.has_permission(
|
|
Permission.ScoAbsChange
|
|
) and sco_preferences.get_preference("handle_billets_abs"):
|
|
H.append(
|
|
f"""
|
|
<h2 style="margin-top: 30px;">Billets d'absence</h2>
|
|
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
|
|
}">Traitement des billets d'absence en attente</a>
|
|
</li></ul>
|
|
"""
|
|
)
|
|
H.append(html_sco_header.sco_footer())
|
|
return "\n".join(H)
|
|
|
|
|
|
@bp.route("/SignaleAssiduiteEtud")
|
|
@scodoc
|
|
@permission_required(Permission.ScoAbsChange)
|
|
def signal_assiduites_etud():
|
|
"""
|
|
signal_assiduites_etud Saisie de l'assiduité d'un étudiant
|
|
|
|
Args:
|
|
etudid (int): l'identifiant de l'étudiant
|
|
|
|
Returns:
|
|
str: l'html généré
|
|
"""
|
|
|
|
etudid = request.args.get("etudid", -1)
|
|
etud: Identite = Identite.query.get_or_404(etudid)
|
|
if etud.dept_id != g.scodoc_dept_id:
|
|
abort(404, "étudiant inexistant dans ce département")
|
|
|
|
header: str = html_sco_header.sco_header(
|
|
page_title="Saisie Assiduités",
|
|
init_qtip=True,
|
|
javascripts=[
|
|
"js/assiduites.js",
|
|
"libjs/moment.new.min.js",
|
|
"libjs/moment-timezone.js",
|
|
],
|
|
cssstyles=CSSSTYLES
|
|
+ [
|
|
"css/assiduites.css",
|
|
],
|
|
)
|
|
|
|
# Gestion des horaires (journée, matin, soir)
|
|
|
|
morning = get_time("assi_morning_time", "08:00:00")
|
|
lunch = get_time("assi_lunch_time", "13:00:00")
|
|
afternoon = get_time("assi_afternoon_time", "18:00:00")
|
|
|
|
return HTMLBuilder(
|
|
header,
|
|
_mini_timeline(),
|
|
render_template(
|
|
"assiduites/signal_assiduites_etud.j2",
|
|
sco=ScoData(etud),
|
|
date=datetime.date.today().isoformat(),
|
|
morning=morning,
|
|
lunch=lunch,
|
|
timeline=_timeline(),
|
|
afternoon=afternoon,
|
|
nonworkdays=_non_work_days(),
|
|
forcer_module=sco_preferences.get_preference(
|
|
"forcer_module", dept_id=g.scodoc_dept_id
|
|
),
|
|
),
|
|
).build()
|
|
|
|
|
|
def _non_work_days():
|
|
non_travail = sco_preferences.get_preference("non_travail", None)
|
|
non_travail = non_travail.replace(" ", "").split(",")
|
|
return ",".join([f"'{i.lower()}'" for i in non_travail])
|
|
|
|
|
|
def _str_to_num(string: str):
|
|
parts = [*map(float, string.split(":"))]
|
|
hour = parts[0]
|
|
minutes = round(parts[1] / 60 * 4) / 4
|
|
return hour + minutes
|
|
|
|
|
|
def get_time(label: str, default: str):
|
|
return _str_to_num(ScoDocSiteConfig.get(label, default))
|
|
|
|
|
|
@bp.route("/ListeAssiduitesEtud")
|
|
@scodoc
|
|
@permission_required(Permission.ScoAbsChange)
|
|
def liste_assiduites_etud():
|
|
"""
|
|
liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
|
|
Args:
|
|
etudid (int): l'identifiant de l'étudiant
|
|
|
|
Returns:
|
|
str: l'html généré
|
|
"""
|
|
|
|
etudid = request.args.get("etudid", -1)
|
|
etud: Identite = Identite.query.get_or_404(etudid)
|
|
if etud.dept_id != g.scodoc_dept_id:
|
|
abort(404, "étudiant inexistant dans ce département")
|
|
|
|
header: str = html_sco_header.sco_header(
|
|
page_title="Liste des assiduités",
|
|
init_qtip=True,
|
|
javascripts=[
|
|
"js/assiduites.js",
|
|
"libjs/moment.new.min.js",
|
|
"libjs/moment-timezone.js",
|
|
],
|
|
cssstyles=CSSSTYLES
|
|
+ [
|
|
"css/assiduites.css",
|
|
],
|
|
)
|
|
|
|
return HTMLBuilder(
|
|
header,
|
|
render_template(
|
|
"assiduites/liste_assiduites.j2",
|
|
sco=ScoData(etud),
|
|
date=datetime.date.today().isoformat(),
|
|
),
|
|
).build()
|
|
|
|
|
|
@bp.route("/SignalAssiduiteGr")
|
|
@scodoc
|
|
@permission_required(Permission.ScoAbsChange)
|
|
def signal_assiduites_group():
|
|
"""
|
|
signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée
|
|
|
|
Returns:
|
|
str: l'html généré
|
|
"""
|
|
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
|
moduleimpl_id: int = request.args.get("moduleimpl_id")
|
|
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
|
group_ids: list[int] = request.args.get("group_ids", None)
|
|
|
|
if group_ids is None:
|
|
group_ids = []
|
|
else:
|
|
group_ids = group_ids.split(",")
|
|
map(str, group_ids)
|
|
|
|
# Vérification du moduleimpl_id
|
|
try:
|
|
moduleimpl_id = int(moduleimpl_id)
|
|
except (TypeError, ValueError):
|
|
moduleimpl_id = None
|
|
# Vérification du formsemestre_id
|
|
try:
|
|
formsemestre_id = int(formsemestre_id)
|
|
except (TypeError, ValueError):
|
|
formsemestre_id = None
|
|
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
|
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
|
|
)
|
|
|
|
if not groups_infos.members:
|
|
return (
|
|
html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
|
|
+ "<h3>Aucun étudiant ! </h3>"
|
|
+ html_sco_header.sco_footer()
|
|
)
|
|
|
|
# --- URL DEFAULT ---
|
|
|
|
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
|
|
|
|
# --- Filtrage par formsemestre ---
|
|
formsemestre_id = groups_infos.formsemestre_id
|
|
|
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
if formsemestre.dept_id != g.scodoc_dept_id:
|
|
abort(404, "groupes inexistants dans ce département")
|
|
|
|
require_module = sco_preferences.get_preference(
|
|
"abs_require_module", formsemestre_id
|
|
)
|
|
|
|
etuds = [
|
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
|
for m in groups_infos.members
|
|
]
|
|
|
|
# --- Restriction en fonction du moduleimpl_id ---
|
|
if moduleimpl_id:
|
|
mod_inscrits = {
|
|
x["etudid"]
|
|
for x in sco_moduleimpl.do_moduleimpl_inscription_list(
|
|
moduleimpl_id=moduleimpl_id
|
|
)
|
|
}
|
|
etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
|
|
if etuds_inscrits_module:
|
|
etuds = etuds_inscrits_module
|
|
else:
|
|
# Si aucun etudiant n'est inscrit au module choisi...
|
|
moduleimpl_id = None
|
|
|
|
# --- Génération de l'HTML ---
|
|
sem = formsemestre.to_dict()
|
|
|
|
if groups_infos.tous_les_etuds_du_sem:
|
|
gr_tit = "en"
|
|
else:
|
|
if len(groups_infos.group_ids) > 1:
|
|
grp = "des groupes"
|
|
else:
|
|
grp = "du groupe"
|
|
gr_tit = (
|
|
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
|
)
|
|
|
|
header: str = html_sco_header.sco_header(
|
|
page_title="Saisie journalière des assiduités",
|
|
init_qtip=True,
|
|
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
|
+ [
|
|
# Voir fonctionnement JS
|
|
"js/etud_info.js",
|
|
"js/abs_ajax.js",
|
|
"js/groups_view.js",
|
|
"js/assiduites.js",
|
|
"libjs/moment.new.min.js",
|
|
"libjs/moment-timezone.js",
|
|
],
|
|
cssstyles=CSSSTYLES
|
|
+ [
|
|
"css/assiduites.css",
|
|
],
|
|
no_side_bar=1,
|
|
)
|
|
|
|
return HTMLBuilder(
|
|
header,
|
|
_mini_timeline(),
|
|
render_template(
|
|
"assiduites/signal_assiduites_group.j2",
|
|
gr_tit=gr_tit,
|
|
sem=sem["titre_num"],
|
|
date=date,
|
|
formsemestre_id=formsemestre_id,
|
|
grp=sco_groups_view.menu_groups_choice(groups_infos),
|
|
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
|
timeline=_timeline(),
|
|
nonworkdays=_non_work_days(),
|
|
formsemestre_date_debut=str(formsemestre.date_debut),
|
|
formsemestre_date_fin=str(formsemestre.date_fin),
|
|
forcer_module=sco_preferences.get_preference(
|
|
"forcer_module",
|
|
formsemestre_id=formsemestre_id,
|
|
dept_id=g.scodoc_dept_id,
|
|
),
|
|
),
|
|
html_sco_header.sco_footer(),
|
|
).build()
|
|
|
|
|
|
@bp.route("/EtatAbsencesDate")
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
def get_etat_abs_date():
|
|
evaluation = {
|
|
"jour": request.args.get("jour"),
|
|
"heure_debut": request.args.get("heure_debut"),
|
|
"heure_fin": request.args.get("heure_fin"),
|
|
"title": request.args.get("desc"),
|
|
}
|
|
date: str = evaluation["jour"]
|
|
group_ids: list[int] = request.args.get("group_ids", None)
|
|
etudiants: list[dict] = []
|
|
|
|
if group_ids is None:
|
|
group_ids = []
|
|
else:
|
|
group_ids = group_ids.split(",")
|
|
map(str, group_ids)
|
|
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
|
|
|
etuds = [
|
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
|
for m in groups_infos.members
|
|
]
|
|
|
|
date_debut = scu.is_iso_formated(
|
|
f"{evaluation['jour']}T{evaluation['heure_debut'].replace('h',':')}", True
|
|
)
|
|
date_fin = scu.is_iso_formated(
|
|
f"{evaluation['jour']}T{evaluation['heure_fin'].replace('h',':')}", True
|
|
)
|
|
|
|
assiduites: Assiduite = Assiduite.query.filter(
|
|
Assiduite.etudid.in_([e["etudid"] for e in etuds])
|
|
)
|
|
assiduites = scass.filter_by_date(
|
|
assiduites, Assiduite, date_debut, date_fin, False
|
|
)
|
|
|
|
for etud in etuds:
|
|
assi = assiduites.filter_by(etudid=etud["etudid"]).first()
|
|
|
|
etat = ""
|
|
if assi != None and assi.etat != 0:
|
|
etat = scu.EtatAssiduite.inverse().get(assi.etat).name
|
|
|
|
etudiant = {
|
|
"nom": f'<a href="{url_for("absences.CalAbs", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>',
|
|
"etat": etat,
|
|
}
|
|
|
|
etudiants.append(etudiant)
|
|
|
|
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
|
|
|
|
header: str = html_sco_header.sco_header(
|
|
page_title=evaluation["title"],
|
|
init_qtip=True,
|
|
)
|
|
|
|
return HTMLBuilder(
|
|
header,
|
|
render_template(
|
|
"assiduites/etat_absence_date.j2", etudiants=etudiants, eval=evaluation
|
|
),
|
|
html_sco_header.sco_footer(),
|
|
).build()
|
|
|
|
|
|
@bp.route("/SignalAssiduiteDifferee")
|
|
@scodoc
|
|
@permission_required(Permission.ScoAbsChange)
|
|
def signal_assiduites_diff():
|
|
group_ids: list[int] = request.args.get("group_ids", None)
|
|
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
|
etudiants: list[dict] = []
|
|
|
|
titre = None
|
|
|
|
# Vérification du formsemestre_id
|
|
try:
|
|
formsemestre_id = int(formsemestre_id)
|
|
except (TypeError, ValueError):
|
|
formsemestre_id = None
|
|
|
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
|
|
if group_ids is None:
|
|
group_ids = []
|
|
else:
|
|
group_ids = group_ids.split(",")
|
|
map(str, group_ids)
|
|
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
|
|
|
if not groups_infos.members:
|
|
return (
|
|
html_sco_header.sco_header(page_title="Assiduités Différées")
|
|
+ "<h3>Aucun étudiant ! </h3>"
|
|
+ html_sco_header.sco_footer()
|
|
)
|
|
|
|
etudiants.extend(
|
|
[
|
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
|
for m in groups_infos.members
|
|
]
|
|
)
|
|
|
|
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
|
|
|
|
header: str = html_sco_header.sco_header(
|
|
page_title="Assiduités Différées",
|
|
init_qtip=True,
|
|
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
|
+ [
|
|
"js/assiduites.js",
|
|
"libjs/moment.new.min.js",
|
|
"libjs/moment-timezone.js",
|
|
],
|
|
)
|
|
|
|
sem = formsemestre.to_dict()
|
|
|
|
if groups_infos.tous_les_etuds_du_sem:
|
|
gr_tit = "en"
|
|
else:
|
|
if len(groups_infos.group_ids) > 1:
|
|
grp = "des groupes"
|
|
else:
|
|
grp = "du groupe"
|
|
gr_tit = (
|
|
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
|
)
|
|
|
|
return HTMLBuilder(
|
|
header,
|
|
render_template(
|
|
"assiduites/signal_assiduites_diff.j2",
|
|
etudiants=etudiants,
|
|
etat_def=sco_preferences.get_preference("assi_etat_defaut"),
|
|
formsemestre_date_debut=str(formsemestre.date_debut),
|
|
formsemestre_date_fin=str(formsemestre.date_fin),
|
|
moduleimpl_select=_module_selector(formsemestre),
|
|
forcer_module=sco_preferences.get_preference(
|
|
"forcer_module",
|
|
formsemestre_id=formsemestre_id,
|
|
dept_id=g.scodoc_dept_id,
|
|
),
|
|
gr=gr_tit,
|
|
sem=sem["titre_num"],
|
|
),
|
|
html_sco_header.sco_footer(),
|
|
).build()
|
|
|
|
|
|
def _module_selector(
|
|
formsemestre: FormSemestre, moduleimpl_id: int = None
|
|
) -> HTMLElement:
|
|
"""
|
|
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
|
|
|
|
Args:
|
|
formsemestre (FormSemestre): Le formsemestre d'où les moduleimpls seront pris.
|
|
|
|
Returns:
|
|
str: La représentation str d'un HTMLSelectElement
|
|
"""
|
|
|
|
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
|
|
modimpls_list: list[dict] = []
|
|
ues = ntc.get_ues_stat_dict()
|
|
for ue in ues:
|
|
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
|
|
|
|
selected = moduleimpl_id is not None
|
|
|
|
modules = []
|
|
|
|
for modimpl in modimpls_list:
|
|
modname: str = (
|
|
(modimpl["module"]["code"] or "")
|
|
+ " "
|
|
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or "")
|
|
)
|
|
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
|
|
|
|
return render_template(
|
|
"assiduites/moduleimpl_selector.j2", selected=selected, modules=modules
|
|
)
|
|
|
|
|
|
def _timeline(formsemestre_id=None) -> HTMLElement:
|
|
return render_template(
|
|
"assiduites/timeline.j2",
|
|
t_start=get_time("assi_morning_time", "08:00:00"),
|
|
t_end=get_time("assi_afternoon_time", "18:00:00"),
|
|
tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
|
|
periode_defaut=sco_preferences.get_preference(
|
|
"periode_defaut", formsemestre_id
|
|
),
|
|
)
|
|
|
|
|
|
def _mini_timeline() -> HTMLElement:
|
|
return render_template(
|
|
"assiduites/minitimeline.j2",
|
|
t_start=get_time("assi_morning_time", "08:00:00"),
|
|
t_end=get_time("assi_afternoon_time", "18:00:00"),
|
|
)
|