Aller au contenu

Administration

L’administration est une sorte de tour de contrôle évoluée, un back office sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé. Elle joue avec les clés primaires, étrangères, les champs et types de champs par introspection, et présente tout ce qu’il faut pour avoir du Create-Read-Update-Delete, c’est-à-dire tout ce qu’il faut pour ajouter, lister, modifier ou supprimer des informations - comme beaucoup d’applications.

Son problème est qu’elle présente une courbe d’apprentissage asymptotique : il est très facile d’arriver rapidement à un bon résultat, au travers d’un périmètre de configuration relativement restreint, mais quoi que vous fassiez, il y a un moment où cette courbe de paramétrage sera tellement ardue que vous aurez plus facile à développer ce que vous souhaitez ajouter en utilisant les autres concepts de Django.

Cette interface doit donc rester dans les mains d’administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement : il n’est pas question de donner des droits aux utilisateurs finaux (même si c’est extrêment tentant durant les premiers tours de roues). Indépendamment de la manière dont vous allez l’utiliser et la configurer, vous finirez par devoir développer une “vraie” application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement.

Une bonne idée consiste à développer l’administration dans un premier temps, en gardant en tête qu’il sera nécessaire de développer des concepts spécifiques. Dans cet objectif, l’administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux.

C’est aussi un excellent outil de prototypage et de preuve de concept.

Elle se base sur plusieurs couches que l’on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez):

  • Le modèle de données,
  • Les validateurs,
  • Les formulaires,
  • Les widgets.

Le modèle de données

Comme expliqué ci-dessus, le modèle de données est constité d’un ensemble de champs typés et de relations. L’administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions.

Si vous vous rappelez de l’application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante :

from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
[...]
]

Cette URL signifie que la partie admin est déjà active et accessible à l’URL <mon_site>/admin. C’est le seul prérequis pour cette partie.

Chaque application nouvellement créée contient par défaut un fichier admin.py, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables. Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:

gwift/wish/models.py
from django.db import models
class WishList(models.Model):
name = models.CharField(max_length=255)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)

Nous pouvons facilement arriver au résultat suivant, en ajoutant quelques lignes de configuration dans ce fichier admin.py :

from django.contrib import admin
"""Nous importons les modèles que nous souhaitons
gérer dans l’admin"""
from .models import Item, WishList
"""Et nous les déclarons comme gérables dans la partie administration."""
admin.site.register(Item)
admin.site.register(WishList)

Rappelez-vous cependant que Django (et Python) suivent clairement le paradigme Orienté-Objet ; toute la force (et la configuration) de cette section d’administration vont être réalisées sur des classes et structures, afin de décrire le comportement auquel nous souhatons arriver. Prenez directement le pli de créer une classe par modèle que vous souhaitez administrer : le nombre de lignes de code ne changera pas grand chose, et vous y gagnerez directement par la suite :

from django.contrib import admin
"""Nos imports sont identiques à la version précédente"""
from .models import Item, WishList
"""Nous définissons une nouvelle classe.
Cette nouvelle classe va servir de point d'entrée pour notre configuration."""
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
pass
@admin.register(WishList)
class WishListAdmin(admin.ModelAdmin):
pass

L’accès à l’administration

Il nous reste une seule étape à réaliser: créer un nouvel utilisateur. Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d’un super-utilisateur, que nous pouvons créer grâce à la commande poetry run python manage.py createsuperuser.

Fenêtre de terminal
$ poetry run python manage.py createsuperuser
Username (leave blank to use 'fred'): fred
Email address: fred@root.org
Password: ******
Password (again): ******
Superuser created successfully.

Ceci nous permet déjà d’ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs.

Quelques conseils de base

str()

Surchargez systématiquement la méthode str(self) pour chaque classe que vous aurez définie dans le modèle. Cela permettra de construire une représentation textuelle pour chaque instance de votre classe. Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l’on manipule. En plus, cette méthode est également appelée lorsque l’administration historisera une action (et comme cette étape sera inaltérable, autant qu’elle soit fixée dans le début).

get_absolute_url()

  • La méthode get_absolute_url(self) retourne l’URL à laquelle nous pouvons accéder pour obtenir les détails d’une instance. Par exemple:
def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])

Les attributs Meta

class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'

Titre de la section d’administration

Le titre peut être modifié de deux manières :

  • Soit en modifiant le template de l’administration - ce qui est franchement barbare et nécessite beaucoup trop de travail que pour être réellement pertinent,
  • Soit en ajoutant l’assignation suivante dans le fichier urls.py :
admin.site.site_header = "Gwift Secret Area"

Prefetch

Ce point en particulier sera abordé dans la section <<Requêtes N+1>> :

Un des problèmes de l’admin est que si on fait des requêtes imbriquées, on va flinguer l’application et le chargement de la page. La solution consiste à utiliser la propriété list_select_related de la classe d’Admin, afin d’appliquer une jointure par défaut et et gagner en performances.

L’affichage

Comme l’interface d’administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, nous trouvons par défaut la possibilité de :

  • Créer de nouveaux éléments
  • Lister les éléments existants
  • Modifier des éléments existants
  • Supprimer un élément en particulier.

Les affichages sont donc de deux types :

  • En liste, pour afficher et filtrer sur certains éléments
  • Les détails d’un élément.

