Le blog: Web, Python, Django, Javascript ...

5 astuces pour améliorer le site d'admin de Django

Une des grandes forces de Django est le site d'administration automatiquement généré depuis le modèle. Celui-ci est largement configurable pour en faire un vrai outil de back-office en très peu de temps. Je vous propose dans cet article quelques astuces pour aller encore plus loin dans la personnalisation de ce site. Des astuces faciles à implémenter et qui vous rendront peut-être de bons services.

1. Définir le format d'un champ date

Par défaut les champs de type date sont sous un format anglophone 'yyyy-mm-dd' alors que nous autres francophones préféront faire les choses en sens inverse et sommes habitués à un format de type 'dd/mm/yyyy'. Heureusement Django permet de définir les formats acceptés via le paramètre input_formats de DateField. Il est donc très facile d'ajouter notre format de date préféré. Attention, en redéfinissant le DateField à ne pas écraser le widget spécifique du site d'administration qui propose un petit bouton affichant un calendrier (qui lui retournera la date au format anglophone qu'il faut donc continuer d'accepter).

Quelques lignes de code valent mieux que des longs discours. Ajouter le code suivant dans votre admin.py:

#les imports a rajouter
from django.forms.models import ModelForm
from django.contrib.admin.widgets import AdminDateWidget
from django.forms.fields import DateField
#definir un formulaire
class MyModelForm(ModelForm):
    #on definit les formats de date acceptes 
    my_date = DateField(input_formats=['%d/%m/%Y','%Y-%m-%d'],                        
                        # et on affecte le widget de date du site d'administration 
                        widget=AdminDateWidget(format='%d/%m/%Y'))
    class Meta:
        model = models.MyModel
class MyModelAdmin(admin.ModelAdmin):    
    #le site d'admin utilisera notre formulaire
    form = MyModelForm    ...

Le site d'administration devrait à présent accepter les dates selon les 2 formats définis et faciliter la saisie aux francophones. A noter que cette techinque fonctionne aussi pour les champs 'inline'.

2. Visualiser l'image d'un champ ImageField

Les champs de type ImageField permettent d'uploader une image dans le répertoire MEDIA_ROOT et de stocker en base l'accès vers ce fichier. Ils sont très utiles pour une application, un CMS par exemple, qui gère des images. Malheureusement, le site d'administration ne permet d'afficher que le chemin du fichier. On est parfois frustré de ne pas pouvoir visualiser cette image directement. Heureusement la aussi avec quelques lignes de code dans le admin.py, nous pouvons ajouter cette fonctionnalité:

Il faut pour cela tout d'abord surchager le widget AdminFileWidget afin de lui faire afficher l'image

from django.utils.safestring import mark_safe
from django.contrib.admin.widgets import AdminFileWidget
class AdminImageWidget(AdminFileWidget):
    """subclass the AdminFileWidget in order to display the image"""
    def render(self, name, value, attrs=None):
        output = []
        if value:
            output.append(u'<div>{0}</div>'.format(value))
        output.append(super(AdminFileWidget, self).render(name, value, attrs))
        if value and getattr(value, "url", None):
            #l'image mais on pourrait aussi mettre un lien
            img = u'<div><img src="{0}" height="128px"/></div>'.format(value.url)
            output.append(img)
        return mark_
safe(u''.join(output))

Et ensuite de surcharger la méthode formfield_for_dbfield qui permet de faire correspondre un contrôle à un champ du modèle.

class MyModelAdmin(admin.ModelAdmin):
    ...
    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name == 'image': #on suppose que le modele a un champ image
            kwargs['widget'] = AdminImageWidget
            kwargs.pop('request', None) #erreur sinon
            return db_field.formfield(**kwargs)
    return super(MyModelAdmin,self).formfield_for_dbfield(db_field, **kwargs) 

Cette technique pourraît être utilisée pour enrichir d'autres types de contrôles (UrlField par exemple). C'est d'ailleurs ce que nous ferons par la suite.

Merci au passage à Psychic Origami pour la forte inspiration.

3. Inclure l'éditeur Tiny_MCE là où vous le voulez

Tiny_MCE est un éditeur HTML écrit en javascript qui peut s'intégrer très facilement au site d'administration de Django. Il suffit pour cela d'inclure le code suivant en faisant pointer vers les chemins de son code et d'un fichier javascript de configuration (textareas.js dans ce cas).

class EmailCfgAdmin(admin.ModelAdmin):
     class Media:
         """Use Tiny_MCE as content editor"""         
         js = ('js/tiny_mce/tiny_mce.js', 'flatcms/js/textareas.js',)

