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

View File

@ -19,9 +19,8 @@ from models import (
load_notif_config, save_notif_config, find_user_by_token, save_presence,
load_auth, save_auth, find_user_by_reset_token,
)
from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues
from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues, DEFAULT_PLAGES
from stats import compute_all_stats
from classement import compute_classement
from notifications import reminder_loop
logging.basicConfig(
@ -97,6 +96,10 @@ def heures_jour_min() -> int:
return int(float(load_config().get("heures_jour", 7.8)) * 60)
def plages_config() -> dict:
return {**DEFAULT_PLAGES, **load_config().get("plages", {})}
def _weeks_by_year(all_weeks: list) -> dict:
result = {}
for yr, wks in groupby(sorted(all_weeks, reverse=True), key=lambda x: x[0]):
@ -113,7 +116,7 @@ def _build_soldes_ctx(year, week, conges, h_jour, user_id):
"aprem_entree": None, "aprem_sortie": None})
for d in dates
]
return {"result": compute_week(pointages, conges, h_jour)}
return {"result": compute_week(pointages, conges, h_jour, plages_config())}
def _build_jour_ctx(date, conges, h_jour, user_id):
@ -151,7 +154,7 @@ def _oob_calc(date, jour):
)
def _oob_week_extras(result):
def _oob_week_extras(result, plages):
html = ""
for jour in result["jours"]:
date = jour["date"]
@ -169,7 +172,7 @@ def _oob_week_extras(result):
cible = None
if not jour.get("is_complete"):
if jour.get("entree_cible"):
suffix = "→12:00" if "aprem" in jour.get("conge", set()) else "→17:00"
suffix = f'{plages["matin_fin"]}' if "aprem" in jour.get("conge", set()) else f'{plages["aprem_fin"]}'
cible = f'{jour["entree_cible"]}{suffix}'
elif jour.get("sortie_cible"):
cible = jour.get("sortie_cible")
@ -178,17 +181,17 @@ def _oob_week_extras(result):
return html
def _oob_time_cells(date, jour):
def _oob_time_cells(date, jour, plages):
cg = jour["conge"]
matin_cg = "matin" in cg or "jour" in cg
aprem_cg = "aprem" in cg or "jour" in cg
matin_warn = not matin_cg and jour["du_min"] > 0 and (
(jour.get("matin_entree") and jour["matin_entree"] > "09:00") or
(jour.get("matin_sortie") and jour["matin_sortie"] < "12:00")
(jour.get("matin_entree") and jour["matin_entree"] > plages["matin_debut"]) or
(jour.get("matin_sortie") and jour["matin_sortie"] < plages["matin_fin"])
)
aprem_warn = not aprem_cg and jour["du_min"] > 0 and (
(jour.get("aprem_entree") and jour["aprem_entree"] > "14:00") or
(jour.get("aprem_sortie") and jour["aprem_sortie"] < "17:00")
(jour.get("aprem_entree") and jour["aprem_entree"] > plages["aprem_debut"]) or
(jour.get("aprem_sortie") and jour["aprem_sortie"] < plages["aprem_fin"])
)
matin_cls = "td-fill cg" if matin_cg else ("td-fill warn" if matin_warn else "td-fill")
aprem_cls = "td-fill cg" if aprem_cg else ("td-fill warn" if aprem_warn else "td-fill")
@ -213,6 +216,7 @@ def _oob_time_cells(date, jour):
def _htmx_conge_and_soldes(request, date, d, conges, h_jour, user_id):
iso = d.isocalendar()
plages = plages_config()
jour = _build_jour_ctx(date, conges, h_jour, user_id)
cg = jour["conge"]
btn_ma_on = " on" if "matin" in cg else ""
@ -229,23 +233,24 @@ def _htmx_conge_and_soldes(request, date, d, conges, h_jour, user_id):
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour, user_id)
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
return HTMLResponse(
_oob_time_cells(date, jour)
_oob_time_cells(date, jour, plages)
+ btns_html
+ _oob_calc(date, jour)
+ _oob_week_extras(soldes_ctx["result"])
+ _oob_week_extras(soldes_ctx["result"], plages)
+ soldes_html
)
def _htmx_calc_and_soldes(request, date, d, conges, h_jour, user_id):
iso = d.isocalendar()
plages = plages_config()
jour = _build_jour_ctx(date, conges, h_jour, user_id)
soldes_ctx = _build_soldes_ctx(iso.year, iso.week, conges, h_jour, user_id)
soldes_html = templates.get_template("_soldes.html").render({"request": request, **soldes_ctx})
return HTMLResponse(
_oob_calc(date, jour)
+ _oob_time_cells(date, jour)
+ _oob_week_extras(soldes_ctx["result"])
+ _oob_time_cells(date, jour, plages)
+ _oob_week_extras(soldes_ctx["result"], plages)
+ soldes_html
)
@ -403,6 +408,7 @@ def semaine(request: Request, year: int, week: int):
return RedirectResponse("/login", status_code=302)
h_jour = heures_jour_min()
plages = plages_config()
conges = load_conges(user_id)
soldes_ctx = _build_soldes_ctx(year, week, conges, h_jour, user_id)
@ -414,7 +420,7 @@ def semaine(request: Request, year: int, week: int):
"aprem_entree": None, "aprem_sortie": None})
for d in dates
]
result = compute_week(pointages, conges, h_jour)
result = compute_week(pointages, conges, h_jour, plages)
prev_d = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date() - timedelta(weeks=1)
next_d = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date() + timedelta(weeks=1)
@ -425,6 +431,7 @@ def semaine(request: Request, year: int, week: int):
"year": year, "week": week,
"result": result,
**soldes_ctx,
"plages": plages,
"prev_year": prev_d.isocalendar().year, "prev_week": prev_d.isocalendar().week,
"next_year": next_d.isocalendar().year, "next_week": next_d.isocalendar().week,
"jours_fr": JOURS_FR,
@ -527,6 +534,9 @@ def settings_page(request: Request):
"request": request,
"current_user": user_id,
"ntfy_topic": notif.get("ntfy_topic") or "",
"ntfy_server": notif.get("ntfy_server") or "https://ntfy.sh",
"rappel_debut_h": notif.get("rappel_debut_h", 7),
"rappel_fin_h": notif.get("rappel_fin_h", 20),
"arrivee_url": f"{base_url}/presence/{notif['token']}/arrivee",
"depart_url": f"{base_url}/presence/{notif['token']}/depart",
"saved": request.query_params.get("saved") == "1",
@ -534,13 +544,22 @@ def settings_page(request: Request):
@app.post("/settings", response_class=HTMLResponse)
def settings_save(request: Request, ntfy_topic: str = Form("")):
def settings_save(
request: Request, ntfy_topic: str = Form(""), ntfy_server: str = Form(""),
rappel_debut_h: int = Form(7), rappel_fin_h: int = Form(20),
):
user_id = _get_user(request)
if not user_id:
return RedirectResponse("/login", status_code=302)
logger.info("réglages notif mis à jour: user_id=%r ntfy_topic=%r", user_id, ntfy_topic)
save_notif_config(user_id, ntfy_topic)
rappel_debut_h = max(0, min(23, rappel_debut_h))
rappel_fin_h = max(rappel_debut_h + 1, min(24, rappel_fin_h))
logger.info(
"réglages notif mis à jour: user_id=%r ntfy_topic=%r ntfy_server=%r plage=%sh-%sh",
user_id, ntfy_topic, ntfy_server, rappel_debut_h, rappel_fin_h,
)
save_notif_config(user_id, ntfy_topic, ntfy_server, rappel_debut_h, rappel_fin_h)
return RedirectResponse("/settings?saved=1", status_code=303)
@ -566,15 +585,3 @@ def presence_depart(token: str):
return {"ok": True}
@app.get("/classement", response_class=HTMLResponse)
def classement_page(request: Request):
user_id = _get_user(request)
if not user_id:
return RedirectResponse("/login", status_code=302)
ranking = compute_classement()
return templates.TemplateResponse("classement.html", {
"request": request,
"ranking": ranking,
"current_user": user_id,
})