ADR-081 : Horodatages gérés par le framework (make:crud)¶
Statut¶
Acceptée.
Décision d'architecture ; relève du mainteneur.
Date¶
2026-07-13
Contexte¶
Retour terrain RéférenCiel 021 (F56). Pour une entité avec options.timestamps: true, le CRUD généré traitait created_at et updated_at comme deux champs ordinaires :
- exposés dans le formulaire (
DateTimeField(required=True)et champsdatetime-localdansform.html), donc à saisir à la main, ce qui n'a pas de sens ; - attendus dans l'
INSERT/UPDATEdu modèle viadata["created_at"], alors que le formulaire ne les fournit pas ; - déclarés
DATETIME NOT NULLsans valeur par défaut ni source de valeur.
Sur une application réelle, tous les CRUD portent le défaut : le corriger a demandé une reprise transverse (formulaires, modèles, vues).
Le normaliseur canonique fabrique déjà created_at/updated_at depuis options.timestamps (_system_datetime_field), mais sans marquer leur nature : rien ne distingue un horodatage géré d'un datetime saisi par l'utilisateur.
Décision¶
Un horodatage issu de options.timestamps est un champ géré par le framework, jamais saisi. La valeur est posée par le modèle généré en Python, pas par la base.
Marqueur managed¶
Le normaliseur pose une clé managed sur ces champs :
created_at:"managed": "timestamp_created";updated_at:"managed": "timestamp_updated".
managed entre au contrat de champ (ALLOWED_FIELD_KEYS), avec une valeur validée (ALLOWED_MANAGED_VALUES). C'est un marqueur interne : les auteurs d'entités ne le posent pas, il naît de options.timestamps. La clé générique se prête à d'autres champs gérés plus tard (par exemple soft_delete).
Comportement du générateur CRUD¶
- Formulaire (classe
Formetform.html) : les champs gérés sont exclus, comme un champ auto-généré (slug avecsource). L'utilisateur ne saisit pas d'horodatage. - Modèle : l'
INSERTposecreated_atetupdated_atàdatetime.now(timezone.utc); l'UPDATEposeupdated_atàdatetime.now(timezone.utc)et exclutcreated_at(stable à l'édition, comme le slug). Aucune de ces colonnes n'est lue depuisdata. - Toutes les vues générées (formulaire, liste, fiche détail) et l'export CSV et le tri : les horodatages gérés sont exclus. Ce sont des métadonnées système, consultables en base ; les afficher alourdissait la liste d'en-têtes techniques (
Created at/Updated at) et donnait un rendu de développeur plutôt qu'une UX utilisateur (retour terrain).
Pas de valeur par défaut SQL¶
Le DDL reste DATETIME NOT NULL, sans DEFAULT CURRENT_TIMESTAMP ni ON UPDATE. Python (le modèle) est la seule autorité sur la valeur, cohérent avec le choix de forge-mvc-sessions-db (pas de double horloge entre la base et le code). Une seule façon officielle d'horodater (principe 11).
Conséquences¶
forge make:crudsur une entité horodatée produit un formulaire sans champ d'horodatage, un modèle qui pose lui-mêmecreated_at/updated_at, et un DDL sans défaut : plus de saisie manuelle, plus deKeyErrorruntime surdata["created_at"].- Surface : ajout d'une clé
managedau contrat de champ (additive). Le générateur ajoute l'importdatetimeseulement quand un horodatage géré est présent (pas d'import inutile). - Le choix « Python seule autorité » est assumé et cohérent avec sessions-db ; il écarte les défauts SQL proposés par le retour terrain.
deleted_at(options.soft_delete) n'est pas couvert par cette décision : hors périmètre F56, à traiter séparément si le besoin se confirme (règle B, révéler avant d'élargir).
Alternatives écartées¶
- Défauts SQL (
DEFAULT CURRENT_TIMESTAMP+ON UPDATE CURRENT_TIMESTAMP).
Proposée par le retour terrain. Écartée : introduit une double horloge (base et code) et contredit la convention établie pour sessions-db. La portabilité entre backends (ADR-054) est aussi plus simple si Python fournit la valeur. - Conserver les horodatages en lecture seule dans la liste et la fiche détail.
Retenue au départ, puis écartée sur retour terrain : les colonnesCreated at/Updated atalourdissaient la liste et donnaient un rendu technique. Les horodatages gérés sont désormais absents de toutes les vues générées ; ils restent en base. - Étendre
_is_generated(slug) au lieu d'un marqueur dédié.
Écartée : mélange deux notions (valeur calculée depuis une source vs horodatage système) ; un marqueurmanageddistinct est plus lisible et extensible.
Référence¶
- Charte :
CHARTE_DOC.md(principe 3, refuser la magie cachée, le SQL et le code restent visibles ; principe 11, une seule façon officielle). - ADR-054 : cœur agnostique BDD (portabilité des valeurs).
- ADR-017 : champ auto-généré stable à l'édition (patron réutilisé).
- ADR-070 : moteur d'entités.
- Retour terrain RéférenCiel 021 (F56).