title: ★ Développer une application RESTful avec Django slug: developper-une-application-restful-avec-django date: 2007-05-01 19:50:52 type: post vignette: images/logos/django.png contextual_title1: Une solution pour faciliter la conception d'applications web RESTful avec Django contextual_url1: 20070807-une-solution-pour-faciliter-la-conception-d-applications-web-restful-avec-django contextual_title2: ★ De l'OpenData au LinkedData : exemple de nosdonnees.fr contextual_url2: 20101130-de-lopendata-au-linkeddata-exemple-de-nosdonneesfr contextual_title3: ★ Pourquoi Python et Django contextual_url3: 20091211-pourquoi-python-et-django
Après vous avoir expliqué la théorie sur l'architecture REST, 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'un projet et pour en aider un autre qui va révolutionner votre notion de l'agrégation (ce sont eux qui le disent en tout cas...).
Pour commencer un petit avertissement :
Nous allons suivre les 4 questions à se poser pour concevoir un protocole REST énoncées par Joe Gregorio :
Compte-tenu de l'avertissement, on ne s'intéresse qu'aux ressources de type flux RSS, elles sont donc très simples à trouver :
Nous verrons qu'il est possible d'en ajouter par la suite.
Nous allons pour cet exemple nous limiter au HTML mais il serait bien sûr intéressant de supporter ATOM, etc.
Pour /feeds/, il faut supporter les méthodes :
Pour /feed/id/, il faut supporter les méthodes :
Voila qui est suffisant pour le moment.
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.
Le modèle le plus simple que l'on peut avoir afin présenter un exemple fonctionnel est le suivant :
class Feed(models.Model): title = models.CharField(maxlength=200) url = models.URLField() state = models.BooleanField()
Le titre pourra être renseigné à partir de l'URL et state permet de d'activer/désactiver un flux au besoin.
Comme convenu dans le protocole, nous avons les deux ressources :
from views import index, item urlpatterns = patterns('', (r'^/feeds/$', index), (r'^/feeds/(?P<object_id>\d+)/$', item), )
C'est là que ça devient intéressant :-). Nous avons deux vues à définir : index et item qui implémentent les méthodes décrites dans le protocole. Commençons avec index :
from models import Feed def index(request): FeedForm = forms.form_for_model(Feed) #POST if request.method == 'POST': form = FeedForm(request.POST) if form.is_valid(): data = form.clean_data feed = Feed() feed.title = data['title'] feed.url = data['url'] feed.state = data['state'] feed.save() #GET return render_to_response("feed/index.html", { 'feed_list': Feed.objects.all(), 'form': FeedForm() })
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é :
{% if feed_list %} <ul> {% for feed in feed_list %} <li><a href="{{ feed.get_absolute_url }}">{{ feed.title }}</a></li> {% endfor %} </ul> {% endif %} <form action="." method="post"> <table>{{ form }}</table> <input type="submit" class="send" name="send" value="Ajouter" /> </form>
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 :
def item(request, object_id): feed = get_object_or_404(Feed, id = object_id) FeedForm = forms.form_for_model(Feed) # PUT if request.method == 'PUT': form = FeedForm(request.POST) if form.is_valid(): data = form.clean_data feed.title = data['title'] feed.url = data['url'] feed.state = data['state'] feed.save() # DELETE elif request.method == 'DELETE': feed.delete() return HttpResponseRedirect('/feeds/') # GET else: form = FeedForm(initial={ 'title' : feed.title, 'url' : feed.url, 'state' : feed.state, }) return render_to_response("feed/item.html", { 'feed': feed, 'form': form })
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.
Attendez, là normalement, si vous avez bien suivi le premier billet consacré à REST, vous devez faire un bond. Bon allez je vous laisse une chance, relisez le code attentivement.
Et oui, comment est-ce que je peux traiter des PUT et des DELETE alors que le navigateur ne connaît pas ces verbes ?
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 disponible sur Django Snippets. Nous n'allons prendre que la partie qui nous intéresse :
_SUPPORTED_TRANSFORMS = ['PUT', 'DELETE'] _MIDDLEWARE_KEY = 'verb' class HttpMethodsMiddleware(object): def process_request(self, request): if request.POST and request.POST.has_key(_MIDDLEWARE_KEY): if request.POST[_MIDDLEWARE_KEY].upper() in _SUPPORTED_TRANSFORMS: request.method = request.POST[_MIDDLEWARE_KEY] return None
Ok, maintenant vous comprenez mieux comment est-ce que les méthodes PUT et DELETE peuvent être détectées.
Le template associé est le suivant :
{{ feed.title }} : {{ feed.url }} ({{ feed.state }}) <form action="" method="post"> <table>{{ form }}</table> <input type="hidden" name="verb" value="PUT" id="id_verb" /> <input type="submit" class="send" name="send" value="Modifier" /> </form> <form action="" method="post"> <table>{{ form.as_hidden }}</table> <input type="hidden" name="verb" value="DELETE" id="id_verb" /> <input type="submit" class="send" name="send" value="Supprimer" /> </form>
Je commence par afficher les données relatives à la ressource actuelle puis le formulaire de modification. Je n'oublie pas le champ caché verb qui me permet de spécifier à la vue qu'il s'agit du verbe http PUT (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 .as_hidden.
Et voila, nous avons notre application simpliste basée sur l'architecture REST. 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 Google Summer of Code est consacré entièrement à ça ! Que du bon en perspective.
Il est souvent d'usage de proposer une page de confirmation avant de supprimer une ressource (et heureusement !). Comment faire avec Django ?
C'est relativement simple, on commence par ajouter la ligne suivante aux URL :
(r'^/feeds/(?P<object_id>\d+)/delete/$', delete),
Il va falloir ensuite créer une vue delete qui va proposer le bouton je suis sûr de vouloir supprimer cette ressource.
def delete(request, object_id): feed = get_object_or_404(Feed, id = object_id) FeedForm = forms.form_for_model(Feed) return render_to_response("feed/delete.html", { 'feed': feed, 'form': FeedForm() })
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.
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 à la volée.
Voila la solution à laquelle je suis arrivé.
Dans les URL, je rajoute la ligne suivante :
(r'^(?P<object_id>\d+)/edit/(?P<boolean_id>[\w-]+)/$', edit),
Et la vue ressemble à :
def edit(request, object_id, boolean_id=None): feed = get_object_or_404(Feed, id = object_id) FeedForm = forms.form_for_model(Feed) if boolean_id is None: form = FeedForm(initial={ 'title' : feed.title, 'url' : feed.url, 'state' : feed.state, }) return render_to_response("feed/edit.html", { 'feed': feed, 'form': FeedForm() }) else: if hasattr(feed, boolean_id): if getattr(feed, boolean_id): setattr(feed, boolean_id, False) else: setattr(feed, boolean_id, True) feed.save() return HttpResponseRedirect(feed.get_absolute_url())
Voila la solution à laquelle je suis arrivé, ça permet de modifier un état d'une ressource feed avec l'URL : /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).
Je vous la soumet pour discussion car j'aimerais bien arriver à quelque chose de mieux. Il faudrait que je regarde comment font les autres frameworks.