From abb460e659c3f7bbd73a3af6f62fdb204894ae1b Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 6 Jun 2024 10:44:07 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Assiduit=C3=A9=20:=20WIP=20saisie=20excel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/assiduites.py | 285 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 284 insertions(+), 1 deletion(-) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 0c137c673..8fba46a06 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -78,8 +78,9 @@ from app.scodoc.sco_permissions import Permission from app.scodoc import html_sco_header from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences -from app.scodoc import sco_groups_view +from app.scodoc import sco_groups_view, sco_groups from app.scodoc import sco_etud +from app.scodoc import sco_excel from app.scodoc import sco_find_etud from app.scodoc import sco_assiduites as scass from app.scodoc import sco_utils as scu @@ -2218,9 +2219,291 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: ) +@bp.route("feuille_abs_hebdo", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.AbsChange) +def feuille_abs_hebdo(): + """ + GET : Renvoie un tableau excel pour permettre la saisie des absences + POST: Enregistre les absences saisies + Affiche un choix de semaine si "week" n'est pas renseigné + """ + + # Récupération des groupes + group_ids: str = request.args.get("group_ids", "") + if group_ids == "": + raise ScoValueError("Paramètre 'group_ids' manquant", dest_url=request.referrer) + + # Vérification du semestre + formsemestre_id: int = request.args.get("formsemestre_id", -1) + formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) + + # Vériication de la semaine + week: str = request.args.get("week", datetime.datetime.now().strftime("%G-W%V")) + + 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.feuilles_abs_hebdo", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + group_ids=group_ids, + 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é: feuille saisie hebdomadaire" + ) + + "

Aucun étudiant !

