Interface admin Django

De base, Django vous simplifie la vie et vous propose de réserver une partie de votre projet pour consulter et éditer les entrées de données. Il s'agit d'une application implémentée de base dans votre projet que vous pouvez désactiver bien évidemment quand bon vous le semble.

L'interface d'administration est accessible en pointant vers /admin :

Quel Username attend Django? Si vous n'avez pas encore crée d'utilisateur, il y a obligatoirement présence d'un superutilisateur dans votre projet lorsque vous lancé la commande suivante:

python manage.py syncdb

(Voir le chapitre sur l'ORM Django )


Une interface en français

Par défaut la langue est en anglais, mais vous pouvez définir le français par defaut en changer la variable LANGUAGE_CODE du fichier settings.py

LANGUAGE_CODE = 'fr-fr'

Consulter l'interface admin

Entrez dans l'interface admin et vous devriez voir cela:

La première chose que l'on remarque c'est que notre modèle Product n'est pas présent. Pour l'ajouter il vous faudra éditer le fichier admin.py de votre application:

backoffice/admin.py

from django.contrib import admin
from models import *

# Register your models here.

admin.site.register(Product)

Puis réactualisez votre interface d'administration:

Créer une entrée avec l'interface d'administration

Cliquons sur Product:

On remarque la présence d'un bouton Add product, ce qui permet d'ajouter une entrée dans la base pour ce modèle. Cliquons dessus

Remplissons les champs que nous avons définis dans notre modèle puis cliquons sur save:

Voila nous avons crée un produit en quelques clics sans créer aucune interface.

Modifier une entrée dans l'interface administration Django

Vous pouvez bien évidemment modifier l'item que vous voulez. Cliquons sur une entrée:

Modifions le code du produit puis validons la sauvegarde en restant sur la fiche produit "Save and continue editing". Ensuite cliquez sur le bouton history

On y voit un historique complet sur les modifications de l'entrée.

CRUD

L'interface d'administration Django est CRUD (Create Read Update et Delete), c'est à dire qu'elle est capable de faire les opérations de bases.

/admin/<app>/                    => liste les modèles de l'application
/admin/<app>/<model>             => liste les objets du modèle
/admin/<app>/<model>/add         => crée un nouveau objet
/admin/<app>/<model>/<id>/       => consulter un objet en particulier
/admin/<app>/<model>/<id>/delete => supprime un objet en particulier

Personnaliser son interface d'administration

Il existe une batterie d'options qui vous permettront de mettre en avant les informations qui vous intéresse.

Par exemple ici nous avons la liste des items Product via deux colonnes: name et code:

backoffice/admin.py

from django.contrib import admin
from models import *

# Register your models here.

class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'code')
    
admin.site.register(Product, ProductAdmin)
admin.site.register(ProductItem)

Les options de ModelAdmin

action une liste d'actions sur la page des listing
action_on_top l'emplacement des actions
action_on_bottom idem
action_selection_counter affiche un compteur à côté des action (True par défaut)
date_hierarchy listing des items avec éclatement des dates
exclude champs à exclure
fields champs à afficher
fieldset créer un fieldset
filter_horizontal filtre horizontal pour le many-to-many
filter_vertical filtre vertical pour le many-to-many
form associer un formulaire
formfield_overrides permet d'écraser des champs
inlines liste d'autre(s) ModelAdmin associé à celui-ci
list_display champs affiché par défaut
list_display_links champs qui pointent vers la fiche de l'item
list_editable champs modifiable depuis la page de listing
list_filter champs que l'on peut filtrer dans la page listing
list_max_show_all nombre d'items affichables lors de l'action "Show All" (200 par défaut)
list_per_page nombre d'items par page
list_select_related Dire à Django d'utiliser selected_related() pour les champs indiqués
ordering trier en fonction des champs indiqués
paginator par défaut django.core.paginator.Paginator est utilisé
prepopulated_fields rempli un champs en même temps qu'un autre (utilisé par exemple par un slug)
preserve_filters conserve les filtres en mémoire
radio_fields Remplace le select par des boutons radio
raw_id_fields Remplace le select par un input texte
readonly_fields Un champ en mode lecture seule
save_as Lors de la sauvegarde d'un objet, au lieu d'une modification il y a création
search_fields Créer un search box dans la page de listing
view_on_site Permet de créer un lien de l'objet vers le front
add_form_template path du template d'ajout
change_form_template path du template de modification
change_list_template path du template de listing
delete_confirmation_template path du template de confirmation de suppression
delete_selected_confirmation_template path du template suppression des éléments sélectionnés
object_history_template path de l'historique de l'objet

