Astreinte : une ligne en cours de saisie survit à l'auto-enregistrement
Un début sans fin (ou l'inverse) était effacé par la sauvegarde
automatique pendant la frappe. La ligne est désormais conservée,
réaffichée, et ignorée des calculs (décompte, repos) tant que la paire
n'est pas complète. Le bloc dépliant reste ouvert pendant tout le cycle
HTMX (restauration aussi après le settle).
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@ -873,14 +873,17 @@ def save_pointage(
|
||||
p_by_date = {p["date"]: p for p in raw}
|
||||
# Les événements d'astreinte ne sont affichés (donc postés) que les jours
|
||||
# qualifiés (astreinte ou lendemain) ; ailleurs on conserve ceux déjà saisis
|
||||
# au lieu de les écraser à vide. Les paires début/fin invalides sont ignorées.
|
||||
# au lieu de les écraser à vide. Une ligne en cours de saisie (début sans
|
||||
# fin, ou l'inverse) est conservée telle quelle : l'auto-enregistrement ne
|
||||
# doit pas effacer une saisie à moitié tapée — les calculs (décompte, repos)
|
||||
# ignorent les événements incomplets tant que la paire n'est pas complète.
|
||||
if jour_astreinte_qualifie(date, set(load_astreintes_effectives(user_id))):
|
||||
events = []
|
||||
# Les descriptions sont optionnelles : sans elles (POST programmatique),
|
||||
# zip s'arrêterait à la liste la plus courte et perdrait les événements.
|
||||
for i, (deb, fin) in enumerate(zip(astreinte_debut, astreinte_fin)):
|
||||
deb, fin = clean(deb), clean(fin)
|
||||
if deb and fin:
|
||||
if deb or fin:
|
||||
desc = astreinte_desc[i] if i < len(astreinte_desc) else ""
|
||||
events.append({"debut": deb, "fin": fin,
|
||||
"desc": (desc or "").strip()[:200] or None})
|
||||
|
||||
@ -496,6 +496,11 @@ function asRestore() {
|
||||
}
|
||||
asRestore();
|
||||
document.addEventListener('htmx:afterSwap', asRestore);
|
||||
// Le settle HTMX (transition après swap) restaure les classes du snapshot pris
|
||||
// à l'insertion : une classe ajoutée entre insertion et settle — comme « open »
|
||||
// par asRestore sur afterSwap — est écrasée. On restaure donc aussi après le
|
||||
// settle, dernier moment du cycle.
|
||||
document.addEventListener('htmx:afterSettle', asRestore);
|
||||
|
||||
// Ajoute une ligne d'intervention vierge dans le jour ciblé
|
||||
window.asAddEvent = function (date) {
|
||||
|
||||
83
app/tests/test_autosave_astreinte.py
Normal file
83
app/tests/test_autosave_astreinte.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Auto-enregistrement des interventions d'astreinte : une ligne en cours de
|
||||
saisie (début sans fin, ou l'inverse) survit au cycle save/re-rendu — le calcul
|
||||
l'ignore tant que la paire est incomplète, mais la valeur saisie reste stockée
|
||||
et affichée, et le bloc dépliant ne se referme pas (restauration après le
|
||||
settle HTMX, cf. semaine.html)."""
|
||||
import sys
|
||||
|
||||
import models # noqa: F401 (DATA_DIR temporaire posé par conftest)
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
import main
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
client = TestClient(main.app)
|
||||
|
||||
|
||||
def _login(uid: str) -> dict:
|
||||
import bcrypt
|
||||
models.save_auth(uid, email=f"{uid}@x.fr",
|
||||
password_hash=bcrypt.hashpw(b"x" * 12, bcrypt.gensalt()).decode())
|
||||
r = client.post("/login/password",
|
||||
data={"email": f"{uid}@x.fr", "password": "x" * 12},
|
||||
follow_redirects=False)
|
||||
assert r.status_code == 302, r.text
|
||||
return {"user_id": r.cookies.get("user_id")}
|
||||
|
||||
|
||||
def _events(uid: str, date: str):
|
||||
from datetime import date as d_
|
||||
d = d_.fromisoformat(date)
|
||||
iso = d.isocalendar()
|
||||
row = next((p for p in models.load_week_pointages(iso.year, iso.week, uid)
|
||||
if p["date"] == date), {})
|
||||
return row.get("astreinte_events")
|
||||
|
||||
|
||||
def test_ligne_partielle_conservee_entre_deux_saves():
|
||||
uid = "u-as-partiel"
|
||||
cookies = _login(uid)
|
||||
models.toggle_astreinte("2026-07-14", uid) # mardi AS
|
||||
|
||||
# 1er save auto : début saisi, fin pas encore — la ligne doit survivre
|
||||
r = client.post("/pointage/2026-07-14", cookies=cookies,
|
||||
data={"astreinte_debut": ["22:00"], "astreinte_fin": [""],
|
||||
"astreinte_desc": [""]}, follow_redirects=False)
|
||||
assert r.status_code == 200, r.text
|
||||
assert _events(uid, "2026-07-14") == [{"debut": "22:00", "fin": None, "desc": None}]
|
||||
# La valeur saisie est réaffichée dans le bloc re-rendu
|
||||
assert 'value="22:00"' in r.text
|
||||
|
||||
# 2e save : la fin complète la paire — l'événement devient calculable
|
||||
r = client.post("/pointage/2026-07-14", cookies=cookies,
|
||||
data={"astreinte_debut": ["22:00"], "astreinte_fin": ["23:30"],
|
||||
"astreinte_desc": [""]}, follow_redirects=False)
|
||||
assert r.status_code == 200, r.text
|
||||
assert _events(uid, "2026-07-14") == [{"debut": "22:00", "fin": "23:30", "desc": None}]
|
||||
assert "astreinte : 1h30" in r.text # décompte arrondi au quart d'heure
|
||||
|
||||
|
||||
def test_event_partiel_ignore_par_les_calculs():
|
||||
"""Un début sans fin ne déclenche ni décompte ni repos de 11h."""
|
||||
uid = "u-as-partiel-calc"
|
||||
cookies = _login(uid)
|
||||
models.toggle_astreinte("2026-07-14", uid)
|
||||
|
||||
client.post("/pointage/2026-07-14", cookies=cookies,
|
||||
data={"astreinte_debut": ["23:00"], "astreinte_fin": [""],
|
||||
"astreinte_desc": [""]}, follow_redirects=False)
|
||||
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
|
||||
assert "astreinte :" not in r.text # rien de décompté
|
||||
assert "retour ≥" not in r.text # pas de retour décalé
|
||||
|
||||
|
||||
def test_ligne_entierement_vide_non_conservee():
|
||||
uid = "u-as-vide"
|
||||
cookies = _login(uid)
|
||||
models.toggle_astreinte("2026-07-14", uid)
|
||||
|
||||
client.post("/pointage/2026-07-14", cookies=cookies,
|
||||
data={"astreinte_debut": ["", "22:00"], "astreinte_fin": ["", "23:00"],
|
||||
"astreinte_desc": ["", ""]}, follow_redirects=False)
|
||||
# La ligne vide n'est pas stockée, la ligne complète l'est
|
||||
assert _events(uid, "2026-07-14") == [{"debut": "22:00", "fin": "23:00", "desc": None}]
|
||||
Reference in New Issue
Block a user