Compare commits

...

26 Commits

Author SHA1 Message Date
02a5b00ecf Changement réf. comp. équivalent: SD <> STID. 2024-06-02 12:05:01 +02:00
dcdf6a8012 Assiduite: supprime lien saisie différée + lien choix semaine 2024-06-02 10:05:15 +02:00
912a213dcd Rafraichissement image lors changement photo etud. Pres. trombi. Photos pour demos. 2024-06-01 14:28:42 +02:00
3575e89dc0 check invalid etudid 2024-06-01 14:27:02 +02:00
21c0625147 formsemestre_report_counts: ajout champ 'boursier' 2024-05-30 16:05:45 +02:00
e18c1d8fd0 Merge branch 'iziram-sco_gen_cal' 2024-05-30 13:31:07 +02:00
5867d0f430 typo 2024-05-30 13:30:37 +02:00
9897ccc659 Numéros pages sur bulletins BUT. Closes #652 2024-05-30 12:08:41 +02:00
Iziram
7575959bd4 sco_gen_cal : calendrier_choix_date + implementation dans Assiduité closes #914 2024-05-30 10:52:13 +02:00
Iziram
2aafbad9e2 sco_gen_cal : hightlight + week_index + jour date 2024-05-30 09:49:33 +02:00
50f2cd7a0f Assiduité: Liens et message temporaire 2024-05-29 19:09:06 +02:00
fd8fbb9e02 Merge branch 'iziram-saisie_hebdo' 2024-05-29 18:42:06 +02:00
Iziram
ebcef76950 Assiduité : signal_assiduites_hebdo : choix heures init defaut closes #911 2024-05-29 17:30:07 +02:00
Iziram
13349776af Assiduité : signal_assiduites_hebdo : bulle info assi closes #912 2024-05-29 17:25:57 +02:00
Iziram
f275286b71 Assiduité : liens saisie hebdo 2024-05-29 16:29:34 +02:00
Iziram
f4f6c13d79 Assiduité : signal_assiduites_hebdo : v2 sans mobile 2024-05-29 15:59:19 +02:00
e7f23efe65 Affichage poids sur tableau de bord module: normalisation par evaluation_type. Closes #886 2024-05-29 12:12:31 +02:00
e44d3fd5dc Améliore visualisation coefficients sur tableau bord module. Closes #886. 2024-05-29 11:55:28 +02:00
fac36fa11c Merge branch 'master' into saisie_hebdo 2024-05-29 10:56:55 +02:00
9289535359 Ajout Identite.nom_prenom() 2024-05-29 10:48:34 +02:00
Iziram
d73b925006 Assiduité : signal_assiduites_hedbo : v1 OK 2024-05-28 20:07:25 +02:00
6749ca70d6 Fix prise en compte evals session 2 avec poids ne couvrant pas toutes les UEs (#811) 2024-05-28 13:51:27 +02:00
Iziram
dea403b03d Assiduité : signal_assiduites_hebdo : verif heure matin < aprem 2024-05-28 09:51:40 +02:00
Iziram
ab9543c310 [WIP] Assiduité : signal_assiduites_hebdo : choix horaires 2024-05-27 23:26:13 +02:00
Iziram
f94998f66b [WIP] Assiduité : corrections saisie_assiduites_hebdo 2024-05-27 22:33:01 +02:00
Iziram
eb88a8ca83 [WIP] Assiduité : saisie_assiduites_hebdo 2024-05-27 17:59:34 +02:00
30 changed files with 1605 additions and 235 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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" """

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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"]

View File

@ -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,
)

View File

@ -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

View File

@ -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>&nbsp;</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:

View File

@ -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

View File

@ -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:

View File

@ -351,6 +351,7 @@ def formsemestre_report_counts(
"statut",
"annee_admission",
"type_admission",
"boursier",
"boursier_prec",
]
if jury_but_mode:

View File

@ -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;
}

View File

@ -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(

View File

@ -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;
}

View File

@ -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>

View 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">&times;</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 %}

View File

@ -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>

View 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 %}

View File

@ -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"""

View File

@ -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(

View File

@ -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

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.967"
SCOVERSION = "9.6.970"
SCONAME = "ScoDoc"

View File

@ -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:

View File

@ -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

View File

@ -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}")