Repository with sources and generator of https://larlet.fr/david/ https://larlet.fr/david/
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

index.html 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <!doctype html>
  2. <html lang=fr>
  3. <head>
  4. <!-- Always define the charset before the title -->
  5. <meta charset=utf-8>
  6. <title>★ Développer une application 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/20070501-developper-une-application-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">★ Développer une application 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>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>
  46. <p>Pour commencer un petit avertissement&nbsp;:</p>
  47. <ul>
  48. <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>
  49. <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>
  50. </ul>
  51. <h2>Définition du protocole</h2>
  52. <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>
  53. <h3>Quelles sont les <abbr title="Universal Resource Identifier">URI</abbr>&nbsp;?</h3>
  54. <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>
  55. <ul>
  56. <li>/feeds/ pour la liste des items&nbsp;;</li>
  57. <li>/feeds/id/ pour un seul item.</li>
  58. </ul>
  59. <p>Nous verrons qu'il est possible d'en ajouter par la suite.</p>
  60. <h3>Quels sont les formats&nbsp;?</h3>
  61. <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>
  62. <h3>Quelles sont les méthodes supportées par chaque URI&nbsp;?</h3>
  63. <p>Pour /feeds/, il faut supporter les méthodes&nbsp;:</p>
  64. <ul>
  65. <li>GET&nbsp;: pour récupérer la liste des items&nbsp;;</li>
  66. <li>POST&nbsp;: pour ajouter un nouvel item.</li>
  67. </ul>
  68. <p>Pour /feed/id/, il faut supporter les méthodes&nbsp;:</p>
  69. <ul>
  70. <li>GET&nbsp;: pour afficher l'item&nbsp;;</li>
  71. <li>PUT&nbsp;: pour mettre à jour l'item&nbsp;;</li>
  72. <li>DELETE&nbsp;: pour supprimer l'item.</li>
  73. </ul>
  74. <p>Voila qui est suffisant pour le moment.</p>
  75. <h3>Quels sont les indicateurs d'état (status codes) à renvoyer&nbsp;?</h3>
  76. <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>
  77. <h2>Implémentation avec Django</h2>
  78. <h3>Modèle de données</h3>
  79. <p>Le modèle le plus simple que l'on peut avoir afin présenter un exemple fonctionnel est le suivant&nbsp;:</p>
  80. <pre>class Feed(models.Model):
  81. title = models.CharField(maxlength=200)
  82. url = models.URLField()
  83. state = models.BooleanField()</pre>
  84. <p>Le titre pourra être renseigné à partir de l'URL et state permet de d'activer/désactiver un flux au besoin.</p>
  85. <h3>URL</h3>
  86. <p>Comme convenu dans le protocole, nous avons les deux ressources&nbsp;:</p>
  87. <pre>from views import index, item
  88. urlpatterns = patterns('',
  89. (r'^/feeds/$', index),
  90. (r'^/feeds/(?P&lt;object_id&gt;\d+)/$', item),
  91. )</pre>
  92. <h3>Les vues</h3>
  93. <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>
  94. <pre>from models import Feed
  95. def index(request):
  96. FeedForm = forms.form_for_model(Feed)
  97. #POST
  98. if request.method == 'POST':
  99. form = FeedForm(request.POST)
  100. if form.is_valid():
  101. data = form.clean_data
  102. feed = Feed()
  103. feed.title = data['title']
  104. feed.url = data['url']
  105. feed.state = data['state']
  106. feed.save()
  107. #GET
  108. return render_to_response("feed/index.html",
  109. {
  110. 'feed_list': Feed.objects.all(),
  111. 'form': FeedForm()
  112. })</pre>
  113. <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>
  114. <pre>{% if feed_list %}
  115. &lt;ul&gt;
  116. {% for feed in feed_list %}
  117. &lt;li&gt;&lt;a href="{{ feed.get_absolute_url }}"&gt;{{ feed.title }}&lt;/a&gt;&lt;/li&gt;
  118. {% endfor %}
  119. &lt;/ul&gt;
  120. {% endif %}
  121. &lt;form action="." method="post"&gt;
  122. &lt;table&gt;{{ form }}&lt;/table&gt;
  123. &lt;input type="submit" class="send" name="send" value="Ajouter" /&gt;
  124. &lt;/form&gt;</pre>
  125. <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>
  126. <pre>def item(request, object_id):
  127. feed = get_object_or_404(Feed, id = object_id)
  128. FeedForm = forms.form_for_model(Feed)
  129. # PUT
  130. if request.method == 'PUT':
  131. form = FeedForm(request.POST)
  132. if form.is_valid():
  133. data = form.clean_data
  134. feed.title = data['title']
  135. feed.url = data['url']
  136. feed.state = data['state']
  137. feed.save()
  138. # DELETE
  139. elif request.method == 'DELETE':
  140. feed.delete()
  141. return HttpResponseRedirect('/feeds/')
  142. # GET
  143. else:
  144. form = FeedForm(initial={
  145. 'title' : feed.title,
  146. 'url' : feed.url,
  147. 'state' : feed.state,
  148. })
  149. return render_to_response("feed/item.html",
  150. { 'feed': feed, 'form': form })</pre>
  151. <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>
  152. <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>
  153. <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>
  154. <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>
  155. <pre>_SUPPORTED_TRANSFORMS = ['PUT', 'DELETE']
  156. _MIDDLEWARE_KEY = 'verb'
  157. class HttpMethodsMiddleware(object):
  158. def process_request(self, request):
  159. if request.POST and request.POST.has_key(_MIDDLEWARE_KEY):
  160. if request.POST[_MIDDLEWARE_KEY].upper() in _SUPPORTED_TRANSFORMS:
  161. request.method = request.POST[_MIDDLEWARE_KEY]
  162. return None</pre>
  163. <p>Ok, maintenant vous comprenez mieux comment est-ce que les méthodes PUT et DELETE peuvent être détectées.</p>
  164. <p>Le template associé est le suivant&nbsp;:</p>
  165. <pre>{{ feed.title }} : {{ feed.url }} ({{ feed.state }})
  166. &lt;form action="" method="post"&gt;
  167. &lt;table&gt;{{ form }}&lt;/table&gt;
  168. &lt;input type="hidden" name="verb" value="PUT" id="id_verb" /&gt;
  169. &lt;input type="submit" class="send" name="send" value="Modifier" /&gt;
  170. &lt;/form&gt;
  171. &lt;form action="" method="post"&gt;
  172. &lt;table&gt;{{ form.as_hidden }}&lt;/table&gt;
  173. &lt;input type="hidden" name="verb" value="DELETE" id="id_verb" /&gt;
  174. &lt;input type="submit" class="send" name="send" value="Supprimer" /&gt;
  175. &lt;/form&gt;</pre>
  176. <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>
  177. <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>
  178. <h2>Et pour quelques <abbr title="Lines Of Code">LOC</abbr> de plus...</h2>
  179. <h3>Ajouter une page de confirmation</h3>
  180. <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>
  181. <p>C'est relativement simple, on commence par ajouter la ligne suivante aux URL&nbsp;:</p>
  182. <pre>(r'^/feeds/(?P&lt;object_id&gt;\d+)/delete/$', delete),</pre>
  183. <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>
  184. <pre>def delete(request, object_id):
  185. feed = get_object_or_404(Feed, id = object_id)
  186. FeedForm = forms.form_for_model(Feed)
  187. return render_to_response("feed/delete.html",
  188. { 'feed': feed, 'form': FeedForm() })</pre>
  189. <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>
  190. <h3>Modifier rapidement des valeurs booléennes</h3>
  191. <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>
  192. <p>Voila la solution à laquelle je suis arrivé.</p>
  193. <p>Dans les URL, je rajoute la ligne suivante&nbsp;:</p>
  194. <pre>(r'^(?P&lt;object_id&gt;\d+)/edit/(?P&lt;boolean_id&gt;[\w-]+)/$', edit),</pre>
  195. <p>Et la vue ressemble à&nbsp;:</p>
  196. <pre>def edit(request, object_id, boolean_id=None):
  197. feed = get_object_or_404(Feed, id = object_id)
  198. FeedForm = forms.form_for_model(Feed)
  199. if boolean_id is None:
  200. form = FeedForm(initial={
  201. 'title' : feed.title,
  202. 'url' : feed.url,
  203. 'state' : feed.state,
  204. })
  205. return render_to_response("feed/edit.html",
  206. { 'feed': feed, 'form': FeedForm() })
  207. else:
  208. if hasattr(feed, boolean_id):
  209. if getattr(feed, boolean_id):
  210. setattr(feed, boolean_id, False)
  211. else:
  212. setattr(feed, boolean_id, True)
  213. feed.save()
  214. return HttpResponseRedirect(feed.get_absolute_url())</pre>
  215. <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>
  216. <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>
  217. </div>
  218. </article>
  219. <footer>
  220. <h6 property="schema:datePublished">— 01/05/2007</h6>
  221. </footer>
  222. </section>
  223. <section>
  224. <div>
  225. <h3>Articles peut-être en rapport</h3>
  226. <ul>
  227. <li><a href="/david/biologeek/archives/20070807-une-solution-pour-faciliter-la-conception-d-applications-web-restful-avec-django/" title="Accès à Une solution pour faciliter la conception d&#39;applications web RESTful avec Django">Une solution pour faciliter la conception d&#39;applications web RESTful avec Django</a></li>
  228. <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>
  229. <li><a href="/david/biologeek/archives/20091211-pourquoi-python-et-django/" title="Accès à ★ Pourquoi Python et Django">★ Pourquoi Python et Django</a></li>
  230. </ul>
  231. </div>
  232. </section>
  233. <section>
  234. <div id="comments">
  235. <h3>Commentaires</h3>
  236. <div class="comment" typeof="schema:UserComments">
  237. <p class="comment-meta">
  238. <span class="comment-author" property="schema:creator">Rik</span> le <span class="comment-date" property="schema:commentTime">02/05/2007</span> :
  239. </p>
  240. <div class="comment-content" property="schema:commentText">
  241. <p>Les ... du titre signifieraient-ils que tu doutes du concept révolutionnaire de Fluffy ?<br />
  242. <br />
  243. Sinon, la partie protocole est très intéressante pour ce qui nous concerne. Une élaboration plus détaillée serait très intéressante pour tu-sais-quoi ;)</p>
  244. </div>
  245. </div>
  246. <div class="comment" typeof="schema:UserComments">
  247. <p class="comment-meta">
  248. <span class="comment-author" property="schema:creator">David, biologeek</span> le <span class="comment-date" property="schema:commentTime">02/05/2007</span> :
  249. </p>
  250. <div class="comment-content" property="schema:commentText">
  251. <p>Du concept pas du tout, en revanche au niveau de la réalisation je ne me prononce pas :-).</p>
  252. </div>
  253. </div>
  254. <div class="comment" typeof="schema:UserComments">
  255. <p class="comment-meta">
  256. <span class="comment-author" property="schema:creator">Guillaume</span> le <span class="comment-date" property="schema:commentTime">03/05/2007</span> :
  257. </p>
  258. <div class="comment-content" property="schema:commentText">
  259. <p>Salut,<br />
  260. <br />
  261. Exemple intéressant en effet. Sur quelles bases associes-tu le PUT à l'UPDATE et le POST au CREATE ? <br />
  262. Y-a-t-il une normalisation de cette approche ? <br />
  263. <br />
  264. Merci</p>
  265. </div>
  266. </div>
  267. <div class="comment" typeof="schema:UserComments">
  268. <p class="comment-meta">
  269. <span class="comment-author" property="schema:creator">David, biologeek</span> le <span class="comment-date" property="schema:commentTime">03/05/2007</span> :
  270. </p>
  271. <div class="comment-content" property="schema:commentText">
  272. <p>Salut Guillaume,<br />
  273. <br />
  274. C'est en effet un raccourci pour simplifier un peu REST que de cantonner ces deux verbes à la création et à la modification. Si l'on s'en tient aux spécifications du W3C, il y a de nombreuses variantes selon le type de modification par exemple : <a href="http://www.w3.org/2001/tag/doc/whenToUseGet.html" title="http://www.w3.org/2001/tag/doc/whenToUseGet.html" rel="nofollow">www.w3.org/2001/tag/doc/w...</a><br />
  275. <br />
  276. REST est en effet normalisé puisque c'est un peu la base du web. Après appliquer Create Retrieve Update Delete (CRUD) à POST, GET, PUT et DELETE, c'est surtout pour faciliter la compréhension de REST. Si l'on souhaite être plus pointilleux/développer une application basée sur cette architecture, il faut en comprendre les mécanismes fins.<br />
  277. <br />
  278. ps : concernant l'attribut verb=&quot;PUT ou DELETE&quot; rajouté dans le formulaire, ce n'est par contre pas du tout standard.</p>
  279. </div>
  280. </div>
  281. </div>
  282. </section>
  283. <footer>
  284. <nav>
  285. <p>
  286. <small>
  287. 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>
  288. </small>
  289. </p>
  290. </nav>
  291. </footer>
  292. </div>
  293. <script src="/static/david/js/larlet-david-3ee43f.js" data-no-instant></script>
  294. <script data-no-instant>InstantClick.init()</script>
  295. </body>
  296. </html>