################################################################################ # Copyright 2024 Louis Paternault # # Publié sous les licences suivantes : # # - [Do What The Fuck You Want To Public License](http://www.wtfpl.net/) # et sa traduction française la [Licence Publique Rien À Branler](http://sam.zoy.org/lprab/) ; # - [CC0 1.0 universel](https://creativecommons.org/publicdomain/zero/1.0/deed.fr) ; # - [GNU GPL 3.0](https://www.gnu.org/licenses/gpl-3.0.html) ou supérieur # - versé dans le domaine public, dans les législations qui le permettent # (ce qui n'est pas le cas de la France). ################################################################################ """Génère des plannings de période (de professeur·e·s).""" # pylint: disable=non-ascii-name, too-few-public-methods import collections import dataclasses import datetime import json import pathlib import sys import textwrap import jinja2 USAGE = textwrap.dedent( f"""\ Génère des plannings de périodes scolaires. {sys.argv[0]} [CONF] CONF : Fichier de configuration au format json. """ ) # Répertoire de travail RÉPERTOIRE = pathlib.Path(__file__).parent / "periodes" TEMPLATE = r""" \documentclass[12pt]{memoir} \usepackage[ pdftitle={Périodes scolaires de l'année 2024-2025}, ]{hyperref} \usepackage{fontspec} \renewcommand{\familydefault}{\sfdefault} \usepackage[]{polyglossia} \setmainlanguage{french} \usepackage[french]{translator} % Internationalized Month and Day names \usepackage{tikz} \usetikzlibrary{calc} \usetikzlibrary{calendar} \usepackage[ a4paper, margin=10mm, ]{geometry} \newcommand{\cellwidth}{25mm} \newcommand{\cellsep}{2mm} \newcommand{\cellheight}{25mm} \pagestyle{empty} \begin{document} (* for période in périodes *) (* for ligne in période *) (( ligne.tex() )) (* endfor *) (* endfor *) \end{document} """ @dataclasses.dataclass class Case: """Une case du calendrier, correspondant à un jour.""" date: datetime.date style: str @property def x(self): """Renvoit l'abscisse de la case.""" return self.date.weekday() class Ligne: """Une « ligne » affichée dans le PDF de sortie (en réalité, un bout de code LaTeX).""" @jinja2.pass_environment def tex(self, env): """Génère le code LaTeX correspondant.""" raise NotImplementedError class Code(Ligne): """Du code LaTeX arbitraire.""" def __init__(self, code): super().__init__() self.code = code @jinja2.pass_environment def tex(self, env): return self.code class Mois(Ligne): """Affiche le nom du mois.""" def __init__(self, numéro): super().__init__() self.numéro = numéro @jinja2.pass_environment def tex(self, env): return env.from_string( r"""\section*{\pgfcalendarmonthname{(( numéro ))}}""" ).render(numéro=self.numéro) class Semaine(Ligne): """Affichage d'une semaine comme une ligne de jours (sous forme de case).""" def __init__(self, jours): super().__init__() self.jours = jours @jinja2.pass_environment def tex(self, env): # pylint: disable=line-too-long return env.from_string( r""" \begin{center} \begin{tikzpicture} \draw[opacity=0] (-.5*\cellwidth, -.5*\cellheight) rectangle (.5*\cellwidth, .5*\cellheight); \begin{scope}[shift={({6 * (\cellwidth + \cellsep)}, 0) }] \draw[opacity=0] (-.5*\cellwidth, -.5*\cellheight) rectangle (.5*\cellwidth, .5*\cellheight); \end{scope} (* for jour in jours *) \begin{scope}[shift={({(( jour.x )) * (\cellwidth + \cellsep)}, 0) }] (* if jour.style *) \fill[% (* if jour.style == "vacances" or jour.style == "dimanche" *) red!20 (* elif jour.style == "samedi" *) red!10 (* endif *) ] (-.5*\cellwidth, -.5*\cellheight) rectangle (.5*\cellwidth, .5*\cellheight); (* endif *) \draw (-.5*\cellwidth, -.5*\cellheight) rectangle (.5*\cellwidth, .5*\cellheight); \draw (.5*\cellwidth, .5*\cellheight) node[anchor=north east]{\pgfcalendarweekdayshortname{((jour.date.weekday() ))}. (( jour.date.day ))}; \end{scope} (* endfor *) \end{tikzpicture} \end{center} """ ).render(jours=self.jours) def génère_lignes(début, vacances, fin): """Génère des lignes (nom de mois, semaine, saut de ligne, etc.). :param str début: Début de la période. :param str fin: Fin de la période. :param str vacances: Début des vacances, à l'intérieur de la période. """ # Conversion des arguments en objets date python début = datetime.date.fromisoformat(début) vacances = datetime.date.fromisoformat(vacances) fin = datetime.date.fromisoformat(fin) mois = collections.defaultdict(lambda: collections.defaultdict(list)) semaine = 0 date = début while date <= fin: if date >= vacances: style = "vacances" elif date.weekday() == 6: style = "dimanche" elif date.weekday() == 5: style = "samedi" else: style = None mois[date.month][semaine].append(Case(date, style=style)) date += datetime.timedelta(days=1) if date.weekday() == 0: semaine += 1 lignes = [] for nom, semaines in mois.items(): lignes.append(Mois(nom)) for jours in semaines.values(): lignes.append(Semaine(jours)) gauche = lignes[: len(lignes) // 2] droite = lignes[len(lignes) // 2 :] if len(gauche) < len(droite): gauche.append(droite.pop(0)) if isinstance(gauche[-1], Mois): droite.insert(0, gauche.pop()) if isinstance(droite[0], Semaine) and len(droite[0].jours) != 7: gauche.append(droite.pop(0)) yield Code(r"\vplace") yield from gauche yield Code(r"\vplace") yield Code(r"\newpage") yield Code(r"\vplace") yield from droite yield Code(r"\vplace") yield Code(r"\newpage") def render(périodes): """Renvoit le fichier LaTeX correspondant aux périodes données en argument.""" environment = jinja2.Environment() environment.block_start_string = "(*" environment.block_end_string = "*)" environment.variable_start_string = "((" environment.variable_end_string = "))" environment.comment_start_string = "(% comment %)" environment.comment_end_string = "(% endcomment %)" environment.line_comment_prefix = "%!" environment.trim_blocks = True environment.lstrip_blocks = True return environment.from_string(TEMPLATE).render( { "périodes": (génère_lignes(*période) for période in périodes), } ) def main(): """Fonction principale.""" # Analyse des arguments de la ligne de commande if "-h" in sys.argv[1:] or "--help" in sys.argv[1:]: print(USAGE) sys.exit(0) if len(sys.argv) != 2: print(USAGE) sys.exit(1) nom = pathlib.Path(sys.argv[1]) with nom.open(encoding="utf8") as source: with open(nom.with_suffix(".tex"), mode="w", encoding="utf8") as dest: dest.write(render(json.load(source))) if __name__ == "__main__": main()