Les méthodes de ModelAdmin

save_model(request, obj, form, change) permet de créer des actions pre et post operation
delete_model(request, obj) permet de créer des actions pre et post delete operation
(request, form, formset, change) permet de créer des actions pre et post operation pour le formset
get_ordering(request) permet de créer un ordre d'affichage
get_search_results(request, queryset, search_term) créer la queryset de recherche
save_related(request, form, formsets, change) permet de créer des actions pre / post operation pour les objets associés au parent
get_readonly_fields(request, obejct=None) retourne une liste des champs non éditable
get_prepopulated_fields(request, None) retourne une liste des champs prérempli
get_list_display(request) retourne les champs affichés par défaut
get_list_display_links(request, list_display) retourne les champs qui pointent vers la fiche de l'item
get_fields(request, obj=None) retourne tous les champs
get_fieldsets(request, obj=None) retourne les fieldset
get_list_filter(request) retourne la même séquence que l'attribut list_filter
get_search_fields(request) retourne la même séquence que l'attribut search_fields
get_inline_instances(request, obj=None) retourne la même séquence que l'attirbut inlines
get_urls() retourne l'URL utilisé par le ModelAdmin
get_form(request, obj=None, **kwargs) retourne un ModelForm utilisé par le ModelAdmin
get_formsets(request, obj=None) yield InlineModelAdmins pour la vue change et add
get_formsets_with_inlines(request, obj=None) yield (Formset, InlineModelAdmin) pour la vue change et add
formfield_for_foreignkey(db_field, request, **kwargs) écrase le formfield par défaut pour une clé étrangère
formfield_for_manytomany(db_field, request, **kwargs) écrase le formfield par défaut pour un m2m
formfield_for_choice_field(db_field, request, **kwargs) écrase le formfield par défaut pour les choix multiple
get_changelist(request, **kwargs) retourne la class ChangeList utilisé pour le listing
get_changelist_form(request, **kwargs) retourne une classe ModelForm pour le Formset dans la page de modification
get_changelist_formset(request, **kwargs) retourne une classe ModelFormSet pour la page de modification
has_add_permission(request) retourne True si l'ajout d'un objet est autorisée
has_change_permission(request, obj=None) retourne True si la modification d'un objet est autorisée
has_delete_permission(request, obj=None) retourne True si la supression d'un objet est autorisée
get_queryset(request) retourne un queryset de toutes les instances de modèle qui peuvent être édités
message_user(request, message, level=messages.INFO, extra_tags='', fail_silently=False) envoie un messager à l'utilisateur
get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) retourne le paginator utilisé
response_add(request, obj, post_url_continue=None) Détermine le HttpResponse pour le vue add
response_change(request, obj) détermine le HttpResponse de la vue change
response_delete(request, obj_display) détermine le HttpResponse de la vue delete
get_changeform_initial_data(request) hook pour les données initiales du form change
add_view(request, form_url='', extra_context=None) la vue Django pour la page ADD
change_view(request, object_id, form_url='', extra_context=None) la vue Django pour la page CHANGE
changelist_view(request, extra_context=None) la vue Django pour la page CHANGELIST
delete_view(request, object_id, extra_context=None) la vue Django pour la page DELETE
history_view(request, object_id, extra_context=None) la vue Django pour la page HISTORY

InlineModelAdmin options

Les modèles Inline peuvent récupérer ces options vu précédemment:

form
fieldsets
fields
formfield_overrides
exclude
filter_horizontal
filter_vertical
ordering
prepopulated_fields
get_queryset()
radio_fields
readonly_fields
raw_id_fields
formfield_for_choice_field()
formfield_for_foreignkey()
formfield_for_manytomany()
has_add_permission()
has_change_permission()
has_delete_permission()