" + + html_sco_header.sco_footer() + ) + + # 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)) + + if request.method == "POST": + flash("Les absences ont bien été enregistrées") + return redirect(request.referrer) + + filename = f"feuille_signal_abs_{week}" + xls = _excel_feuille_abs( + formsemestre=formsemestre, groups_infos=groups_infos, hebdo_jours=hebdo_jours + ) + return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) + + # --- Fonctions internes --- +def _excel_feuille_abs( + formsemestre: FormSemestre, + groups_infos: sco_groups_view.DisplayedGroupsInfos, + hebdo_jours: list[tuple[bool, str]], +): + """ + Génère un fichier excel pour la saisie des absences hebdomadaires + + Colonnes : + - A : [formsemestre_id][etudid...] + - B : [nom][nom...] + - C : [prenom][prenom...] + - D : [groupes][groupe...] + - 2 colonnes (matin/aprem) par jour de la semaine + """ + + ws = sco_excel.ScoExcelSheet("Saisie_ABS") + + # == Préparation des données == + lines: list[tuple] = [] + + for membre in groups_infos.members: + etudid = membre["etudid"] + groups = sco_groups.get_etud_groups(etudid, formsemestre_id=formsemestre.id) + grc = sco_groups.listgroups_abbrev(groups) + line = [ + str(etudid), + membre["nom"].upper(), + membre["prenom"].lower().capitalize(), + membre["etat"], + grc, + ] + line += ["" for _ in range(len(hebdo_jours) * 2)] + lines.append(line) + + # == Préparation du fichier == + + # colonnes + lettres: str = "EFGHIJKLMNOPQRSTUVWXYZ" + # ajuste largeurs colonnes (unite inconnue, empirique) + ws.set_column_dimension_width("A", 11.0 / 7) # codes + # ws.set_column_dimension_hidden("A", True) # codes + ws.set_column_dimension_width("B", 164.00 / 7) # noms + ws.set_column_dimension_width("C", 109.0 / 7) # prenoms + ws.set_column_dimension_width("D", 164.0 / 7) # groupes + + i: int = 0 + for jour in hebdo_jours: + ws.set_column_dimension_width(lettres[i], 100.0 / 7) + ws.set_column_dimension_width(lettres[i + 1], 100.0 / 7) + if jour[0]: + ws.set_column_dimension_hidden(lettres[i], True) + ws.set_column_dimension_hidden(lettres[i + 1], True) + i += 2 + + # fontes + font_base = sco_excel.Font(name="Arial", size=12) + font_bold = sco_excel.Font(name="Arial", bold=True) + font_italic = sco_excel.Font( + name="Arial", size=12, italic=True, color=sco_excel.COLORS.RED.value + ) + font_titre = sco_excel.Font(name="Arial", bold=True, size=14) + font_purple = sco_excel.Font(name="Arial", color=sco_excel.COLORS.PURPLE.value) + font_brown = sco_excel.Font(name="Arial", color=sco_excel.COLORS.BROWN.value) + + # bordures + side_thin = sco_excel.Side(border_style="thin", color=sco_excel.COLORS.BLACK.value) + border_top = sco_excel.Border(top=side_thin) + border_right = sco_excel.Border(right=side_thin) + border_sides = sco_excel.Border(left=side_thin, right=side_thin, bottom=side_thin) + + # fonds + fill_light_yellow = sco_excel.PatternFill( + patternType="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value + ) + + # styles + style_titres = {"font": font_titre} + style_expl = {"font": font_italic} + + style_ro = { # cells read-only + "font": font_purple, + "border": border_right, + } + style_dem = { + "font": font_brown, + "border": border_top, + } + style_nom = { # style pour nom, prenom, groupe + "font": font_base, + "border": border_top, + } + style_abs = { + "font": font_bold, + "fill": fill_light_yellow, + "border": border_sides, + } + + # filtre + filter_top = 6 + filter_bottom = filter_top + len(lines) + filter_left = "A" + filter_right = lettres[len(hebdo_jours) * 2 - 1] + ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}") + + # == Ecritures statiques == + ws.append_single_cell_row( + "Saisir les assiduités dans les cases jaunes", style=style_expl + ) + ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl) + # Nom du semestre + ws.append_single_cell_row( + scu.unescape_html(formsemestre.titre_annee()), style_titres + ) + # ligne blanche + ws.append_blank_row() + + # == Ecritures dynamiques == + # Ecriture des entêtes + row = [ws.make_cell("", style=style_titres) for _ in range(4)] + for jour in hebdo_jours: + row.append(ws.make_cell(" ".join(jour[1]), style=style_titres)) + row.append(ws.make_cell("", style=style_titres)) + ws.append_row(row) + + row = [ + ws.make_cell(f"!{formsemestre.id}", style=style_ro), + ws.make_cell("Nom", style=style_titres), + ws.make_cell("Prénom", style=style_titres), + ws.make_cell("Groupe", style=style_titres), + ] + + for jour in hebdo_jours: + row.append(ws.make_cell("Matin", style=style_titres)) + row.append(ws.make_cell("Après-Midi", style=style_titres)) + + ws.append_row(row) + + # Ecriture des données + for line in lines: + st = style_nom + if line[3] != "I": + st = style_dem + if line[3] == "D": # demissionnaire + s = "DEM" + else: + s = line[3] # etat autre + else: + s = line[4] # groupes TD/TP/... + ws.append_row( + [ + ws.make_cell("!" + line[0], style_ro), # code + ws.make_cell(line[1], st), + ws.make_cell(line[2], st), + ws.make_cell(s, st), + ] + + [ws.make_cell(" ", style_abs) for _ in range(len(hebdo_jours) * 2)] + ) + + # ligne blanche + ws.append_blank_row() + + return ws.generate() + + def _dateiso_to_datefr(date_iso: str) -> str: """ _dateiso_to_datefr Transforme une date iso en date format français From 89c530747211cc76840b030d228821b9cc7a4df9 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 4 Jul 2024 16:46:07 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Assiduit=C3=A9=20:=20import=20excels=20clos?= =?UTF-8?q?es=20#926?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_status.py | 39 ++- .../pages/feuille_abs_formsemestre.j2 | 114 +++++++ .../pages/signal_assiduites_hebdo.j2 | 82 ++++- app/views/assiduites.py | 317 +++++++++++++++++- 4 files changed, 532 insertions(+), 20 deletions(-) create mode 100644 app/templates/assiduites/pages/feuille_abs_formsemestre.j2 diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 4053e694e..4c3748f41 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -910,6 +910,31 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: }">Ajouter une partition""" ) + # --- Formulaire importation Assiduité excel (si autorisé) + if current_user.has_permission(Permission.AbsChange): + H.append( + f"""

