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

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 :

  • 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.