core(tools): rebuild tools

This commit is contained in:
2025-08-13 14:51:15 +02:00
parent b17f598d68
commit 0012224b8d
12 changed files with 1118 additions and 187 deletions

104
.gitignore vendored Normal file
View File

@@ -0,0 +1,104 @@
# Environnements virtuels Python
.venv/
venv/
env/
.env
# Cache Python
__pycache__/
*.py[cod]
*$py.class
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# uv
uv.lock
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# LaTeX temporaires
*.aux
*.log
*.out
*.toc
*.bbl
*.blg
*.fls
*.fdb_latexmk
*.synctex.gz
*.nav
*.snm
*.vrb
# Fichiers de sauvegarde
*.bak
*.backup
*~
# Fichiers temporaires
*.tmp
*.temp
# Vidéos (mentionné dans le Makefile)
video/
# Fichiers spécifiques au projet
*.ppm

28
Makefile Normal file
View File

@@ -0,0 +1,28 @@
CLEUSB=Cle8G
COMMON_EXCLUDE=--exclude "__pycache__" --exclude "venv/" --exclude ".git" --exclude ".gitignore" --exclude ".*" --exclude "**/*.ppm"
install:
git config core.hooksPath ./tools/git/hooks/
uv sync
update:
uv sync --upgrade
clean:
git clean -idx -e venv/ -e video/
sequence:
uv run python -m tools.scripts.new_sequence
eval:
uv run python -m tools.scripts.new_eval
rsync_cleUSB: clean
rsync -rtv -u --del --exclude "venv" ./ $(COMMON_EXCLUDE) /run/media/lafrite/$(CLEUSB)/Enseignements
rsync -rtv -u $(COMMON_EXCLUDE) ../Divers/ /run/media/lafrite/$(CLEUSB)/Divers
rsync -rtv -u $(COMMON_EXCLUDE) ../Notes/ /run/media/lafrite/$(CLEUSB)/Notes
rsync -rtv -u $(COMMON_EXCLUDE) ../Productions\ Eleves/ /run/media/lafrite/$(CLEUSB)/Productions
.PHONY:

18
pyproject.toml Normal file
View File

@@ -0,0 +1,18 @@
[project]
name = "enseignements"
version = "1.0.0"
description = "Scripts pour la création de contenus éducatifs"
authors = [{name = "Benjamin Bertrand"}]
requires-python = ">=3.8"
dependencies = [
"prompt-toolkit>=3.0.0",
"pyyaml>=6.0",
"questionary>=2.0.0",
]
[tool.uv]
dev-dependencies = []
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

1
tools/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Package tools

View File

@@ -0,0 +1,39 @@
# Configuration partagée pour les scripts de création de contenus
# Ce fichier est utilisé par new_sequence.py, new_eval.py et autres scripts
# Configuration générale
general:
author: "Benjamin Bertrand"
skeleton_path: "./tools/skeleton"
sequence_number_format: "%02d"
# Configuration des classes/niveaux
# Configuration des classes/niveaux
classes:
- id: "1"
display_name: "Première Générale Enseignement Scientifique math"
directory: "1G_EnsSci"
- id: "1"
display_name: "Première Générale spé math"
directory: "1G_math"
- id: "3"
display_name: "2nd"
directory: "2nd"
- id: "4"
display_name: "Terminale STMG"
directory: "Tstmg"
# Types de séquences disponibles
sequence_types:
- id: "1"
display_name: "Classique"
directory: "classique"
- id: "2"
display_name: "Plan de travail"
directory: "plan_de_travail"
# Configuration pour les dates
date_config:
short_format: "+%B %Y" # Format pour date_short

102
tools/scripts/README.md Normal file
View File

