Repository with sources and generator of https://larlet.fr/david/ https://larlet.fr/david/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

article.md 16KB

5 vuotta sitten
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. title: ★ Astuces et bonnes pratiques Django
  2. slug: astuces-et-bonnes-pratiques-django
  3. date: 2008-02-11 02:14:05
  4. type: post
  5. vignette: images/logos/django.png
  6. contextual_title1: ★ Django-ROA, pour une architecture orientée ressources
  7. contextual_url1: 20090526-django-roa-pour-une-architecture-orientee-ressources
  8. contextual_title2: Sortie de Django 1.0, une année de nouveautés
  9. contextual_url2: 20080902-sortie-de-django-10-une-annee-de-nouveautes
  10. contextual_title3: ★ Découvrons OAuth avec mixin (et django-oauth)
  11. contextual_url3: 20080713-decouvrons-oauth-avec-mixin-et-django-oauth
  12. <p>Développant avec <a href="http://www.django-fr.org/">Django</a> 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 <a href="https://larlet.fr/david/biologeek/archives/20060121-bonnes-pratiques-de-la-programmation-en-python/">bonnes pratiques</a>, 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.</p>
  13. <h2>Une arborescence de fichiers qui tient la route</h2>
  14. <p>Je pense qu'il y a deux stratégies lorsqu'on démarre un projet&nbsp;:</p>
  15. <ul>
  16. <li>une seule énorme application (au sens Django du terme)&nbsp;;</li>
  17. <li>plusieurs applications si possibles découplées.</li>
  18. </ul>
  19. <p>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...</p>
  20. <p>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 <a href="https://larlet.fr/david/biologeek/archives/20070629-architecture-orientee-ressource-pour-faire-des-services-web-restful/">RESTification</a> de votre projet)&nbsp;:</p>
  21. <pre>monsuperprojet/
  22. projetdjango/
  23. __init__.py
  24. manage.py
  25. applidjango1/
  26. applidjango2/
  27. applidjango3/
  28. applitierce1/
  29. applitierce2/</pre>
  30. <p>De cette manière, vous n'avez qu'un seul dossier à mettre dans votre <strong>$PYTHONPATH</strong>&nbsp;: 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 <strong>manage.py</strong> si vous souhaitez ne pas surcharger votre <strong>$PYTHONPATH</strong> (par exemple si vous bossez sur plusieurs projets qui ont les mêmes noms d'applis...)&nbsp;:</p>
  31. <pre>
  32. <code>import sys
  33. sys.path = ['/chemin/vers/monsuperprojet'] + sys.path</code>
  34. </pre>
  35. <p>À ajouter bien sûr avant d'importer les settings.</p>
  36. <p>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.</p>
  37. <h2>Des fichiers statiques séparés</h2>
  38. <p>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&nbsp;:</p>
  39. <pre><code>if settings.DEBUG:
  40. from django.views.static import serve
  41. urlpatterns += patterns('',
  42. (r'^media/(?P&lt;path&gt;.*)$',
  43. serve,
  44. dict(
  45. document_root = os.path.join(settings.PROJECT_PATH, 'media'),
  46. show_indexes = True
  47. )
  48. ),
  49. )</code>
  50. </pre>
  51. <p>avec <em>settings.PROJECT_PATH</em> défini ainsi&nbsp;:</p>
  52. <pre><code>PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))</code></pre>
  53. <p>Puisqu'on parle des settings, n'oubliez pas qu'il faut toujours les importer avec&nbsp;:</p>
  54. <pre><code>from django.conf import settings</code></pre>
  55. <p>Et <strong>jamais</strong> directement de votre projet.</p>
  56. <p>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 <em>style.20080214.css</em> ou autres noms de fichiers barbares, le paramètre <strong>MEDIA_URL</strong> 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 <em>django.utils.version.get_svn_revision</em>) ou la date, on obtient&nbsp;:</p>
  57. <pre>http://media.biologeek.com/20080210/css/biologeek.css
  58. &lt;------------ MEDIA_URL ----------&gt; &lt;--- static ----&gt;</pre>
  59. <p><strong>[edit du 26 mars]</strong>&nbsp;: voir aussi <a href="http://www.djangosnippets.org/snippets/666/">ce snippet</a> à ce sujet.</p>
  60. <h2>Des raccourcis biens pratiques</h2>
  61. <p>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 <strong>dictionnaire d'expression régulières communément utilisées</strong> réutilisable au besoin, ça donne&nbsp;:</p>
  62. <pre><code>import lasuperfonction
  63. re_urls = {
  64. 'username': '(?P&lt;username&gt;\w+)/',
  65. 'pk_value': '(?P&lt;pk_value&gt;\d+)/',
  66. 'foo_args': '(?P&lt;foo_args&gt;\d+)?/?',
  67. etc
  68. }
  69. urlpatterns = pattern('',
  70. url(r'%(username)s%(pk_value)s%(foo_args)s' % locals(), lasuperfonction)
  71. )</code>
  72. </pre>
  73. <p>ce qui permet d'avoir des URL un peu plus lisibles. Au passage, deux détails&nbsp;:</p>
  74. <ul>
  75. <li>l'utilisation de <strong>locals()</strong> n'est pas trop coûteuse ici car les fichiers d'urls ne contiennent en théorie que des urls&nbsp;;</li>
  76. <li>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 <em>login_required(lasuperfonction)</em>.</li>
  77. </ul>
  78. <p>Concernant les raccourcis, il est bien pratique d'avoir ses propres fonctions de base comme <em>send_mail</em> si vous souhaitez ensuite faire de l'envoi en asynchrone ou <em>direct_to_template</em> si vous souhaitez un jour <a href="http://www.b-list.org/weblog/2007/nov/27/performance/#c36262">précompiler vos templates</a> sans avoir à modifier tous vos fichiers (<strong>[edit]</strong>&nbsp;: une <a href="http://www.djangosnippets.org/snippets/596/">solution élégante</a>) ou par exemple si vous souhaitez modifier le chemin par défaut vers lequel redirige le décorateur <em>permission_required</em>&nbsp;:</p>
  79. <pre>from django.contrib.auth.decorators import permission_required as django_permission_required</pre>
  80. <pre><code>def permission_required(perm):
  81. return django_permission_required(perm, login_url='/')</code></pre>
  82. <p>L'avantage de Django c'est d'être totalement en Python, ce qui permet de modifier facilement les différents points bloquants au besoin :-).</p>
  83. <h2>Des GenericForeignKey encore plus puissantes</h2>
  84. <p>J'ai passé pas mal de temps à essayer d'utiliser deux <a href="http://www.djangoproject.com/documentation/models/generic_relations/">GenericForeignKey</a> 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&nbsp;:</p>
  85. <pre><code>class Log(models.Model):
  86. item = generic.GenericForeignKey(ct_field="item_content_type", fk_field="item_object_id")
  87. item_content_type = models.ForeignKey(ContentType, related_name="log_item")
  88. item_object_id = models.IntegerField()
  89. position = generic.GenericForeignKey(ct_field="position_content_type", fk_field="position_object_id")
  90. position_content_type = models.ForeignKey(ContentType, related_name="log_position")
  91. position_object_id = models.IntegerField()</code>
  92. </pre>
  93. <p>De cette manière, vous pouvez procéder aux requêtes habituelles&nbsp;:</p>
  94. <pre><code>queryset = Log.objects.filter(item_content_type = user_ct, item_object_id = user_id)</code></pre>
  95. <p>Mais aussi utiliser directement les objets pour la création&nbsp;:</p>
  96. <pre><code>log = Log(item = user, position = last_step)</code></pre>
  97. <p>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 <em>list_display</em>&nbsp;:</p>
  98. <pre><code> class Admin:
  99. list_display = ('item',)</code>
  100. </pre>
  101. <p>Il va donc falloir passer par une petite astuce, on commence par spécifier un nom de fonction&nbsp;:</p>
  102. <pre><code> class Admin:
  103. list_display = ('get_item_for_admin',)</code></pre>
  104. <p>Puis on crée le lien vers l'objet en question&nbsp;:</p>
  105. <pre><code> def get_item_for_admin(self):
  106. url = '../../%s/%s/%s/' % ( self.item.__class__._meta.app_label,
  107. self.item.__class__._meta.module_name,
  108. self.item.id)
  109. return '&lt;a href="%s" title=""&gt;%s&lt;/a&gt;' % (url, self.item)
  110. get_item_for_admin.short_description = _('Item')
  111. get_item_for_admin.allow_tags = True</code>
  112. </pre>
  113. <p>Hop, les liens vont être disponibles pour vos GenericForeignKey.</p>
  114. <h2>Le meilleur pour la fin</h2>
  115. <h3>Une application non testée est une application morte-née</h3>
  116. <p>Vraiment. <strong>La qualité d'une application devrait se mesurer au nombre de tests</strong>, 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.</p>
  117. <p>Heureusement, Django offre un framework vraiment intéressant pour tester vos applications alors il serait dommage de s'en passer&nbsp;! 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 <del>de vous faire coder directement comme un Dieu</del> 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&nbsp;:</p>
  118. <pre>python manage.py test users.TestAccounts</pre>
  119. <p>soit en spécifiant le fichier de doctests sous réserve d'<a href="http://code.djangoproject.com/ticket/6364">appliquer ce patch</a>&nbsp;:</p>
  120. <pre>python manage.py test users.emails</pre>
  121. <h3>Parlez vous unicode&nbsp;?</h3>
  122. <p>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 <strong>.html</strong> sous peine de devoir toucher à <strong>make-message.py</strong>).</p>
  123. <h3>Des IntegerFields pour les choix</h3>
  124. <p>Je ne vais pas trop me fatiguer sur ce point car c'est <a href="http://www.b-list.org/weblog/2007/nov/02/handle-choices-right-way/">très bien expliqué par James Bennett</a>. 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.</p>
  125. <h3>Des managers pour les requêtes récurrentes</h3>
  126. <p>Ici aussi, c'est déjà traité par Jared Kuolt à <a href="http://superjared.com/entry/django-quick-tip-1-managers/">deux</a> <a href="http://superjared.com/entry/django-quick-tips-15-manager-methods/">reprises</a>. Vous avez même une solution pour <a href="http://www.djangosnippets.org/snippets/562/">enchaîner</a> ces nouvelles fonctions au besoin.</p>
  127. <p>Une autre dernière astuce si vous avez à utiliser vos modèles Django dans un script indépendant de Django&nbsp;:</p>
  128. <pre><code>import os
  129. os.environ['DJANGO_SETTINGS_MODULE'] = 'projetdjango.settings'
  130. from django.conf import settings</code>
  131. </pre>
  132. <p>Vous pouvez ensuite importer vos modèles sans problème normalement.</p>
  133. <p>Enfin, <strong>usez et abusez des ressources fournies par Django</strong> (<em>middlewares</em>, <em>context_processors</em>, <em>managers</em>, etc) et Python (<em>property</em>, <em>list-comprehension</em>, <em>decorator</em>, etc). Ce sont vos meilleurs amis... avec le cache :-).</p>
  134. <p><strong>[edit du 20 mars]</strong>&nbsp;: après une relecture de <a href="http://python.net/~goodger/projects/pycon/2007/idiomatic/">Code Like a Pythonista</a>, j'ai découvert qu'il existait une solution plus simple que <abbr title="Decorate Sort Undecorate">DSU</abbr> pour trier plusieurs modèles Django selon un champ, par exemple la date de billets et de brèves de blog&nbsp;:</p>
  135. <pre><code>self.items = list(Post.published.all()[:10]) + list(Thought.published.all()[:20])</code>
  136. </pre>
  137. <p>Voici la version originale compatible python2.3&nbsp;:</p>
  138. <pre><code>to_sort = [(item.publication_date, item) for item in self.items]
  139. to_sort.sort()
  140. self.items = [item[1] for item in reversed(to_sort)]</code>
  141. </pre>
  142. <p>Et voici celle qui utilise l'argument <strong>key</strong> apparu à la version 2.4&nbsp;:</p>
  143. <pre><code>self.items.sort(key=lambda item: item.publication_date, reverse=True)</code></pre>
  144. <p>Élégant non&nbsp;?</p>
  145. <p><strong>[edit du 26 mars]</strong>&nbsp;: Pour répondre au commentaire de Philippe&nbsp;:</p>
  146. <blockquote><p>Une petite note sur des choses à pas faire comme dans la doc serait peut-être bienvenu&nbsp;? Comme par exemple l'histoire du auto_now_add&nbsp;?</p></blockquote>
  147. <p>Je n'ai plus tout en tête mais effectivement rappeler que <a href="http://www.b-list.org/weblog/2006/nov/02/django-tips-auto-populated-fields/#c2637">auto_now_add est deprecated</a> ne fait pas de mal.</p>
  148. <blockquote><p>Un petit retour d'expérience sur les migrations de schema éventuellement&nbsp;?</p></blockquote>
  149. <p>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 :-).</p>
  150. <blockquote><p>Un petit retour d'expérience sur l'utilisation du trunk pour tous tes projets (notamment la maintenance que ça engendre)&nbsp;?</p></blockquote>
  151. <p>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 <a href="http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges">changements de Django</a> ce qui se fait sans douleur pour l'instant, à part pour le passage en unicode bien sûr...</p>
  152. <p>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&nbsp;: «&nbsp;comment j'ai fait pour coder ça comme ça il y a 6 mois ?! » ;-).</p>