Skip to content

7. Les états

This content is not available in your language yet.

Un emprunt est sorti, puis rendu, ou perdu. C’est une machine à états, et on ne l’écrit pas à la main.

Déclarer le cycle

models/workflow.py :

"""``library_loan`` lifecycle: out, then returned or lost."""
from __future__ import annotations
from typing import TYPE_CHECKING
from kewos.views import register_status_colors
from kewos.workflow import StateMachine, has_state_machine, register_state_machine
if TYPE_CHECKING:
from kewos.orm.registry import Registry
_STATE_COLORS = {"out": "info", "returned": "success", "lost": "destructive"}
def _build() -> StateMachine:
machine = StateMachine(initial="out", states=("out", "returned", "lost"))
machine.add_transition(trigger="check_in", source="out", dest="returned")
machine.add_transition(trigger="declare_lost", source="out", dest="lost")
return machine
def register_loan_workflow(_registry: Registry) -> None:
"""Bind the loan state machine and its status colours (idempotent)."""
if not has_state_machine("library_loan"):
register_state_machine("library_loan", _build())
register_status_colors("library_loan", "state", _STATE_COLORS)

Et on l’appelle depuis models/__init__.py, après les modèles.

Ce que ça produit

Le formulaire d'emprunt : la piste d'états, et les deux boutons de transition

Trois choses sont apparues, sans une ligne d’interface :

  • La piste d’états, avec l’étape courante mise en avant et les suivantes en attente.
  • Les boutons de transition, « Check in » et « Declare lost », qui n’apparaissent que lorsque la transition est possible depuis l’état courant. Depuis returned, il n’y en a aucun.
  • Les couleurs sémantiques : un emprunt sorti est neutre, un emprunt rendu est vert, un emprunt perdu est rouge. Elles viennent des jetons du thème, jamais d’une couleur en dur, et elles se retrouvent dans la liste et dans le kanban.

L’écueil que j’ai rencontré en l’écrivant

Ma première tentative affichait workflow.check_in sur le bouton : une clé de traduction non résolue, servie telle quelle à l’utilisateur. J’avais écrit la clé sous action.check_in.

La bonne forme est workflow.<trigger>.

{
"workflow.check_in": "Check in",
"workflow.declare_lost": "Declare lost"
}

Aucune erreur n’est levée dans ce cas : une clé absente s’affiche brute. C’est exactement le genre de défaut qui passe une revue de code et se voit en démonstration.

Garder une transition

Une transition peut refuser de s’exécuter.

machine.add_transition(
trigger="check_in",
source="out",
dest="returned",
guard=lambda instance: instance.taken_on is not None,
)

Le garde renvoie faux, la transition est refusée, l’interface affiche l’erreur. On ne peut pas contourner en appelant l’API : la transition passe par le même chemin.

Demander des informations au moment de la transition

form= déclare les champs que l’utilisateur doit renseigner avant que la transition ne parte.

machine.add_transition(
trigger="declare_lost",
source="out",
dest="lost",
form=("lost_note",),
)

Le moteur affiche une boîte de dialogue avec ces champs, et l’effet peut alors s’appuyer dessus. C’est ainsi qu’on garantit qu’un emprunt déclaré perdu porte toujours une explication.

Agir au passage

effect= reçoit la session, et s’exécute dans la transaction de la transition. Ce qui est écrit là est annulé avec elle si quelque chose échoue plus loin.

def _mark_returned(session, _classes, instance) -> None:
"""Stamp the return date and free the copy, atomically with the transition."""
instance.returned_on = date.today()
machine.add_transition(
trigger="check_in", source="out", dest="returned", effect=_mark_returned
)

C’est l’endroit pour tracer, pour émettre un événement, pour mettre à jour un compteur. Ce n’est pas l’endroit pour appeler un service externe : cela tiendrait la transaction ouverte pendant un appel réseau. Émettez un événement, et laissez un traitement de fond s’en charger.

Un état définitif

Un modèle peut déclarer qu’à partir d’un certain état, plus rien ne se modifie :

registry.register_model(
"library_loan",
module="library",
fields=[...],
immutable_after_state=("returned", "lost"),
)

Une correction se fait alors par contre-écriture, pas par modification. C’est la même règle que pour une écriture comptable, et pour la même raison : l’historique doit rester défendable.

Et ensuite

L’intelligence artificielle.