@@ -0,0 +1,102 @@
# Scripts de création de contenus
Ce répertoire contient les scripts Python interactifs pour créer de nouveaux contenus éducatifs avec interface moderne.
## Scripts disponibles
### `new_sequence.py`
Script Python interactif pour créer une nouvelle séquence pédagogique.
**Utilisation :**
```bash
make sequence
# ou directement
uv run python -m tools.scripts.new_sequence
```
**Fonctionnalités :**
- **Calendrier interactif** : Sélection de date avec navigation intuitive (flèches, PageUp/PageDown)
- Sélection de la classe via menu moderne (prompt_toolkit)
- Détection automatique du numéro de séquence
- Saisie interactive du titre et résumé
- **Système de tags intelligent** : Auto-complétion depuis l'existant + tags personnalisés
- Choix du type de séquence (classique/plan de travail)
- Génération des fichiers depuis les templates avec substitution de variables
### `new_eval.py`
Script Python interactif pour créer une nouvelle évaluation.
**Utilisation :**
```bash
make eval
# ou directement
uv run python -m tools.scripts.new_eval
```
**Fonctionnalités :**
- **Calendrier interactif partagé** : Même interface moderne que new_sequence
- Sélection de la classe depuis la configuration
- Saisie du nom et de la durée de l'évaluation
- Génération automatique du répertoire DS_YYYY-MM-DD
- Création des fichiers LaTeX depuis les templates
## Architecture Python
### Module commun (`common_widgets.py`)
- **CalendarWidget** : Calendrier interactif avec prompt_toolkit
- Navigation : ←→↑↓ (jours/semaines), PageUp/PageDown (mois)
- Affichage français des mois et jours
- Style coloré (aujourd'hui, sélection, weekends)
- **Fonctions utilitaires** : Formatage des dates en français
### Configuration locale française
- Mois et jours affichés en français dans le calendrier
- Dates formatées automatiquement en français
- Fallback robuste si locale française non disponible
## Configuration
Les scripts utilisent le fichier **`tools/config/year_config.yaml`** pour :
- Définir les **classes communes** pour séquences et évaluations
- Configurer les types de séquences
- Paramétrer les formats de dates français
- Définir l'auteur et les chemins des templates
## Templates
Les fichiers de base sont dans `tools/skeleton/` :
- `sequence/common/` : Templates partagés (index.rst, exercises.tex)
- `sequence/classique/` : Template séquence classique
- `sequence/plan_de_travail/` : Template plan de travail
- `eval/` : Templates d'évaluation (sujet.tex, exercises.tex)
Les variables substituées sont :
- `${title}`, `${author}`, `${date}`
- `${tribe}` (classe), `${tags}`, `${summary}`
- `${date_short}` pour l'affichage français des dates
## Fonctionnalités avancées
### Interface moderne
- **Calendrier visuel interactif** avec navigation intuitive
- **Menus modernes** avec prompt_toolkit/questionary
- **Code mutualisé** entre séquences et évaluations
- **Gestion robuste** des erreurs et annulations
- **Configuration unifiée** dans year_config.yaml
- **Support Unicode** natif et dates françaises
## Installation et dépendances
```bash
make install # Configure uv et installe les dépendances
```
### Dépendances Python
- `prompt-toolkit` : Interface utilisateur interactive
- `pyyaml` : Lecture du fichier de configuration
- `questionary` : Menus et saisies modernes
### Prérequis système
- Python 3.8+
- `uv` pour la gestion des dépendances
- Locale française pour l'affichage optimal des dates

View File

@@ -0,0 +1 @@
# Package pour les scripts de création de contenus

View File

@@ -0,0 +1,291 @@
"""
Module commun contenant les widgets réutilisables pour les scripts de création de contenus.
"""
import calendar
import locale
from datetime import datetime, date
from typing import Optional
# Configuration du français
try:
# Essayer d'abord fr_FR.UTF-8
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
except locale.Error:
try:
# Fallback vers fr_FR
locale.setlocale(locale.LC_TIME, 'fr_FR')
except locale.Error:
try:
# Autre fallback
locale.setlocale(locale.LC_TIME, 'French_France.1252')
except locale.Error:
# Si aucune locale française disponible, continuer en anglais
pass
# Configuration du calendrier en français
calendar.setfirstweekday(0) # Lundi en premier
from prompt_toolkit import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout, HSplit
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.styles import Style
class CalendarWidget:
"""Widget calendrier interactif avec prompt_toolkit."""
def __init__(self, initial_date: date = None):
self.current_date = initial_date or date.today()
self.selected_date = self.current_date
self.current_month = self.current_date.month
self.current_year = self.current_date.year
self.result = None
# Style du calendrier
self.style = Style.from_dict({
'calendar.header': 'bold #ffffff bg:#0066cc',
'calendar.day': '#000000',
'calendar.today': 'bold #ffffff bg:#cc6600',
'calendar.selected': 'bold #ffffff bg:#006600',
'calendar.weekend': '#666666',
'calendar.other_month': '#cccccc',
'instructions': 'italic #666666',
})
def _get_calendar_text(self) -> FormattedText:
"""Génère le texte formaté du calendrier."""
# En-tête avec mois et année en français
month_name = calendar.month_name[self.current_month].capitalize()
header = f"{month_name} {self.current_year}"
# Créer le calendrier
cal = calendar.monthcalendar(self.current_year, self.current_month)
# Jours de la semaine en français (avec calendar.day_abbr configuré par locale)
try:
weekdays = [calendar.day_abbr[i][:2].capitalize() for i in range(7)]
except:
# Fallback manuel si locale ne fonctionne pas
weekdays = ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di']
text = []
# En-tête
text.append(('class:calendar.header', f" {header:^20} "))
text.append(('', '\n'))
# Jours de la semaine
text.append(('class:calendar.header', ' '.join([f"{day:>2}" for day in weekdays])))
text.append(('', '\n'))
# Jours du mois
today = date.today()
for week in cal:
week_text = []
for day in week:
if day == 0:
week_text.append(' ')
else:
day_date = date(self.current_year, self.current_month, day)
style_class = 'class:calendar.day'
# Déterminer le style
if day_date == self.selected_date:
style_class = 'class:calendar.selected'
elif day_date == today:
style_class = 'class:calendar.today'
elif day_date.weekday() >= 5: # Weekend
style_class = 'class:calendar.weekend'
week_text.append((style_class, f"{day:2d}"))
if day != week[-1] or day == 0:
week_text.append(('', ' '))
text.extend(week_text)
text.append(('', '\n'))
text.append(('', '\n'))
text.append(('class:instructions', 'Flèches: naviguer | Espace: sélectionner | Entrée: confirmer | Échap: annuler'))
return FormattedText(text)
def _create_layout(self) -> Layout:
"""Crée le layout du calendrier."""
return Layout(
HSplit([
Window(
content=FormattedTextControl(
text=self._get_calendar_text,
show_cursor=False
),
height=12,
dont_extend_width=True,
)
])
)
def _create_key_bindings(self) -> KeyBindings:
"""Crée les raccourcis clavier."""
kb = KeyBindings()
@kb.add('left')
def move_left(event):
if self.selected_date.day > 1:
self.selected_date = self.selected_date.replace(day=self.selected_date.day - 1)
else:
# Mois précédent
if self.current_month > 1:
self.current_month -= 1
else:
self.current_month = 12
self.current_year -= 1
# Dernier jour du mois précédent
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
self.selected_date = date(self.current_year, self.current_month, last_day)
@kb.add('right')
def move_right(event):
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
if self.selected_date.day < last_day:
self.selected_date = self.selected_date.replace(day=self.selected_date.day + 1)
else:
# Mois suivant
if self.current_month < 12:
self.current_month += 1
else:
self.current_month = 1
self.current_year += 1
self.selected_date = date(self.current_year, self.current_month, 1)
@kb.add('up')
def move_up(event):
try:
self.selected_date = self.selected_date.replace(day=self.selected_date.day - 7)
except ValueError:
# Semaine précédente dans le mois précédent
if self.current_month > 1:
self.current_month -= 1
else:
self.current_month = 12
self.current_year -= 1
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
new_day = min(self.selected_date.day, last_day)
self.selected_date = date(self.current_year, self.current_month, new_day)
@kb.add('down')
def move_down(event):
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
try:
self.selected_date = self.selected_date.replace(day=self.selected_date.day + 7)
if self.selected_date.day > last_day:
raise ValueError()
except ValueError:
# Semaine suivante dans le mois suivant
if self.current_month < 12:
self.current_month += 1
else:
self.current_month = 1
self.current_year += 1
new_day = min(self.selected_date.day + 7 - last_day,
calendar.monthrange(self.current_year, self.current_month)[1])
self.selected_date = date(self.current_year, self.current_month, new_day)
@kb.add('pageup')
def prev_month(event):
if self.current_month > 1:
self.current_month -= 1
else:
self.current_month = 12
self.current_year -= 1
# Ajuster le jour sélectionné si nécessaire
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
if self.selected_date.day > last_day:
self.selected_date = date(self.current_year, self.current_month, last_day)
else:
self.selected_date = date(self.current_year, self.current_month, self.selected_date.day)
@kb.add('pagedown')
def next_month(event):
if self.current_month < 12:
self.current_month += 1
else:
self.current_month = 1
self.current_year += 1
# Ajuster le jour sélectionné si nécessaire
last_day = calendar.monthrange(self.current_year, self.current_month)[1]
if self.selected_date.day > last_day:
self.selected_date = date(self.current_year, self.current_month, last_day)
else:
self.selected_date = date(self.current_year, self.current_month, self.selected_date.day)
@kb.add('enter')
@kb.add(' ') # Espace
def confirm(event):
self.result = self.selected_date
event.app.exit()
@kb.add('escape')
@kb.add('c-c')
def cancel(event):
self.result = None
event.app.exit()
return kb
def show(self) -> Optional[date]:
"""Affiche le calendrier et retourne la date sélectionnée."""
app = Application(
layout=self._create_layout(),
key_bindings=self._create_key_bindings(),
style=self.style,
full_screen=False
)
app.run()
return self.result
def select_date_with_calendar(prompt_text: str = "Sélectionnez une date",
initial_date: date = None) -> str:
"""Fonction helper pour sélectionner une date avec le calendrier."""
print(f"\n{prompt_text}")
print("Navigation: ←→↑↓ (flèches), PageUp/PageDown (mois), Espace/Entrée (confirmer), Échap (annuler)")
calendar_widget = CalendarWidget(initial_date)
selected_date = calendar_widget.show()
if selected_date is None:
raise KeyboardInterrupt("Sélection de date annulée")
return selected_date.strftime('%Y-%m-%d')
def format_date_short(date_str: str, format_string: str = "%d %B %Y") -> str:
"""Formate une date courte selon le format spécifié en français."""
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
# Utiliser la locale française configurée
formatted = date_obj.strftime(format_string)
# Si la locale ne fonctionne pas, faire une traduction manuelle
if not any(month in formatted.lower() for month in ['jan', 'fév', 'mar', 'avr', 'mai', 'jun']):
# Fallback: traduction manuelle des mois anglais vers français
months_en_to_fr = {
'January': 'janvier', 'February': 'février', 'March': 'mars',
'April': 'avril', 'May': 'mai', 'June': 'juin',
'July': 'juillet', 'August': 'août', 'September': 'septembre',
'October': 'octobre', 'November': 'novembre', 'December': 'décembre'
}
for en_month, fr_month in months_en_to_fr.items():
formatted = formatted.replace(en_month, fr_month)
return formatted
except ValueError:
return date_str

193
tools/scripts/new_eval.py Executable file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
Script Python pour créer de nouvelles évaluations.
Remplace le script bash new_eval.sh en utilisant prompt_toolkit.
"""
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any
import yaml
import questionary
from questionary import Choice
from .common_widgets import select_date_with_calendar, format_date_short
# Configuration des messages
MESSAGES = {
'date_prompt': 'Sélectionnez la date de l\'évaluation',
'class_prompt': 'Choisissez un niveau',
'name_prompt': 'Nom de l\'évaluation',
'duration_prompt': 'Temps pour le travailler',
'confirm_prompt': 'Créer l\'évaluation avec ces paramètres ?',
'creation_success': 'Évaluation créée avec succès dans: {path}',
'creation_cancelled': 'Création annulée par l\'utilisateur.',
}
class EvaluationCreator:
def __init__(self):
self.config_file = Path('./tools/config/year_config.yaml')
self.config = self._load_config()
self.skeleton_path = Path('./tools/skeleton/eval')
def _load_config(self) -> Dict[str, Any]:
"""Charge le fichier de configuration YAML."""
if not self.config_file.exists():
print(f"Erreur: Fichier de configuration {self.config_file} non trouvé")
sys.exit(1)
with open(self.config_file, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _select_class(self) -> tuple[str, str]:
"""Sélection de la classe."""
choices = [
Choice(title=cls['display_name'], value=(cls['id'], cls['directory']))
for cls in self.config['classes']
]
selection = questionary.select(
MESSAGES['class_prompt'],
choices=choices
).ask()
if selection is None:
print(MESSAGES['creation_cancelled'])
sys.exit(0)
return selection
def _get_date_input(self) -> str:
"""Demande la date à l'utilisateur avec le calendrier interactif."""
return select_date_with_calendar(MESSAGES['date_prompt'])
def _get_name_input(self) -> str:
"""Demande le nom de l'évaluation."""
name = questionary.text(MESSAGES['name_prompt']).ask()
if not name:
print(MESSAGES['creation_cancelled'])
sys.exit(0)
return name
def _get_duration_input(self) -> str:
"""Demande la durée de l'évaluation."""
duration = questionary.text(MESSAGES['duration_prompt']).ask()
return duration or ""
def _get_display_name(self, class_id: str) -> str:
"""Récupère le nom d'affichage d'une classe par son ID."""
for cls in self.config['classes']:
if cls['id'] == class_id:
return cls['display_name']
return class_id
def _confirm_creation(self, date: str, class_id: str, class_dir: str,
name: str, duration: str) -> bool:
"""Dialogue de confirmation avec résumé des paramètres."""
class_display = self._get_display_name(class_id)
confirmation_text = f"""Date: {date}
Classe: {class_display}
Nom: {name}
Durée: {duration}"""
print("\\n" + confirmation_text + "\\n")
confirmed = questionary.confirm(MESSAGES['confirm_prompt']).ask()
return confirmed if confirmed is not None else False
def _create_evaluation_directory(self, class_dir: str, date: str) -> Path:
"""Crée le répertoire de l'évaluation."""
evaluations_dir = Path(class_dir) / 'Evaluations'
evaluations_dir.mkdir(parents=True, exist_ok=True)
eval_dir = evaluations_dir / f'DS_{date}'
eval_dir.mkdir(parents=True, exist_ok=True)
return eval_dir
def _process_template(self, template_file: Path, output_file: Path,
template_vars: Dict[str, str]):
"""Traite un fichier template avec substitution des variables."""
try:
with open(template_file, 'r', encoding='utf-8') as f:
content = f.read()
# Substitution simple des variables (équivalent à envsubst)
for var, value in template_vars.items():
content = content.replace(f'${{{var}}}', value)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content)
except (IOError, UnicodeDecodeError) as e:
print(f"Erreur lors du traitement du template {template_file}: {e}")
def _copy_templates(self, eval_path: Path, template_vars: Dict[str, str]):
"""Copie et traite les templates d'évaluation."""
if not self.skeleton_path.exists():
print(f"Erreur: Répertoire de templates {self.skeleton_path} non trouvé")
sys.exit(1)
# Traiter tous les fichiers templates
for template_file in self.skeleton_path.glob('*.tex'):
output_file = eval_path / template_file.name
self._process_template(template_file, output_file, template_vars)
def run(self):
"""Méthode principale pour exécuter le script."""
try:
# Étape 1: Sélection de la classe
class_id, class_dir = self._select_class()
# Étape 2: Sélection de la date
date = self._get_date_input()
# Étape 3: Saisie du nom
name = self._get_name_input()
# Étape 4: Saisie de la durée
duration = self._get_duration_input()
# Étape 5: Confirmation
if not self._confirm_creation(date, class_id, class_dir, name, duration):
print(MESSAGES['creation_cancelled'])
return
# Étape 6: Création de l'évaluation
eval_path = self._create_evaluation_directory(class_dir, date)
# Préparation des variables de template
template_vars = {
'name': name,
'date': date,
'date_short': format_date_short(date),
'tribe': class_dir,
'duration': duration,
}
# Copie et traitement des templates
self._copy_templates(eval_path, template_vars)
print(MESSAGES['creation_success'].format(path=eval_path))
except KeyboardInterrupt:
print(f"\\n{MESSAGES['creation_cancelled']}")
sys.exit(0)
except Exception as e:
print(f"Erreur inattendue: {e}")
sys.exit(1)
def main():
"""Point d'entrée principal."""
creator = EvaluationCreator()
creator.run()
if __name__ == '__main__':
main()

