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 :
- une seule énorme application (au sens Django du terme) ;
- plusieurs applications si possibles découplées.
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 :
- l'utilisation de locals() n'est pas trop coûteuse ici car les fichiers d'urls ne contiennent en théorie que des urls ;
- il est vraiment très puissant d'utiliser les fonctions et non leurs 'noms' car ça permet d'appliquer directement les décorateurs à ce niveau, par exemple login_required(lasuperfonction).
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 ?! » ;-).
Commentaires
NiCoS le 17/02/2008 :
J'ai pas tout tout compris mais je me le garde sous le coude pour le jour où... :-)
Merci !
Stan le 27/02/2008 :
Merci (un merci general pour ce site, d'ailleurs) !
Malheureusement, meme avec cette astuce, je bute toujours sur le probleme elementaire de chargement des css.
J'ai l'impression que seules les adresses commencant par le prefixe donne par settings.ADMIN_MEDIA_PREFIX seraient acceptees, or je prefererais avoir des "media" (e.g. css) distincts pour chaque appli Django (et non pas au niveau du projet).
Philippe Mironov le 25/03/2008 :
Quelques sujets que je serai heureux de lire sur ce blog :
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 ?
Un petit retour d'experience sur les migrations de schema eventuellement ?
Un petit retour d'experience sur l'utilisation du trunk pour tous tes projets (nottement la maintenance que ça engendre) ?
David, biologeek le 26/03/2008 :
@Stan : avoir des media distincts pour chaque application c'est possible, il suffit de mettre APPLI_FOO_MEDIA_URL, APPLI_BAR_MEDIA_URL, etc dans tes settings et d'utiliser ensuite ces settings vu que tu dois de toute façon passer par un context_processor pour les récupérer dans les templates.
@Philippe Mironov : répondu dans le billet.
Philippe Mironov le 27/03/2008 :
Un grand merci pour ces réponses.
Louevie le 16/05/2008 :
Bonjour,
Je m'intéresse en ce moment au framework pour développez des sites avec python et j'ai vu que Zope était l'un des premiers. Je voulais savoir quels sont les détails qui t'ont fait choisir django plutôt qu'un autre?
Nager le 09/07/2008 :
Salue, je débute sous python et m'intéresse actuellement au web sous python. Mon problème est de pouvoir installer "django" sans avoir internet car je n'ai pas internet et voudrais récupéré les binaires dans un cybercafé et le ramener chez moi pour travailler comment faire s.v.p je suis sous ubuntu 7.10 quand je veux un logiciel je me rend sur package.ubuntu et récupéré mon paquet et ses dépendance. Ses fastidieux mais quand on a choisit Linux on doit faire avec. Comment donc faire pour django svp
David, biologeek le 10/07/2008 :
@Louevie : j'ai répondu dans ce billet https://larlet.fr/david/biologeek/archives/20080521-conferences-django-pour-pycon-fr/
N'hésite pas si tu veux des précisions.
@Nager : il faut soit récupérer les sources avec svn ce qui te permettra de tester la dernière version à jour, soit télécharger l'archive d'une version officielle sur http://www.djangoproject.com/download/
La dernière version est la 0.96.2 au moment où j'écris ce commentaire.
Sinon pour Ubuntu, il y a le paquet python-django qui te permet de l'installer comme un paquet classique, c'est documenté ici : http://www.djangoproject.com/documentation/distributions/#ubuntu
Bon courage :)
étudiant marocain le 27/11/2008 :
Salut
je commence à m'intéresser aux frameworks de développement Web, mais j'hésite entre symphony et zend, pourriez vous m'indiquer lequel choisir et pourquoi ? merci
David, biologeek le 27/11/2008 :
@étudiant marocain : on ne peut pas choisir sans avoir un besoin. Ou alors si, mais Django ;-).
étudiant marocain le 02/12/2008 :
Merci pour la réponse.
Je comprends pas ce que vous voulez dire par besoin ?
Ce qui me préoccupe c'est la maintenabilité de mon code c'est tout :-)
cambuntu le 16/12/2008 :
salut j'ai un problème, Django ne gère pas dans la Module admin l'unicité d'un champ du modèle.
Si dans un modèle j'ai marque un champ unique alors en le remplissant si on rentre la même valeur deux fois django génère des erreurs en mode debug pas de jolie erreurs comme lorsqu'il valide un formulaire comment résoudre ce problème
Aussi j'ai deux champs date je voudrais que le module admin valide le fait qu'une date soit supérieur a l'autre comment le faire
David, biologeek le 17/12/2008 :
Ça fait partie de la validation des modèles, qui sera normalement dans Django 1.1 (cf. http://code.djangoproject.com/wiki/Version1.1Roadmap)
Twinsview le 06/03/2009 :
Pour les frameworks : Django, Ruby On Rails ou rien :)