SPB Git

Repository Public Research preview

generer_calendriers.py 13.8 KB 198 lines python

1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""Calendrier du professeur (multipage, Letter paysage) pour IMM1003 et IMM1033 — HTML → WeasyPrint."""4import calendar, html, subprocess, sys5from datetime import date, timedelta6from pathlib import Path7from donnees_cours import COURS, ETUDE, ABANDON89HERE = Path(__file__).resolve().parent10LOGO = (HERE / "uqo-logo.png").as_uri()11AUJ = date(2026, 10, 17)12MOIS = ["", "janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août",13        "septembre", "octobre", "novembre", "décembre"]14MOIS_C = ["", "janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."]15JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]16SHORT = {"cours":"Séance","enonce":"Énoncé TP","remise":"Remise TP","examen":"Examen","pratique":"Examen pratique","suivi":"Correction / notes","jalon":"Jalon UQO"}17CAT = {18  "cours":    ("Séance de cours (Zoom, 19 h–22 h)", "#005f85"),19  "enonce":   ("Énoncé de TP à publier (professeur)", "#b45309"),20  "remise":   ("Remise de TP par les étudiants (Moodle, 23 h 59)", "#b91c1c"),21  "examen":   ("Examen : ouverture / remise (maison, 72 h)", "#6d28d9"),22  "pratique": ("Examen de pratique formatif en classe", "#0f766e"),23  "suivi":    ("Correction, solutionnaire, saisie des notes (date indicative)", "#4b5563"),24  "jalon":    ("Jalon UQO (semaine d'études, limite d'abandon)", "#71717a"),25}26E = html.escape2728def fdate(d, long=False):29    s = f"{d.day if d.day != 1 else '1er'} {MOIS[d.month] if long else MOIS_C[d.month]}"30    return f"{JOURS[d.weekday()]} {s}" if long else s3132def events_by_day(c):33    ev = {}34    def add(d, cat, t, sub=""):35        ev.setdefault(d, []).append((cat, t, sub))36    for i, (d, theme) in enumerate(c["seances"], 1):37        add(d, "cours", f"S{i} · 19 h–22 h", theme)38    for d, cat, t, sub in c["taches"]:39        add(d, cat, t, sub)40    d = ETUDE[0]41    while d <= ETUDE[1]:42        add(d, "jalon", "Semaine d'études", "Aucune activité d'enseignement"); d += timedelta(1)43    add(ABANDON, "jalon", "Limite d'abandon sans échec", "Au moins une note remise avant cette date")44    order = {"cours": 0, "pratique": 1, "enonce": 2, "remise": 3, "examen": 4, "suivi": 5, "jalon": 6}45    for d in ev: ev[d].sort(key=lambda e: order[e[0]])46    return ev4748def month_page(c, y, m, ev):49    cal = calendar.Calendar(firstweekday=0)50    weeks = cal.monthdatescalendar(y, m)51    n = len(weeks); h = min(30, (142 - 1.2 * (n + 1)) / n - 2.8)52    out = [f'<section class="page month m{m}"><h1>{MOIS[m].capitalize()} {y}</h1><style>.m{m} td.day{{height:{h:.1f}mm}}</style>',53           '<table class="grid"><thead><tr>' + "".join(f'<th class="{"we" if i>4 else ""}">{(j[:3]+".") if i>4 else j}</th>' for i, j in enumerate(JOURS)) + "</tr></thead><tbody>"]54    for w in weeks:55        out.append("<tr>")56        for d in w:57            cls = ["day"]58            if d.month != m: cls.append("out")59            if d.weekday() > 4: cls.append("we")60            if d == AUJ: cls.append("today")61            if ETUDE[0] <= d <= ETUDE[1] and d.weekday() < 5: cls.append("etude")62            chips = ""63            if d.month == m:64                compact = len(ev.get(d, [])) >= 3 or d.weekday() > 465                for cat, t, sub in ev.get(d, []):66                    if compact and cat != "cours": sub = ""67                    if cat == "jalon" and t == "Semaine d'études":68                        chips += f'<div class="chip jalon band">{E(t)}</div>'; continue69                    chips += f'<div class="chip {cat}"><b>{E(t)}</b>' + (f'<span>{E(sub)}</span>' if sub else "") + "</div>"70            num = f'<div class="num">{d.day}</div>'71            out.append(f'<td class="{" ".join(cls)}">{num}{chips}</td>')72        out.append("</tr>")73    out.append("</tbody></table></section>")74    return "".join(out)7576def summary_page(c):77    rows = ""78    for key, court, long_, pct, ouv, rem in c["evaluations"]:79        verbe = "Ouverture Moodle" if court in ("Intra", "Final") else "Énoncé publié"80        rows += (f"<tr><td><b>{E(court)}</b><br><span class='muted'>{E(long_)}</span></td><td class='c'>{pct} %</td>"81                 f"<td class='dt'>{E(fdate(ouv, True))}<br><span class='muted'>{verbe}</span></td>"82                 f"<td class='dt'><b>{E(fdate(rem, True))}</b><br><span class='muted'>{'au plus tard' if court=='Final' else '23 h 59 · Moodle'}</span></td></tr>")83    legend = "".join(f'<li><span class="sw" style="background:{col}"></span>{E(lbl)}</li>' for k, (lbl, col) in CAT.items())84    n = len(c["seances"])85    return f'''<section class="page cover">86<table class="hero"><tr><td><div class="kicker">Calendrier du professeur · Automne 2026</div>87<h1>{E(c["code"])} <span class="thin">{E(c["titre"])}</span></h1>88<p class="lead">Groupe {c["groupe"]} · {E(c["horaire"])} · {E(c["mode"])}<br>89Du {E(fdate(c["debut"], True))} au {E(fdate(c["fin"], True))} 2026 · {n} séances · semaine d'études du 12 au 16 octobre</p></td>90<td class="prof">Simon-Pierre Boucher, Ph. D.<br><span class="muted">Département des sciences administratives</span></td></tr></table>91<table class="cols"><tr><td class="col1">92<h2>Échéancier des évaluations</h2>93<table class="tbl"><thead><tr><th>Évaluation</th><th class="c">Pond.</th><th>Publication</th><th>Remise des étudiants</th></tr></thead><tbody>{rows}94<tr class="total"><td>Total</td><td class="c">100 %</td><td colspan="2"></td></tr>95</tbody></table><p class="note">Travaux et examens à la maison, individuels, sur données réelles UQO Éval ; remise sur Moodle avec déclarations d'intégrité et d'outils ; retard : −10 % par jour ; rien n'est accepté après la publication du solutionnaire.</p></td>96<td class="col2"><h2>Légende</h2><ul class="legend">{legend}</ul>97<h2>Repères</h2><ul class="bul">98<li><b>3 novembre</b> : date limite d'abandon sans mention d'échec — le résultat d'au moins une évaluation (TP1) doit avoir été remis aux étudiants.</li>99<li><b>Examens de 72 h</b> : le questionnaire est ouvert sur Moodle 72 h avant la remise ; les dates d'ouverture de l'intra IMM1003 et du final sont à fixer dans le devoir Moodle.</li>100<li><b>Examen de pratique</b> (séance 6) : formatif, même format que l'intra, solutionnaire détaillé publié sur Moodle et sur le site du cours.</li>101<li><b>Source</b> : plan de cours officiel du 13 octobre 2026 ; dates de remise vérifiées dans les devoirs Moodle.</li></ul></td></tr></table>102</section>'''103104def checklist_page(c, ev):105    items = []106    for d in sorted(ev):107        for cat, t, sub in ev[d]:108            if cat == "jalon" and t == "Semaine d'études" and d != ETUDE[0]: continue109            if cat == "jalon" and t == "Semaine d'études": sub = "12 au 16 octobre · aucune activité d'enseignement"110            items.append((d, cat, t, sub))111    def table(its):112        rows = ""113        for d, cat, t, sub in its:114            rows += (f'<tr class="{cat}"><td class="box"><span class="cb"></span></td><td class="d">{E(fdate(d, True))}</td>'115                     f'<td><span class="pill" style="background:{CAT[cat][1]}">{E(SHORT[cat])}</span></td>'116                     f'<td><b>{E(t)}</b>' + (f' <span class="muted">— {E(sub)}</span>' if sub else "") + "</td></tr>")117        return f'<table class="tbl todo"><thead><tr><th></th><th>Date</th><th>Type</th><th>Tâche</th></tr></thead><tbody>{rows}</tbody></table>'118    p1 = [i for i in items if i[0].month <= 10]; p2 = [i for i in items if i[0].month >= 11]119    return (f'<section class="page list"><h1>Liste des tâches <span class="thin">· septembre – octobre</span></h1>{table(p1)}</section>'120            f'<section class="page list"><h1>Liste des tâches <span class="thin">· novembre – décembre</span></h1>{table(p2)}</section>')121122CSS = '''123@page { size: Letter landscape; margin: 10mm 11mm 12mm 11mm;124  @bottom-left { content: "%(code)s · %(titre)s · Calendrier du professeur — Automne 2026"; font: 7.5pt "Avenir Next", "Helvetica Neue", sans-serif; color: #6b7280; }125  @bottom-right { content: "Page " counter(page) " / " counter(pages); font: 7.5pt "Avenir Next", "Helvetica Neue", sans-serif; color: #6b7280; } }126* { box-sizing: border-box; }127body { font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; color: #111827; font-size: 9pt; margin: 0; }128.page { page-break-after: always; position: relative; }129.page:last-child { page-break-after: auto; }130.page::before { content: ""; display: block; height: 12mm; background: url("%(logo)s") no-repeat left center; background-size: auto 10.5mm; border-bottom: 2px solid #005f85; margin-bottom: 3.5mm; }131.page::after { content: "%(code)s · Automne 2026"; position: absolute; top: 0; right: 0; height: 12mm; line-height: 12mm; font-weight: 600; color: #005f85; font-size: 11pt; letter-spacing: .02em; }132h1 { font-size: 20pt; font-weight: 700; margin: 0 0 3mm; color: #0f172a; letter-spacing: -.01em; }133h1 .thin { font-weight: 400; color: #334155; }134h2 { font-size: 10.5pt; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: #005f85; margin: 5mm 0 2mm; }135.muted { color: #6b7280; font-weight: 400; }136.c { text-align: center; }137/* couverture */138.hero { width: 100%%; border-collapse: collapse; border-bottom: 1px solid #e5e7eb; } .hero td { vertical-align: bottom; padding: 0 0 4mm; }139.kicker { font-size: 9pt; text-transform: uppercase; letter-spacing: .14em; color: #b45309; font-weight: 600; margin-bottom: 2mm; }140.lead { font-size: 10pt; color: #374151; margin: 1mm 0 0; line-height: 1.45; }141.prof { text-align: right; font-weight: 600; font-size: 10pt; line-height: 1.4; width: 60mm; }142.cols { width: 100%%; border-collapse: collapse; margin-top: 2mm; } .cols td { vertical-align: top; }143.cols td.col1 { width: 60%%; padding-right: 8mm; } .note { font-size: 8pt; color: #6b7280; margin: 2mm 0 0; line-height: 1.4; } .cols td.col2 { padding-left: 2mm; }144.tbl { width: 100%%; border-collapse: collapse; font-size: 9pt; }145.tbl th { text-align: left; font-size: 8pt; text-transform: uppercase; letter-spacing: .06em; color: #475569; border-bottom: 1.5px solid #005f85; padding: 2mm 2mm; }146.tbl td { padding: 1.7mm 2mm; border-bottom: 1px solid #e5e7eb; vertical-align: top; line-height: 1.35; }147.tbl td.dt { white-space: nowrap; } .tbl tr.total td { background: #f1f5f9; font-weight: 600; font-size: 8pt; }148.legend { list-style: none; padding: 0; margin: 0; font-size: 9pt; }149.legend li { display: flex; align-items: center; gap: 2.5mm; padding: 1.3mm 0; }150.sw { display: inline-block; width: 5mm; height: 3.2mm; border-radius: 1mm; }151.bul { padding-left: 4mm; margin: 0; font-size: 8.8pt; line-height: 1.4; } .bul li { margin-bottom: 1.6mm; }152/* grille mensuelle */153.month h1 { font-size: 17pt; margin-bottom: 2mm; }154.grid { width: 100%%; border-collapse: separate; border-spacing: 1.2mm; table-layout: fixed; }155.grid th { font-size: 8pt; text-transform: uppercase; letter-spacing: .08em; color: #475569; text-align: left; padding: 0 1.5mm 1mm; width: 17.6%%; }156.grid th.we { width: 6%%; }157.grid td.day { vertical-align: top; background: #fff; border: 1px solid #d1d5db; border-radius: 1.5mm; padding: 1mm 1.2mm; }158.grid td.we { background: #f8fafc; } .grid td.we .chip { font-size: 6.2pt; padding: .6mm .8mm; border-left-width: 1.4mm; } .grid td.out { background: #f3f4f6; border-color: #e5e7eb; }159.grid td.out .num { color: #c4c8cf; }160.grid td.etude { background: repeating-linear-gradient(135deg, #f9fafb 0 3mm, #f1f3f5 3mm 6mm); }161.grid td.today { border: 2px solid #b45309; }162.num { font-size: 10pt; font-weight: 600; color: #374151; text-align: right; margin-bottom: .8mm; }163.today .num::before { content: "aujourd'hui"; display: block; font-size: 5.8pt; color: #b45309; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; }164.chip { font-size: 7.1pt; line-height: 1.2; border-radius: 1mm; padding: .8mm 1.2mm; margin-bottom: .7mm; border-left: 2.2mm solid; background: #f3f4f6; color: #111827; }165.chip b { display: block; font-weight: 600; } .chip span { display: block; color: #4b5563; font-size: 6.6pt; margin-top: .3mm; }166.chip.cours { background: #005f85; border-color: #003f5a; color: #fff; } .chip.cours span { color: #d6ecf5; }167.chip.enonce { background: #fef3c7; border-color: #b45309; }168.chip.remise { background: #fee2e2; border-color: #b91c1c; }169.chip.examen { background: #ede9fe; border-color: #6d28d9; }170.chip.pratique { background: #ccfbf1; border-color: #0f766e; }171.chip.suivi { background: #f3f4f6; border-color: #4b5563; border-left-style: dashed; }172.chip.jalon { background: #e4e4e7; border-color: #71717a; }173.chip.band { font-size: 6.6pt; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; color: #52525b; text-align: center; border-left: 0; }174/* liste */175.todo { font-size: 7.7pt; } .todo th, .todo td { padding: .9mm 2mm; line-height: 1.25; }176.todo thead { display: table-header-group; } .todo tr { page-break-inside: avoid; }177.todo td.box { width: 7mm; } .cb { display: inline-block; width: 3.2mm; height: 3.2mm; border: 1.2px solid #64748b; border-radius: .7mm; }178.todo td.d { width: 32mm; font-weight: 600; white-space: nowrap; }179.pill { display: inline-block; color: #fff; font-size: 6.8pt; font-weight: 600; letter-spacing: .04em; padding: .6mm 1.6mm; border-radius: 3mm; white-space: nowrap; }180.todo tr.cours td { background: #f0f7fb; }181'''182183def build(code):184    c = COURS[code]185    ev = events_by_day(c)186    body = summary_page(c) + "".join(month_page(c, 2026, m, ev) for m in (9, 10, 11, 12)) + checklist_page(c, ev)187    css = CSS % {"code": c["code"], "titre": c["titre"].replace('"', ''), "logo": LOGO}188    doc = f'<!doctype html><html lang="fr"><head><meta charset="utf-8"><title>Calendrier {c["code"]}</title><style>{css}</style></head><body>{body}</body></html>'189    out_html = HERE / f"Calendrier_prof_{code}_A26.html"190    out_pdf = HERE / f"Calendrier_prof_{code}_A26.pdf"191    out_html.write_text(doc, encoding="utf-8")192    subprocess.run(["weasyprint", str(out_html), str(out_pdf)], check=True)193    print("→", out_pdf)194195if __name__ == "__main__":196    for code in (sys.argv[1:] or COURS):197        build(code)198
Files main
Menu

Theme