title: ★ Astuces et bonnes pratiques Django slug: astuces-et-bonnes-pratiques-django date: 2008-02-11 02:14:05 type: post vignette: images/logos/django.png contextual_title1: ★ Django-ROA, pour une architecture orientée ressources contextual_url1: 20090526-django-roa-pour-une-architecture-orientee-ressources contextual_title2: Sortie de Django 1.0, une année de nouveautés contextual_url2: 20080902-sortie-de-django-10-une-annee-de-nouveautes contextual_title3: ★ Découvrons OAuth avec mixin (et django-oauth) contextual_url3: 20080713-decouvrons-oauth-avec-mixin-et-django-oauth

Développant avec Django depuis maintenant près de deux ans (ça rajeunit pas tout ça...), je suis encore surpris de découvrir de nouvelles possibilités de temps en temps. Dans mon combat pour les bonnes pratiques, je pense qu'il y a quelques bases à avoir pour se lancer dans un projet d'envergure avec Django. Je vais essayer de lister les miennes, n'hésitez pas à ajouter les vôtres pour que ça devienne une ressource collaborative.

Une arborescence de fichiers qui tient la route

Je pense qu'il y a deux stratégies lorsqu'on démarre un projet :

Les deux approches sont intéressantes et nécessitent de toute façon d'avoir un découpage des fichiers habituels (models, views, etc) en modules python pour s'y retrouver et un dossier de bibliothèques tierces car la réutilisation a quand même du bon. Bon ça aussi c'est discutable, certains préférant les fichiers de 3000 lignes, encore une fois ce ne sont que des conseils...

Au final, la structure que j'adopte maintenant est la suivante (je ne donne pas le détail des applications car il varie beaucoup en fonction du degré de RESTification de votre projet) :

monsuperprojet/
  projetdjango/
    __init__.py
    manage.py
  applidjango1/
  applidjango2/
  applidjango3/
  applitierce1/
  applitierce2/

De cette manière, vous n'avez qu'un seul dossier à mettre dans votre $PYTHONPATH : monsuperprojet et vos applications sont totalement découplées (vous n'avez jamais besoin de connaître projetdjango). Remarquez qu'il est possible de spécifier le chemin directement dans votre manage.py si vous souhaitez ne pas surcharger votre $PYTHONPATH (par exemple si vous bossez sur plusieurs projets qui ont les mêmes noms d'applis...) :

import sys
sys.path = ['/chemin/vers/monsuperprojet'] + sys.path

À ajouter bien sûr avant d'importer les settings.

Pour revenir à l'arborescence, je n'ai pas vraiment de bonnes pratiques pour la place des templates. Certains les séparent complètement, d'autres les mettent dans les applications. À vous de voir en fonction de vos besoins.

Des fichiers statiques séparés

La doc de Django recommande de gérer ses fichiers statiques à part pour gagner en performances (téléchargements en parallèle sur un autre sous-domaine, etc). Bon bien sûr c'est un peu overkill pour un blog et encore plus lorsque vous développez. Il existe une solution assez simple pour s'en sortir avec le serveur de développement :

if settings.DEBUG:
    from django.views.static import serve
    urlpatterns += patterns('',
        (r'^media/(?P<path>.*)$',
            serve,
            dict(
                document_root = os.path.join(settings.PROJECT_PATH, 'media'),
                show_indexes = True
            )
        ),
    )

avec settings.PROJECT_PATH défini ainsi :

PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))

Puisqu'on parle des settings, n'oubliez pas qu'il faut toujours les importer avec :

from django.conf import settings

Et jamais directement de votre projet.