L'inconvénient est qu'ainsi tous les contôles de type TextArea (TextWidget de Django) deviennent éditables avec Tiny_MCE. Imaginons le cas d'un éditeur d'e-mail ou l'on souhaiterait un TextArea éditable en HTML pour le corps du message et un second simplement en texte pour le corps de message alternatif pour les destinataires ne pouvant pas lire du HTML.

La encore la solution tient en quelques lignes.

Tout d'abord en définissant un "déselecteur" dans le textareas.js

tinyMCE.init({
    ...
    editor_deselector : "nomce",
    ...
});

Ensuite, en ajoutant la classe correspondante sur les contrôles à ne pas rendre éditable par Tiny_MCE:

class EmailCfgAdmin(admin.ModelAdmin):
    class Media:
        """Use Tiny_MCE as content editor"""
        js = ('js/tiny_mce/tiny_mce.js', 'flatcms/js/textareas.js',)
    fieldsets = (
        ...
        (_('Content'), {'fields': ('html_body', 'text_body')}),
    )
    
    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name == 'text_body':
            kwargs['widget'] = AdminTextareaWidget(attrs={'class':'nomce'})                                    
        return super(EmailCfgAdmin,self).formfield_for_dbfield(db_field,**kwargs)

4. Faire afficher les valeurs d'un ManyToManyField en mode raw_id

Django propose un mode raw_id pour les ForeignKeyField et les ManyToManyField permettant de remplacer la boîte de sélection standard par un champ texte affichant les identifiants des objets de la relation.

Ceci est très pratique lorsque le nombre de possibilités dans la relation devient très grand et que l'on ne s'y retrouve plus dans la boîte de sélection. Le bouton loupe du raw_id va alors offrir un moyen de sélection bien pensé.

Malheureusement contraitrement au ForeignKeyField, le ManyToManyField ne permet pas d'afficher à droite du champ la valeur de la sélection. La lecture des identifiants de base étant difficile, je vous propose de pallier à ce problème avec le contrôle suivant:

Django ManyToManyField RawId

from django.contrib.admin.widgets import ManyToManyRawIdWidget
import unicodedata
from django.utils.html import escape
class VerboseManyToManyRawIdWidget(ManyToManyRawIdWidget):
    """
    A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
    in a <select multiple> box. 
    Display user-friendly value like the ForeignKeyRawId widget
    """
    def __init__(self, rel, attrs=None):
        self._rel = rel
        super(VerboseManyToManyRawIdWidget, self).__init__(rel, attrs)
    def label_for_value(self, value):
        values = value.split(',')
        str_values = []
        key = self.rel.get_related_field().name
        for v in values:
            try:
                obj = self.rel.to._default_manager.using(self.db).get(**{key: v})
                # gere les erreurs unicode 
                x = unicodedata.normalize('NFKD', u'%s' % obj).encode('ascii','ignore')
                # supprime le code HTML
                str_values += [escape(x)]
            except self.rel.to.DoesNotExist:
                str_values += [u'???']
        return u'&nbsp;<strong>%s</strong>' % (u',&nbsp;'.join(str_values))

De la même manière, on surcharge la méthode formfield_for_dbfield qui pour activer le contrôle:

class MyAdmin(admin.ModelAdmin):
     ...
     def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name in ('groups',):
            kwargs.pop('request', None)
            kwargs['widget'] = VerboseManyToManyRawIdWidget(db_field.rel)
            return db_field.formfield(**kwargs)
        return super(MyAdmin,self).formfield_for_dbfield(db_field,**kwargs)

5. Ecraser le fichier lors de la mise à jour d'un FileField

Vous avez peut-être remarqué que lors de la mise à jour d'un champ de type FileField (ou ImageField qui en dérive), si le fichier uploadé existe déjà celui-ci est renommé. Ce comportement peut paraître plus sûr et éviter de supprimer des données nécessaires au site. Toutefois, cela cause aussi sur le serveur l'existence d'un grand nombre de fichiers obsolètes et force à renommer le fichier avec un nom différent de celui d'origine. Il est donc parfois préférable de demander l'écrasement du fichier existant avec le code suivant:

Ici tout se passe au niveau du modèle et non pas de l'admin. On définit tout d'abord un nouveau "Storage" qui définit comment les données sont stockées sur le serveur. On vérifie simplement si le fichier existe et si c'est le cas on le supprime.

from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
    def get_available_name(self, name):
        if self.exists(name):
            self.delete(name)
        return name

Puis on définit ce "Storage" comme mode de sauvegarde pour notre champ FileField

