Contrat d'une ressource admin¶
Une ressource admin décrit comment une entité d'un projet Forge est administrée : quelle entité, sous quel slug d'URL, avec quels libellés, et quels champs sont montrés en liste et éditables en formulaire.
Statut : châssis en cours
Cette page documente le contrat (AdminResource) et le registre (AdminRegistry) livrés par le ticket ADMIN-RESOURCE-CONTRACT-001.
Les vues qui consomment ce contrat viendront par les tickets suivants.
Principe¶
Le contrat est une déclaration Python, pas un nouveau fichier de configuration.
L'entité reste décrite par son contrat JSON ; la ressource admin est une couche de présentation au-dessus, écrite en code explicite et modifiable.
La ressource valide sa propre forme à la construction.
Elle ne lit ni le contrat d'entité ni la base : le rapprochement avec l'entité réelle (existence de l'entité, des champs) relèvera d'une vérification ultérieure (forge admin:doctor).
AdminResource¶
AdminResource est une dataclass immuable (frozen).
| Attribut | Type | Rôle |
|---|---|---|
entity |
str |
nom canonique de l'entité (PascalCase, ex. Article) |
slug |
str |
segment d'URL sous /admin/ (minuscules, tirets, ex. articles) |
label |
str |
libellé singulier |
plural_label |
str |
libellé pluriel |
list_fields |
tuple[str, ...] |
champs affichés en liste (au moins un) |
form_fields |
tuple[str, ...] |
champs éditables en formulaire (au moins un) |
table |
str |
table physique (snake_case) ; non dérivable du nom d'entité |
order_by |
str |
colonne de tri par défaut (vide → premier list_fields) |
pk |
str |
colonne de clé primaire pour la vue détail (défaut id) |
timestamps |
bool |
la table porte created_at et updated_at gérés (défaut False) |
Règles de validation, sinon AdminResourceError :
entityen PascalCase ;slugen minuscules, chiffres et tirets, commençant par une lettre ;labeletplural_labelnon vides ;list_fieldsetform_fieldsnon vides, chaque nom en snake_case, sans doublon ;tableetpken snake_case ;order_by, s'il est fourni, en snake_case ;created_atetupdated_atabsents deform_fieldsquandtimestampsvautTrue: ce sont des champs gérés, que le framework pose lui-même.
Horodatages gérés¶
Une entité engendrée avec options.timestamps porte created_at et updated_at en NOT NULL sans défaut SQL : l'autorité sur la valeur est Python, jamais le moteur (ADR-081).
Déclarez-le sur la ressource, et le back-office les posera à la création comme à la modification.
AdminResource(
entity="Article", slug="articles", label="Article", plural_label="Articles",
table="articles", list_fields=("titre",), form_fields=("titre",),
timestamps=True,
)
Sans cette déclaration, la création échoue
Le back-office ignorait ce mécanisme jusqu'à ADMIN-MANAGED-TIMESTAMPS-001.
Créer un enregistrement levait alors une violation de contrainte sur les quatre backends,
et modifier laissait updated_at figé, ce qui est plus discret et plus durable.
Le défaut par défaut reste False : une ressource déclarée avant ce ticket ne change pas de comportement.
from forge_mvc_admin import AdminResource
article = AdminResource(
entity="Article",
slug="articles",
label="Article",
plural_label="Articles",
list_fields=("title", "published_at"),
form_fields=("title", "body"),
table="articles",
)
La liste (GET /admin/<slug>) construit un SELECT contraint à partir de ce contrat : seuls la table, les colonnes list_fields et la colonne de tri entrent dans la requête (identifiants validés en liste blanche), la pagination passe par des paramètres.
Aucune introspection, pas d'ORM.
La fiche détail (GET /admin/<slug>/<id>) lit une ligne par sa clé primaire (WHERE <pk> = ?, valeur paramétrée) et affiche pk puis list_fields puis form_fields (colonnes uniques).
La création (GET/POST /admin/<slug>/new) affiche un formulaire des form_fields puis insère une ligne : seules ces colonnes sont écrites (liste blanche), les valeurs sont paramétrées, une valeur vide devient NULL, et le jeton CSRF est vérifié.
En cas de succès, redirection vers la fiche créée.
À ce stade il n'y a pas de validation par champ (pas de contrat au runtime) : les contraintes restent celles de la base.
L'édition (GET/POST /admin/<slug>/<id>/edit) pré-remplit le formulaire depuis la ligne existante puis fait un UPDATE … WHERE <pk> = ? (mêmes garanties que la création : colonnes en liste blanche, valeurs paramétrées, CSRF).
La fiche détail propose un lien « Modifier ».
La suppression est contrôlée : GET /admin/<slug>/<id>/delete affiche une page de confirmation (lecture seule), et seule la soumission POST /admin/<slug>/<id>/delete exécute DELETE … WHERE <pk> = ? (jamais en GET, CSRF vérifié), puis redirige vers la liste.
La fiche détail propose un lien « Supprimer »
vers la page de confirmation.
AdminRegistry¶
Le registre rassemble les ressources d'un projet.
Il est explicite : l'application enregistre ses ressources à la main, il n'y a aucune découverte automatique.
Forge Core n'enregistre rien ; le registre par défaut est vide tant que l'application ne l'a pas peuplé.
| Méthode | Rôle |
|---|---|
register(resource) |
enregistre une ressource ; lève si le slug est déjà pris |
get(slug) |
retourne la ressource d'un slug ; lève si inconnue |
all() |
retourne les ressources dans leur ordre d'enregistrement |
slug in registry |
teste la présence d'un slug |
len(registry) |
nombre de ressources |
L'ordre d'enregistrement est préservé : il pilotera l'ordre de la navigation.
from forge_mvc_admin import registry, AdminResource
registry.register(article)
# Ailleurs, l'application peut aussi créer son propre registre :
from forge_mvc_admin import AdminRegistry
mon_registre = AdminRegistry()
Limites assumées¶
- Le contrat ne vérifie pas que l'entité ou les champs existent réellement.
- Aucun rendu, aucune route, aucune action ne sont fournis par ce ticket.
- Les types de champs et les widgets ne sont pas encore pris en compte.