View File

@@ -1,70 +0,0 @@
#!/bin/bash
display_result() {
dialog --title "$1" \
--no-collapse \
--msgbox "$result" 0 0
}
exec 3>&1
selection=$(dialog \
--backtitle "Création d'une nouvelle évaluation: Classe" \
--title "Menu" \
--clear \
--cancel-label "Exit" \
--menu "Choisir un niveau:" 0 0 5 \
"1" "2nd" \
"2" "SNT" \
"3" "Enseignements Scientifique" \
"4" "Première NSI" \
"5" "Première Technologique" \
2>&1 1>&3)
exec 3>&-
case $selection in
1 )
tribe="2nd"
;;
2 )
tribe="SNT"
;;
3 )
tribe="EnseignementsScientifique"
;;
4 )
tribe="1NSI"
;;
5 )
tribe="1ST"
;;
esac
exec 3>&1
date=$(dialog --calendar "Date" 0 0 2>&1 1>&3 | awk -F "/" '{print $3"-"$2"-"$1}')
exec 3>&-
exec 3>&1
name=$(dialog \
--inputbox "Nom de l'évaluation" \
0 0 \
2>&1 1>&3)
exec 3>&-
exec 3>&1
duration=$(dialog \
--inputbox "Temps pour le travailler" \
0 0 \
2>&1 1>&3)
exec 3>&-
mkdir -p $tribe/Evaluations/
sequence_path=$tribe/Evaluations/DS_${date}/
mkdir -p $sequence_path
export name=$name
export date=$date
export date_short=`date --date="$date 00:00" "+%d %B %Y"`
export tribe=$tribe
export duration=$duration
envsubst < ./tools/skeleton/eval/exercises.tex > $sequence_path/exercises.tex
envsubst < ./tools/skeleton/eval/sujet.tex > $sequence_path/sujet.tex

