SPB Git

Repository Public Research preview

generer_grilles_notes.py 13.6 KB 213 lines python

1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""Grille de notes Excel par cours : étudiants + évaluations lus sur Moodle (moodle_grades.py), notes déjà saisies,4cases vides pour le reste, total, note littérale UQO, statistiques. Usage : generer_grilles_notes.py [IMM1003 IMM1033]"""5import json, re, sys, unicodedata6from datetime import datetime7from pathlib import Path8from openpyxl import Workbook9from openpyxl.drawing.image import Image as XLImage10from openpyxl.styles import Alignment, Border, Font, PatternFill, Side11from openpyxl.utils import get_column_letter as L12from openpyxl.worksheet.datavalidation import DataValidation13from openpyxl.formatting.rule import FormulaRule14from donnees_cours import BAREME, COURS1516HERE = Path(__file__).resolve().parent17BLEU, BLEU_F, GRIS, VERT, ORANGE = "005F85", "003F5A", "F3F4F6", "DCFCE7", "FEF3C7"18MOIS_C = ["", "janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."]19thin = Side(style="thin", color="CBD5E1"); med = Side(style="medium", color=BLEU)20BORD = Border(left=thin, right=thin, top=thin, bottom=thin)21FONT = "Avenir Next"2223def strip_acc(s):24    return "".join(ch for ch in unicodedata.normalize("NFD", s) if unicodedata.category(ch) != "Mn")2526def split_name(raw):27    """'BDDavid Bissonnette' → (prénom 'David', nom 'Bissonnette') grâce aux initiales Moodle (Nom, Prénom)."""28    init, full = raw[:2], raw[2:].strip()29    toks = full.split()30    li = strip_acc(init[0]).upper()31    for i in range(1, len(toks)):32        if strip_acc(toks[i])[0].upper() == li:33            return " ".join(toks[:i]), " ".join(toks[i:])34    return " ".join(toks[:-1]), toks[-1]3536def fdate(d): return f"{d.day if d.day != 1 else '1er'} {MOIS_C[d.month]}"3738def val(v):39    v = (v or "").strip()40    if v in ("", "-", "–"): return None41    try: return float(v.replace(",", ".").replace(" ", "").replace(" ", ""))42    except ValueError: return v4344def build(code):45    c = COURS[code]46    raw = json.loads((HERE / f"{code.lower()}_moodle_notes.json").read_text(encoding="utf-8"))47    heads = raw["heads"]; evals = c["evaluations"]48    # correspondance colonne Moodle ↔ évaluation du plan (par préfixe du libellé)49    col_of = {}50    for k, (key, court, long_, pct, ouv, rem) in enumerate(evals):51        idx = next(i for i, h in enumerate(heads) if h["text"].startswith(key))52        col_of[k] = heads[idx]["id"]53    students = []54    for r in raw["rows"]:55        pre, nom = split_name(r["name"])56        grades = {cell["id"]: val(cell["v"]) for cell in r["cells"]}57        students.append((nom, pre, r["email"], grades))58    students.sort(key=lambda s: (strip_acc(s[0]).lower(), strip_acc(s[1]).lower()))5960    wb = Workbook(); ws = wb.active; ws.title = "Notes"61    ws.sheet_view.showGridLines = False62    ne = len(evals)63    C0 = 4                      # première colonne d'évaluation (D)64    CT = C0 + ne                # Total65    CP = CT + 1                 # % sur le corrigé66    CL = CP + 1                 # lettre67    CR = CL + 1                 # remarques68    # --- en-tête69    img = XLImage(str(HERE / "uqo-logo.png")); img.width, img.height = 125, 6070    ws.add_image(img, "A1")71    ws.row_dimensions[1].height = 24; ws.row_dimensions[2].height = 20; ws.row_dimensions[3].height = 1672    ws["C1"] = f"{c['code']} — {c['titre']}"; ws["C1"].font = Font(name=FONT, size=16, bold=True, color=BLEU_F)73    ws["C2"] = f"Grille de notes · Automne 2026 · Groupe {c['groupe']} · {c['horaire']} · Professeur : Simon-Pierre Boucher, Ph. D."74    ws["C2"].font = Font(name=FONT, size=10, color="475569")75    ws["C3"] = (f"Notes déjà saisies lues sur Moodle (rapport de l'évaluateur, cours {c['moodle_id']}) le "76                f"{datetime.now():%d/%m/%Y à %H h %M} · cases blanches = à saisir · barème des points = pondération du plan de cours (13 oct. 2026)")77    ws["C3"].font = Font(name=FONT, size=8.5, italic=True, color="6B7280")78    # --- lignes d'en-tête du tableau (5-7)79    H1, H2, H3 = 5, 6, 780    hdr_fill = PatternFill("solid", fgColor=BLEU); hdr_font = Font(name=FONT, bold=True, color="FFFFFF", size=10)81    sub_fill = PatternFill("solid", fgColor="E0F2FE"); sub_font = Font(name=FONT, size=8.5, color="0F3B52")82    for col, txt in ((1, "N°"), (2, "Nom"), (3, "Prénom")):83        ws.merge_cells(start_row=H1, start_column=col, end_row=H3, end_column=col)84        cell = ws.cell(H1, col, txt); cell.fill = hdr_fill; cell.font = hdr_font85        cell.alignment = Alignment(horizontal="center" if col == 1 else "left", vertical="center", indent=0 if col == 1 else 1)86    for k, (key, court, long_, pct, ouv, rem) in enumerate(evals):87        col = C0 + k88        t = ws.cell(H1, col, court); t.fill = hdr_fill; t.font = hdr_font; t.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)89        m = ws.cell(H2, col, pct); m.number_format = '"/ "0'; m.fill = sub_fill; m.font = Font(name=FONT, bold=True, size=9, color="0F3B52"); m.alignment = Alignment(horizontal="center")90        d = ws.cell(H3, col, ("Remise " if court not in ("Final",) else "≤ ") + fdate(rem)); d.fill = sub_fill; d.font = sub_font; d.alignment = Alignment(horizontal="center", wrap_text=True)91    for col, txt, sub in ((CT, "Total", "/ 100"), (CP, "% du corrigé", "sur les pts évalués"), (CL, "Lettre", "sur % du corrigé"), (CR, "Remarques", "")):92        t = ws.cell(H1, col, txt); t.fill = hdr_fill; t.font = hdr_font; t.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)93        s2 = ws.cell(H2, col, sub); s2.fill = sub_fill; s2.font = sub_font; s2.alignment = Alignment(horizontal="center")94        s3 = ws.cell(H3, col); s3.fill = sub_fill95        if col == CR:96            ws.merge_cells(start_row=H1, start_column=col, end_row=H3, end_column=col)97    ws.row_dimensions[H1].height = 30; ws.row_dimensions[H3].height = 2698    # --- lignes étudiants99    R0 = H3 + 1100    thresholds = ",".join(str(b[0]) for b in sorted(BAREME)); letters = ",".join(f'"{b[1]}"' for b in sorted(BAREME))101    rng_pts = f"${L(C0)}${H2}:${L(CT-1)}${H2}"102    done_fill = PatternFill("solid", fgColor=VERT)103    for i, (nom, pre, email, grades) in enumerate(students):104        r = R0 + i105        ws.cell(r, 1, i + 1).alignment = Alignment(horizontal="center")106        ws.cell(r, 2, nom).font = Font(name=FONT, bold=True, size=10)107        ws.cell(r, 3, pre)108        for k in range(ne):109            g = grades.get(col_of[k]); cell = ws.cell(r, C0 + k)110            if g is not None:111                cell.value = g; cell.fill = done_fill112            cell.number_format = "0.00"; cell.alignment = Alignment(horizontal="center")113        rg = f"{L(C0)}{r}:{L(CT-1)}{r}"114        ws.cell(r, CT, f'=IF(COUNT({rg})=0,"",SUM({rg}))').number_format = "0.00"115        ws.cell(r, CP, f'=IF(COUNT({rg})=0,"",SUM({rg})/SUMPRODUCT(--ISNUMBER({rg}),{rng_pts})*100)').number_format = '0.0" %"'116        ws.cell(r, CL, f'=IF({L(CP)}{r}="","",LOOKUP(ROUND({L(CP)}{r},0),{{{thresholds}}},{{{letters}}}))')117        for col in (CT, CP, CL):118            ws.cell(r, col).alignment = Alignment(horizontal="center"); ws.cell(r, col).font = Font(name=FONT, bold=(col != CP), size=10)119        ws.cell(r, CT).fill = PatternFill("solid", fgColor=GRIS); ws.cell(r, CL).fill = PatternFill("solid", fgColor=GRIS)120        if i % 2: 121            for col in (1, 2, 3, CR): ws.cell(r, col).fill = PatternFill("solid", fgColor="F8FAFC")122        for col in range(1, CR + 1):123            ws.cell(r, col).border = BORD124            if ws.cell(r, col).font.name != FONT: ws.cell(r, col).font = Font(name=FONT, size=10)125        ws.row_dimensions[r].height = 20126    RN = R0 + len(students) - 1127    # --- statistiques128    RS = RN + 2129    ws.cell(RS - 1, 2, "Statistiques (sur les notes saisies)").font = Font(name=FONT, bold=True, size=9, color=BLEU_F)130    for j, (lbl, fn) in enumerate((("Notes saisies", "COUNT"), ("Moyenne", "AVERAGE"), ("Médiane", "MEDIAN"), ("Minimum", "MIN"), ("Maximum", "MAX"))):131        r = RS + j132        ws.cell(r, 2, lbl).font = Font(name=FONT, size=9, color="475569"); ws.cell(r, 2).border = BORD133        ws.cell(r, 3).border = BORD134        for col in list(range(C0, CT)) + [CT, CP]:135            rg = f"{L(col)}${R0}:{L(col)}${RN}"136            f = f"=COUNT({rg})" if fn == "COUNT" else f'=IF(COUNT({rg})=0,"",{fn}({rg}))'137            cell = ws.cell(r, col, f); cell.number_format = "0" if fn == "COUNT" else ("0.0" if col != CP else '0.0" %"')138            cell.alignment = Alignment(horizontal="center"); cell.font = Font(name=FONT, size=9); cell.border = BORD139            cell.fill = PatternFill("solid", fgColor=GRIS)140    # --- validation (0 … max) et surlignage des cases à saisir141    for k, (key, court, long_, pct, ouv, rem) in enumerate(evals):142        col = L(C0 + k)143        dv = DataValidation(type="decimal", operator="between", formula1="0", formula2=str(pct), allow_blank=True,144                            error=f"Note entre 0 et {pct}", errorTitle=court)145        ws.add_data_validation(dv); dv.add(f"{col}{R0}:{col}{RN}")146    ws.conditional_formatting.add(f"{L(C0)}{R0}:{L(CT-1)}{RN}",147        FormulaRule(formula=[f"ISNUMBER({L(C0)}{R0})"], fill=done_fill, font=Font(name=FONT, bold=True, color="14532D")))148    # --- largeurs, figer, impression149    widths = {1: 5, 2: 24, 3: 20, CT: 10, CP: 15, CL: 14, CR: 34}150    for k in range(ne): widths[C0 + k] = 13151    for col, w in widths.items(): ws.column_dimensions[L(col)].width = w152    ws.freeze_panes = ws.cell(R0, C0)153    ws.print_title_rows = f"{H1}:{H3}"; ws.page_setup.orientation = "landscape"; ws.page_setup.fitToWidth = 1; ws.page_setup.fitToHeight = 0154    ws.sheet_properties.pageSetUpPr.fitToPage = True155    ws.oddFooter.left.text = f"{c['code']} · Grille de notes · Automne 2026"; ws.oddFooter.right.text = "Page &P / &N"156    # légende157    RL = RS + 6158    ws.cell(RL, 2, "Légende").font = Font(name=FONT, bold=True, size=9, color=BLEU_F)159    ws.cell(RL + 1, 2).fill = done_fill; ws.cell(RL + 1, 3, "Note déjà saisie sur Moodle").font = Font(name=FONT, size=9)160    ws.cell(RL + 2, 2).border = BORD; ws.cell(RL + 2, 3, "Case à remplir (évaluation à venir ou à corriger)").font = Font(name=FONT, size=9)161    ws.cell(RL + 3, 3, "Total = somme des points ; % du corrigé = points obtenus ÷ points des évaluations déjà notées ; lettre = barème UQO appliqué au % du corrigé (provisoire tant que tout n'est pas noté ; = total à la fin)").font = Font(name=FONT, size=8.5, italic=True, color="6B7280")162163    # --- feuille Évaluations164    we = wb.create_sheet("Évaluations")165    we.sheet_view.showGridLines = False166    for col, txt in enumerate(("Évaluation", "Description", "Points", "Énoncé / ouverture", "Remise", "Nb notes saisies"), 1):167        cell = we.cell(1, col, txt); cell.fill = hdr_fill; cell.font = hdr_font; cell.alignment = Alignment(vertical="center")168    for k, (key, court, long_, pct, ouv, rem) in enumerate(evals):169        r = 2 + k170        we.cell(r, 1, court).font = Font(name=FONT, bold=True); we.cell(r, 2, long_); we.cell(r, 3, pct)171        we.cell(r, 4, f"{fdate(ouv)} 2026"); we.cell(r, 5, f"{fdate(rem)} 2026" + (" (au plus tard)" if court == "Final" else " · 23 h 59"))172        we.cell(r, 6, f"=COUNT(Notes!{L(C0+k)}{R0}:{L(C0+k)}{RN})")173        for col in range(1, 7): we.cell(r, col).border = BORD; we.cell(r, col).font = Font(name=FONT, size=10, bold=(col == 1))174    we.cell(2 + ne, 1, "Total").font = Font(name=FONT, bold=True); we.cell(2 + ne, 3, f"=SUM(C2:C{1+ne})").font = Font(name=FONT, bold=True)175    for col, w in ((1, 12), (2, 62), (3, 8), (4, 20), (5, 26), (6, 16)): we.column_dimensions[L(col)].width = w176177    # --- feuille Barème178    wbm = wb.create_sheet("Barème UQO")179    wbm.sheet_view.showGridLines = False180    for col, txt in enumerate(("Note (N)", "Lettre", "Appréciation", "Valeur numérique"), 1):181        cell = wbm.cell(1, col, txt); cell.fill = hdr_fill; cell.font = hdr_font182    prev = 101183    for i, (lo, let, app, num) in enumerate(BAREME):184        r = 2 + i185        wbm.cell(r, 1, f"{lo} - {prev - 1}" if lo else "0 - 54"); wbm.cell(r, 2, let); wbm.cell(r, 3, app); wbm.cell(r, 4, num)186        for col in range(1, 5): wbm.cell(r, col).border = BORD; wbm.cell(r, col).font = Font(name=FONT, size=10, bold=(col == 2))187        prev = lo188    wbm.cell(2 + len(BAREME), 1, "S"); wbm.cell(2 + len(BAREME), 3, "Exigence satisfaite")189    wbm.cell(4 + len(BAREME), 1, "La notation doit être arrondie à l'entier supérieur lorsque la première décimale est égale ou supérieure à cinq. "190             "Barème officiel — Règlement des études de premier cycle de l'UQO.").font = Font(name=FONT, size=9, italic=True, color="6B7280")191    for col, w in ((1, 12), (2, 8), (3, 18), (4, 16)): wbm.column_dimensions[L(col)].width = w192193    # --- feuille Source Moodle194    wsm = wb.create_sheet("Source Moodle")195    wsm.append(["Lu le", datetime.now().strftime("%Y-%m-%d %H:%M"), "Cours Moodle", c["moodle_id"], raw.get("title", "")])196    wsm.append(["Nom Moodle (brut)", "Courriel"] + [h["text"] for h in heads])197    for r in raw["rows"]:198        wsm.append([r["name"], r["email"]] + [cell["v"] for cell in r["cells"]])199    for col in range(1, 3 + len(heads)): wsm.column_dimensions[L(col)].width = 28200    wsm.column_dimensions["A"].width = 34201202    for sh in (we, wbm, wsm):203        sh.page_setup.orientation = "landscape"; sh.page_setup.fitToWidth = 1; sh.page_setup.fitToHeight = 1204        sh.sheet_properties.pageSetUpPr.fitToPage = True205    ws.print_area = f"A1:{L(CR)}{RL + 3}"206    out = HERE / f"Grille_de_notes_{code}_A26.xlsx"207    wb.save(out); print("→", out, f"({len(students)} étudiants, {sum(1 for s in students for v in s[3].values() if v is not None and True) } notes saisies incl. total)")208    return students209210if __name__ == "__main__":211    for code in (sys.argv[1:] or COURS):212        build(code)213
Files main
Menu

Theme