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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. title: Une solution pour faciliter la conception d'applications web RESTful avec Django
  2. slug: une-solution-pour-faciliter-la-conception-d-applications-web-restful-avec-django
  3. date: 2007-08-07 01:03:11
  4. type: post
  5. vignette: images/logos/django.png
  6. contextual_title1: ★ Architecture web moderne et agile
  7. contextual_url1: 20080604-architecture-web-moderne-et-agile
  8. contextual_title2: ★ De l'OpenData au LinkedData : exemple de nosdonnees.fr
  9. contextual_url2: 20101130-de-lopendata-au-linkeddata-exemple-de-nosdonneesfr
  10. contextual_title3: ★ Pourquoi Python et Django
  11. contextual_url3: 20091211-pourquoi-python-et-django
  12. <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>
  13. <h2>Objectif ressource</h2>
  14. <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>
  15. <h2>Préparons le terrain</h2>
  16. <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>
  17. <pre>_SUPPORTED_TRANSFORMS = ['PUT', 'DELETE']
  18. _MIDDLEWARE_KEY = '_method'
  19. class HttpMethodsMiddleware(object):
  20. def process_request(self, request):
  21. if request.POST and _MIDDLEWARE_KEY in request.POST:
  22. if request.POST[_MIDDLEWARE_KEY].upper() in _SUPPORTED_TRANSFORMS:
  23. request.method = request.POST[_MIDDLEWARE_KEY].upper()
  24. return None</pre>
  25. <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>
  26. <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>
  27. <pre>def create_retrieve_permissions(app, created_models, verbosity):
  28. app_models = get_models(app)
  29. for klass in app_models:
  30. ctype = ContentType.objects.get_for_model(klass)
  31. codename = _get_permission_codename('retrieve', klass._meta)
  32. name = 'Can %s %s' % ('retrieve', klass._meta.verbose_name)
  33. p, created = Permission.objects.get_or_create(codename=codename,
  34. content_type__pk=ctype.id,
  35. defaults={'name': name, 'content_type': ctype})
  36. if created and verbosity &gt;= 2:
  37. print "Adding permission '%s'" % p
  38. dispatcher.connect(create_retrieve_permissions, signal=signals.post_syncdb)</pre>
  39. <p>La permission est créée à chaque fin de <strong>syncdb</strong> pour l'ensemble des modèles existants.</p>
  40. <h2>Passons aux choses sérieuses</h2>
  41. <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>
  42. <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>
  43. <pre>/{model}/{id};{noun}</pre>
  44. <p>Vous savez qu'une URL se construit avec Django de la façon suivante&nbsp;:</p>
  45. <pre>urlpatterns = patterns('',
  46. (expression régulière, fonction de la vue),
  47. )</pre>
  48. <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>
  49. <p>On va donc avoir une définition des URL qui va ressembler à ça&nbsp;:</p>
  50. <pre>urlpatterns = patterns('',
  51. (r'^/todos/(?:(?P&lt;id&gt;\d+)/?)?(?:;(?P&lt;noun&gt;\w+))?/?$',
  52. TodoCollection()),
  53. )</pre>
  54. <p>Ok, maintenant voyons voir ce qui se cache dans TodoCollection&nbsp;:</p>
  55. <pre>class TodoCollection(GenericCollection):
  56. def __call__(self, request, **kw):
  57. return super(TodoCollection, self).__call__(request, 'todo', 'todo', **kw)</pre>
  58. <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>
  59. <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>
  60. <ul>
  61. <li>si c'est un GET sans id, redirection vers la fonction <strong>list</strong>&nbsp;;</li>
  62. <li>si c'est un POST, redirection vers la fonction <strong>create</strong>&nbsp;;</li>
  63. <li>si c'est un GET avec id, redirection vers la fonction <strong>retrieve</strong>&nbsp;;</li>
  64. <li>si c'est un PUT avec id, redirection vers la fonction <strong>update</strong>&nbsp;;</li>
  65. <li>si c'est un DELETE avec id, redirection vers la fonction <strong>delete</strong>&nbsp;;</li>
  66. </ul>
  67. <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>
  68. <p>/todos/{id};update_form</p>
  69. <p>qui redirigera vers la fonction <strong>get_update_form</strong> de votre classe Collection.</p>
  70. <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>
  71. <h2>Trêve de confirmation</h2>
  72. <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>
  73. <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>
  74. <p>Elle impose tout de même d'ajouter un champ, deux fonctions et un manager à votre modèle&nbsp;:</p>
  75. <pre>class TrashManager(models.Manager):
  76. def get_query_set(self):
  77. return super(TrashManager, self) \
  78. .get_query_set().filter(trashed_at__isnull=True)
  79. def get_trashed(self):
  80. return super(TrashManager, self) \
  81. .get_query_set().filter(trashed_at__isnull=False)
  82. class Todo(models.Model):
  83. [...]
  84. trashed_at = models.DateTimeField(blank=True, null=True, editable=False)
  85. objects = TrashManager()
  86. def trash(self):
  87. self.trashed_at = datetime.now()
  88. self.save()
  89. def restore(self):
  90. self.trashed_at = None
  91. self.save()</pre>
  92. <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>
  93. <pre>model.objects.get_trashed()</pre>
  94. <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>
  95. <p>Nous allons devoir créer une nouvelle méthode à notre classe gérant ce cas particulier, celle-ci sera accessible via&nbsp;:</p>
  96. <pre>/todos/{id};trash</pre>
  97. <p>qui redirigera vers la fonction (simplifiée pour l'exemple)&nbsp;:</p>
  98. <pre> def put_trash(self):
  99. self.item_instance.trash()
  100. message = _("Item trashed successfully. \
  101. &lt;a href=\"%s;recover?_method=put\" title=\"\"&gt;Restore?&lt;/a&gt; \
  102. " % self.id)
  103. self.request.user.message_set.create(message=message)</pre>
  104. <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>
  105. <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>
  106. <pre> def put_recover(self):
  107. self.model.objects.get_trashed().get(pk=self.id).restore()
  108. message = _("Item recovered successfully.")
  109. self.request.user.message_set.create(message=message)
  110. return HttpResponseRedirect('.')</pre>
  111. <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>
  112. <h2>Conclusion et évolutions</h2>
  113. <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>
  114. <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>
  115. <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>
  116. <ul>
  117. <li>Gérer différents formats de représentation des ressources (json, xml, etc).</li>
  118. <li>Gérer les différentes erreurs possibles avec les codes appropriés (403, etc).</li>
  119. <li>Rendre possible l'utilisation d'une classe avec url_reverse, notamment pour utiliser le tag {% url %} dans les templates.</li>
  120. </ul>
  121. <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>