+ + Importation de l'assiduité depuis un fichier excel +

""" + ) + + # --- Lien Traitement Justificatifs: + + if current_user.has_permission( + Permission.AbsJustifView + ) and current_user.has_permission(Permission.JustifValidate): + H.append( + f"""

+ + Traitement des justificatifs d'absence +

""" + ) + H.append("") return "\n".join(H) @@ -1134,20 +1159,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): "", ] - # --- Lien Traitement Justificatifs: - - if current_user.has_permission( - Permission.AbsJustifView - ) and current_user.has_permission(Permission.JustifValidate): - H.append( - f"""

- - Traitement des justificatifs d'absence -

""" - ) - # --- Lien mail enseignants: adrlist = list(mails_enseignants - {None, ""}) if adrlist: diff --git a/app/templates/assiduites/pages/feuille_abs_formsemestre.j2 b/app/templates/assiduites/pages/feuille_abs_formsemestre.j2 new file mode 100644 index 000000000..236154200 --- /dev/null +++ b/app/templates/assiduites/pages/feuille_abs_formsemestre.j2 @@ -0,0 +1,114 @@ +{% extends "sco_page.j2" %} +{% block styles %} +{{super()}} + + + +{% endblock styles %} + +{% block app_content %} + +

Importation de l'assiduité depuis un fichier excel

+

{{titre_form}}

+
+
+

Avertissement : le fichier doit respecter le format suivant

+ +
    +
  • + colonne A : Identifiant de l'étudiant +
  • +
  • colonne B : Date de début
  • +
  • colonne C : Date de fin
  • +
  • colonne D : État (ABS,RET,PRE, ABS si vide)
  • +
  • colonne E : code du module
  • +
+ +

: Colonne optionnelle, les cases peuvent être vides

+

: Formats autorisés : +

    +
  • +
    aaaa-mm-jjThh:mm:ss
    +
  • +
  • +
    jj/mm/aaaa hh:mm:ss
    +
  • +
+

+ +

La première ligne du fichier ne doit pas être une ligne d'entête

+ + +
+ + +
+ + +
+ +
+
+
+ +{% if erreurs %} +
+
+

Les erreurs suivantes ont été trouvées dans le fichier excel :

+
    + {% for erreur in erreurs %} +
  • + + {{erreur[0]}} + → + Ligne : {{erreur[1][0]}} + +
  • + {% endfor %} +
+
+
+{% endif %} + +{% endblock app_content %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 index 36985a8de..44f554141 100644 --- a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 @@ -113,6 +113,26 @@ cursor: pointer; } + #excel-content { + margin: 4px 0; + display: flex; + flex-direction: column; + gap: 4px; + justify-content: space-evenly; + } + + .hint { + font-style: italic; + } + + #excel-errors { + background-color: rgb(241, 209, 209); + } + + .stdlink{ + width: fit-content; + } + @@ -657,6 +677,8 @@ matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`; apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`; + document.getElementById("excel-heures").value = Object.values(temps).map((el)=>Object.values(el).join(",")).join(","); + recupAssiduitesHebdo(updateTable); } @@ -685,6 +707,14 @@ updateTemps(temps); + const select = document.getElementById("moduleimpl_select"); + const excelModule = document.getElementById("excel-module"); + select.addEventListener("change", (ev)=>{ + excelModule.value = ev.target.value; + }); + + excelModule.value = select.value; +