Mais possèdent ces options spécifiques:

model le modèle associé
fk_name le nom de la clé étrangère du modèle
formset par défaut BaseInlineFormSet
form le formulaire associé. Il sera passé par inlineformset_factory()
extra le nombre de lignes extra pour créer des nouvelles entrées
max_num le nombre maximum de form à afficher dans le inline
min_num le nombre minimum de form à afficher dans le inline
raw_id_fields remplace le select des clés étrangères par un input
template le template utilisé
verbose_name un nom verbeux pour le modèle (à mettre dans le class Meta)
verbose_name_plural le nom verbeux utilisé au pluriel
can_delete indique si l'objet inline est supprimable
get_formset(request, obj=None, **kwargs) retourne une classe BaseInlineFormSet pour l'utilisation des vues vue ADD/CHANGE.
get_extra(request, obj=None, **kwargs) retourne le nombre d'extra
get_max_num(request, obj=None, **kwargs) retourne le nombre maximum de form
get_min_num(request, obj=None, **kwargs) retourne le nombre minimum de form

Remplacer les templates par défaut

Les templates par défaut se trouvent dans le dossier contrib/admin/templates/admin

Pour écraser ces templates vous devez créer un dossier admin dans le dossier template de votre projet.

Par exemple si vous voulez changer le template qui modifie les objets, vous devez créer le fichier suivant:

template/admin/backoffice/change_list.html

Tous les templates ne sont pas écrasables, voici ceux qui le sont:

app_index.html
change_form.html
change_list.html
delete_confirmation.html
object_history.html

Un site d'administration Django est représenté par une instance de django.contrib.admin.sites.AdminSite Vous pouvez personnaliser votre administration avec les options suivantes:

AdminSite.site_headerTexte renseigné dans la balise h1 en haut de page. Par défaut la valeur est Django Administration
AdminSite.site_titleTexte renseigné à la fin de la balise title de chaque page. Par défaut la valeur est Django site admin
AdminSite.index_titleLe texte renseigné au haut de la page d'index de l'administration. Par défaut: Site administration
index_templatepath vers un template personnalisé qui sera utilisé par la vue principale de l'administration
app_index_templatepath vers un template personnalisé qui sera utilisé par la vue principale de l'application
login_templatepath vers un template pour le login
login_formsous-classe de AuthenticationForm qui sera utilisée par la vue du login d'administration
logout_templatepath vers le template de logout
password_change_templatetemplate pour le changement de mot de passe
password_change_done_templatetemplate pour la confirmation du changement de mot de passe

Exemples

Passons maintenant à la pratique avec les modèles suivants:

backoffice/models.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.db import models
from django.utils import timezone

PRODUCT_STATUS = (
    (0, 'Offline'),
    (1, 'Online'),
    (2, 'Out of stock')              
)

class Product(models.Model):
    """
    Produit : prix, code, etc.
    """
    
    class Meta:
        verbose_name = "Produit"
        
    name          = models.CharField(max_length=100)
    code          = models.CharField(max_length=10, null=True, blank=True, unique=True)
    price_ht      = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire HT")
    price_ttc     = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire TTC")
    status        = models.SmallIntegerField(choices=PRODUCT_STATUS, default=0)
    date_creation =  models.DateTimeField(default=timezone.now(),blank=True, verbose_name="Date création") 
    
    def __unicode__(self):
        return u"{0} [{1}]".format(self.name, self.code)

class ProductItem(models.Model):
    """
    Déclinaison de produit déterminée par des attributs comme la couleur, etc.
    """
    
    class Meta:
        verbose_name = "Déclinaison Produit"
        
    product     = models.ForeignKey('Product', related_name="product_item")
    code        = models.CharField(max_length=10, null=True, blank=True, unique=True)
    code_ean13  = models.CharField(max_length=13)
    attributes  = models.ManyToManyField("ProductAttributeValue", related_name="product_item", null=True, blank=True)
       
    def __unicode__(self):
        return u"{0} [{1}]".format(self.product.name, self.code)
    
