Rend les plages horaires et les rappels ntfy configurables, supprime le

classement, ajoute une suite de tests pytest

- calcul.py: matin_debut/fin, aprem_debut/fin et pause_dejeuner_fin sont
  paramétrables via config.json > plages (moteur de calcul de l'heure de
  sortie optimale inclus, pas seulement l'affichage). Défauts identiques
  à l'ancien comportement, validé par la suite de tests.
- stats.py: les seuils de conformité utilisent la même config.
- main.py/templates: warn de saisie et suffixes de cible dynamiques.
- Rappels ntfy: serveur, topic et plage horaire (rappel_debut_h/fin_h)
  configurables par utilisateur depuis /settings, plus en dur dans le code.
- Page classement supprimée (pas de compétition entre utilisateurs).
- Nouvelle suite pytest (app/tests/), à lancer via
  `docker compose run --rm pointeuse pytest -v`.
This commit is contained in:
Antoine
2026-07-17 22:40:55 +02:00
parent 0b47e4a625
commit c2d7d0e3ac
19 changed files with 419 additions and 415 deletions

155
app/tests/test_calcul.py Normal file
View File

@ -0,0 +1,155 @@
from calcul import (
compute_week, heures_dues, heures_travaillees, hhmm_to_minutes, minutes_to_hhmm,
)
HEURES_JOUR_MIN = 468 # 7h48 = 7.8 * 60
def _pointage(date, matin_entree=None, matin_sortie=None, aprem_entree=None, aprem_sortie=None):
return {
"date": date,
"matin_entree": matin_entree, "matin_sortie": matin_sortie,
"aprem_entree": aprem_entree, "aprem_sortie": aprem_sortie,
}
def test_hhmm_to_minutes():
assert hhmm_to_minutes("09:00") == 540
assert hhmm_to_minutes("00:00") == 0
assert hhmm_to_minutes(None) == 0
def test_minutes_to_hhmm():
assert minutes_to_hhmm(468) == "7h48"
assert minutes_to_hhmm(0) == "0h00"
assert minutes_to_hhmm(-90) == "-1h30"
def test_heures_dues_weekend():
assert heures_dues("2026-07-18", [], HEURES_JOUR_MIN) == 0 # samedi
def test_heures_dues_conge_jour():
conges = [{"date": "2026-07-13", "type": "jour"}]
assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == 0
def test_heures_dues_conge_demi_journee():
conges = [{"date": "2026-07-13", "type": "matin"}]
assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == HEURES_JOUR_MIN // 2
def test_heures_dues_matin_et_aprem_congés_egale_jour_complet():
conges = [{"date": "2026-07-13", "type": "matin"}, {"date": "2026-07-13", "type": "aprem"}]
assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == 0
def test_heures_dues_jour_normal():
assert heures_dues("2026-07-13", [], HEURES_JOUR_MIN) == HEURES_JOUR_MIN
def test_heures_travaillees_journee_complete():
p = _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54")
assert heures_travaillees(p) == 204 + 264 # 468
def test_heures_travaillees_pas_pointe():
assert heures_travaillees(_pointage("2026-07-13")) == 0
def test_heures_travaillees_en_cours_aujourdhui(freeze_today):
# freeze_today = 2026-07-17 10:30 ; entrée à 09:00 -> 90 min écoulées
p = _pointage("2026-07-17", matin_entree="09:00")
assert heures_travaillees(p) == 90
def _semaine_complete(dates_travaillees):
"""4 jours complets à exactement HEURES_JOUR_MIN, pas de congé."""
return [
_pointage(d, "09:00", "12:24", "13:30", "17:54")
for d in dates_travaillees
]
def test_compute_week_semaine_entierement_complete_solde_zero():
dates = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"]
pointages = _semaine_complete(dates)
result = compute_week(pointages, [], HEURES_JOUR_MIN)
assert result["total_travaille"] == "39h00"
assert result["total_du"] == "39h00"
assert result["solde_min"] == 0
assert all(j["is_complete"] for j in result["jours"])
def test_compute_week_cumul_visible_seulement_si_chaine_ininterrompue():
dates = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"]
pointages = _semaine_complete(dates)
# mercredi incomplet (pas d'aprem)
pointages[2] = _pointage("2026-07-15", "09:00", "12:24")
result = compute_week(pointages, [], HEURES_JOUR_MIN)
jours = result["jours"]
assert jours[0]["cumul_visible"] is True
assert jours[1]["cumul_visible"] is True
assert jours[2]["cumul_visible"] is False # jour incomplet
assert jours[3]["cumul_visible"] is False # chaîne cassée après
assert jours[4]["cumul_visible"] is False
def test_compute_week_sortie_cible_journee_en_cours(freeze_today):
"""Régression: capture exactement les seuils 9h/12h/13h30/17h codés en dur.
Lundi-jeudi complets à 468 min. Vendredi (aujourd'hui, frozen) pointé
seulement le matin 09:00-12:00 (180 min). Le calcul doit produire une
cible de sortie et d'entrée dérivées des seuils métier actuels.
"""
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"]
pointages = _semaine_complete(dates_completes)
pointages.append(_pointage("2026-07-17", matin_entree="09:00", matin_sortie="12:00"))
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert vendredi["sortie_cible"] == "18:18"
assert vendredi["entree_cible"] == "12:12"
def test_compute_week_jour_futur_non_entame_pas_de_cible(freeze_today):
"""Un jour strictement après "aujourd'hui" et pas encore entamé n'a pas de cible."""
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"]
pointages = _semaine_complete(dates_completes)
pointages.append(_pointage("2026-07-20")) # lundi suivant, rien pointé
result = compute_week(pointages, [], HEURES_JOUR_MIN)
lundi_suivant = result["jours"][5]
assert lundi_suivant["entree_cible"] is None
assert lundi_suivant["sortie_cible"] is None
def test_compute_week_jour_courant_non_entame_calcule_quand_meme_une_cible(freeze_today):
""""Aujourd'hui" sans rien pointé n'est PAS traité comme futur (date > today est faux à égalité)."""
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"]
pointages = _semaine_complete(dates_completes)
pointages.append(_pointage("2026-07-17"))
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert vendredi["sortie_cible"] == "21:18"
assert vendredi["entree_cible"] is None
def test_compute_week_plages_personnalisees(freeze_today):
"""Les seuils de plage sont bien paramétrables, pas juste figés sur les défauts."""
plages = {
"matin_debut": "08:00", "matin_fin": "13:00",
"aprem_debut": "14:00", "aprem_fin": "18:00",
"pause_dejeuner_fin": "14:00",
}
heures_jour = (13 * 60 - 8 * 60) + (18 * 60 - 14 * 60) # 540 = journée complète sous ce schéma
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"]
pointages = [_pointage(d, "08:00", "13:00", "14:00", "18:00") for d in dates_completes]
pointages.append(_pointage("2026-07-17", "08:00", "13:00")) # matin complet, aprem pas commencé
result = compute_week(pointages, [], heures_jour, plages)
vendredi = result["jours"][4]
assert vendredi["sortie_cible"] == "18:00"
assert vendredi["entree_cible"] is None