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.

index.html 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <!doctype html>
  2. <html lang=fr>
  3. <head>
  4. <!-- Always define the charset before the title -->
  5. <meta charset=utf-8>
  6. <title>Une solution pour faciliter la conception d&#39;applications web RESTful avec Django — Biologeek — David Larlet</title>
  7. <!-- Define a viewport to mobile devices to use - telling the browser to assume that the page is as wide as the device (width=device-width) and setting the initial page zoom level to be 1 (initial-scale=1.0) -->
  8. <meta name="viewport" content="width=device-width, initial-scale=1"/>
  9. <!-- Fake favicon, to avoid extra request to the server -->
  10. <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  11. <link type="application/atom+xml" rel="alternate" title="Feed" href="/david/log/" />
  12. <link rel="manifest" href="/manifest.json">
  13. <link rel="stylesheet" href="/static/david/css/larlet-david-_J6Rv.css" data-instant-track />
  14. <noscript>
  15. <style type="text/css">
  16. /* Otherwise fonts are loaded by JS for faster initial rendering. See scripts at the bottom. */
  17. body {
  18. font-family: 'EquityTextB', serif;
  19. }
  20. h1, h2, h3, h4, h5, h6, time, nav a, nav a:link, nav a:visited {
  21. font-family: 'EquityCapsB', sans-serif;
  22. font-variant: normal;
  23. }
  24. </style>
  25. </noscript>
  26. <!-- Canonical URL for SEO purposes -->
  27. <link rel="canonical" href="https://larlet.fr/david/biologeek/archives/20070807-une-solution-pour-faciliter-la-conception-d-applications-web-restful-avec-django">
  28. </head>
  29. <body>
  30. <div>
  31. <header>
  32. <nav>
  33. <p>
  34. <small>
  35. Je suis <a href="/david/" title="Profil public">David Larlet</a>, <a href="/david/pro/" title="Activité professionnelle">artisan</a> du web qui vous <a href="/david/pro/accompagnement/" title="Activité d’accompagnement">accompagne</a><span class="more-infos"> dans l’acquisition de savoirs pour concevoir des <a href="/david/pro/produits-essentiels/" title="Qu’est-ce qu’un produit essentiel ?">produits essentiels</a></span>. <span class="more-more-infos">Discutons ensemble d’une <a href="/david/pro/devis/" title="En savoir plus">non-demande de devis</a>.</span> Je partage ici mes <a href="/david/blog/" title="Expériences bienveillantes">réflexions</a> et <a href="/david/correspondances/2017/" title="Lettres hebdomadaires">correspondances</a>.
  36. </small>
  37. </p>
  38. </nav>
  39. </header>
  40. <section>
  41. <h1 property="schema:name">Une solution pour faciliter la conception d&#39;applications web RESTful avec Django</h1>
  42. <article typeof="schema:BlogPosting">
  43. <div property="schema:articleBody">
  44. <img src="/static/david/biologeek/images/logos/django.png" alt="vignette" style="float:left; margin: 0.5em 1em;" property="schema:thumbnailUrl" />
  45. <p>Voila la solution à laquelle je suis arrivé après avoir testé les solutions existantes&nbsp;: <a href="http://code.google.com/p/djangocollection/">django collection</a>, <a href="http://code.google.com/p/django-crudapi/">django crudapi</a> et <a href="http://code.google.com/p/django-restful-model-views/">django restful model views</a>. J'attends beaucoup du <a href="http://code.google.com/p/django-rest-interface/"><abbr title="Google Summer of Code">GSoC</abbr> consacré à l'inclusion native de REST dans Django</a> et les choses vont dans le bon sens de ce côté là mais j'avais besoin d'une solution <em>maintenant</em>. Enfin notez bien le <strong>une</strong> dans le titre qui est très important, il y a énormément d'interprétations de l'architecture <abbr title="Representational State Transfer">REST</abbr>, cette implémentation essaye de s'inspirer de celle définie dans le livre RESTful Web Services&nbsp;: l'<a href="https://larlet.fr/david/biologeek/archives/20070629-architecture-orientee-ressource-pour-faire-des-services-web-restful/">architecture orientée ressource (ROA)</a>.</p>
  46. <h2>Objectif ressource</h2>
  47. <p><strong>Le but principal de cette solution est de simplifier le développement de l'accès à vos ressources. Pour cela une classe générique s'occupe de dispatcher votre requête dans la bonne méthode en fonction du verbe HTTP</strong>. Dans le cas d'une application authentifiée, vous pouvez contrôler les accès aux ressources en fonction des permissions de Django. Enfin la notion d'annulation a été incluse car j'apprécie cette approche (cf. en fin de billet).</p>
  48. <h2>Préparons le terrain</h2>
  49. <p>La première chose à faire est d'utiliser le middleware permettant d'effectuer de fausses requêtes HTTP PUT et DELETE à partir d'un navigateur (ce qui n'est pas possible actuellement)&nbsp;:</p>
  50. <pre>_SUPPORTED_TRANSFORMS = ['PUT', 'DELETE']
  51. _MIDDLEWARE_KEY = '_method'
  52. class HttpMethodsMiddleware(object):
  53. def process_request(self, request):
  54. if request.POST and _MIDDLEWARE_KEY in request.POST:
  55. if request.POST[_MIDDLEWARE_KEY].upper() in _SUPPORTED_TRANSFORMS:
  56. request.method = request.POST[_MIDDLEWARE_KEY].upper()
  57. return None</pre>
  58. <p>Cela permet de passer de convertir le verbe POST en PUT ou DELETE en fonction de l'argument <strong>_method</strong> (généralement caché dans un formulaire).</p>
  59. <p>Les permissions par défaut de Django n'incluent pas la notion d'affichage, il est donc nécessaire d'ajouter cette permission à tous vos modèles. Comme tout ce qui est long et fastidieux avec Django, il existe une solution élégante utilisant les <em>signals</em> pour automatiser tout ça&nbsp;:</p>
  60. <pre>def create_retrieve_permissions(app, created_models, verbosity):
  61. app_models = get_models(app)
  62. for klass in app_models:
  63. ctype = ContentType.objects.get_for_model(klass)
  64. codename = _get_permission_codename('retrieve', klass._meta)
  65. name = 'Can %s %s' % ('retrieve', klass._meta.verbose_name)
  66. p, created = Permission.objects.get_or_create(codename=codename,
  67. content_type__pk=ctype.id,
  68. defaults={'name': name, 'content_type': ctype})
  69. if created and verbosity &gt;= 2:
  70. print "Adding permission '%s'" % p
  71. dispatcher.connect(create_retrieve_permissions, signal=signals.post_syncdb)</pre>
  72. <p>La permission est créée à chaque fin de <strong>syncdb</strong> pour l'ensemble des modèles existants.</p>
  73. <h2>Passons aux choses sérieuses</h2>
  74. <p>Je ne vais pas trop coller de code ici, vous le retrouverez commenté dans le paquet. Je vais plus expliquer les concepts pour comprendre comment l'utiliser.</p>
  75. <p>Nous allons implémenter une todo liste basique permettant d'exploiter les différentes fonctionnalités développées. Nos URI devront avoir le modèle suivant (conformément au <a href="http://bitworking.org/news/wsgicollection">design définit par Joe Gregorio</a>)&nbsp;:</p>
  76. <pre>/{model}/{id};{noun}</pre>
  77. <p>Vous savez qu'une URL se construit avec Django de la façon suivante&nbsp;:</p>
  78. <pre>urlpatterns = patterns('',
  79. (expression régulière, fonction de la vue),
  80. )</pre>
  81. <p>L'idée ici est de passer non pas une fonction à votre pattern mais une classe, ou plus précisément une instance de classe. En instanciant cette classe, vous allez appeler la méthode __call__ de la classe (ça c'est du python) qui va faire tout le travail de redirection vers la bonne méthode de la classe en fonction du verbe HTTP de la requête.</p>
  82. <p>On va donc avoir une définition des URL qui va ressembler à ça&nbsp;:</p>
  83. <pre>urlpatterns = patterns('',
  84. (r'^/todos/(?:(?P&lt;id&gt;\d+)/?)?(?:;(?P&lt;noun&gt;\w+))?/?$',
  85. TodoCollection()),
  86. )</pre>
  87. <p>Ok, maintenant voyons voir ce qui se cache dans TodoCollection&nbsp;:</p>
  88. <pre>class TodoCollection(GenericCollection):
  89. def __call__(self, request, **kw):
  90. return super(TodoCollection, self).__call__(request, 'todo', 'todo', **kw)</pre>
  91. <p>TodoCollection hérite de GenericCollection qui est la classe de base permettant d'automatiser l'accès aux ressources. Seule la fonction __call__ doit être réécrite pour tenir compte du nom de l'application et du modèle (vous pouvez encore plus automatiser en passant ces argument dans l'URL ou dans un dictionnaire mais nous verrons par la suite que cette nouvelle classe peut être utile pour y placer les fonction spécifiques à nos ressources).</p>
  92. <p>Arrivé dans GenericCollection, la requête va être redirigée vers la fonction appropriée selon les équivalences suivantes (notez le caractère facultatif des paramètres <em>id</em> et <em>noun</em> dans l'expression régulière)&nbsp;:</p>
  93. <ul>
  94. <li>si c'est un GET sans id, redirection vers la fonction <strong>list</strong>&nbsp;;</li>
  95. <li>si c'est un POST, redirection vers la fonction <strong>create</strong>&nbsp;;</li>
  96. <li>si c'est un GET avec id, redirection vers la fonction <strong>retrieve</strong>&nbsp;;</li>
  97. <li>si c'est un PUT avec id, redirection vers la fonction <strong>update</strong>&nbsp;;</li>
  98. <li>si c'est un DELETE avec id, redirection vers la fonction <strong>delete</strong>&nbsp;;</li>
  99. </ul>
  100. <p>Dans le cas où il y a argument <strong>noun</strong>, la redirection s'effectue vers la fonction <strong>verbeHTTP_noun</strong> ce qui permet d'ajouter des vues si vous avez des besoins spécifiques, typiquement pour afficher un formulaire de mise à jour d'une ressource vous devrez pointer vers une URI de ce type&nbsp;:</p>
  101. <p>/todos/{id};update_form</p>
  102. <p>qui redirigera vers la fonction <strong>get_update_form</strong> de votre classe Collection.</p>
  103. <p>Vous avez compris le principe&nbsp;? Étudions maintenant une fonctionnalité en particulier (attention la suite est un peu brutale, pour un vrai exemple je vous recommande celui du paquet).</p>
  104. <h2>Trêve de confirmation</h2>
  105. <p>Un <a href="http://www.alistapart.com/articles/neveruseawarning">récent article sur A List Apart</a> évoquait les avantages d'une annulation possible d'une action par rapport à une confirmation préalable. Il est vrai que les messages de confirmation sont toujours frustrants et sont de toute manière tellement banalisés aujourd'hui qu'ils ne sont plus lus par l'utilisateur.</p>
  106. <p>Suite à cet article, une <a href="http://simonwillison.net/2007/Jul/17/undo/">discussion intéressante sur son implémentation dans Django</a> chez Simon Willison a aboutit à <a href="http://nathanostgard.com/archives/2007/7/18/undelete-in-django/">son implémentation</a> chez Nathan Ostgard m'ont permis d'intégrer cette fonctionnalité à moindre coût.</p>
  107. <p>Elle impose tout de même d'ajouter un champ, deux fonctions et un manager à votre modèle&nbsp;:</p>
  108. <pre>class TrashManager(models.Manager):
  109. def get_query_set(self):
  110. return super(TrashManager, self) \
  111. .get_query_set().filter(trashed_at__isnull=True)
  112. def get_trashed(self):
  113. return super(TrashManager, self) \
  114. .get_query_set().filter(trashed_at__isnull=False)
  115. class Todo(models.Model):
  116. [...]
  117. trashed_at = models.DateTimeField(blank=True, null=True, editable=False)
  118. objects = TrashManager()
  119. def trash(self):
  120. self.trashed_at = datetime.now()
  121. self.save()
  122. def restore(self):
  123. self.trashed_at = None
  124. self.save()</pre>
  125. <p>Cela vous permet de placer les ressources «&nbsp;supprimées » dans une corbeille temporaire permettant leur récupération après suppression malencontreuse (l'effet Ooops!). La base doit ensuite être régulièrement purgée ce qui est simplifié par l'accès aux éléments de la corbeille avec le manager&nbsp;:</p>
  126. <pre>model.objects.get_trashed()</pre>
  127. <p>Voyons maintenant ce que ça donne avec ma solution. Ce cas est très particulier et c'est intéressant car on va accéder aux limites de la solution justement. En effet, la suppression doit normalement être effectuée via un DELETE mais dans notre cas une mise dans la corbeille signifie une modification de la ressource et donc un PUT. Cruel dilemme&nbsp;!</p>
  128. <p>Nous allons devoir créer une nouvelle méthode à notre classe gérant ce cas particulier, celle-ci sera accessible via&nbsp;:</p>
  129. <pre>/todos/{id};trash</pre>
  130. <p>qui redirigera vers la fonction (simplifiée pour l'exemple)&nbsp;:</p>
  131. <pre> def put_trash(self):
  132. self.item_instance.trash()
  133. message = _("Item trashed successfully. \
  134. &lt;a href=\"%s;recover?_method=put\" title=\"\"&gt;Restore?&lt;/a&gt; \
  135. " % self.id)
  136. self.request.user.message_set.create(message=message)</pre>
  137. <p>Cette fonction est bien accessible via un PUT ce qui reste correct (si un client à ce service web est implémenté, il utilisera directement DELETE sans avoir besoin de confirmation). Un lien est ensuite proposé à l'utilisateur pour annuler cette «&nbsp;suppression » qui lui est beaucoup moins RESTful...</p>
  138. <p>En effet, pour simplifier la récupération j'ai préféré placer un lien (ce qui n'est pas forcément conforme aux règles d'ergonomie qui stipulent qu'une donnée ne doit être modifiée qu'à travers un formulaire, d'où l'aspect si <del>hideux</del> reconnaissable des boutons de formulaires). Du coup je suis obligé de passer la méthode en argument pour faire un faux PUT, histoire d'être tout de même cohérent au niveau de ma classe&nbsp;:</p>
  139. <pre> def put_recover(self):
  140. self.model.objects.get_trashed().get(pk=self.id).restore()
  141. message = _("Item recovered successfully.")
  142. self.request.user.message_set.create(message=message)
  143. return HttpResponseRedirect('.')</pre>
  144. <p>Ouf, on y est tout de même arrivé (il faut modifier le middleware du début pour qu'il utilise request.REQUEST et non request.POST si vous souhaitez faire du faux PUT avec du GET).</p>
  145. <h2>Conclusion et évolutions</h2>
  146. <p><strong>La solution permettant de gérer des collections génériques est satisfaisante (même s'il manque encore quelques fonctionnalités) et constitue un bon exemple de ce qui peut être fait à la fois avec Django et avec REST et c'est plus dans cette optique que je la publie.</strong></p>
  147. <p>Il est parfois difficile de s'en tenir à une architecture RESTful dans les cas extrêmes et certains impératifs peuvent nécessiter quelques entorses aux règles que vous vous êtes fixé. Le plus important est d'en être conscient et d'assumer ses choix en connaissance de cause. Dans mon exemple sur l'annulation je sais très bien que c'est une fonctionnalité qui ne sera pas utilisée par un programme tiers.</p>
  148. <p>Enfin deux mots sur ce qu'il resterait à implémenter (je reste dans le conditionnel car je pense que le GSoC permettra prochainement de faire la même chose en mieux, du moins je l'espère et je participe en ce sens)&nbsp;:</p>
  149. <ul>
  150. <li>Gérer différents formats de représentation des ressources (json, xml, etc).</li>
  151. <li>Gérer les différentes erreurs possibles avec les codes appropriés (403, etc).</li>
  152. <li>Rendre possible l'utilisation d'une classe avec url_reverse, notamment pour utiliser le tag {% url %} dans les templates.</li>
  153. </ul>
  154. <p>En attendant, vous pouvez toujours <a href="#">télécharger la version actuelle</a> et jouer avec&nbsp;! (n'oubliez pas d'ajouter le dossier à votre $PYTHONPATH...)</p>
  155. </div>
  156. </article>
  157. <footer>
  158. <h6 property="schema:datePublished">— 07/08/2007</h6>
  159. </footer>
  160. </section>
  161. <section>
  162. <div>
  163. <h3>Articles peut-être en rapport</h3>
  164. <ul>
  165. <li><a href="/david/biologeek/archives/20080604-architecture-web-moderne-et-agile/" title="Accès à ★ Architecture web moderne et agile">★ Architecture web moderne et agile</a></li>
  166. <li><a href="/david/biologeek/archives/20101130-de-lopendata-au-linkeddata-exemple-de-nosdonneesfr/" title="Accès à ★ De l&#39;OpenData au LinkedData : exemple de nosdonnees.fr">★ De l&#39;OpenData au LinkedData : exemple de nosdonnees.fr</a></li>
  167. <li><a href="/david/biologeek/archives/20091211-pourquoi-python-et-django/" title="Accès à ★ Pourquoi Python et Django">★ Pourquoi Python et Django</a></li>
  168. </ul>
  169. </div>
  170. </section>
  171. <section>
  172. <div id="comments">
  173. <h3>Commentaires</h3>
  174. <div class="comment" typeof="schema:UserComments">
  175. <p class="comment-meta">
  176. <span class="comment-author" property="schema:creator">vdemeester</span> le <span class="comment-date" property="schema:commentTime">08/08/2007</span> :
  177. </p>
  178. <div class="comment-content" property="schema:commentText">
  179. <p>Salut,<br />
  180. <br />
  181. encore un article super intéressant, au moment où je me posais le même genre de question/problème pour mon application django en cours. (ça fait longtemps que j'ai pas commenté dis donc :D)<br />
  182. <br />
  183. &quot;* Gérer différents formats de représentation des ressources (json, xml, etc).&quot;<br />
  184. <br />
  185. Justement, imaginons que je veuille supporter les différents formats, quelles seraient les solutions pour faire comprendre à nos méthode que c'est tel ou tel format qui est passé ?<br />
  186. <br />
  187. Sinon, et atompub alors, on en parle pas ? :P</p>
  188. </div>
  189. </div>
  190. <div class="comment" typeof="schema:UserComments">
  191. <p class="comment-meta">
  192. <span class="comment-author" property="schema:creator">David, biologeek</span> le <span class="comment-date" property="schema:commentTime">08/08/2007</span> :
  193. </p>
  194. <div class="comment-content" property="schema:commentText">
  195. <p>Salut Vincent,<br />
  196. <br />
  197. Pour les formats, soit dans les headers (mais les crawlers ne vont avoir qu'une seule représentation), soit dans l'url : avant (/json/lasuite) ou après (/lasuite.json) je sais pas encore ce qui est le plus pratique.<br />
  198. <br />
  199. Pour atompub, ça arrive à grand pas : <a href="http://code.google.com/p/django-atompub/" title="http://code.google.com/p/django-atompub/" rel="nofollow">code.google.com/p/django-...</a><br />
  200. <br />
  201. J'en parlerais probablement à ce moment là.</p>
  202. </div>
  203. </div>
  204. <div class="comment" typeof="schema:UserComments">
  205. <p class="comment-meta">
  206. <span class="comment-author" property="schema:creator">Brice Carpentier</span> le <span class="comment-date" property="schema:commentTime">21/08/2007</span> :
  207. </p>
  208. <div class="comment-content" property="schema:commentText">
  209. <p>À noter que le middleware NE FONCTIONNE PAS avec mod_python (request.method y est une propriété uniquement accessible en écriture). Je viens de m'en apercevoir et suis actuellement à la recherche d'un contournement de ce problème.</p>
  210. </div>
  211. </div>
  212. </div>
  213. </section>
  214. <footer>
  215. <nav>
  216. <p>
  217. <small>
  218. Je réponds quasiment toujours aux <a href="m&#x61;ilto:d&#x61;vid%40l&#x61;rlet&#46;fr" title="Envoyer un email">emails</a> (<a href="/david/signature/" title="Ma signature actuelle avec possibilité de chiffrement">signés</a>) et vous pouvez me rencontrer à Montréal. <span class="more-infos">N’hésitez pas à <a href="/david/log/" title="Être tenu informé des mises à jour">vous abonner</a> pour être tenu informé des publications récentes.</span>
  219. </small>
  220. </p>
  221. </nav>
  222. </footer>
  223. </div>
  224. <script src="/static/david/js/larlet-david-3ee43f.js" data-no-instant></script>
  225. <script data-no-instant>InstantClick.init()</script>
  226. </body>
  227. </html>