class ProductAttribute(models.Model):
    """
    Attributs produit
    """
    
    class Meta:
        verbose_name = "Attribut"
        
    name =  models.CharField(max_length=100)
    
    def __unicode__(self):
        return self.name
    
class ProductAttributeValue(models.Model):
    """
    Valeurs des attributs
    """
    
    class Meta:
        verbose_name = "Valeur attribut"
        ordering = ['position']
        
    value              = models.CharField(max_length=100)
    product_attribute  = models.ForeignKey('ProductAttribute', verbose_name="Unité")
    position           = models.PositiveSmallIntegerField("Position", null=True, blank=True)
     
    def __unicode__(self):
        return u"{0} [{1}]".format(self.value, self.product_attribute)

Inlines

Nous voulons pouvoir créer des déclinaisons de produits directement sur la fiche produit:

backoffice/admin.py

from django.contrib import admin
from models import *

class ProductItemAdmin(admin.TabularInline):
    model = ProductItem

class ProductAdmin(admin.ModelAdmin):
    model = Product
    inlines = [ProductItemAdmin,]

admin.site.register(Product, ProductAdmin)

Résulat:

http://localhost:8000/admin/backoffice/product/1/

Voila notre fiche produit avec possibilité de créer des déclinaisons très simplement. La première chose que l'on remarque c'est la facilité du code pour créer une interface complète avec ajout / modification / suppression / controles de données. Il nous aura fallu 10 lignes de code, sans compter les modèles.

La seconde chose que l'on remarque c'est le select multiple pas du tout user-friendly pour le champ m2m attributes dans le cas où les attributs dépassent la centaine d'items. Il est possible de changer ce widget avec l'option filter_vertical:

class ProductItemAdmin(admin.TabularInline):
    model = ProductItem
    filter_vertical = ("attributes",)

class ProductAdmin(admin.ModelAdmin):
    model = Product
    inlines = [ProductItemAdmin,]

admin.site.register(Product, ProductAdmin)

Cela permet de passer de ce widget:

à celui la

Alors évidemment aucun widget n'est meilleur que l'autre mais l'un peut être meilleur que l'autre dans des situations différentes.

Editez les items directement dans le listing

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    inlines = [ProductItemAdmin,]
    list_display = ["id", "name", "price_ht", "price_ttc", "code"]
    list_editable = ["name", "price_ht", "price_ttc"]

http://localhost:8000/admin/backoffice/product/

Remplacer le select par un input

Dans certain cas il peut être intéressant de ne pas afficher un select mais plutôt un input simple. Dans cet input il faudra indiquer la clé de l'item que l'on désire associer au lieu de le sélectionner manuellement dans une liste. Le cas le plus évident sera les cas où les items seraient si nombreux que le chargement de la page serait trop lent. On utilisera donc une solution Ajax. Cette option permet donc de répondre à ce genre de besoin.

class ProductItemAdmin(admin.TabularInline):
    model = ProductItem
    raw_id_fields = ["attributes"]

http://localhost:8000/admin/backoffice/product/1/

Bouton radio au lieu d'un select

Vous pouvez remplacer un select par des boutons radio comme ceci:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    radio_fields = {"status": admin.VERTICAL}

http://localhost:8000/admin/backoffice/product/1/

Ajouter une searchbox

Il est possbile d'ajouter une searchbox qui effectue des recherches sur plusieurs champs:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ('id', 'name', 'date_creation', 'status')
    search_fields = ('name', 'status')

http://localhost:8000/admin/backoffice/product/

Dans notre exemple nous recherchons tous les produits où status est égal à 1, donc online

Créer un filtre

Notre exemple de recherche précédent ressemble assez à du bricolage. Comment l'utilisateur lambda peu-il savoir que le "online" est définit par la valeur 1? Il existe une autre option qui vous permet de filtrer les données plus proprement pour ce genre de cas:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ('id', 'name', 'date_creation', 'status')
    list_filter = ('status', 'date_creation')

http://localhost:8000/admin/backoffice/product/


Vous pouvez affiner votre filtre en utilisant la classe admin.SimpleListFilter:

backoffice/admin.py