Si vous avez besoin de versionner vos fichiers (ici aussi pour les performances car il vaut mieux avoir un cache très long et un nom de fichier qui change lorsque vous mettez à jour l'application), inutile de vous casser la tête avec des style.20080214.css ou autres noms de fichiers barbares, le paramètre MEDIA_URL est l'unique modification à effectuer, vous pouvez par exemple le suffixer avec le numéro de révision de votre dépôt (je vous laisse jouer avec django.utils.version.get_svn_revision) ou la date, on obtient :

http://media.biologeek.com/20080210/css/biologeek.css
<------------ MEDIA_URL ----------> <--- static ---->

[edit du 26 mars] : voir aussi ce snippet à ce sujet.

Des raccourcis biens pratiques

Le système d'expressions régulières des URL de Django est très puissant. Mais il faut avouer qu'il est un peu rebutant au premier abord et qu'on a vite fait d'oublier un - ou un \+. J'ai trouvé une solution assez élégante à ça en constituant un dictionnaire d'expression régulières communément utilisées réutilisable au besoin, ça donne :

import lasuperfonction

re_urls = {
    'username': '(?P<username>\w+)/',
    'pk_value': '(?P<pk_value>\d+)/',
    'foo_args': '(?P<foo_args>\d+)?/?',
    etc
}

urlpatterns = pattern('',
    url(r'%(username)s%(pk_value)s%(foo_args)s' % locals(), lasuperfonction)
)

ce qui permet d'avoir des URL un peu plus lisibles. Au passage, deux détails :

Concernant les raccourcis, il est bien pratique d'avoir ses propres fonctions de base comme send_mail si vous souhaitez ensuite faire de l'envoi en asynchrone ou direct_to_template si vous souhaitez un jour précompiler vos templates sans avoir à modifier tous vos fichiers ([edit] : une solution élégante) ou par exemple si vous souhaitez modifier le chemin par défaut vers lequel redirige le décorateur permission_required :

from django.contrib.auth.decorators import permission_required as django_permission_required
def permission_required(perm):
    return django_permission_required(perm, login_url='/')

L'avantage de Django c'est d'être totalement en Python, ce qui permet de modifier facilement les différents points bloquants au besoin :-).

Des GenericForeignKey encore plus puissantes

J'ai passé pas mal de temps à essayer d'utiliser deux GenericForeignKey dans un même modèle donc on peut considérer ça comme une astuce (même si ça ne doit pas concerner grand monde). Lorsqu'on en arrive à un tel modèle c'est généralement qu'il y a une grande inconnue dans le contenu qui va être stocké. C'est typiquement le cas d'un log qui doit à la fois prendre en compte un item et une position par exemple :

class Log(models.Model):
    item = generic.GenericForeignKey(ct_field="item_content_type", fk_field="item_object_id")
    item_content_type = models.ForeignKey(ContentType, related_name="log_item")
    item_object_id = models.IntegerField()

    position = generic.GenericForeignKey(ct_field="position_content_type", fk_field="position_object_id")
    position_content_type = models.ForeignKey(ContentType, related_name="log_position")
    position_object_id = models.IntegerField()

De cette manière, vous pouvez procéder aux requêtes habituelles :

queryset = Log.objects.filter(item_content_type = user_ct, item_object_id = user_id)

Mais aussi utiliser directement les objets pour la création :

log = Log(item = user, position = last_step)

Si vous utilisez l'interface d'administration de Django auto-générée, il peut être intéressant de créer les liens vers ces objets génériques liés. Malheureusement, on ne peut pas spécifier directement une GenericForeignKey dans list_display :

     class Admin:
        list_display = ('item',)

Il va donc falloir passer par une petite astuce, on commence par spécifier un nom de fonction :

    class Admin:
        list_display = ('get_item_for_admin',)

Puis on crée le lien vers l'objet en question :

    def get_item_for_admin(self):
        url = '../../%s/%s/%s/' % ( self.item.__class__._meta.app_label,
                                    self.item.__class__._meta.module_name,
                                    self.item.id)
        return '<a href="%s" title="">%s</a>' % (url, self.item)
    get_item_for_admin.short_description = _('Item')
    get_item_for_admin.allow_tags = True

Hop, les liens vont être disponibles pour vos GenericForeignKey.

Le meilleur pour la fin

Une application non testée est une application morte-née

Vraiment. La qualité d'une application devrait se mesurer au nombre de tests, sinon c'est ce que j'appelle du code poubelle. Prenez cette bonne habitude dès le début car c'est difficile d'expliquer à vos boss qu'il va falloir passer le mois prochain à ajouter des tests.

Heureusement, Django offre un framework vraiment intéressant pour tester vos applications alors il serait dommage de s'en passer ! Au bout d'un moment, les tests vont devenir énormes (du moins je vous le souhaite) et prendre pas mal du temps à se lancer, ce qui va avoir pour conséquence de vous faire coder directement comme un Dieu de transformer la séance d'écriture des tests en corvée. N'hésitez pas à ce moment là à ne lancer qu'une partie de vos tests (s'ils sont suffisamment découplés... mais c'est le cas bien évidemment), soit en spécifiant la classe du test unitaire :

python manage.py test users.TestAccounts

soit en spécifiant le fichier de doctests sous réserve d'appliquer ce patch :

python manage.py test users.emails

Parlez vous unicode ?

Il est très important aussi de penser à l'internationalisation de votre application dès le début car il est vraiment pénible de devoir repasser ensuite sur l'ensemble des chaînes pouvant être traduites (et n'oubliez pas de nommer vos fichiers de templates en .html sous peine de devoir toucher à make-message.py).

Des IntegerFields pour les choix

Je ne vais pas trop me fatiguer sur ce point car c'est très bien expliqué par James Bennett. C'est très important au niveau des performances et c'est très pénible à reprendre si vous n'avez pas pris la peine d'utiliser cette méthode dès le début. On ne l'oublie qu'une seule fois généralement.

Des managers pour les requêtes récurrentes

Ici aussi, c'est déjà traité par Jared Kuolt à deux reprises. Vous avez même une solution pour enchaîner ces nouvelles fonctions au besoin.

Une autre dernière astuce si vous avez à utiliser vos modèles Django dans un script indépendant de Django :

import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'projetdjango.settings'
from django.conf import settings

Vous pouvez ensuite importer vos modèles sans problème normalement.

Enfin, usez et abusez des ressources fournies par Django (middlewares, context_processors, managers, etc) et Python (property, list-comprehension, decorator, etc). Ce sont vos meilleurs amis... avec le cache :-).

[edit du 20 mars] : après une relecture de Code Like a Pythonista, j'ai découvert qu'il existait une solution plus simple que DSU pour trier plusieurs modèles Django selon un champ, par exemple la date de billets et de brèves de blog :

self.items = list(Post.published.all()[:10]) + list(Thought.published.all()[:20])

Voici la version originale compatible python2.3 :

to_sort = [(item.publication_date, item) for item in self.items]
to_sort.sort()
self.items = [item[1] for item in reversed(to_sort)]

Et voici celle qui utilise l'argument key apparu à la version 2.4 :

self.items.sort(key=lambda item: item.publication_date, reverse=True)

Élégant non ?

[edit du 26 mars] : Pour répondre au commentaire de Philippe :

Une petite note sur des choses à pas faire comme dans la doc serait peut-être bienvenu ? Comme par exemple l'histoire du auto_now_add ?

Je n'ai plus tout en tête mais effectivement rappeler que auto_now_add est deprecated ne fait pas de mal.

Un petit retour d'expérience sur les migrations de schema éventuellement ?

Il n'y a pas vraiment de solution pour l'instant, ormis les grosses mises à jour du modèle c'est assez simple à faire à la main. Dès qu'on touche aux données c'est toujours assez critique donc ne pas oublier de tester sur une iso-prod avant :-).

Un petit retour d'expérience sur l'utilisation du trunk pour tous tes projets (notamment la maintenance que ça engendre) ?

Le problème du trunk arrive lorsqu'on laisse un projet en sommeil quelques mois/années et qu'il faut alors tout mettre à jour d'un coup. Tant que ça reste du quotidien, les modifications sont généralement minimes donc c'est rapidement fait. Ma stratégie dans le cas d'une reprise c'est de reprendre point par point ceux listés sur la page des changements de Django ce qui se fait sans douleur pour l'instant, à part pour le passage en unicode bien sûr...

Il ne faut pas se voiler la vérité, le web évolue toujours plus rapidement et un projet s'entretient sous peine de devenir irrécupérable. C'est presque un avantage d'être sur une version qui avance au quotidien et qui permet de conserver cette agilité. Je n'ai jamais ressenti ça comme une contrainte personnellement, je suis davantage gêné par mon propre code qui reflète ma progression : « comment j'ai fait pour coder ça comme ça il y a 6 mois ?! » ;-).