341
tools/scripts/new_sequence.py Executable file
View File

@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
Script Python pour créer de nouvelles séquences pédagogiques.
Remplace le script bash new_sequence.sh en utilisant prompt_toolkit.
"""
import os
import sys
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any
import yaml
import questionary
from questionary import Choice
from .common_widgets import select_date_with_calendar, format_date_short
# Configuration des messages
MESSAGES = {
'date_prompt': 'Sélectionnez la date de la séquence',
'class_prompt': 'Choisissez un niveau',
'existing_sequences': 'Séquences existantes',
'new_sequence_prompt': 'Nom de la nouvelle séquence (n°{number})',
'summary_prompt': 'Résumé de la séquence',
'tags_selection': 'Sélectionnez les tags existants',
'tags_custom': 'Ajouter des tags personnalisés (séparés par des virgules)',
'type_prompt': 'Type de séquence',
'confirm_prompt': 'Créer la séquence avec ces paramètres ?',
'creation_success': 'Séquence créée avec succès dans: {path}',
'creation_cancelled': 'Création annulée par l\'utilisateur.',
}
class SequenceCreator:
def __init__(self):
self.config_file = Path('./tools/config/year_config.yaml')
self.config = self._load_config()
def _load_config(self) -> Dict[str, Any]:
"""Charge le fichier de configuration YAML."""
if not self.config_file.exists():
print(f"Erreur: Fichier de configuration {self.config_file} non trouvé")
sys.exit(1)
with open(self.config_file, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _get_date_input(self) -> str:
"""Demande la date à l'utilisateur avec le calendrier interactif."""
return select_date_with_calendar(MESSAGES['date_prompt'])
def _select_class(self) -> tuple[str, str]:
"""Sélection de la classe."""
choices = [
Choice(title=cls['display_name'], value=(cls['id'], cls['directory']))
for cls in self.config['classes']
]
selection = questionary.select(
MESSAGES['class_prompt'],
choices=choices
).ask()
if selection is None:
print(MESSAGES['creation_cancelled'])
sys.exit(0)
return selection
def _get_existing_sequences(self, class_directory: str) -> tuple[List[str], int]:
"""Récupère les séquences existantes dans le répertoire de classe."""
class_path = Path(class_directory)
if not class_path.exists():
print(f"Erreur: Le répertoire de classe '{class_directory}' n'existe pas")
sys.exit(1)
# Chercher les répertoires qui correspondent au pattern NN_*
existing_sequences = []
for item in class_path.glob('[0-9][0-9]_*'):
if item.is_dir():
existing_sequences.append(item.name)
existing_sequences.sort()
next_number = len(existing_sequences) + 1
return existing_sequences, next_number
def _get_sequence_title(self, existing_sequences: List[str], next_number: int) -> str:
"""Demande le titre de la nouvelle séquence."""
prompt_text = MESSAGES['new_sequence_prompt'].format(number=next_number)
if existing_sequences:
existing_text = "\\n".join(existing_sequences)
full_prompt = f"{MESSAGES['existing_sequences']}:\\n{existing_text}\\n\\n{prompt_text}"
else:
full_prompt = prompt_text
title = questionary.text(full_prompt).ask()
if not title:
print(MESSAGES['creation_cancelled'])
sys.exit(0)
return title
def _get_summary(self) -> str:
"""Demande le résumé de la séquence."""
summary = questionary.text(MESSAGES['summary_prompt']).ask()
return summary or ""
def _collect_existing_tags(self) -> List[str]:
"""Collecte les tags existants depuis les fichiers RST."""
tags = set()
# Rechercher dans tous les répertoires de classes
for class_config in self.config['classes']:
class_dir = Path(class_config['directory'])
if class_dir.exists():
# Chercher dans les index.rst des séquences
for rst_file in class_dir.glob('*/index.rst'):
try:
with open(rst_file, 'r', encoding='utf-8') as f:
for line in f:
if line.strip().startswith(':tags:'):
tag_line = line.strip().replace(':tags:', '').strip()
if tag_line and not tag_line.startswith('${'):
# Séparer les tags par virgule et nettoyer
for tag in tag_line.split(','):
tag = tag.strip()
if tag:
tags.add(tag)
except (IOError, UnicodeDecodeError):
continue
return sorted(list(tags))
def _select_tags(self) -> str:
"""Sélection intelligente des tags."""
existing_tags = self._collect_existing_tags()
selected_tags = []
# Si il y a des tags existants, proposer une sélection multiple
if existing_tags:
choices = [Choice(title=tag, value=tag) for tag in existing_tags]
selected_list = questionary.checkbox(
MESSAGES['tags_selection'],
choices=choices
).ask()
if selected_list:
selected_tags.extend(selected_list)
# Proposer d'ajouter des tags personnalisés
custom_tags = questionary.text(MESSAGES['tags_custom']).ask()
if custom_tags:
# Séparer par virgules et nettoyer
for tag in custom_tags.split(','):
tag = tag.strip()
if tag:
selected_tags.append(tag)
return ', '.join(selected_tags)
def _select_sequence_type(self) -> tuple[str, str]:
"""Sélection du type de séquence."""
choices = [
Choice(title=seq_type['display_name'], value=(seq_type['id'], seq_type['directory']))
for seq_type in self.config['sequence_types']
]
selection = questionary.select(
MESSAGES['type_prompt'],
choices=choices
).ask()
if selection is None:
print(MESSAGES['creation_cancelled'])
sys.exit(0)
return selection
def _get_display_name(self, config_list: List[Dict], selected_id: str) -> str:
"""Récupère le nom d'affichage d'un élément par son ID."""
for item in config_list:
if item['id'] == selected_id:
return item['display_name']
return selected_id
def _confirm_creation(self, date: str, class_id: str, class_dir: str,
title: str, summary: str, tags: str,
type_id: str, type_dir: str) -> bool:
"""Dialogue de confirmation avec résumé des paramètres."""
class_display = self._get_display_name(self.config['classes'], class_id)
type_display = self._get_display_name(self.config['sequence_types'], type_id)
confirmation_text = f"""Date: {date}
Classe: {class_display}
Titre: {title}
Résumé: {summary}
Tags: {tags}
Type: {type_display}"""
print("\\n" + confirmation_text + "\\n")
confirmed = questionary.confirm(MESSAGES['confirm_prompt']).ask()
return confirmed if confirmed is not None else False
def _create_sequence_directory(self, class_dir: str, next_number: int, title: str) -> Path:
"""Crée le répertoire de la séquence."""
# Nettoyer le titre pour créer un nom de répertoire valide
title_path = title.replace(' ', '_')
# Supprimer les caractères spéciaux (version simplifiée)
title_path = ''.join(c for c in title_path if c.isalnum() or c in '._-')
number_format = self.config['general']['sequence_number_format']
sequence_name = f"{number_format % next_number}_{title_path}"
sequence_path = Path(class_dir) / sequence_name
sequence_path.mkdir(parents=True, exist_ok=True)
return sequence_path
def _copy_templates(self, sequence_path: Path, sequence_type: str,
template_vars: Dict[str, str]):
"""Copie et traite les templates."""
skeleton_path = Path(self.config['general']['skeleton_path']) / 'sequence'
# Templates communs
common_path = skeleton_path / 'common'
if common_path.exists():
for template_file in common_path.glob('*'):
if template_file.is_file():
self._process_template(template_file, sequence_path, template_vars)
# Templates spécifiques au type
type_path = skeleton_path / sequence_type
if type_path.exists():
for template_file in type_path.glob('*'):
if template_file.is_file():
self._process_template(template_file, sequence_path, template_vars)
def _process_template(self, template_file: Path, output_dir: Path,
template_vars: Dict[str, str]):
"""Traite un fichier template avec substitution des variables."""
output_file = output_dir / template_file.name
try:
with open(template_file, 'r', encoding='utf-8') as f:
content = f.read()
# Substitution simple des variables (équivalent à envsubst)
for var, value in template_vars.items():
content = content.replace(f'${{{var}}}', value)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content)
except (IOError, UnicodeDecodeError) as e:
print(f"Erreur lors du traitement du template {template_file}: {e}")
def _format_date_short(self, date_str: str) -> str:
"""Formate la date courte selon la configuration."""
try:
short_format = self.config['date_config']['short_format']
# Convertir le format strftime
short_format = short_format.replace('+', '')
return format_date_short(date_str, short_format)
except KeyError:
return format_date_short(date_str)
def run(self):
"""Méthode principale pour exécuter le script."""
try:
# Étape 1: Sélection de la date
date = self._get_date_input()
# Étape 2: Sélection de la classe
class_id, class_dir = self._select_class()
# Étape 3: Récupération des séquences existantes
existing_sequences, next_number = self._get_existing_sequences(class_dir)
# Étape 4: Saisie du titre
title = self._get_sequence_title(existing_sequences, next_number)
# Étape 5: Saisie du résumé
summary = self._get_summary()
# Étape 6: Sélection des tags
tags = self._select_tags()
# Étape 7: Sélection du type de séquence
type_id, type_dir = self._select_sequence_type()
# Étape 8: Confirmation
if not self._confirm_creation(date, class_id, class_dir, title,
summary, tags, type_id, type_dir):
print(MESSAGES['creation_cancelled'])
return
# Étape 9: Création de la séquence
sequence_path = self._create_sequence_directory(class_dir, next_number, title)
# Préparation des variables de template
template_vars = {
'title': title,
'title_under': '#' * len(title),
'author': self.config['general']['author'],
'date': date,
'date_short': self._format_date_short(date),
'tribe': class_dir,
'tags': tags,
'summary': summary,
}
# Copie et traitement des templates
self._copy_templates(sequence_path, type_dir, template_vars)
print(MESSAGES['creation_success'].format(path=sequence_path))
except KeyboardInterrupt:
print(f"\\n{MESSAGES['creation_cancelled']}")
sys.exit(0)
except Exception as e:
print(f"Erreur inattendue: {e}")
sys.exit(1)
def main():
"""Point d'entrée principal."""
creator = SequenceCreator()
creator.run()
if __name__ == '__main__':
main()

