forked from ScoDoc/ScoDoc
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
02a5b00ecf | |||
dcdf6a8012 | |||
912a213dcd | |||
3575e89dc0 | |||
21c0625147 | |||
e18c1d8fd0 | |||
5867d0f430 | |||
9897ccc659 | |||
|
7575959bd4 | ||
|
2aafbad9e2 | ||
50f2cd7a0f | |||
fd8fbb9e02 | |||
|
ebcef76950 | ||
|
13349776af | ||
|
f275286b71 | ||
|
f4f6c13d79 | ||
e7f23efe65 | |||
e44d3fd5dc | |||
fac36fa11c | |||
9289535359 | |||
|
d73b925006 | ||
6749ca70d6 | |||
|
dea403b03d | ||
|
ab9543c310 | ||
|
f94998f66b | ||
|
eb88a8ca83 |
|
@ -360,12 +360,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
Argument:
|
||||
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
||||
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
|
@ -427,8 +428,11 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
Evaluation.EVALUATION_SESSION2,
|
||||
)
|
||||
|
||||
# Vrai si toutes les UEs ont bien une note de session 2 calculée:
|
||||
etuds_use_session2 = np.all(np.isfinite(etuds_moy_module_s2), axis=1)
|
||||
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
|
||||
mod_coefs = modimpl_coefs_df[modimpl.id]
|
||||
etuds_use_session2 = np.all(
|
||||
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
|
||||
)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
etuds_moy_module_s2,
|
||||
|
|
|
@ -183,7 +183,9 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
def notes_sem_load_cube(
|
||||
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
||||
) -> tuple:
|
||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||
et assemble le cube.
|
||||
|
@ -208,7 +210,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||
for modimpl in formsemestre.modimpls_sorted:
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
|
|
|
@ -59,16 +59,17 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
|
|
|
@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
return "type_departement mismatch"
|
||||
# Table d'équivalences entre refs:
|
||||
equiv = self._load_config_equivalences()
|
||||
# Même specialité (ou alias) ?
|
||||
if self.specialite != other.specialite and other.specialite not in equiv.get(
|
||||
"alias", []
|
||||
):
|
||||
return "specialite mismatch"
|
||||
# mêmes parcours ?
|
||||
eq_parcours = equiv.get("parcours", {})
|
||||
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||
|
@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
def _load_config_equivalences(self) -> dict:
|
||||
"""Load config file ressources/referentiels/equivalences.yaml
|
||||
used to define equivalences between distinct referentiels
|
||||
return a dict, with optional keys:
|
||||
alias: list of equivalent names for speciality (eg SD == STID)
|
||||
parcours: dict with equivalent parcours acronyms
|
||||
"""
|
||||
try:
|
||||
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||
|
|
|
@ -199,6 +199,11 @@ class Identite(models.ScoDocModel):
|
|||
@classmethod
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
abort(404, "etudid invalide")
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
|
@ -299,9 +304,10 @@ class Identite(models.ScoDocModel):
|
|||
|
||||
@property
|
||||
def nomprenom(self, reverse=False) -> str:
|
||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
"""DEPRECATED
|
||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
nom = self.nom_usuel or self.nom
|
||||
prenom = self.prenom_str
|
||||
|
@ -309,6 +315,12 @@ class Identite(models.ScoDocModel):
|
|||
return f"{nom} {prenom}".strip()
|
||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||
|
||||
def nom_prenom(self) -> str:
|
||||
"""Civilite NOM Prénom
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
|
||||
|
||||
@property
|
||||
def prenom_str(self):
|
||||
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
|
||||
|
|
|
@ -340,6 +340,21 @@ class Module(models.ScoDocModel):
|
|||
# Liste seulement les coefs définis:
|
||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
def get_ue_coefs_descr(self) -> str:
|
||||
"""Description des coefficients vers les UEs (APC)"""
|
||||
coefs_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in self.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coefs_descr:
|
||||
descr = "Coefs: " + coefs_descr
|
||||
else:
|
||||
descr = "(pas de coefficients) "
|
||||
return descr
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee:
|
||||
|
|
|
@ -226,6 +226,7 @@ class BulletinGenerator:
|
|||
server_name=self.server_name,
|
||||
filigranne=self.filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=self.multi_pages,
|
||||
)
|
||||
)
|
||||
try:
|
||||
|
|
|
@ -106,6 +106,7 @@ def assemble_bulletins_pdf(
|
|||
pagesbookmarks=pagesbookmarks,
|
||||
filigranne=filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
|
||||
)
|
||||
)
|
||||
document.multiBuild(story)
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
Lecture et conversion des ics.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import timezone
|
||||
import glob
|
||||
import os
|
||||
|
@ -229,7 +230,7 @@ def translate_calendar(
|
|||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
moduleimpl_id=modimpl.id,
|
||||
jour=event["jour"],
|
||||
day=event["jour"],
|
||||
)
|
||||
if modimpl and group
|
||||
else None
|
||||
|
|
|
@ -822,7 +822,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
</div>
|
||||
<div class="sem-groups-assi">
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
|
@ -832,12 +832,32 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
jour=datetime.date.today().isoformat(),
|
||||
day=datetime.date.today().isoformat(),
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Saisir l'assiduité</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
# YYYY-Www (ISO 8601) :
|
||||
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_hebdo",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
week=current_week,
|
||||
)}">Saisie hebdomadaire</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
|
@ -847,7 +867,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
)}">
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
|
@ -864,21 +884,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
"""
|
||||
)
|
||||
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_diff",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisir l'assiduité`">
|
||||
(Saisie différée)</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>") # /sem-groups-assi
|
||||
if partition_is_empty:
|
||||
H.append(
|
||||
|
@ -1188,17 +1193,7 @@ def formsemestre_tableau_modules(
|
|||
mod_descr = "Module " + (mod.titre or "")
|
||||
is_apc = mod.is_apc() # SAE ou ressource
|
||||
if is_apc:
|
||||
coef_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in mod.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coef_descr:
|
||||
mod_descr += " Coefs: " + coef_descr
|
||||
else:
|
||||
mod_descr += " (pas de coefficients) "
|
||||
mod_descr += " " + mod.get_ue_coefs_descr()
|
||||
else:
|
||||
mod_descr += ", coef. " + str(mod.coefficient)
|
||||
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
|
||||
|
|
|
@ -54,11 +54,11 @@ class Jour:
|
|||
"""
|
||||
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
|
||||
|
||||
def get_date(self) -> str:
|
||||
def get_date(self, fmt=scu.DATE_FMT) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
Renvoie la date du jour au format fmt ou "dd/mm/yyyy" par défaut
|
||||
"""
|
||||
return self.date.strftime(scu.DATE_FMT)
|
||||
return self.date.strftime(fmt)
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
|
@ -93,12 +93,22 @@ class Calendrier:
|
|||
Représente un calendrier
|
||||
Permet d'obtenir les informations sur les jours
|
||||
et générer une représentation html
|
||||
|
||||
highlight: str
|
||||
-> ["jour", "semaine", "mois"]
|
||||
permet de mettre en valeur lors du passage de la souris
|
||||
"""
|
||||
|
||||
def __init__(self, date_debut: datetime.date, date_fin: datetime.date):
|
||||
def __init__(
|
||||
self,
|
||||
date_debut: datetime.date,
|
||||
date_fin: datetime.date,
|
||||
highlight: str = None,
|
||||
):
|
||||
self.date_debut = date_debut
|
||||
self.date_fin = date_fin
|
||||
self.jours: dict[str, list[Jour]] = {}
|
||||
self.highlight: str = highlight
|
||||
|
||||
def _get_dates_between(self) -> list[datetime.date]:
|
||||
"""
|
||||
|
@ -130,11 +140,13 @@ class Calendrier:
|
|||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||
# Ajouter le jour à la liste correspondante au mois
|
||||
if month not in organized:
|
||||
organized[month] = []
|
||||
organized[month] = {} # semaine {22: []}
|
||||
|
||||
jour: Jour = self.instanciate_jour(date)
|
||||
|
||||
organized[month].append(jour)
|
||||
semaine = date.strftime("%G-W%V")
|
||||
if semaine not in organized[month]:
|
||||
organized[month][semaine] = []
|
||||
organized[month][semaine].append(jour)
|
||||
|
||||
self.jours = organized
|
||||
|
||||
|
@ -150,4 +162,53 @@ class Calendrier:
|
|||
get_html Renvoie le code html du calendrier
|
||||
"""
|
||||
self.organize_by_month()
|
||||
return render_template("calendrier.j2", calendrier=self.jours)
|
||||
return render_template(
|
||||
"calendrier.j2", calendrier=self.jours, highlight=self.highlight
|
||||
)
|
||||
|
||||
|
||||
class JourChoix(Jour):
|
||||
"""
|
||||
Représente un jour dans le calendrier pour choisir une date
|
||||
"""
|
||||
|
||||
def get_html(self):
|
||||
return ""
|
||||
|
||||
|
||||
class CalendrierChoix(Calendrier):
|
||||
"""
|
||||
Représente un calendrier pour choisir une date
|
||||
"""
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||
return JourChoix(date)
|
||||
|
||||
|
||||
def calendrier_choix_date(
|
||||
date_debut: datetime.date,
|
||||
date_fin: datetime.date,
|
||||
url: str,
|
||||
mode: str = "jour",
|
||||
titre: str = "Choisir une date",
|
||||
):
|
||||
"""
|
||||
Permet d'afficher un calendrier pour choisir une date et renvoyer sur une url.
|
||||
|
||||
mode : str
|
||||
- "jour" -> ajoutera "&day=yyyy-mm-dd" à l'url (ex: 2024-05-30)
|
||||
- "semaine" -> ajoutera "&week=yyyy-Www" à l'url (ex : 2024-W22)
|
||||
|
||||
titre : str
|
||||
- texte à afficher au dessus du calendrier
|
||||
"""
|
||||
|
||||
calendrier: CalendrierChoix = CalendrierChoix(date_debut, date_fin, highlight=mode)
|
||||
|
||||
return render_template(
|
||||
"choix_date.j2",
|
||||
calendrier=calendrier.get_html(),
|
||||
url=url,
|
||||
titre=titre,
|
||||
mode=mode,
|
||||
)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
##############################################################################
|
||||
|
||||
"""Affichage étudiants d'un ou plusieurs groupes
|
||||
sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
|
||||
sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
|
||||
"""
|
||||
|
||||
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
|
||||
|
@ -585,8 +585,8 @@ def groups_table(
|
|||
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
||||
etud_info["_prenom_target"] = fiche_url
|
||||
|
||||
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
|
||||
etud_info["etudid"]
|
||||
etud_info["_nom_disp_td_attrs"] = (
|
||||
'id="%s" class="etudinfo"' % (etud_info["etudid"])
|
||||
)
|
||||
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
||||
if etud_info["etat"] == "D":
|
||||
|
@ -983,7 +983,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
|||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||
jour=datetime.date.today().isoformat(),
|
||||
day=datetime.date.today().isoformat(),
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||
)
|
||||
|
@ -998,12 +998,12 @@ def form_choix_saisie_semaine(groups_infos):
|
|||
return ""
|
||||
query_args = parse_qs(request.query_string)
|
||||
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
|
||||
semaine = datetime.date.today().isocalendar().week
|
||||
semaine = datetime.datetime.now().strftime("%G-W%V")
|
||||
return f"""
|
||||
<button onclick="window.location='{url_for(
|
||||
"assiduites.signal_assiduites_diff",
|
||||
"assiduites.signal_assiduites_hebdo",
|
||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||
semaine=semaine,
|
||||
week=semaine,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
moduleimpl_id=moduleimpl_id
|
||||
|
|
|
@ -146,29 +146,48 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
|||
return htmlutils.make_menu("actions", menu_eval, alone=True)
|
||||
|
||||
|
||||
def _ue_coefs_html(coefs_lst) -> str:
|
||||
def _ue_coefs_html(modimpl: ModuleImpl) -> str:
|
||||
""" """
|
||||
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
|
||||
H = """
|
||||
coefs_lst = modimpl.module.ue_coefs_list()
|
||||
max_coef = max(x[1] for x in coefs_lst) if coefs_lst else 1.0
|
||||
H = f"""
|
||||
<div id="modimpl_coefs">
|
||||
<div>Coefficients vers les UE</div>
|
||||
"""
|
||||
if coefs_lst:
|
||||
H += (
|
||||
f"""
|
||||
<div class="coefs_histo" style="--max:{max_coef}">
|
||||
"""
|
||||
+ "\n".join(
|
||||
[
|
||||
f"""<div style="--coef:{coef};
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
"><div>{coef}</div>{ue.acronyme}</div>"""
|
||||
for ue, coef in coefs_lst
|
||||
if coef > 0
|
||||
]
|
||||
<div>Coefficients vers les UEs
|
||||
<span><a class="stdlink" href="{
|
||||
url_for(
|
||||
"notes.edit_modules_ue_coefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=modimpl.module.formation.id,
|
||||
semestre_idx=modimpl.formsemestre.semestre_id,
|
||||
)
|
||||
+ "</div>"
|
||||
}">détail</a>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
if coefs_lst:
|
||||
H += _html_hinton_map(
|
||||
colors=(uc[0].color for uc in coefs_lst),
|
||||
max_val=max_coef,
|
||||
size=36,
|
||||
title=modimpl.module.get_ue_coefs_descr(),
|
||||
values=(uc[1] for uc in coefs_lst),
|
||||
)
|
||||
# (
|
||||
# f"""
|
||||
# <div class="coefs_histo" style="--max:{max_coef}">
|
||||
# """
|
||||
# + "\n".join(
|
||||
# [
|
||||
# f"""<div style="--coef:{coef};
|
||||
# {'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
# "><div>{coef}</div>{ue.acronyme}</div>"""
|
||||
# for ue, coef in coefs_lst
|
||||
# if coef > 0
|
||||
# ]
|
||||
# )
|
||||
# + "</div>"
|
||||
# )
|
||||
else:
|
||||
H += """<div class="missing_value">non définis</div>"""
|
||||
H += "</div>"
|
||||
|
@ -195,13 +214,22 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
Evaluation.date_debut.desc(),
|
||||
).all()
|
||||
nb_evaluations = len(evaluations)
|
||||
max_poids = max(
|
||||
[
|
||||
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
|
||||
for e in evaluations
|
||||
]
|
||||
or [0]
|
||||
)
|
||||
# Le poids max pour chaque catégorie d'évaluation
|
||||
max_poids_by_type: dict[int, float] = {}
|
||||
for eval_type in (
|
||||
Evaluation.EVALUATION_NORMALE,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
Evaluation.EVALUATION_BONUS,
|
||||
):
|
||||
max_poids_by_type[eval_type] = max(
|
||||
[
|
||||
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
|
||||
for e in evaluations
|
||||
if e.evaluation_type == eval_type
|
||||
]
|
||||
or [0.0]
|
||||
)
|
||||
#
|
||||
sem_locked = not formsemestre.etat
|
||||
can_edit_evals = (
|
||||
|
@ -265,7 +293,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
H.append(scu.icontag("lock32_img", title="verrouillé"))
|
||||
H.append("""</td><td class="fichetitre2">""")
|
||||
if modimpl.module.is_apc():
|
||||
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
|
||||
H.append(_ue_coefs_html(modimpl))
|
||||
else:
|
||||
H.append(
|
||||
f"""Coef. dans le semestre: {
|
||||
|
@ -318,12 +346,28 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids={group_id}&jour={
|
||||
}?group_ids={group_id}&day={
|
||||
datetime.date.today().isoformat()
|
||||
}&formsemestre_id={formsemestre.id}
|
||||
&moduleimpl_id={moduleimpl_id}
|
||||
"
|
||||
>Saisie Absences journée</a></span>
|
||||
>Saisie Absences</a></span>
|
||||
"""
|
||||
)
|
||||
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||
H.append(
|
||||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_hebdo",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group_id,
|
||||
week=current_week,
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)
|
||||
}
|
||||
"
|
||||
>Saisie Absences (Hebdo)</a></span>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
|
@ -335,8 +379,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
group_ids=group_id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||
)}"
|
||||
>Saisie Absences Différée</a></span>
|
||||
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisie Absences`"
|
||||
>(Saisie Absences Différée)</a></span>
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -462,7 +506,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
eval_index=eval_index,
|
||||
nb_evals=nb_evaluations,
|
||||
is_apc=nt.is_apc,
|
||||
max_poids=max_poids,
|
||||
max_poids=max_poids_by_type.get(evaluation.evaluation_type, 10000.0),
|
||||
)
|
||||
)
|
||||
eval_index -= 1
|
||||
|
@ -781,27 +825,27 @@ def _ligne_evaluation(
|
|||
#
|
||||
if etat["nb_notes"] == 0:
|
||||
H.append(f"""<tr class="{tr_class}"><td></td>""")
|
||||
if modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td colspan="8" class="eval_poids">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append('<td colspan="8"></td>')
|
||||
# if modimpl.module.is_apc():
|
||||
# H.append(
|
||||
# f"""<td colspan="8" class="eval_poids">{
|
||||
# evaluation.get_ue_poids_str()}</td>"""
|
||||
# )
|
||||
# else:
|
||||
# H.append('<td colspan="8"></td>')
|
||||
H.append("""</tr>""")
|
||||
else: # il y a deja des notes saisies
|
||||
gr_moyennes = etat["gr_moyennes"]
|
||||
first_group = True
|
||||
# first_group = True
|
||||
for gr_moyenne in gr_moyennes:
|
||||
H.append(f"""<tr class="{tr_class}"><td> </td>""")
|
||||
if first_group and modimpl.module.is_apc():
|
||||
H.append(
|
||||
f"""<td class="eval_poids" colspan="4">{
|
||||
evaluation.get_ue_poids_str()}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<td colspan="4"></td>""")
|
||||
first_group = False
|
||||
# if first_group and modimpl.module.is_apc():
|
||||
# H.append(
|
||||
# f"""<td class="eval_poids" colspan="4">{
|
||||
# evaluation.get_ue_poids_str()}</td>"""
|
||||
# )
|
||||
# else:
|
||||
H.append("""<td colspan="4"></td>""")
|
||||
# first_group = False
|
||||
if gr_moyenne["group_name"] is None:
|
||||
name = "Tous" # tous
|
||||
else:
|
||||
|
@ -857,26 +901,47 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
|||
ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids }
|
||||
if not ue_poids:
|
||||
return ""
|
||||
if max_poids < scu.NOTES_PRECISION:
|
||||
values = [poids * (evaluation.coefficient) for poids in ue_poids.values()]
|
||||
colors = [db.session.get(UniteEns, ue_id).color for ue_id in ue_poids]
|
||||
return _html_hinton_map(
|
||||
classes=("evaluation_poids",),
|
||||
colors=colors,
|
||||
max_val=max_poids,
|
||||
title=f"Poids de l'évaluation vers les UEs: {evaluation.get_ue_poids_str()}",
|
||||
values=values,
|
||||
)
|
||||
|
||||
|
||||
def _html_hinton_map(
|
||||
classes=(),
|
||||
colors=(),
|
||||
max_val: float | None = None,
|
||||
size=12,
|
||||
title: str = "",
|
||||
values=(),
|
||||
) -> str:
|
||||
"""Représente une liste de nombres sous forme de carrés"""
|
||||
if max_val is None:
|
||||
max_val = max(values)
|
||||
if max_val < scu.NOTES_PRECISION:
|
||||
return ""
|
||||
H = (
|
||||
"""<div class="evaluation_poids">"""
|
||||
return (
|
||||
f"""<div class="hinton_map {" ".join(classes)}"
|
||||
style="--size:{size}px;"
|
||||
title="{title}"
|
||||
data-tooltip>"""
|
||||
+ "\n".join(
|
||||
[
|
||||
f"""<div title="poids vers {ue.acronyme}: {poids:g}">
|
||||
<div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px;
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
f"""<div>
|
||||
<div style="--boxsize:{size*math.sqrt(value/max_val)}px;
|
||||
{'background-color: ' + color + ';' if color else ''}
|
||||
"></div>
|
||||
</div>"""
|
||||
for ue, poids in (
|
||||
(db.session.get(UniteEns, ue_id), poids)
|
||||
for ue_id, poids in ue_poids.items()
|
||||
)
|
||||
for value, color in zip(values, colors)
|
||||
]
|
||||
)
|
||||
+ "</div>"
|
||||
)
|
||||
return H
|
||||
|
||||
|
||||
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:
|
||||
|
|
|
@ -247,6 +247,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
|
||||
filigranne=None,
|
||||
preferences=None, # dictionnary with preferences, required
|
||||
with_page_numbers=False,
|
||||
):
|
||||
"""Initialise our page template."""
|
||||
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||
|
@ -259,8 +260,9 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
self.pdfmeta_subject = subject
|
||||
self.server_name = server_name
|
||||
self.filigranne = filigranne
|
||||
self.page_number = 1
|
||||
self.footer_template = footer_template
|
||||
self.with_page_numbers = with_page_numbers
|
||||
self.page_number = 1
|
||||
if self.preferences:
|
||||
self.with_page_background = self.preferences["bul_pdf_with_background"]
|
||||
else:
|
||||
|
@ -337,6 +339,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
|
||||
def draw_footer(self, canv, content):
|
||||
"""Print the footer"""
|
||||
# called 1/page
|
||||
try:
|
||||
canv.setFont(
|
||||
self.preferences["SCOLAR_FONT"],
|
||||
|
@ -353,6 +356,9 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
self.preferences["pdf_footer_y"] * mm,
|
||||
content + " " + (self.preferences["pdf_footer_extra"] or ""),
|
||||
)
|
||||
if self.with_page_numbers:
|
||||
canv.drawString(190.0 * mm, 6 * mm, f"Page {self.page_number}")
|
||||
|
||||
canv.restoreState()
|
||||
|
||||
def footer_string(self) -> str:
|
||||
|
@ -389,11 +395,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
canv.drawCentredString(0, 0, SU(filigranne))
|
||||
canv.restoreState()
|
||||
doc.filigranne = None
|
||||
|
||||
def afterPage(self):
|
||||
"""Called after all flowables have been drawn on a page.
|
||||
Increment pageNum since the page has been completed.
|
||||
"""
|
||||
# Increment page number
|
||||
self.page_number += 1
|
||||
|
||||
|
||||
|
|
|
@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str):
|
|||
return None
|
||||
|
||||
|
||||
def get_etud_photo_url(etudid, size="small"):
|
||||
def get_etud_photo_url(etudid, size="small", seed=None):
|
||||
"L'URL scodoc vers la photo de l'étudiant"
|
||||
kwargs = {"seed": seed} if seed else {}
|
||||
return (
|
||||
url_for(
|
||||
"scolar.get_photo_image",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etudid,
|
||||
size=size,
|
||||
**kwargs,
|
||||
)
|
||||
if has_request_context()
|
||||
else ""
|
||||
|
@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
|
|||
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
||||
|
||||
"""
|
||||
photo_url = get_etud_photo_url(etud["etudid"], size=size)
|
||||
if fast:
|
||||
return photo_url
|
||||
return get_etud_photo_url(etud["etudid"], size=size)
|
||||
photo_url = get_etud_photo_url(
|
||||
etud["etudid"], size=size, seed=hash(etud.get("photo_filename"))
|
||||
)
|
||||
path = photo_pathname(etud["photo_filename"], size=size)
|
||||
if not path:
|
||||
# Portail ?
|
||||
|
@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int):
|
|||
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
||||
error_message = None
|
||||
try:
|
||||
r = requests.get(url, timeout=portal_timeout)
|
||||
r = requests.get(
|
||||
url,
|
||||
timeout=portal_timeout,
|
||||
params={
|
||||
"nom": etud.nom or "",
|
||||
"prenom": etud.prenom or "",
|
||||
"civilite": etud.civilite,
|
||||
},
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
error_message = "ConnectionError"
|
||||
except requests.Timeout:
|
||||
|
|
|
@ -351,6 +351,7 @@ def formsemestre_report_counts(
|
|||
"statut",
|
||||
"annee_admission",
|
||||
"type_admission",
|
||||
"boursier",
|
||||
"boursier_prec",
|
||||
]
|
||||
if jury_but_mode:
|
||||
|
|
|
@ -1085,18 +1085,35 @@ span.spanlink:hover {
|
|||
}
|
||||
|
||||
.trombi_box {
|
||||
display: inline-block;
|
||||
width: 110px;
|
||||
vertical-align: top;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
width: 140px;
|
||||
/* Constant width for the box */
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
/* Ensures trombi-photo is above trombi_legend */
|
||||
align-items: center;
|
||||
/* Centers content horizontally */
|
||||
}
|
||||
|
||||
span.trombi_legend {
|
||||
display: inline-block;
|
||||
.trombi-photo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Centers image horizontally within the photo container */
|
||||
margin-bottom: 10px;
|
||||
/* Adds some space between the photo and the legend */
|
||||
}
|
||||
|
||||
span.trombi-photo {
|
||||
.trombi-photo img {
|
||||
width: auto;
|
||||
/* Maintains aspect ratio */
|
||||
height: 120px;
|
||||
/* Sets the height to 90px */
|
||||
max-width: 100%;
|
||||
/* Ensures the image doesn't exceed the container's width */
|
||||
}
|
||||
|
||||
/* span.trombi_legend {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -1106,7 +1123,9 @@ span.trombi_box a {
|
|||
|
||||
span.trombi_box a img {
|
||||
display: inline-block;
|
||||
}
|
||||
height: 128px;
|
||||
width: auto;
|
||||
} */
|
||||
|
||||
.trombi_nom {
|
||||
display: block;
|
||||
|
@ -2096,8 +2115,9 @@ div.evaluation_titre {
|
|||
vertical-align: super;
|
||||
}
|
||||
|
||||
|
||||
/* visualisation poids évaluations */
|
||||
.evaluation_poids {
|
||||
.hinton_map {
|
||||
height: 12px;
|
||||
display: inline-flex;
|
||||
text-align: center;
|
||||
|
@ -2105,10 +2125,10 @@ div.evaluation_titre {
|
|||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.evaluation_poids>div {
|
||||
.hinton_map>div {
|
||||
display: inline-flex;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
border: 1px solid rgb(180, 180, 180);
|
||||
|
@ -2116,9 +2136,9 @@ div.evaluation_titre {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.evaluation_poids>div>div {
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
.hinton_map>div>div {
|
||||
height: var(--boxsize);
|
||||
width: var(--boxsize);
|
||||
background: #09c;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,13 @@ async function async_post(path, data, success, errors) {
|
|||
const responseData = await response.json();
|
||||
success(responseData);
|
||||
} else {
|
||||
throw new Error("Network response was not ok.");
|
||||
if (response.status == 404) {
|
||||
response.json().then((data) => {
|
||||
if (errors) errors(data);
|
||||
});
|
||||
} else {
|
||||
throw new Error("Network response was not ok.");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -615,7 +621,10 @@ function erreurModuleImpl(message) {
|
|||
|
||||
openAlertModal("Sélection du module", content);
|
||||
}
|
||||
if (message == "L'étudiant n'est pas inscrit au module") {
|
||||
if (
|
||||
message == "L'étudiant n'est pas inscrit au module" ||
|
||||
message == "param 'moduleimpl_id': etud non inscrit"
|
||||
) {
|
||||
const HTML = `
|
||||
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
|
@ -822,7 +831,7 @@ function dateCouranteEstTravaillee() {
|
|||
const nouvelleDate = retourJourTravail(date);
|
||||
$("#date").datepicker("setDate", nouvelleDate);
|
||||
let msg = "Le jour sélectionné";
|
||||
if ((new Date()).format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
|
||||
if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
|
||||
msg = "Aujourd'hui";
|
||||
}
|
||||
const att = document.createTextNode(
|
||||
|
|
|
@ -430,3 +430,23 @@ class Duration {
|
|||
function hasTimeConflict(period, interval) {
|
||||
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
|
||||
}
|
||||
|
||||
// Fonction auxiliaire pour obtenir le numéro de semaine ISO d'une date donnée
|
||||
function getISOWeek(date) {
|
||||
const target = new Date(date.valueOf());
|
||||
const dayNr = (date.getUTCDay() + 6) % 7;
|
||||
target.setUTCDate(target.getUTCDate() - dayNr + 3);
|
||||
const firstThursday = target.valueOf();
|
||||
target.setUTCMonth(0, 1);
|
||||
if (target.getUTCDay() !== 4) {
|
||||
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
|
||||
}
|
||||
return 1 + Math.ceil((firstThursday - target) / 604800000);
|
||||
}
|
||||
|
||||
// Fonction auxiliaire pour obtenir le nombre de semaines ISO dans une année donnée
|
||||
function getISOWeeksInYear(year) {
|
||||
const date = new Date(year, 11, 31);
|
||||
const week = getISOWeek(date);
|
||||
return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week;
|
||||
}
|
||||
|
|
|
@ -571,15 +571,31 @@ window.addEventListener("load", main);
|
|||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
||||
|
||||
<div class="ue_warning warning">
|
||||
Attention, cette page va prochainement être remplacée par un mode de saisie hebdomadaire.
|
||||
<p>
|
||||
Pour saisir l'assiduité à une seule date quelconque, utiliser la page
|
||||
<a class="stdlink" href="{{
|
||||
url_for('assiduites.signal_assiduites_group', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, group_ids=group_ids)
|
||||
}}" target="_blank">
|
||||
saisir l'assiduité</a>.
|
||||
Attention, cette page va prochainement être supprimée, car il est plus facile d'utiliser
|
||||
<ul>
|
||||
la page
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('assiduites.signal_assiduites_group',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids)
|
||||
}}">
|
||||
saisie de l'assiduité</a> pour saisir à une seule date quelconque
|
||||
</li>
|
||||
<li>ou <a class="stdlink" href="{{
|
||||
url_for('assiduites.signal_assiduites_hebdo',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
)
|
||||
}}">saisie hebdomadaire</a> pour saisir sur une semaine.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois,
|
||||
qui va bientôt être retiré.
|
||||
</p>
|
||||
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois, qui va bientôt être remplacé/simplifié.
|
||||
<p>N'hésitez pas à commenter sur le <a href="{{scu.SCO_DISCORD_ASSISTANCE}}">salon Discord</a>
|
||||
si vous avez d'autres besoins.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
899
app/templates/assiduites/pages/signal_assiduites_hebdo.j2
Normal file
899
app/templates/assiduites/pages/signal_assiduites_hebdo.j2
Normal file
|
@ -0,0 +1,899 @@
|
|||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
|
||||
<style>
|
||||
.rbtn::before {
|
||||
--size: 1.5em;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.ui-timepicker-container,
|
||||
#ui-datepicker-div {
|
||||
z-index: 5 !important;
|
||||
}
|
||||
|
||||
#new_periode,
|
||||
#actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
#actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#actions label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fix {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
justify-content: space-between;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#fix>.box {
|
||||
border: 1px solid #444;
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
width: 5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#moduleimpl_select {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
position: relative;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
th {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.premier th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.second th {
|
||||
position: sticky;
|
||||
top: 38px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
.sticky-col {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rbtn:not(:checked)::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.grayed {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
.conflit {
|
||||
background-color: var(--color-conflit);
|
||||
}
|
||||
|
||||
.conflit_calendar{
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.timePicker-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.timePicker-modal.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.timePicker-modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 300px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.timePicker-close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.timePicker-close:hover,
|
||||
.timePicker-close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-picker-container {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#confirmButton {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#confirmButton:hover {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.etudinfo{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
const readonly = "{{readonly | safe}}" == "True";
|
||||
const non_present = "{{non_present | safe}}" == "True";
|
||||
|
||||
const etuds = [
|
||||
{% for etud in etudiants %}
|
||||
{
|
||||
id: {{etud.etudid}},
|
||||
nom: "{{etud.nom}}",
|
||||
prenom: "{{etud.prenom}}"
|
||||
},
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
let days = [
|
||||
{% for jour in hebdo_jours %}
|
||||
{
|
||||
date : new Date(Date.fromFRA("{{jour[1][1]}}")),
|
||||
visible : "{{not jour[0]}}" == "True",
|
||||
nom : "{{jour[1][0]}}",
|
||||
},
|
||||
{% endfor %}
|
||||
] // [0]=Lundi ... [6]=Dimanche -> à 00h00
|
||||
|
||||
//Une fonction d'action quand un bouton est cliqué
|
||||
// 3 possibilités :
|
||||
// - assiduite_id = null -> créer nv assi avec état du bouton
|
||||
// - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité
|
||||
// - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité
|
||||
async function actionButton(btn, same = false) {
|
||||
let td = btn.parentElement;
|
||||
let tr = td.parentElement;
|
||||
let etudid = tr.getAttribute("etudid");
|
||||
let etud = etuds.find((etud) => etud.id == etudid);
|
||||
let etat = btn.value;
|
||||
let assiduite_id = td.getAttribute("assiduite_id");
|
||||
let dayInfo = [td.getAttribute("day"), td.getAttribute("time")]// [0]=[0..6] [1]=am/pm
|
||||
let day = days[dayInfo[0]].date;
|
||||
dayInfo[1] = dayInfo[1] == "am" ? "matin" : "apresmidi";
|
||||
let deb = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].debut);
|
||||
let fin = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].fin);
|
||||
|
||||
const assi = {
|
||||
etudid: etudid,
|
||||
etat: etat,
|
||||
moduleimpl_id: document.getElementById("moduleimpl_select").value,
|
||||
date_debut: deb.toFakeIso(),
|
||||
date_fin: fin.toFakeIso(),
|
||||
}
|
||||
|
||||
let cancelEvent = false;
|
||||
|
||||
if (assiduite_id != "") {
|
||||
if (same) {
|
||||
// Suppression
|
||||
await async_post(
|
||||
`../../api/assiduite/delete`,
|
||||
[assiduite_id],
|
||||
(data) => {
|
||||
if (data.success.length > 0) {
|
||||
envoiToastEtudiant("remove", etud);
|
||||
td.setAttribute("assiduite_id", "");
|
||||
} else {
|
||||
console.error(data.errors["0"].message);
|
||||
cancelEvent = true;
|
||||
erreurModuleImpl(data.errors["0"].message);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de la suppression de l'assiduité", error);
|
||||
cancelEvent = true;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Modification
|
||||
await async_post(
|
||||
`../../api/assiduite/${assiduite_id}/edit`,
|
||||
assi,
|
||||
(data) => {
|
||||
envoiToastEtudiant(etat, etud);
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de la modification de l'assiduité", error);
|
||||
cancelEvent = true;
|
||||
erreurModuleImpl(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Création
|
||||
await async_post(
|
||||
`../../api/assiduite/${etud.id}/create`,
|
||||
[assi],
|
||||
(data) => {
|
||||
if (data.success.length > 0) {
|
||||
envoiToastEtudiant(etat, etud);
|
||||
//mise à jour de l'assiduité_id dans le td
|
||||
td.setAttribute("assiduite_id", data.success["0"].message.assiduite_id);
|
||||
} else {
|
||||
console.error(data.errors["0"].message);
|
||||
erreurModuleImpl(data.errors["0"].message);
|
||||
cancelEvent = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de la création de l'assiduité", error);
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return cancelEvent;
|
||||
|
||||
}
|
||||
|
||||
async function recupAssiduitesHebdo(callback) {
|
||||
const etudIds = etuds.map((etud) => etud.id).join(",");
|
||||
const date_debut = days[0].date.startOf("day").format("YYYY-MM-DDTHH:mm");
|
||||
const date_fin = days[6].date.endOf("day").format("YYYY-MM-DDTHH:mm");
|
||||
|
||||
url =
|
||||
`../../api/assiduites/group/query?date_debut=${date_debut}` +
|
||||
`&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`;
|
||||
|
||||
await fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
let assiduites = []
|
||||
Object.keys(data).forEach((etudid) => {
|
||||
assiduites.push(...data[etudid]);
|
||||
});
|
||||
callback(assiduites);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error(
|
||||
"There has been a problem with your fetch operation:",
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function updateTable(assiduites) {
|
||||
|
||||
const img_conflit = `
|
||||
<a
|
||||
class="conflit_calendar"
|
||||
title="Des assiduités existent déjà pour cette période. Cliquez ici pour voir le calendrier de l'assiduité de l'étudiant"
|
||||
data-tooltip
|
||||
target="_blank"
|
||||
>📅</a>`
|
||||
|
||||
// Suppression existant
|
||||
document.querySelectorAll("td.btns").forEach((el) => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
let day = days[i].date;
|
||||
|
||||
let morningPeriod = {
|
||||
deb: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.debut),
|
||||
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.fin),
|
||||
}
|
||||
let afternoonPeriod = {
|
||||
deb: (new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.debut)),
|
||||
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.fin),
|
||||
}
|
||||
const assiduitesByDay = {
|
||||
matin: assiduites.filter((assi) => {
|
||||
const period = {
|
||||
deb: new Date(assi.date_debut),
|
||||
fin: new Date(assi.date_fin)
|
||||
}
|
||||
return hasTimeConflict(period, morningPeriod);
|
||||
}),
|
||||
apresmidi: assiduites.filter((assi) => {
|
||||
const period = {
|
||||
deb: new Date(assi.date_debut),
|
||||
fin: new Date(assi.date_fin)
|
||||
}
|
||||
return hasTimeConflict(period, afternoonPeriod);
|
||||
})
|
||||
};
|
||||
|
||||
// Récupération des tr étudiants
|
||||
let trs = document.querySelectorAll("tr[etudid]");
|
||||
|
||||
trs.forEach((tr) => {
|
||||
let etudid = tr.getAttribute("etudid");
|
||||
|
||||
if (!days[i].visible && i >= 5) {
|
||||
return;
|
||||
} else if (!days[i].visible) {
|
||||
tr.insertAdjacentHTML("beforeend", "<td class='grayed btns' colspan='2'></td>");
|
||||
return;
|
||||
}
|
||||
|
||||
let etudAssiMorning = assiduitesByDay.matin.filter((a) => {
|
||||
return a.etudid == etudid;
|
||||
});
|
||||
let etudAssiAfternoon = assiduitesByDay.apresmidi.filter((a) => {
|
||||
return a.etudid == etudid;
|
||||
});
|
||||
|
||||
// Créations des boutons
|
||||
// matin
|
||||
let tdMatin = document.createElement("td");
|
||||
tdMatin.classList.add("btns");
|
||||
tdMatin.setAttribute("day", i);
|
||||
tdMatin.setAttribute("time", "am");
|
||||
|
||||
tr.appendChild(tdMatin);
|
||||
|
||||
// après-midi
|
||||
let tdApresmidi = document.createElement("td");
|
||||
tdApresmidi.classList.add("btns");
|
||||
tdApresmidi.setAttribute("day", i);
|
||||
tdApresmidi.setAttribute("time", "pm");
|
||||
tr.appendChild(tdApresmidi);
|
||||
|
||||
|
||||
// Peuplement des boutons en fonction des assiduités
|
||||
let boutons = `
|
||||
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||
class="rbtn retard" value="retard">
|
||||
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||
class="rbtn absent" value="absent">
|
||||
`
|
||||
|
||||
if (!non_present) {
|
||||
boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||
class="rbtn present" value="present">`+boutons;
|
||||
}
|
||||
|
||||
// matin
|
||||
tdMatin.innerHTML = boutons
|
||||
tdMatin.setAttribute("assiduite_id", "")
|
||||
if (etudAssiMorning.length != 0) {
|
||||
let assi = etudAssiMorning[0];
|
||||
const deb = new Date(assi.date_debut);
|
||||
const fin = new Date(assi.date_fin);
|
||||
|
||||
// si dates == periode -> cocher bouton correspondant
|
||||
// Sinon supprimer boutons et mettre case "rouge" + tooltip
|
||||
|
||||
if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) {
|
||||
let etat = assi.etat.toLowerCase();
|
||||
const input = tdMatin.querySelector(`[value="${etat}"]`)
|
||||
if (input) {
|
||||
input.checked = true;
|
||||
}
|
||||
tdMatin.setAttribute("assiduite_id", assi.assiduite_id);
|
||||
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
|
||||
saisie = saisie.split(" ").join(" à ");
|
||||
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
|
||||
tdMatin.setAttribute("title", text);
|
||||
tdMatin.setAttribute("data-tooltip", "");
|
||||
|
||||
} else {
|
||||
tdMatin.innerHTML = img_conflit;
|
||||
tdMatin.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
|
||||
tdMatin.classList.add("conflit");
|
||||
}
|
||||
}
|
||||
|
||||
// après-midi
|
||||
tdApresmidi.innerHTML = boutons
|
||||
tdApresmidi.setAttribute("assiduite_id", "")
|
||||
if (etudAssiAfternoon.length != 0) {
|
||||
let assi = etudAssiAfternoon[0];
|
||||
const deb = new Date(assi.date_debut);
|
||||
const fin = new Date(assi.date_fin);
|
||||
|
||||
// si dates == periode -> cocher bouton correspondant
|
||||
// Sinon supprimer boutons et mettre case "rouge" + tooltip
|
||||
|
||||
if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) {
|
||||
let etat = assi.etat.toLowerCase();
|
||||
const input = tdApresmidi.querySelector(`[value="${etat}"]`)
|
||||
if (input) {
|
||||
input.checked = true;
|
||||
}
|
||||
tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id);
|
||||
|
||||
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
|
||||
saisie = saisie.split(" ").join(" à ");
|
||||
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
|
||||
tdApresmidi.setAttribute("title", text);
|
||||
tdApresmidi.setAttribute("data-tooltip", "");
|
||||
} else {
|
||||
tdApresmidi.innerHTML = img_conflit;
|
||||
tdApresmidi.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
|
||||
tdApresmidi.classList.add("conflit");
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("td .rbtn").forEach((el) => {
|
||||
el.addEventListener("click", async (e) => {
|
||||
|
||||
if (readonly) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
let target = e.target;
|
||||
let parent = target.parentElement;
|
||||
|
||||
let isCancelled = await actionButton(target, !target.checked);
|
||||
if (isCancelled) {
|
||||
e.preventDefault();
|
||||
target.checked = !target.checked;
|
||||
return;
|
||||
}
|
||||
|
||||
let inputs = parent.querySelectorAll(".rbtn");
|
||||
inputs.forEach((input) => {
|
||||
if (input != target) {
|
||||
input.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
enableTooltips("table");
|
||||
|
||||
}
|
||||
|
||||
// Une fonction pour changer de semaine (précédente ou suivante)
|
||||
// fait juste un location.href avec les bons paramètres
|
||||
function changeWeek(prev = false) {
|
||||
const currentUrl = new URL(window.location.href); // Récupère l'URL actuelle
|
||||
const params = new URLSearchParams(currentUrl.search); // Récupère les paramètres de l'URL
|
||||
let currentWeekParam = params.get('week');
|
||||
|
||||
// Extraire l'année et le numéro de semaine du paramètre de la semaine actuelle
|
||||
const [year, week] = currentWeekParam.split('-W').map(Number);
|
||||
|
||||
// Calculer la nouvelle semaine et l'année
|
||||
let newYear = year;
|
||||
let newWeek = week + (prev ? -1 : 1);
|
||||
|
||||
if (newWeek < 1) {
|
||||
newYear -= 1; // Passer à l'année précédente
|
||||
newWeek = getISOWeeksInYear(newYear); // Dernière semaine de l'année précédente
|
||||
} else if (newWeek > getISOWeeksInYear(newYear)) {
|
||||
newYear += 1; // Passer à l'année suivante
|
||||
newWeek = 1; // Première semaine de l'année suivante
|
||||
}
|
||||
|
||||
// Formater le nouveau paramètre de semaine
|
||||
const newWeekParam = `${newYear}-W${String(newWeek).padStart(2, '0')}`;
|
||||
params.set('week', newWeekParam); // Mettre à jour le paramètre 'week'
|
||||
currentUrl.search = params.toString(); // Mettre à jour les paramètres de l'URL
|
||||
window.location.href = currentUrl.toString(); // Rediriger vers la nouvelle URL
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Une fonction pour gérer le bouton "tout le monde présent"
|
||||
// coche tous les boutons de la colonne
|
||||
function allPresent(day, time) {
|
||||
// Version naive : coche tous les boutons de la colonne
|
||||
// TODO - Optimiser avec une seule requête API
|
||||
let tds = document.querySelectorAll(`td[day="${day}"][time="${time}"]`);
|
||||
const real_time = time == "am" ? "matin" : "apresmidi";
|
||||
const assi = {
|
||||
etat: "present",
|
||||
moduleimpl_id: document.getElementById("moduleimpl_select").value,
|
||||
date_debut: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].debut).toFakeIso(),
|
||||
date_fin: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].fin).toFakeIso(),
|
||||
}
|
||||
|
||||
let toCreate = []; // [{etudid:<int>}]
|
||||
let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}]
|
||||
|
||||
tds.forEach((td) => {
|
||||
// on ne touche pas aux conflits
|
||||
if (td.classList.contains("conflit")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tr = td.parentElement;
|
||||
const etudid = Number(tr.getAttribute("etudid"));
|
||||
|
||||
const assiduite_id = td.getAttribute("assiduite_id");
|
||||
if (assiduite_id == "") {
|
||||
toCreate.push({ etudid: etudid });
|
||||
} else {
|
||||
toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) });
|
||||
}
|
||||
})
|
||||
|
||||
// Création
|
||||
toCreate = toCreate.map((el) => {
|
||||
return {
|
||||
...assi,
|
||||
etudid: el.etudid,
|
||||
}
|
||||
});
|
||||
|
||||
// Modification
|
||||
toEdit = toEdit.map((el) => {
|
||||
return {
|
||||
...assi,
|
||||
etudid: el.etudid,
|
||||
assiduite_id: el.assiduite_id,
|
||||
}
|
||||
});
|
||||
|
||||
// Appel API
|
||||
let counts = {
|
||||
create: toCreate.length,
|
||||
edit: toEdit.length
|
||||
}
|
||||
const promiseCreate = async_post(
|
||||
`../../api/assiduites/create`,
|
||||
toCreate,
|
||||
async (data) => {
|
||||
if (data.errors.length > 0) {
|
||||
console.error(data.errors);
|
||||
data.errors.forEach((err) => {
|
||||
let obj = toCreate[err.indice];
|
||||
let etu = etuds.find((el) => el.id == obj.etudid);
|
||||
|
||||
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
|
||||
const toast = generateToast(text, "var(--color-error)", 10);
|
||||
pushToast(toast);
|
||||
});
|
||||
}
|
||||
counts.create = data.success.length;
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de la création de l'assiduité", error);
|
||||
}
|
||||
);
|
||||
const promiseEdit = async_post(
|
||||
`../../api/assiduites/edit`,
|
||||
toEdit,
|
||||
async (data) => {
|
||||
if (data.errors.length > 0) {
|
||||
console.error(data.errors);
|
||||
data.errors.forEach((err) => {
|
||||
let obj = toEdit[err.indice];
|
||||
let etu = etuds.find((el) => el.id == obj.etudid);
|
||||
|
||||
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
|
||||
const toast = generateToast(text, "var(--color-error)");
|
||||
pushToast(toast);
|
||||
});
|
||||
}
|
||||
counts.edit = data.success.length;
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de l'édition de l'assiduité", error);
|
||||
}
|
||||
);
|
||||
|
||||
// Affiche un loader
|
||||
afficheLoader();
|
||||
|
||||
Promise.all([promiseCreate, promiseEdit]).then(async () => {
|
||||
retirerLoader();
|
||||
await recupAssiduitesHebdo(updateTable);
|
||||
envoiToastTous("present", counts.create + counts.edit);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
function updateTemps(temps){
|
||||
let matin = document.getElementById("text-matin");
|
||||
let apresmidi = document.getElementById("text-apresmidi");
|
||||
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
|
||||
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
|
||||
|
||||
recupAssiduitesHebdo(updateTable);
|
||||
}
|
||||
|
||||
const temps = {
|
||||
matin: {
|
||||
debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}"
|
||||
},
|
||||
apresmidi: {
|
||||
debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}",
|
||||
fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.getElementById("text-matin").addEventListener("click", (e)=>{
|
||||
e.preventDefault();
|
||||
openModal(true);
|
||||
});
|
||||
|
||||
document.getElementById("text-apresmidi").addEventListener("click", (e)=>{
|
||||
e.preventDefault();
|
||||
openModal(false);
|
||||
});
|
||||
|
||||
updateTemps(temps);
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
function openModal(morning = true){
|
||||
|
||||
let text = morning ? "du matin" : "de l'après-midi";
|
||||
const modal = document.getElementById("timePickerModal");
|
||||
modal.querySelector("#timePicker-modal-text").textContent = text;
|
||||
|
||||
let time1 = $("#time1");
|
||||
let time2 = $("#time2");
|
||||
|
||||
// Réinitialiser les champs
|
||||
time1.val(morning ? temps.matin.debut : temps.apresmidi.debut);
|
||||
time2.val(morning ? temps.matin.fin : temps.apresmidi.fin);
|
||||
|
||||
// Définir l'action du bouton de confirmation
|
||||
|
||||
document.getElementById("confirmButton").onclick = function(){
|
||||
let debut = time1.val();
|
||||
let fin = time2.val();
|
||||
|
||||
if (debut == "" || fin == ""){
|
||||
alert("Veuillez remplir les deux champs");
|
||||
return;
|
||||
}
|
||||
|
||||
if (debut >= fin){
|
||||
alert("L'heure de début doit être inférieure à l'heure de fin");
|
||||
return;
|
||||
}
|
||||
|
||||
if (morning){
|
||||
if (fin > temps.apresmidi.debut){
|
||||
alert("L'heure de fin du matin doit être inférieure à l'heure de début de l'après-midi");
|
||||
return;
|
||||
}
|
||||
temps.matin.debut = debut;
|
||||
temps.matin.fin = fin;
|
||||
} else {
|
||||
if (debut < temps.matin.fin){
|
||||
alert("L'heure de début de l'après-midi doit être supérieure à l'heure de fin du matin");
|
||||
return;
|
||||
}
|
||||
temps.apresmidi.debut = debut;
|
||||
temps.apresmidi.fin = fin;
|
||||
}
|
||||
|
||||
updateTemps(temps);
|
||||
modal.classList.remove("show");
|
||||
}
|
||||
|
||||
modal.classList.add("show");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", ()=>{
|
||||
const modal = document.getElementById("timePickerModal");
|
||||
modal.querySelector(".timePicker-close").onclick = function() {
|
||||
modal.classList.remove("show");
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', function(e) {
|
||||
if (e.key === "Escape" && modal.classList.contains("show")) {
|
||||
modal.classList.remove("show");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.querySelectorAll("th .rbtn").forEach((el)=>{
|
||||
el.addEventListener("click", (e)=>{
|
||||
allPresent(...el.id.split("-"));
|
||||
e.preventDefault();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock scripts %}
|
||||
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2>
|
||||
<br>
|
||||
<div id="actions" class="flex">
|
||||
<button onclick="changeWeek(true)">Semaine précédente</button>
|
||||
<label for="moduleimpl_select">
|
||||
Module:
|
||||
{{moduleimpl_select | safe}}
|
||||
</label>
|
||||
<button onclick="changeWeek(false)">Semaine suivante</button>
|
||||
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine<a></span>
|
||||
</div>
|
||||
|
||||
<h3 id="tableau-dates">
|
||||
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
|
||||
</h3>
|
||||
|
||||
{% if readonly %}
|
||||
<h4
|
||||
title="Vous n'avez pas les permissions nécessaires afin de modifier les assiduités"
|
||||
data-tooltip
|
||||
>
|
||||
Ouvert en mode <span class="rouge">lecture seule</span>.
|
||||
</h4>
|
||||
|
||||
{% endif %}
|
||||
<table id="table">
|
||||
<thead>
|
||||
<tr class="premier">
|
||||
<th rowspan="2">Étudiants</th>
|
||||
|
||||
{% for jour in hebdo_jours %}
|
||||
|
||||
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||
<th colspan="2" class="{{'grayed' if jour[0] else ''}}" >{{ jour[1][0] }} {{jour[1][1] }}</th>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr class="second">
|
||||
{% for jour in hebdo_jours %}
|
||||
|
||||
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||
<th class="{{'grayed' if jour[0] else ''}}">Matin</th>
|
||||
<th class="{{'grayed' if jour[0] else ''}}">Après-midi</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% if not readonly and not non_present %}
|
||||
<tr>
|
||||
{# Ne pas afficher si preference "non presences" / "readonly" #}
|
||||
<th></th>
|
||||
{% for jour in hebdo_jours %}
|
||||
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||
<th class="{{'grayed' if jour[0] else ''}}">
|
||||
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-am" class="rbtn present" {{'disabled' if jour[0] else ''}}>
|
||||
</th>
|
||||
<th class="{{'grayed' if jour[0] else ''}}">
|
||||
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-pm" class="rbtn present" {{'disabled' if jour[0] else ''}}>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for etud in etudiants %}
|
||||
<tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}">
|
||||
<td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nom_prenom() }}</td>
|
||||
{# Sera rempli en JS #}
|
||||
{# Ne pas afficher bouton présent si pref "non présences" #}
|
||||
{# <td>
|
||||
<input type="checkbox" name="" id="" class="rbtn present">
|
||||
<input type="checkbox" name="" id="" class="rbtn retard">
|
||||
<input type="checkbox" name="" id="" class="rbtn absent">
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="" id="" class="rbtn present">
|
||||
<input type="checkbox" name="" id="" class="rbtn retard">
|
||||
<input type="checkbox" name="" id="" class="rbtn absent">
|
||||
</td> #}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="timePickerModal" class="timePicker-modal">
|
||||
<div class="timePicker-modal-content">
|
||||
<span class="timePicker-close">×</span>
|
||||
<h2>Choisissez les horaires <span id="timePicker-modal-text"></span></h2>
|
||||
<div class="time-picker-container">
|
||||
<label for="time1">Début</label>
|
||||
<input type="text" id="time1" name="time1" class="timepicker" placeholder="hh:mm">
|
||||
</div>
|
||||
<div class="time-picker-container">
|
||||
<label for="time2">Fin</label>
|
||||
<input type="text" id="time2" name="time2" class="timepicker" placeholder="hh:mm">
|
||||
</div>
|
||||
<span>
|
||||
<button id="confirmButton">Confirmer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% endblock app_content %}
|
|
@ -1,10 +1,11 @@
|
|||
<div class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="mois">
|
||||
{% for mois,semaines in calendrier.items() %}
|
||||
<div class="mois {{'highlight' if highlight=='mois'}}">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="jours">
|
||||
{% for jour in jours %}
|
||||
<div class="jour {{jour.get_class()}}">
|
||||
{% for semaine in semaines %}
|
||||
<div class="jours {{'highlight' if highlight=='semaine'}}" week_index="{{semaine}}">
|
||||
{% for jour in semaines[semaine] %}
|
||||
<div class="jour {{jour.get_class()}} {{'highlight' if highlight=='jour'}}" date="{{jour.get_date('%Y-%m-%d')}}">
|
||||
<span class="nom">{{jour.get_nom()}}</span>
|
||||
<div class="contenu">
|
||||
{{jour.get_html() | safe}}
|
||||
|
@ -12,6 +13,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -84,5 +86,8 @@
|
|||
border-left: solid 3px var(--couleur);
|
||||
border-right: solid 3px var(--couleur);
|
||||
}
|
||||
.highlight:hover{
|
||||
border: solid 3px yellow;
|
||||
}
|
||||
|
||||
</style>
|
62
app/templates/choix_date.j2
Normal file
62
app/templates/choix_date.j2
Normal file
|
@ -0,0 +1,62 @@
|
|||
{% extends "sco_page.j2" %}
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
|
||||
<style>
|
||||
.highlight {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.highlight * {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
#gtrcontent h2.titre {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.content{
|
||||
width: 90%;
|
||||
max-width: 1600px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block app_content %}
|
||||
<div class="content">
|
||||
<h2 class="titre">{{titre}}</h2>
|
||||
{{calendrier | safe}}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock app_content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
|
||||
<script>
|
||||
|
||||
const mode = "{{mode}}";
|
||||
const url = new URL(window.location.origin + "{{url | safe}}");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", ()=>{
|
||||
const highlight = document.querySelectorAll(".highlight");
|
||||
highlight.forEach((el)=>{
|
||||
el.addEventListener("click", (e)=>{
|
||||
if (mode == "jour"){
|
||||
const date = el.getAttribute("date");
|
||||
url.searchParams.set("day", date);
|
||||
}
|
||||
if (mode == "semaine"){
|
||||
const date = el.getAttribute("week_index");
|
||||
url.searchParams.set("week", date);
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock scripts %}
|
|
@ -894,63 +894,6 @@ def calendrier_assi_etud():
|
|||
)
|
||||
|
||||
|
||||
@bp.route("/choix_date", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def choix_date() -> str:
|
||||
"""
|
||||
choix_date Choix de la date pour la saisie des assiduités
|
||||
|
||||
Route utilisée uniquement si la date courante n'est pas dans le semestre
|
||||
concerné par la requête vers une des pages suivantes :
|
||||
- saisie_assiduites_group
|
||||
"""
|
||||
formsemestre_id = request.args.get("formsemestre_id")
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
|
||||
group_ids = request.args.get("group_ids")
|
||||
moduleimpl_id = request.args.get("moduleimpl_id")
|
||||
form = ChoixDateForm(request.form)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.cancel.data:
|
||||
return redirect(url_for("scodoc.index"))
|
||||
# Vérifier si date dans semestre
|
||||
ok: bool = False
|
||||
try:
|
||||
date: datetime.date = datetime.datetime.strptime(
|
||||
form.date.data, scu.DATE_FMT
|
||||
).date()
|
||||
if date < formsemestre.date_debut or date > formsemestre.date_fin:
|
||||
form.set_error(
|
||||
"La date sélectionnée n'est pas dans le semestre.", form.date
|
||||
)
|
||||
else:
|
||||
ok = True
|
||||
except ValueError:
|
||||
form.set_error("Date invalide", form.date)
|
||||
|
||||
if ok:
|
||||
return redirect(
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
jour=date.isoformat(),
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/choix_date.j2",
|
||||
form=form,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
deb=formsemestre.date_debut.strftime(scu.DATE_FMT),
|
||||
fin=formsemestre.date_fin.strftime(scu.DATE_FMT),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/signal_assiduites_group")
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
|
@ -965,7 +908,7 @@ def signal_assiduites_group():
|
|||
# formsemestre_id est optionnel si modimpl est indiqué
|
||||
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())
|
||||
date: str = request.args.get("day", datetime.date.today().isoformat())
|
||||
heures: list[str] = [
|
||||
request.args.get("heure_deb", ""),
|
||||
request.args.get("heure_fin", ""),
|
||||
|
@ -1028,14 +971,23 @@ def signal_assiduites_group():
|
|||
|
||||
if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin:
|
||||
# Si le jour est hors semestre, renvoyer vers choix date
|
||||
return redirect(
|
||||
url_for(
|
||||
"assiduites.choix_date",
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
flash(
|
||||
"La date sélectionnée n'est pas dans le semestre. Choisissez une autre date."
|
||||
)
|
||||
|
||||
return sco_gen_cal.calendrier_choix_date(
|
||||
formsemestre.date_debut,
|
||||
formsemestre.date_fin,
|
||||
url=url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=",".join(group_ids),
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
day="placeholder",
|
||||
),
|
||||
mode="jour",
|
||||
titre="Choix de la date",
|
||||
)
|
||||
|
||||
# --- Restriction en fonction du moduleimpl_id ---
|
||||
|
@ -1987,6 +1939,173 @@ def traitement_justificatifs():
|
|||
)
|
||||
|
||||
|
||||
@bp.route("signal_assiduites_hebdo")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def signal_assiduites_hebdo():
|
||||
"""
|
||||
signal_assiduites_hebdo
|
||||
|
||||
paramètres obligatoires :
|
||||
- formsemestre_id : id du formsemestre
|
||||
- groups_id : id des groupes (séparés par des virgules -> 1,2,3)
|
||||
|
||||
paramètres optionnels :
|
||||
- week : date semaine (iso 8601 -> 20XX-WXX), par défaut la semaine actuelle
|
||||
- moduleimpl_id : id du moduleimpl (par défaut None)
|
||||
|
||||
|
||||
Permissions :
|
||||
- ScoView -> page en lecture seule
|
||||
- AbsChange -> page en lecture/écriture
|
||||
"""
|
||||
|
||||
# Récupération des paramètres
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id", None)
|
||||
group_ids: str = request.args.get("group_ids", "") # ex: "1,2,3"
|
||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
week: str = request.args.get("week", datetime.datetime.now().strftime("%G-W%V"))
|
||||
# Vérification des paramètres
|
||||
if group_ids == "" or formsemestre_id == -1:
|
||||
raise ScoValueError("Paramètres manquants", dest_url=request.referrer)
|
||||
|
||||
# Récupération du moduleimpl
|
||||
try:
|
||||
moduleimpl_id: int = int(moduleimpl_id)
|
||||
except (ValueError, TypeError):
|
||||
moduleimpl_id: str | None = None if moduleimpl_id != "autre" else moduleimpl_id
|
||||
|
||||
# Récupération du formsemestre
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
# Vérification semaine dans format iso 8601 et formsemestre
|
||||
regex_iso8601 = r"^\d{4}-W\d{2}$"
|
||||
if week and not re.match(regex_iso8601, week):
|
||||
raise ScoValueError("Semaine invalide", dest_url=request.referrer)
|
||||
|
||||
fs_deb_iso8601 = formsemestre.date_debut.strftime("%Y-W%W")
|
||||
fs_fin_iso8601 = formsemestre.date_fin.strftime("%Y-W%W")
|
||||
|
||||
# Utilisation de la propriété de la norme iso 8601
|
||||
# les chaines sont triables par ordre alphanumérique croissant
|
||||
# et produiront le même ordre que les dates par ordre chronologique croissant
|
||||
if (not week) or week < fs_deb_iso8601 or week > fs_fin_iso8601:
|
||||
if week:
|
||||
flash(
|
||||
"""La semaine n'est pas dans le semestre,
|
||||
choisissez la semaine sur laquelle saisir l'assiduité"""
|
||||
)
|
||||
return sco_gen_cal.calendrier_choix_date(
|
||||
date_debut=formsemestre.date_debut,
|
||||
date_fin=formsemestre.date_fin,
|
||||
url=url_for(
|
||||
"assiduites.signal_assiduites_hebdo",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
week="placeholder",
|
||||
),
|
||||
mode="semaine",
|
||||
titre="Choix de la semaine",
|
||||
)
|
||||
|
||||
# Vérification des groupes
|
||||
group_ids = group_ids.split(",") if group_ids != "" else []
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True
|
||||
)
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Assiduité: saisie hebdomadaire")
|
||||
+ "<h3>Aucun étudiant ! </h3>"
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
# Récupération des étudiants
|
||||
etudiants: list[Identite] = [
|
||||
Identite.get_etud(etudid=m["etudid"]) for m in groups_infos.members
|
||||
]
|
||||
|
||||
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>"
|
||||
)
|
||||
|
||||
# Gestion des jours
|
||||
jours: dict[str, list[str]] = {
|
||||
"lun": [
|
||||
"Lundi",
|
||||
datetime.datetime.strptime(week + "-1", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"mar": [
|
||||
"Mardi",
|
||||
datetime.datetime.strptime(week + "-2", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"mer": [
|
||||
"Mercredi",
|
||||
datetime.datetime.strptime(week + "-3", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"jeu": [
|
||||
"Jeudi",
|
||||
datetime.datetime.strptime(week + "-4", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"ven": [
|
||||
"Vendredi",
|
||||
datetime.datetime.strptime(week + "-5", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"sam": [
|
||||
"Samedi",
|
||||
datetime.datetime.strptime(week + "-6", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
"dim": [
|
||||
"Dimanche",
|
||||
datetime.datetime.strptime(week + "-7", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||
],
|
||||
}
|
||||
|
||||
non_travail = sco_preferences.get_preference("non_travail")
|
||||
non_travail = non_travail.replace(" ", "").split(",")
|
||||
|
||||
hebdo_jours: list[tuple[bool, str]] = []
|
||||
for key, val in jours.items():
|
||||
hebdo_jours.append((key in non_travail, val))
|
||||
|
||||
url_choix_semaine = url_for(
|
||||
"assiduites.signal_assiduites_hebdo",
|
||||
group_ids=",".join(map(str, groups_infos.group_ids)),
|
||||
week="",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/signal_assiduites_hebdo.j2",
|
||||
title="Assiduité: saisie hebdomadaire",
|
||||
gr=gr_tit,
|
||||
etudiants=etudiants,
|
||||
moduleimpl_select=_module_selector(
|
||||
formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
|
||||
),
|
||||
hebdo_jours=hebdo_jours,
|
||||
readonly=not current_user.has_permission(Permission.AbsChange),
|
||||
non_present=sco_preferences.get_preference(
|
||||
"non_present",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
url_choix_semaine=url_choix_semaine,
|
||||
)
|
||||
|
||||
|
||||
def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
|
||||
"""Génère la liste des assiduités d'un étudiant pour le bulletin mail"""
|
||||
|
||||
|
|
|
@ -56,7 +56,9 @@ def refcomp(refcomp_id):
|
|||
@permission_required(Permission.ScoView)
|
||||
def refcomp_show(refcomp_id):
|
||||
"""Affichage du référentiel de compétences."""
|
||||
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id)
|
||||
referentiel_competence: ApcReferentielCompetences = (
|
||||
ApcReferentielCompetences.query.get_or_404(refcomp_id)
|
||||
)
|
||||
# Autres référentiels "équivalents" pour proposer de changer les formations:
|
||||
referentiels_equivalents = referentiel_competence.equivalents()
|
||||
return render_template(
|
||||
|
|
|
@ -15,4 +15,10 @@ QLIO: # la clé est 'specialite'
|
|||
ATN: MTD
|
||||
# competences: # titres de compétences ('nom_court' dans le XML)
|
||||
|
||||
SD: STID
|
||||
STID: # passage de STID à SD
|
||||
alias:
|
||||
- SD
|
||||
|
||||
SD: # pour revenir en arrière au besoin
|
||||
alias:
|
||||
- STID
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.967"
|
||||
SCOVERSION = "9.6.970"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
|
@ -253,8 +253,10 @@ def test_module_moy(test_client):
|
|||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_notes = mod_results.evals_notes
|
||||
assert evals_notes[evaluation1.id].dtype == np.float64
|
||||
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
formsemestre, modimpls=formsemestre.modimpls_sorted
|
||||
)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||
return etuds_moy_module
|
||||
|
||||
# --- Notes ordinaires:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Test calcul moyennes UE
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from tests.unit import setup
|
||||
|
||||
|
@ -63,7 +64,10 @@ def test_ue_moy(test_client):
|
|||
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
|
||||
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
|
||||
# Recalcul des moyennes
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||
modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
formsemestre, modimpls=formsemestre.modimpls_sorted
|
||||
)
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre, modimpl_coefs_df)
|
||||
# Masque de tous les modules _sauf_ les bonus (sport)
|
||||
modimpl_mask = [
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
|
@ -117,7 +121,10 @@ def test_ue_moy(test_client):
|
|||
exception_raised = True
|
||||
assert exception_raised
|
||||
# Recalcule les notes:
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||
modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
formsemestre, modimpls=formsemestre.modimpls_sorted
|
||||
)
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre, modimpl_coefs_df)
|
||||
etuds = formsemestre.etuds.all()
|
||||
modimpl_mask = [
|
||||
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
|
||||
|
|
|
@ -28,7 +28,20 @@ script_dir = Path(os.path.abspath(__file__)).parent
|
|||
os.chdir(script_dir)
|
||||
|
||||
# Les "photos" des étudiants
|
||||
FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg")))
|
||||
if os.path.exists("/opt/ExtraFaces"):
|
||||
FAKE_FACES_PATHS = list((Path("extra_faces").glob("*/*.jpg")))
|
||||
FAKE_FACES_PATHS_BY_CIVILITE = {
|
||||
"M": list((Path("extra_faces").glob("M/*.jpg"))),
|
||||
"F": list((Path("extra_faces").glob("F/*.jpg"))),
|
||||
"X": list((Path("extra_faces").glob("X/*.jpg"))),
|
||||
}
|
||||
else:
|
||||
FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg")))
|
||||
FAKE_FACES_PATHS_BY_CIVILITE = {
|
||||
"M": FAKE_FACES_PATHS,
|
||||
"F": FAKE_FACES_PATHS,
|
||||
"X": FAKE_FACES_PATHS,
|
||||
}
|
||||
|
||||
# Etudiant avec tous les champs (USPN)
|
||||
ETUD_TEMPLATE_FULL = open(script_dir / "etud_template.xml").read()
|
||||
|
@ -84,16 +97,22 @@ def make_random_etape_etuds(etape, annee):
|
|||
return "\n".join(L)
|
||||
|
||||
|
||||
def get_photo_filename(nip: str) -> str:
|
||||
def get_photo_filename(nip: str, civilite: str | None = None) -> str:
|
||||
"""get an existing filename for a fake photo, found in faces/
|
||||
Returns a path relative to the current working dir
|
||||
If civilite is not None, use it to select a subdir
|
||||
"""
|
||||
#
|
||||
nb_faces = len(FAKE_FACES_PATHS)
|
||||
print("get_photo_filename")
|
||||
if civilite:
|
||||
faces = FAKE_FACES_PATHS_BY_CIVILITE[civilite]
|
||||
else:
|
||||
faces = FAKE_FACES_PATHS
|
||||
nb_faces = len(faces)
|
||||
if nb_faces == 0:
|
||||
print("WARNING: aucun fichier image disponible !")
|
||||
return ""
|
||||
return FAKE_FACES_PATHS[hash(nip) % nb_faces]
|
||||
print(faces[hash(nip) % nb_faces])
|
||||
return faces[hash(nip) % nb_faces]
|
||||
|
||||
|
||||
class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
|
@ -139,7 +158,9 @@ class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
return
|
||||
elif ("getPhoto" in self.path) or ("scodocPhoto" in self.path):
|
||||
nip = query_components["nip"][0]
|
||||
self.path = str(get_photo_filename(nip))
|
||||
civilite = query_components.get("civilite")
|
||||
civilite = civilite[0] if civilite else None
|
||||
self.path = str(get_photo_filename(nip, civilite=civilite))
|
||||
print(f"photo for nip={nip}: {self.path}")
|
||||
else:
|
||||
print(f"Error 404: path={self.path}")
|
||||
|
|
Loading…
Reference in New Issue
Block a user