Aller au contenu

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 :

  • entity en PascalCase ;
  • slug en minuscules, chiffres et tirets, commençant par une lettre ;
  • label et plural_label non vides ;
  • list_fields et form_fields non vides, chaque nom en snake_case, sans doublon ;
  • table et pk en snake_case ; order_by, s'il est fourni, en snake_case ;
  • created_at et updated_at absents de form_fields quand timestamps vaut True : 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.