class ProductFilter(admin.SimpleListFilter):
   
    title = 'filtre produit'
    parameter_name = 'custom_status'

    def lookups(self, request, model_admin):
        return (
            ('online', 'En ligne'),
            ('offline', 'Hors ligne'),
        )

    def queryset(self, request, queryset):
 
        if self.value() == 'online':
            return queryset.filter(status=1)
                                    
        if self.value() == 'offline':
            return queryset.filter(status=0)

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ('id', 'name', 'date_creation', 'status')
    list_filter = (ProductFilter,)

Option hiérarchie par date

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ('id', 'name', 'date_creation', 'status')
    date_hierarchy = 'date_creation'

http://localhost:8000/admin/backoffice/product/

Vous remarquerez la présence d'un lien "Septembre 2014" au dessus des actions qui vous permet de naviguer / filtrer par des intervalles de dates

Trier par défaut

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ('id', 'name', 'date_creation', 'status')
    ordering = ('-date_creation',)

http://localhost:8000/admin/backoffice/product/

Utiliser le signe - pour indiquer un ordre décroissant

Créer une action pour une sélection

Vous pouvez ajouter une action dans la liste des actions très simplement avec le module admin. Par exemple nous voulons changer le status des produits sélectionnés en "online":

backoffice/admin.py

def set_product_online(modeladmin, request, queryset):
    queryset.update(status=1)
set_product_online.short_description = "Mettre en ligne"

class ProductAdmin(admin.ModelAdmin):
    model = Product
    actions = [set_product_online]

http://localhost:8000/admin/backoffice/product/

Je sélectionne l'action puis je clique sur envoyer. Pour les produits sélectionnés, le status aura la valeur 1 comme il est définit dans la fonction set_product_online que j'ai crée.

Créer une colonne personnalisée

Vous pouvez créer une colonne personnalisée et y mettre les informations que vous voulez:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    
    list_display = ('id', 'name', 'date_creation', 'status', 'tax')
    
    def tax(self, instance):
        return instance.price_ttc - instance.price_ht
    tax.short_description = "Taxes"

A noter que la colonne n'est pas triable.

Les paramètres de la colonne personnalisée

Nous avons vu dans l'exemple précédent que nous pouvions ajouter une colonne dans le listing en créant une fonction. Il existe des paramètres pour ces fonctions:

short_description = "xxx"  → courte description
allow_tags = True          → autorise les balises HTML
boolean = True             → est un booléen
admin_order_field = "xxx"  → colonne triable

Modifier le queryset de l'admin

Il est possible de modifier le queryset par défaut. Prenons un exemple d'optimisation pour comprendre l'intérêt d'une telle option:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ("code", "name", "items_code")
     
    def items_code(self, instance):
        string = ""
        for product_item in instance.product_item.all():
            string+=product_item.code
        return string
    items_code.admin_order_field = 'product_item'

    
    def queryset(self, request):
        qs = super(ProductAdmin, self).queryset(request)
        qs = qs.prefetch_related("product_item")
        return qs

On passe de 100 requètes SQL à 5, vous pouvez voir le résultat dans votre debugger type "django-debug-toolbar"

Le paramètre items_code.admin_order_field = 'product_item' permet d'indiquer à Django qu'on aimerait rendre cette colonne triable.


Un autre exemple pour calculer le nombre de déclinaisons avec la méthode annotate:

backoffice/admin.py

from django.db.models import Count

class ProductAdmin(admin.ModelAdmin):
    model = Product
    list_display = ("code", "name", "product_item_count")
    
    def product_item_count(self, obj):
      return obj.product_item_count
    
    def queryset(self, request):
        qs = super(ProductAdmin, self).queryset(request)
        return qs.annotate(product_item_count=Count('product_item'))

prepopulated_fields facilite vos slug

Il est possible de proposer à l'utilisateur un service qui au moment où celui-ci rempli un champ d'autres champs se remplissent en même temps. Souvent utilisé lors de l'écriture d'un titre, son slug se crée en temps réel. Prenoms l'exemple d'un nom pour créer un code:

backoffice/admin.py

class ProductAdmin(admin.ModelAdmin):
    model = Product
    prepopulated_fields = {"code": ("name", )}

http://localhost:8000/admin/backoffice/product/add/