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 :
- cet exemple est très basique et ne développe ni toutes les possibilités de REST, ni toutes les fonctionnalités d'un agrégateur. Je souhaite juste montrer une implémentation possible avec Django de l'usage des verbes HTTP (GET, POST, PUT et DELETE) ;
- nous allons nous intéresser aux fils RSS et à leur maniement uniquement pour rendre l'exemple relativement générique.
Définition du protocole
Nous allons suivre les 4 questions à se poser pour concevoir un protocole REST énoncées par Joe Gregorio :
Quelles sont les URI ?
Compte-tenu de l'avertissement, on ne s'intéresse qu'aux ressources de type flux RSS, elles sont donc très simples à trouver :
- /feeds/ pour la liste des items ;
- /feeds/id/ pour un seul item.
Nous verrons qu'il est possible d'en ajouter par la suite.
Quels sont les formats ?
Nous allons pour cet exemple nous limiter au HTML mais il serait bien sûr intéressant de supporter ATOM, etc.
Quelles sont les méthodes supportées par chaque URI ?
Pour /feeds/, il faut supporter les méthodes :
- GET : pour récupérer la liste des items ;
- POST : pour ajouter un nouvel item.
Pour /feed/id/, il faut supporter les méthodes :
- GET : pour afficher l'item ;
- PUT : pour mettre à jour l'item ;
- DELETE : pour supprimer l'item.
Voila qui est suffisant pour le moment.
Quels sont les indicateurs d'état (status codes) à renvoyer ?
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.
Implémentation avec Django
Modèle de données
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.
URL
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), )
Les vues
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.
Et pour quelques LOC de plus...
Ajouter une page de confirmation
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.
Modifier rapidement des valeurs booléennes
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.
Commentaires
Rik le 02/05/2007 :
Les ... du titre signifieraient-ils que tu doutes du concept révolutionnaire de Fluffy ?
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 ;)
David, biologeek le 02/05/2007 :
Du concept pas du tout, en revanche au niveau de la réalisation je ne me prononce pas :-).
Guillaume le 03/05/2007 :
Salut,
Exemple intéressant en effet. Sur quelles bases associes-tu le PUT à l'UPDATE et le POST au CREATE ?
Y-a-t-il une normalisation de cette approche ?
Merci
David, biologeek le 03/05/2007 :
Salut Guillaume,
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 : www.w3.org/2001/tag/doc/w...
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.
ps : concernant l'attribut verb="PUT ou DELETE" rajouté dans le formulaire, ce n'est par contre pas du tout standard.