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)

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.
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> = ? LIMIT 1 (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.