class MyModel(models.Model):
    file = models.FileField(upload_to='my_folder', storage=OverwriteStorage())

Avant chaque mise à jour, un éventuel fichier de même nom sera supprimé afin d'éviter de renommer celui qui est téléchargé.

Voilà ces quelques astuces qui me sont utiles assez régulièrement. N'hésitez pas à commenter si vous avez des propositions d'améliorations. J'espère que cela vous aidera à personnaliser le site d'administration de Django que personnellement j'aime beaucoup utiliser.


Commentaires rss
Posté par psam le samedi 24 juillet 2010 à 19:49
Pour le tiny_mce, je cherche une solution pour accorder sa langue avec celle de l'interface. Pas évident, car il faut servir language: dans le tinyMCE.init(), or le textareas.js est un fichier, pas une view.

Posté par luc le samedi 24 juillet 2010 à 21:04
Pourquoi ne pas faire générer le textarea.js par django plutôt que d'utiliser un fichier statique? Ca devrait marcher sans probleme je pense.
Cordialement

Posté par psam le dimanche 25 juillet 2010 à 02:51
Parce que le 'flatcms/js/textareas.js' sous class Media a le MEDIA_URL (/medias/) en préfixe, et que normalement l'apache sert ce fichier (c'est ce qu'on attend de lui) alors il faudrait écrire par exemple 'flatcms/js/textareas-as-a-view/' pour que la request arrive à Django et avoir un urlpatterns qui capte /medias/flatcms/js/textareas-as-a-view/ pour le refiler à une view. Possible, mais ce mix introduit de la confusion.

Posté par luc le lundi 26 juillet 2010 à 16:13
C'est vrai que les urls peuvent devenir difficiles à décrypter. C'est peut-être gérable par des redirections au niveau d'Apache mais ca a d'autres inconvénients.
Autre possibilité: avoir plusieurs textarea.js et faire pointer l'admin de Django avec
'/js/textarea-fr.js' if settings.LANGUAGE_CODE=='fr-FR' else '/js/textarea-en.js'

Posté par psam le lundi 26 juillet 2010 à 22:24
Finalement, la solution que j'ai mis en fonction est :

var tmce_language = document.documentElement['lang'].substring(0,2);
// fallback for some Django languages not supported by tiny_mce
if ("fy ga km kn".indexOf(tmce_language) > -1) tmce_language = "en";
tinyMCE.init({
//...
language: tmce_language,
//...
});

Elle doit fonctionner pour toute l'admin puisque son base.html prévoit cet attribut 'lang'.

Posté par Mathieu Agopian le lundi 09 août 2010 à 09:36
Bonjour,

merci beaucoup pour ces astuces fort utiles (en particulier pour la 5, je ne pensais pas que c'était aussi simple!).

Une petite remarque concernant le "format" du blog : les morceaux de code trop larges sont tronqués!
Si tu rajoutais (par exemple) un style "overflow: auto;" sur tes <pre> ça devrait régler le problème en rajoutant (quand c'est nécessaire) une scrollbar horizontale.

Posté par providenz le jeudi 12 août 2010 à 21:46
Luc, merci également pour l'écrasage de fichier, je vais en avoir besoin sous peu.

Posté par luc le dimanche 15 août 2010 à 16:00
Merci à vous 2 pour vos retours. C'est vrai que le renommage du fichier est parfois énervant.

Merci aussi à Mathieu pour le truc CSS. Le mini-blog que j'utilise ne gère pas encore très bien les formats CSS. "Le cordonnier est souvent le plus mal chaussé" :) Mais je vais essayer d'y remédier

Posté par joujou le jeudi 12 septembre 2013 à 16:54
Merci pour les astuces surtout la 4 est très utile!!
je suis débutante
mais j'ai pas su comment inclure le code je le rajoute ds quel fichier??

Posté par luc le vendredi 13 septembre 2013 à 10:28
Le code pour les 4 premiers se trouvent dans l'admin.py (même si les widgets peuvent être inclus dans un fichier spécifique widgets.py que l'on importe ensuite).
Le 5) est à définir dans le models.py

Nom: Email: URL: Commentaire: Si vous saisissez quelque chose dans ce champ, votre commentaire sera considéré comme étant indésirable: Captcha: captcha

Luc JEAN

09.65.20.15.70

ljean@apidev.fr

Luc JEAN

Suivez les nouveautés

Wikio RSS  RSS Blog Python Django selenium Rss commentaires
Paperblog : Les meilleurs actualités issues des blogs Follow luc_apidev on Twitter