Affichage des détails d’un élément

Le fonctionnement habituel d’une application est d’afficher un ensemble d’éléments, puis d’autoriser la sélection d’un élément en particulier pour agir dessus, soit en l’affichant, soit en le modifiant, soit en le supprimant. L’administration étant orientée sur la gestion des données, l’affichage n’est pas géré ; il est donc possible de supprimer un élement ou de le modifier. Cette section se concentre sur la modification.

. Un formulaire de modification par défaut pour un élément dans l’administration

… montrer le regroup_by et les collapse.

Affichage en liste

Par défaut, l’affichage en liste se base sur la propriété __str__() de chaque élément, afin d’en proposer une représentation textuelle. Cette représentation est cependant totalement impersonnelle, n’a aucun intérêt et ce sera sans doute la première chose que vous modifierez dans votre projet tout neuf.

Pour ce type d’affichage, le plus simple consiste à jouer sur la propriété list_display, afin de personnaliser les informations que nous visualiserons dans le tableau affiché :

[...]
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin): <2>
list_display = ("__str__", "wishlist_name")
list_filter = ("wishlist_name",)
[...]

Filtres de listes

Chaque liste permet de spécifier des filtres spécifiques; ceux-ci peuvent être :

  • Appliqués à la liste (list_filter)
  • Horizontaux (filter_horizontal)
  • Verticaux (filter_vertical)
  • Temporels (date_hierarchy)

Appliqués à la liste

La version la plus simple des filtres consiste à définir une propriété list_filter au niveau de la classe ModelAdmin :

[...]
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
list_display = ("__str__", "wishlist_name")
list_filter = ("wishlist__name",) <1>
[...]

<1> list_filter est un tuple, qui prend autant de valeurs que nous souhaitons définir de filtres différents. Nous utilisons ici un lookup de type __ pour définir précisément sur quel champ nous souhaitons pouvoir appliquer un filtre.

Temporels

Les filtres temporels ajoutent la possibilité de naviguer chronologiquement parmi des données. Il sera nécessaire de leur donner des informations horodatées, comme par exemple une date de création ou de modification :

[...]
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
list_display = ("__str__", "wishlist_name")
list_filter = ("wishlist__name",)
date_hierarchy = "modified_at" <1>
[...]

<1> Cette propriété-ci prend comme valeur le nom d’une propriété typé comme étant une date ou un moment. Il ne s’agit donc pas d’un tuple ou d’une liste !

Filtres horizontaux

Les filtres horizontaux sont représentés par des clés de type ManyToMany. Le prérequis sera donc de disposer d’un champ de ce type-là.

Filtres verticaux

Idem pour les filtres verticaux, qui se basent également des champs de type ManyToMany, mais qui représenteront l’information verticalement.

Permissions

On l’a dit plus haut, il vaut mieux éviter de proposer un accès à l’administration à vos utilisateurs. Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression.

Cela se joue au niveau du ModelAdmin, en implémentant les méthodes suivantes:

def has_add_permission(self, request):
return True
def has_delete_permission(self, request):
return True
def has_change_permission(self, request):
return True

On peut accéder aux informations de l’utilisateur actuellement connecté au travers de l’objet request.user.

Relations

Relations 1-N

Les relations 1-n sont implémentées au travers de formsets (que l’on a normalement déjà décrits plus haut). L’administration permet de les définir d’une manière extrêmement simple, grâce à quelques propriétés.

L’implémentation consiste tout d’abord à définir le comportement du type d’objet référencé (la relation -N), puis à inclure cette définition au niveau du type d’objet référençant (la relation 1-).

class WishInline(TabularInline):
model = Wish
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...

Et voilà : l’administration d’une liste de souhaits (Wishlist) pourra directement gérer des relations multiples vers des souhaits.

Autocomplétion

TBC

Actions sur des sélections

Les actions permettent de partir d’une liste d’éléments, et autorisent un utilisateur à appliquer une action sur une sélection d’éléments. Par défaut, il existe déjà une action de suppression.

Les paramètres d’entrée sont :

  • L’instance de classe
  • La requête entrante
  • Le queryset correspondant à la sélection.
def double_quantity(self, request, queryset):
for obj in queryset.all():
obj.field += 1
obj.save()
double_quantity.short_description = "Doubler la quantité des souhaits."

Et pour informer l’utilisateur de ce qui a été réalisé, on peut aussi lui passer un petit message en utilisant le framework de messaging :

if rows_updated = 0:
self.message_user(request, "Aucun élément n'a été impacté.")
else:
self.message_user(request, "{} élément(s) mis à jour".format(rows_updated))

Documentation

Nous l’avons dit plus haut, l’administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données. Pour cela, il suffit de suivre les bonnes pratiques, puis d’activer la documentation à partir des URLs:

Conclusions

Les différents filtres disponibles permettent de créer rapidement des facettes, telles que nous pouvons les trouver sur des moteurs de recherche récents. Ces filtres restent limités, mais peuvent être mis en place très rapidement, et offrent ainsi une interface facile à mettre en place, tout en offrant quelques fonctionnalités de base personnalisées, sans avoir à dégainer un outil d’accès à des données relationelles comme PgAdmin ou MySQLAdmin).