Repository with sources and generator of https://larlet.fr/david/ https://larlet.fr/david/
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

4 роки тому
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. title: ★ Développer une application RESTful avec Django
  2. slug: developper-une-application-restful-avec-django
  3. date: 2007-05-01 19:50:52
  4. type: post
  5. vignette: images/logos/django.png
  6. contextual_title1: Une solution pour faciliter la conception d'applications web RESTful avec Django
  7. contextual_url1: 20070807-une-solution-pour-faciliter-la-conception-d-applications-web-restful-avec-django
  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>Après vous avoir expliqué <a href="https://larlet.fr/david/biologeek/archives/20070413-pour-ne-plus-etre-en-rest-comprendre-cette-architecture/">la théorie sur l'architecture <abbr title="REpresentational State Transfer">REST</abbr></a>, rien de vaut un exemple concret pour bien comprendre le mécanisme. J'ai longtemps hésité entre la classique todolist et un agrégateur pour l'exemple mais j'ai finalement opté pour ce dernier en souvenir d'<a href="https://larlet.fr/david/biologeek/archives/20050820-votre-aggregateur-manquerait-il-de-fonctionnalites/">un projet</a> et pour <a href="https://fluffy.web0.eu/">en aider un autre</a> qui va révolutionner votre notion de l'agrégation (ce sont <a href="http://planet.inertie.org/">eux qui le disent</a> en tout cas...).</p>
  13. <p>Pour commencer un petit avertissement&nbsp;:</p>
  14. <ul>
  15. <li>cet exemple est très basique et ne développe ni toutes les possibilités de <abbr title="REpresentational State Transfer">REST</abbr>, ni toutes les fonctionnalités d'un agrégateur. <strong>Je souhaite juste montrer une implémentation possible avec Django de l'usage des verbes <abbr title="HyperText Transfer Protocol">HTTP</abbr></strong> (GET, POST, PUT et DELETE)&nbsp;;</li>
  16. <li>nous allons nous intéresser aux fils <abbr title="RDF Site Summary">RSS</abbr> et à leur maniement uniquement pour rendre l'exemple relativement générique.</li>
  17. </ul>
  18. <h2>Définition du protocole</h2>
  19. <p>Nous allons suivre les <a href="http://bitworking.org/news/How_to_create_a_REST_Protocol">4 questions à se poser pour concevoir un protocole REST</a> énoncées par Joe Gregorio&nbsp;:</p>
  20. <h3>Quelles sont les <abbr title="Universal Resource Identifier">URI</abbr>&nbsp;?</h3>
  21. <p>Compte-tenu de l'avertissement, on ne s'intéresse qu'aux ressources de type flux RSS, elles sont donc très simples à trouver&nbsp;:</p>
  22. <ul>
  23. <li>/feeds/ pour la liste des items&nbsp;;</li>
  24. <li>/feeds/id/ pour un seul item.</li>
  25. </ul>
  26. <p>Nous verrons qu'il est possible d'en ajouter par la suite.</p>
  27. <h3>Quels sont les formats&nbsp;?</h3>
  28. <p>Nous allons pour cet exemple nous limiter au <abbr title="HyperText Markup Language">HTML</abbr> mais il serait bien sûr intéressant de supporter <acronym>ATOM</abbr>, etc.</p>
  29. <h3>Quelles sont les méthodes supportées par chaque URI&nbsp;?</h3>
  30. <p>Pour /feeds/, il faut supporter les méthodes&nbsp;:</p>
  31. <ul>
  32. <li>GET&nbsp;: pour récupérer la liste des items&nbsp;;</li>
  33. <li>POST&nbsp;: pour ajouter un nouvel item.</li>
  34. </ul>
  35. <p>Pour /feed/id/, il faut supporter les méthodes&nbsp;:</p>
  36. <ul>
  37. <li>GET&nbsp;: pour afficher l'item&nbsp;;</li>
  38. <li>PUT&nbsp;: pour mettre à jour l'item&nbsp;;</li>
  39. <li>DELETE&nbsp;: pour supprimer l'item.</li>
  40. </ul>
  41. <p>Voila qui est suffisant pour le moment.</p>
  42. <h3>Quels sont les indicateurs d'état (status codes) à renvoyer&nbsp;?</h3>
  43. <p>Pour ne pas compliquer les choses, on va laisser ça partiellement de côté pour le moment. On ne va gérer que les erreurs 404 lorsque l'on essaye de modifier/supprimer un item non connu.</p>
  44. <h2>Implémentation avec Django</h2>
  45. <h3>Modèle de données</h3>
  46. <p>Le modèle le plus simple que l'on peut avoir afin présenter un exemple fonctionnel est le suivant&nbsp;:</p>
  47. <pre>class Feed(models.Model):
  48. title = models.CharField(maxlength=200)
  49. url = models.URLField()
  50. state = models.BooleanField()</pre>
  51. <p>Le titre pourra être renseigné à partir de l'URL et state permet de d'activer/désactiver un flux au besoin.</p>
  52. <h3>URL</h3>
  53. <p>Comme convenu dans le protocole, nous avons les deux ressources&nbsp;:</p>
  54. <pre>from views import index, item
  55. urlpatterns = patterns('',
  56. (r'^/feeds/$', index),
  57. (r'^/feeds/(?P&lt;object_id&gt;\d+)/$', item),
  58. )</pre>
  59. <h3>Les vues</h3>
  60. <p>C'est là que ça devient intéressant :-). Nous avons deux vues à définir&nbsp;: <em>index</em> et <em>item</em> qui implémentent les méthodes décrites dans le protocole. Commençons avec index&nbsp;:</p>
  61. <pre>from models import Feed
  62. def index(request):
  63. FeedForm = forms.form_for_model(Feed)
  64. #POST
  65. if request.method == 'POST':
  66. form = FeedForm(request.POST)
  67. if form.is_valid():
  68. data = form.clean_data
  69. feed = Feed()
  70. feed.title = data['title']
  71. feed.url = data['url']
  72. feed.state = data['state']
  73. feed.save()
  74. #GET
  75. return render_to_response("feed/index.html",
  76. {
  77. 'feed_list': Feed.objects.all(),
  78. 'form': FeedForm()
  79. })</pre>
  80. <p>On commence par créer un formulaire à partir du modèle Feed qui va nous servir par la suite. Pour cette vue, il faut traiter les deux verbes GET et POST. Dans le cas de POST, on remplit le formulaire préalablement créé avec les donnée issues de la requête et on vérifie ensuite son intégrité. Si tout va bien, on crée la nouvelle ressource. Dans le cas de GET, on affiche la liste des items ainsi qu'un formulaire pour compléter la liste. Voici le template associé&nbsp;:</p>
  81. <pre>{% if feed_list %}
  82. &lt;ul&gt;
  83. {% for feed in feed_list %}
  84. &lt;li&gt;&lt;a href="{{ feed.get_absolute_url }}"&gt;{{ feed.title }}&lt;/a&gt;&lt;/li&gt;
  85. {% endfor %}
  86. &lt;/ul&gt;
  87. {% endif %}
  88. &lt;form action="." method="post"&gt;
  89. &lt;table&gt;{{ form }}&lt;/table&gt;
  90. &lt;input type="submit" class="send" name="send" value="Ajouter" /&gt;
  91. &lt;/form&gt;</pre>
  92. <p>Je ne pense pas qu'il y ait besoin de décrire ce qu'il se passe ici, le langage de template de django est assez explicite. Chaque ressource est accessible via son URL qui permet d'accéder à la vue item&nbsp;:</p>
  93. <pre>def item(request, object_id):
  94. feed = get_object_or_404(Feed, id = object_id)
  95. FeedForm = forms.form_for_model(Feed)
  96. # PUT
  97. if request.method == 'PUT':
  98. form = FeedForm(request.POST)
  99. if form.is_valid():
  100. data = form.clean_data
  101. feed.title = data['title']
  102. feed.url = data['url']
  103. feed.state = data['state']
  104. feed.save()
  105. # DELETE
  106. elif request.method == 'DELETE':
  107. feed.delete()
  108. return HttpResponseRedirect('/feeds/')
  109. # GET
  110. else:
  111. form = FeedForm(initial={
  112. 'title' : feed.title,
  113. 'url' : feed.url,
  114. 'state' : feed.state,
  115. })
  116. return render_to_response("feed/item.html",
  117. { 'feed': feed, 'form': form })</pre>
  118. <p>Considérant la ressource, on peut grâce à cette vue, soit l'afficher (traitement de POST, le formulaire est pré-rempli), la modifier (traitement de PUT), soit la supprimer (traitement de DELETE, retour à la vue d'index). C'est conforme au protocole.</p>
  119. <p>Attendez, là normalement, si vous avez bien suivi le <a href="https://larlet.fr/david/biologeek/archives/20070413-pour-ne-plus-etre-en-rest-comprendre-cette-architecture/">premier billet consacré à REST</a>, vous devez faire un bond. Bon allez je vous laisse une chance, relisez le code attentivement.</p>
  120. <p>Et oui, <strong>comment est-ce que je peux traiter des PUT et des DELETE alors que le navigateur ne connaît pas ces verbes ?</strong></p>
  121. <p>Il va falloir pour cela définir votre propre middleware django (petit bout de code qui permet de modifier les requêtes à la volée), heureusement le traitement des verbes http a déjà été codé et est <a href="http://www.djangosnippets.org/snippets/174/">disponible sur Django Snippets</a>. Nous n'allons prendre que la partie qui nous intéresse&nbsp;:</p>
  122. <pre>_SUPPORTED_TRANSFORMS = ['PUT', 'DELETE']
  123. _MIDDLEWARE_KEY = 'verb'
  124. class HttpMethodsMiddleware(object):
  125. def process_request(self, request):
  126. if request.POST and request.POST.has_key(_MIDDLEWARE_KEY):
  127. if request.POST[_MIDDLEWARE_KEY].upper() in _SUPPORTED_TRANSFORMS:
  128. request.method = request.POST[_MIDDLEWARE_KEY]
  129. return None</pre>
  130. <p>Ok, maintenant vous comprenez mieux comment est-ce que les méthodes PUT et DELETE peuvent être détectées.</p>
  131. <p>Le template associé est le suivant&nbsp;:</p>
  132. <pre>{{ feed.title }} : {{ feed.url }} ({{ feed.state }})
  133. &lt;form action="" method="post"&gt;
  134. &lt;table&gt;{{ form }}&lt;/table&gt;
  135. &lt;input type="hidden" name="verb" value="PUT" id="id_verb" /&gt;
  136. &lt;input type="submit" class="send" name="send" value="Modifier" /&gt;
  137. &lt;/form&gt;
  138. &lt;form action="" method="post"&gt;
  139. &lt;table&gt;{{ form.as_hidden }}&lt;/table&gt;
  140. &lt;input type="hidden" name="verb" value="DELETE" id="id_verb" /&gt;
  141. &lt;input type="submit" class="send" name="send" value="Supprimer" /&gt;
  142. &lt;/form&gt;</pre>
  143. <p>Je commence par afficher les données relatives à la ressource actuelle puis le formulaire de modification. <strong>Je n'oublie pas le champ caché verb qui me permet de spécifier à la vue qu'il s'agit du verbe http PUT</strong> (la seconde partie du middleware de django snippets permet de faire la modification à la volée si vous êtes intéressé). Enfin je crée un bouton de suppression en cachant le formulaire avec <strong>.as_hidden</strong>.</p>
  144. <p>Et voila, <strong>nous avons notre application simpliste basée sur l'architecture REST</strong>. Vous aurez remarqué que l'implémentation avec Django est un peu fastidieuse et c'est beaucoup plus simple avec Ruby on Rails 1.2. Heureusement, un <a href="http://code.google.com/soc/django/appinfo.html?csaid=63138DE89788093">Google Summer of Code est consacré entièrement à ça</a>&nbsp;! Que du bon en perspective.</p>
  145. <h2>Et pour quelques <abbr title="Lines Of Code">LOC</abbr> de plus...</h2>
  146. <h3>Ajouter une page de confirmation</h3>
  147. <p>Il est souvent d'usage de proposer une page de confirmation avant de supprimer une ressource (et heureusement !). Comment faire avec Django&nbsp;?</p>
  148. <p>C'est relativement simple, on commence par ajouter la ligne suivante aux URL&nbsp;:</p>
  149. <pre>(r'^/feeds/(?P&lt;object_id&gt;\d+)/delete/$', delete),</pre>
  150. <p>Il va falloir ensuite créer une vue <em>delete</em> qui va proposer le bouton <em>je suis sûr de vouloir supprimer cette ressource</em>.</p>
  151. <pre>def delete(request, object_id):
  152. feed = get_object_or_404(Feed, id = object_id)
  153. FeedForm = forms.form_for_model(Feed)
  154. return render_to_response("feed/delete.html",
  155. { 'feed': feed, 'form': FeedForm() })</pre>
  156. <p>Le template est le même que la dernière partie du précédent. Il faut d'ailleurs y modifier le lien vers la suppression pour qu'il pointe vers cette page.</p>
  157. <h3>Modifier rapidement des valeurs booléennes</h3>
  158. <p>Ici, c'est un peu expérimental encore et je sais que ce n'est pas vraiment RESTful mais il est souvent intéressant de pouvoir modifier une ressource avec un simple lien sans avoir à créer tout un formulaire pour l'occasion (surtout lorsqu'il y a de nombreux champs avec des contraintes, etc). Par exemple ici, si l'on veut pouvoir modifier l'état du flux <em>à la volée</em>.</p>
  159. <p>Voila la solution à laquelle je suis arrivé.</p>
  160. <p>Dans les URL, je rajoute la ligne suivante&nbsp;:</p>
  161. <pre>(r'^(?P&lt;object_id&gt;\d+)/edit/(?P&lt;boolean_id&gt;[\w-]+)/$', edit),</pre>
  162. <p>Et la vue ressemble à&nbsp;:</p>
  163. <pre>def edit(request, object_id, boolean_id=None):
  164. feed = get_object_or_404(Feed, id = object_id)
  165. FeedForm = forms.form_for_model(Feed)
  166. if boolean_id is None:
  167. form = FeedForm(initial={
  168. 'title' : feed.title,
  169. 'url' : feed.url,
  170. 'state' : feed.state,
  171. })
  172. return render_to_response("feed/edit.html",
  173. { 'feed': feed, 'form': FeedForm() })
  174. else:
  175. if hasattr(feed, boolean_id):
  176. if getattr(feed, boolean_id):
  177. setattr(feed, boolean_id, False)
  178. else:
  179. setattr(feed, boolean_id, True)
  180. feed.save()
  181. return HttpResponseRedirect(feed.get_absolute_url())</pre>
  182. <p>Voila la solution à laquelle je suis arrivé, ça permet de modifier un état d'une ressource feed avec l'URL&nbsp;: /feeds/1/edit/state/ (pour rester dans l'exemple) mais ça prend toute sa puissance lorsqu'il y a de nombreuses valeurs booléennes. Par ailleurs, la vue /edit/ permet d'afficher un formulaire de modification si l'on ne souhaite pas que celui-ci soit présent sur la page de la ressource (c'est la première partie de la vue edit).</p>
  183. <p><strong>Je vous la soumet pour discussion car j'aimerais bien arriver à quelque chose de mieux</strong>. Il faudrait que je regarde comment font les autres frameworks.</p>