View File

@@ -1,117 +0,0 @@
#!/bin/bash
display_result() {
dialog --title "$1" \
--no-collapse \
--msgbox "$result" 0 0
}
exec 3>&1
date=$(dialog --calendar "Date" 0 0 2>&1 1>&3 | awk -F "/" '{print $3"-"$2"-"$1}')
exec 3>&-
exec 3>&1
selection=$(dialog \
--backtitle "Création d'une nouvelle évaluation: Classe" \
--title "Menu" \
--clear \
--cancel-label "Exit" \
--menu "Choisir un niveau:" 0 0 5 \
"1" "2nd" \
"2" "SNT" \
"3" "Enseignements Scientifique" \
"4" "Première NSI" \
"5" "Première Technologique" \
2>&1 1>&3)
exec 3>&-
case $selection in
1 )
tribe="2nd"
;;
2 )
tribe="SNT"
;;
3 )
tribe="Enseignement_Scientifique"
;;
4 )
tribe="1NSI"
;;
5 )
tribe="1ST"
;;
esac
exec 3>&1
cd $tribe
existing_seq=$(ls -d [0-9][0-9]_*)
nbr_seq=$(echo $existing_seq | wc -w)
next_seq_number=$(expr $nbr_seq)
title=$(dialog \
--inputbox "Séquences trouvée\n${existing_seq/ /\n} \nNom de la nouvelle sequence (n°$next_seq_number)" \
0 0 \
2>&1 1>&3)
exec 3>&-
cd ..
exec 3>&1
summary=$(dialog \
--inputbox "Résumé de la séquence" \
0 0 \
2>&1 1>&3)
exec 3>&-
## ajouter les tags
exec 3>&1
tags=$(dialog \
--inputbox "Liste des tags séparés par une virgule" \
0 0 \
2>&1 1>&3)
exec 3>&-
## Plan de travail ou classique
exec 3>&1
selection=$(dialog \
--backtitle "Création d'une nouvelle séquence: Type" \
--title "Menu" \
--clear \
--cancel-label "Exit" \
--menu "Type de séquence:" 0 0 4 \
"1" "Classique" \
"2" "Plan de travail" \
2>&1 1>&3)
exec 3>&-
case $selection in
1 )
sequence_type="classique"
;;
2 )
sequence_type="plan_de_travail"
;;
esac
title_path=$(echo ${title// /_} | iconv -f utf8 -t ascii//TRANSLIT | tr -cd '[:alnum:]._-')
sequence_path=$tribe/$(printf "%02d" $next_seq_number)_${title_path}/
mkdir -p $sequence_path
export title=$title
export title_under=${title//?/#}
export author='Benjamin Bertrand'
export date=$date
export date_short=`date --date="$date 00:00" "+%B %Y"`
export tribe=$tribe
export tags=$tags
export summary=$summary
SKELETONPATH=./tools/skeleton/sequence
for i in `ls $SKELETONPATH/common/`
do
envsubst < $SKELETONPATH/common/$i > $sequence_path/$i
done
for i in `ls $SKELETONPATH/$sequence_type`
do
envsubst < $SKELETONPATH/$sequence_type/$i > $sequence_path/$i
done