Gérer une arborescence d'un catalogue en utilisant Django

Depuis ma 'conversion' à python, j'ai eu - entre autre - l'occasion de découvrir le framework de développement web django. Après avoir fait un peu de tour du marché des framework python open source, c'est vraiment celui qui m'a paru le plus prometteur avec une approche à la fois intelligente et très pragmatique du développement web. Depuis ce temps, je n'avais pas vraiment eu l'occasion de développer un site en particulier, l'occasion s'est présentée il y a environ un mois où j'ai eu le besoin de maquetter un petit site de gestion de catalogue de films (une sorte de proof of concept), pour lequel il a donc fallu constuire une arborescence du catalogue. Voici donc une petite application django qui permet de gérer un catalogue hierarchique.

[cliquez ici pour accéder directement au téléchargement]

Fonctionnalités et technologies utilisées

A ce stade, le catalogue affiche une page web avec un catalogue hierarchique que l'on peut gérer dynamiquement.
Toutes les modifications de ce catalogue sont réalisées en AJAX.
Les fonctionnalités sont donc les suivantes :

Les techno utilisées sont :

Django

Comme à mon habitude, mon but n'est pas de réécrire ce qui a déjà été présenté et expliqué dans d'autres sites bien mieux que je ne pourrais le faire.
Pour débuter sur django et pour aller plus loin ensuite, il existe de plus en plus de ressources sur le net.
Je ne saurais trop vous conseiller de suivre les étapes du book django.
Le site officiel de django qui regroupe tout ce qui est nécesaire :
Ainsi que le site français django-fr
J'ai aussi trouvé beaucoup d'information très intéressantes et pertinentes sur le site de David Larlet : Biologeek

Django et AJAX

Le principal reproche que l'on fait généralement à django (notamment quand on le compare à rails :-) est qu'il ne gère pas les applications AJAX nativement.
Nous allons voir plus loin que faire des applications AJAX avec django non seulement ne pose aucun problème mais en plus est extrèmement simple (comme souvent d'aileurs avec ce framework).

Etape 1, la modélisation des données

Le principe général est que chaque élément est un noeud du catalogue et qu'il a donc un élémént 'père'. Cela forme ainsi une structure hiérarchique potentiellement infinie. Les éléments qui n'ont pas de père sont les éléments en tête du catalogue.



class Catalogue(models.Model):
    node_name = models.CharField(maxlength=200)
    node_parent = models.ForeignKey('self', blank=True, null=True)
    path = models.CharField(maxlength=200, editable=False, null=True)
    indent_level = models.PositiveSmallIntegerField(editable=False, null=True)
    level_order = models.PositiveSmallIntegerField(editable=True, null=True)


l'élément de catalogue a également trois informations supplémentaires qui seront mises à jour automatiquement par l'application :

Ces 3 champs étant mis à jour dynamiquement, il est nécessaire, dans la définition du modèle, de réaliser des actions particulières lors de l'INSERT, l'UPDATE ou le DELETE d'un élémént.
Dans les modèles django, c'est effectué en spécialisant les méthodes save() et delete() : a chaque action de save() [en cas d'INSERT ou UPDATE] ou de delete(), ces méthodes seront appelées et feront les calculs et modifications nécessaires à la mise à jour de ces données.

    def save(self):
        """Redéfinition du save afin de remplir les champs path, indent_level et level_order
        """
        if self.id == None:
          # C'est un insert : On fait un premier save pour récupérer un id
          self.path = ''
          self.indent_level = 0
          #self.level_order=0
          super(Catalogue, self).save() # call the real save()

        # On modifie son path en ajoutant son id au path du parent
        parent = self.node_parent
        if self.node_parent != None:
          self.path=parent.path + '.%s' % self.id
          self.indent_level= parent.indent_level + 1
        else:
          self.path = '%s' % self.id
          self.indent_level = 0
        super(Catalogue, self).save() # call the real save()

        #Ensuite on resauvegarde les enfants pour remettre à jour path et indent_level
        for son in Catalogue.objects.filter(node_parent=self):
          son.save()


    def delete(self):
        """Traitement du delete du catalogue afin notamment de raccrocher les fils au nouveau père
        """
        #Identifier les fils
        sons = Catalogue.objects.filter(node_parent=self)
        for son in sons:
          #pour chaque fils, le rattacher à mon propre pere
          son.node_parent=self.node_parent
          son.save()

        super(Catalogue, self).delete() # call the real delete()


On peut remarquer ici que le delete ne supprime pas l'arborescence qui en sous l'élément effacé. C'est un choix de ma part, à la fois pour des questions de sécurité (eviter l'erreur fatale) et pour des raisons de gestions, cela me parait plus logique au jour le jour d'avoir des manipulations unitaires que des suppressions de catalogues complets.
Toutefois il serait imaginable de coder une sorte de 'super delete' qui supprimerait l'élément et tous ses enfants.

Etape 2, les URLS

L'application et donc les URLS sont d'inspiration REST. Je ne peut pas encore parler de RESTFULL car je n'ai pas encore assez creusé le sujet. A ce sujet les articles de David sur son site, notamment celui ci assez récent sont des très bon points de départ.

La gestion des URLs est déléguée dans l'application, ainsi la config d'urls.py du projet est assez simple :

urlpatterns = patterns('',
    (r'^catalogue/', include('myproject.catalogue.urls')),

le fichier urls.py du projet catalogue est plus intéressant :

from django.conf.urls.defaults import *
from myproject.catalogue import views

urlpatterns = patterns('myproject.catalogue.views',
    (r'xml/?', 'catalogueXML'),
    (r'move/?', 'moveCatalogueItem'),
    (r'insert/?', 'insertCatalogueItem'),
    (r'delete/?', 'deleteCatalogueItem'),
    (r'update/?', 'updateCatalogueItem'),
    (r'/?$', 'catalogue'),

Les URLS globales seront donc de la forme : /catalogue/xml/, etc...

Voici leur rôle :

A chaque URL correspond une vue que nous allons décrire maintenant

Etape 3, Les vues

la vue catalogue

Cette vue est ultra simple et pourrait dans l'absolu être hébergée n'importe où : comme toutes les autres relations entre la page web et le site sont en AJAX, django n'a pas besoin de gérer cette vue. Elle est ajoutée ici pour des raisons de cohérence et de simplicité. Elle ne fait rien d'autre que d'afficher un template de la page catalogue.html. Nous reviendrons sur cette page plus bas car c'est là que se situe tout le code javascript.

la vue catalogueXML

cette vue renvoie une structure XML représentant l'arbre (ou une partie de l'arbre) du catalogue.
Elle prend en argument en POST le 'node_path' du node de l'arbre a partir duquel on souhaite récupérer l'arborescence. Si on n'envoie aucun paramètre c'est l'arbre entier qui est retourné.

A noter : toutes les vues gère à la fois la récupération des arguments en GET ou en POST, c'est principalement pour des raisons de debug.

Le parcours du catalogue en base est réalisé de façon récursive et cette méthode appelle donc une sous méthode récursive (recurseCatalogueXML) qui elle même continue à parcourir les sous éléments du noeud.
Il y a d'autres façon de faire, certainement plus performantes (notamment en utilisant un parcours linéraire en ordonant les résultats du node_path), mais alors les fonctions pour générer la structure XML sont plus complexes à écrire. J'ai choisi ici la lisiblité au détriment de la performance.

def catalogueXML(request):
    node=None
    try:
        if request.method == 'GET':
            debug('catalogueXML - method:GET')
            node_path = request.GET['node_path']
        elif request.method== 'POST':
            debug('catalogueXML - method:POST')
            node_path = request.POST['node_path']
        try:
            node = Catalogue.objects.get(path=node_path)
        except ObjectDoesNotExist:
            node = None
    except KeyError:
        # Pas de request, on part du root catalogue
        node = None

    xml = ''
    xml = recurseCatalogueXML(node)

    t = loader.get_template('catalogue_xml.xml')
    c = Context({
        'snippet': xml,
    })
    return HttpResponse(t.render(c), mimetype='application/xml')



def recurseCatalogueXML(parent):
    if parent == None:
      items_list = Catalogue.objects.filter(node_parent__isnull=True).order_by('level_order')
    else:
      items_list = Catalogue.objects.filter(node_parent=parent).order_by('level_order')

    xml = ''

    for item in items_list:
        tmp_xml = recurseCatalogueXML(item)
        if tmp_xml == '':
            #pas d'enfants
            xml+= '\n<node text="'+item.node_name+'" id="'+item.path+'" />'
        else:
            #des enfants
            xml+= '\n<node text="'+item.node_name+'" id="'+item.path+'">' + tmp_xml + '</node>'

    return xml


la vue updateCatalogueItem

C'est la vue 'active' la plus simple. Elle prend en argument le path de l'item à modifier ainsi que le nouveau nom, récupère l'élément, le modifie et retourne le nouveau nom.

A noter : Pour cette vue et toutes les autres vues de modification, j'ai choisit aussi la simplicité concernant la gestion des valeurs de retour, en renvoyant du texte simple.
Une bonne application bien faite et bien propre renverrait une structure JSON ou XML avec des valeurs normalisées. Je le modifierais peut-être à l'avenir dans ce sens (notamment en permettant de choisir le format de retout JSON ou XML). Il sera de toute façon obligatoire de le faire à partir du moment où il est nécessaire de retourner plusieurs éléments, ce qui est d'ailleurs en théorie le cas ici).

Dans cet exemple, si l'update n'a pas pu être réalisé, la vue renverra 'NOK' et dans les autres cas le nouveau nom du noeud. Il y a donc un problème théorique si on renomme notre noeud en 'NOK' et que tout se passe bien... j'ai un peu triché là dessus côté client, nous verrons tout à l'heure.

def updateCatalogueItem(request):
    if request.method == 'GET':
        debug('updateCatalogueItem - method:GET')
        node_path = request.GET['node_path']
        item_new_name = request.GET['node_name']
    elif request.method== 'POST':
        debug('updateCatalogueItem - method:POST')
        node_path = request.POST['node_path']
        item_new_name = request.POST['node_name']

    debug('item:'+node_path)
    debug('new name:'+item_new_name)

    result=''
    try:
        node_to_be_updated = Catalogue.objects.get(path=node_path)
        node_to_be_updated.node_name = item_new_name
        node_to_be_updated.save()
        result=item_new_name
    except ObjectDoesNotExist:
        result='NOK'

    return HttpResponse(result)

la vue deleteCatalogueItem

Cette vue prends un seul paramètre : le node_path du noeud à supprimer et renvoye 'OK' ou 'NOK'.

En fait cette vue ne fait rien d'intelligent (elle récupère le noeud et le supprime) car toute l'intelligence de traitement (notamment rattacher les noeud fils au père du noeud supprimé) sont fait dans les modèles que nous avons vu tout à l'heure.

def deleteCatalogueItem(request):
    if request.method == 'GET':
        debug('deleteCatalogueItem - method:GET')
        node_path = request.GET['node_path']
    elif request.method== 'POST':
        debug('deleteCatalogueItem - method:POST')
        node_path = request.POST['node_path']

    debug('node_path :'+node_path)

    result=''
    try:
        node_to_be_deleted = Catalogue.objects.get(path=node_path)
        node_to_be_deleted.delete()
        result='OK'
    except ObjectDoesNotExist:
        result='NOK'

    return HttpResponse(result)

 

la vue insertCatalogueItem

Cette vue est un peu plus complexe car elle doit gérer 3 cas différents :
Elle prend donc 3 arguments :

def insertCatalogueItem(request):
    if request.method == 'GET':
        debug('insertCatalogueItem - method:GET')
        new_node_name = request.GET['node_name']
        to_item = request.GET['to_item']
        insert_type = request.GET['insert_type'] # before, after or inside
    elif request.method== 'POST':
        debug('insertCatalogueItem - method:POST')
        new_node_name = request.POST['node_name']
        to_item = request.POST['to_item']
        insert_type = request.POST['insert_type'] # before, after or inside

    debug('node_name :'+new_node_name )
    debug('to:'+to_item)
    debug('type:'+insert_type)


Comme il faut gérer des notions comme avant ou après un noeud, il faut gérer l'ordre, c'est donc pour cela que le champ 'level_order' existe.
Donc pour insérer l'item au bon endroit, il est nécessaire de connaitre son père et son numéro d'ordre à côté de ses frères.
La première chose à faire est donc de déterminer ces éléments en fonction des paramètres :

    #On récupère le père
    if insert_type == 'before':
        new_node_parent=Catalogue.objects.get(path=to_item).node_parent
        new_level_order=Catalogue.objects.get(path=to_item).level_order
    elif insert_type == 'after':
        new_node_parent=Catalogue.objects.get(path=to_item).node_parent
        new_level_order=Catalogue.objects.get(path=to_item).level_order+1
    elif insert_type == 'inside':
        try:
            new_node_parent=Catalogue.objects.get(path=to_item)
        except ObjectDoesNotExist:
            # Il n'y a pas d'objet c'est vraissemblablement le premier
            new_node_parent = None

        try:
            items_list=Catalogue.objects.filter(node_parent=new_node_parent).order_by('-level_order')
            new_level_order = items_list[0].level_order+1
        except IndexError:
            new_level_order = 1


En cas d'insertion before ou after, le parent est le même : c'est le parent du node mis en référence.
En cas d'insertion inside, le parent est directement le node mis en référence.
J'ai choisi pour l'insersion inside d'insérer par défaut en dernière position, il faut donc récupérer quelle est la dernière position des fils du node en référence.

Ensuite, en cas d'insertion before ou after, on doit éventuellement décaler tous les autres items 'frères' d'un cran afin de laisser la place à celui qui arrive :
   
   
# Pour les insert before et after : On update le level order de tous les enregistrements suivant du même niveau
    if insert_type == 'before' or insert_type == 'after':
        items_list = Catalogue.objects.filter(node_parent=new_node_parent, level_order__gte=new_level_order).order_by('level_order')
        for item in items_list:
            item.level_order+=1
            item.save()


Enfin on crée l'élément avec ces valeurs et on retourne le path :

    # On crée l'enregistrement
    p = Catalogue.objects.create(node_name=new_node_name, node_parent=new_node_parent, level_order=new_level_order)

    return HttpResponse(p.path)


la vue moveCatalogueItem

Comme pour l'insert, cette vue va gérer trois types de mouvements : before, after ou inside un élément.

def moveCatalogueItem(request):
    if request.method == 'GET':
        debug('moveCatalogueItem - method:GET')
        item = request.GET['item']
        to_item = request.GET['to_item']
        move_type = request.GET['move_type'] # before, after or inside
    elif request.method== 'POST':
        debug('moveCatalogueItem - method:POST')
        item = request.POST['item']
        to_item = request.POST['to_item']
        move_type = request.POST['move_type'] # before, after or inside

    debug('item:'+item)
    debug('to:'+to_item)
    debug('type:'+move_type)

 
Ensuite, nous allons changer le parent de l'item en fonction des paramètres d'entrées pour le mettre au bon endroit :

    # le père de l'item est maintenant le même père que le to
    # On récupère le père de to
    catalogue_item_to = Catalogue.objects.get(path=to_item)
    # On récupère l'item
    catalogue_item = Catalogue.objects.get(path=item)

    # On met le bon parent
    if move_type == 'inside':
        catalogue_item.node_parent = catalogue_item_to
    else:
        catalogue_item.node_parent = catalogue_item_to.node_parent
    catalogue_item.level_order = 0
    catalogue_item.save()


Enfin on gère l'ordre, en décalant tous les éléments qu'il est nécessaire de décaler.
Cette partie est une des premières que j'avais réalisé, je m'aperçois aujourd'hui qu'elle est largement optimisable (éviter de parcourir tous les éléments)

    items_list = Catalogue.objects.filter(node_parent=catalogue_item.node_parent).order_by('level_order')
    offset = 0
    for item in items_list:
        if item.path == to_item:
            if move_type == 'before':
                offset = 1
                catalogue_item.level_order = item.level_order # on donne le numéro d'ordre à l'item inséré puis on ajoute l'offset aux items suivants
                catalogue_item.save()
            elif move_type == 'after':
                #En insertion after, l'augmentation d'offset des items ne doit se produire qu'au tour suivant, on va donc 'forcer' un tour
                offset = 1
                catalogue_item.level_order = item.level_order+1
                catalogue_item.save()
                continue
            elif move_type == 'inside':
                pass
        if offset != 0:
            debug_string = 'On ajoute %s' % offset + ' a item %s' % item.node_name
            debug(debug_string)
            item.level_order+=offset
            item.save()
        last_level_order = item.level_order

    if offset == 0:
        # on a pas trouvé, donc on insére à la fin
        catalogue_item.level_order = last_level_order+1
        catalogue_item.save()

    result = catalogue_item.path
    return HttpResponse(result)

   

Etape 4, le côté client en javascript

Résumé des flux AJAX

Voici un petit dessin qui résume les différentes interfaces que l'on a définie dans l'application django et donc ce que le côté client doit transmettre et recevoir :

Le code HTML

Le code html est constitué de deux parties :
le div qui va recevoir l'arbre :
  <div id="mytree_catalogue">
  </div>


les formulaires qui servent à gérer les différentes actions possibles sur les éléments du catalogue.
Ces formulaires sont très basiques et très moches. Le but est de montrer les fonctionnalités. Chacun intégrera ces fonctions à sa manière dans son site. Une implémentation sympa serait de gérer le bouton droit quand on est sur un élément de l'arbre et afficher un petit menu avec ces différentes possibilités. C'est éventuellement une évolution que j'apporterais à l'avenir.

Pour plus de lisibilité, j'ai séparé les différentes fonctions en différentes boites :
    <div id="boite1" class="form_div">
        <input type="button" value=" expand all " onclick="tree.expand()" />
        <input type="button" value=" collapse all " onclick="tree.collapse()" />
    </div>
    <div id="boite2" class="form_div">
        <form id="insertForm" action="/catalogue/insert/" method="post" name="insertForm">
            <input type="text" value="" name="node_name" /><br />
            <input type="hidden" value="" name="to_item" />
            <input type="hidden" value="before" name="insert_type" />
            <input type="submit" value=" insert before" name = "button" onclick="$('insertForm').insert_type.value='before';" />
            <input type="submit" value=" insert after" name = "button" onclick="$('insertForm').insert_type.value='after'; " />
            <input type="submit" value=" insert inside" name = "button" onclick="$('insertForm').insert_type.value='inside';" />
        </form>
    </div>
    <div id="boite3" class="form_div">
        <form id="deleteForm" action="/catalogue/delete/" method="post" name="deleteForm">
            <input type="hidden" value="" name="node_path" />
            <input type="submit" value=" delete selected " name= "button" />
        </form>
    </div>
    <div id="boite4" class="form_div">
        <form id="updateForm" action="/catalogue/update/" method="post" name="updateForm">
            <input type="text" value="" name="node_name" />
            <input type="hidden" value="" name="node_path" />
            <input type="submit" value=" update selected " name= "button" />
        </form>
    </div>

Le javascript

Au chargement de la page, la fonction main() est appelée, tout est à l'intérieur.

La première chose à faire est d'initaliser le mootree :


  function main() {
    tree = new MooTreeControl({
            div: 'mytree_catalogue',
            mode: 'files',
            theme: '/site_media/js/mootree.gif',
            grid: true,
            onSelect: function() {
                //alert('coucou');
                $('updateForm').node_name.value = this.selected.text;
                $('updateForm').node_path.value = this.selected.id;

            },
            onReplace: function(from,to,where){
                var myXHR = new XHR({
                    method: 'post',
                    onSuccess: function(new_path) {
                        from.id = new_path;

                    },
                    headers: {'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'}
                    }).send('/catalogue/move/', 'item='+from.id+'&to_item='+to.id+'&move_type='+where);

                //alert([from.id,to.id,where])
            },
        nodeOptions: {
            text: 'Catalogue',
            open: true
        }
        });

L'évènement onReplace est un événement ajouté par moro sur le forum mootools il permet de gérer les déplacement d'items. Donc en cas d'événement de déplacement d'item, on lance cette fonction qui va réaliser un appel AJAX vers le serveur sur l'url /catalogue/move avec les bons paramètres.
En cas de retour ok, on met à jour l'id de l'élément du tree. (dans mootree, j'ai mappé l'id sur le 'path' du modèle de donnée python).
Avec cette seule fonction, on a déjà géré le déplacement d'items au sein de l'arbre (merci à mootree et à moro..)

le onSelect permet de faire des action lorsqu'on sélectionne un élément de l'arbre, cela sert ici à renseigner le champ 'nom de l'arbre' du formulaire de modification (c'est plus pratique et cela évite de le retaper).

Une fois le mootree initialisé, il faut le remplir, cela se fait avec la fonction 'load' :

    tree.root.load('/catalogue/xml/')

N'ayant founi aucun paramètre à /catalogue/xml/, nous chargeons l'arbre en entier comme nous l'avons vu tout à l'heure.

Ensuite il faut gérer les différentes actions des formulaires.

Commençons par le formulaire d'insertion :


    $('insertForm').addEvent('submit', function(e) {
        // Prevent the submit event
        new Event(e).stop();

        // récupère l'item actuellement sélectionné
        if (tree.selected != null) {
            this.to_item.value = tree.selected.id;

            this.send({
                //update: log,
                onComplete: function(path_inserted) {
                    //log.removeClass('ajax-loading');
                    //alert(text);

                    if ($('insertForm').insert_type.value=='before') {
                        node = tree.selected.parent.insert({text:$('insertForm').node_name.value, id:path_inserted});
                        node.injectBefore(tree.selected);
                    }
                    if ($('insertForm').insert_type.value=='after') {
                        node = tree.selected.parent.insert({text:$('insertForm').node_name.value, id:path_inserted});
                        node.injectAfter(tree.selected);
                    }
                    if ($('insertForm').insert_type.value=='inside') {
                        node = tree.selected.insert({text:$('insertForm').node_name.value, id:path_inserted});
                    }
                }
            });
        }
    });

la fonction mootols $('insertForm').addEvent permet d'ajouter un gestionnaire d'évenement sur le formulaire (ici sur l'évènement 'submit'). Donc a chaque fois que le formulaire sera validé, l'événement se déclenchera.
La variable 'this' au sein de cette fonction représente donc le formulaire.

Pour réaliser une insertion, il faut avoir sélectionné un élément de l'arbre.
On le récupère en javascript via tree.selected

ensuite le this.send est un raccouci mootools qui déclenche l'envoi en AJAX du formulaire vers l'url et en utilisant la méthode définie dans le formulaire (dans le code html du formulaire que l'on a vu plus haut).

Au retour, on déclenche l'événement 'onComplete'.
Dans cette fonction, on va ajouter l'élément à l'arbre côté client (car à ce moment il est déjà ajouté côté serveur). L'insertion est différente en fonction des types d'insertion (before, after ou inside), mais le principe est le même : on utilise la méthode 'insert' de mootree.

Ensuite le formulaire de suppression :

Il est construit sur le même principe que l'insertion
 
    $('deleteForm').addEvent('submit', function(e) {
        new Event(e).stop();

        if (tree.selected != null) {
            this.node_path.value = tree.selected.id;
            this.send({
                onComplete: function(result) {
                    // On récupère le père (pour le rechargemetn de la partie du tree)
                    node_parent = tree.selected.parent;
                    // Ici effacer le node du tree
                    tree.selected.remove();
                    // reload du tree
                    // TODO: faire en sorte de ne reloader que à partir du père (nécessite aussi de changer le code serveur)
                    node_parent.load('/catalogue/xml/?node_path='+node_parent.id);
                    //tree.root.load('/catalogue/xml/');
                }
            });
        }

    });

Au retour de l'appel Ajax, une fois l'élément supprimé côté serveur, on le supprime dans l'arbre mootree.
Ensuite on déclenche un reload du sous arbre en partant du père de l'élément sélectionné : ceci est fait parceque la méthode remove() de mootree, contrairement à ce qu'on a choisit de faire côté serveur supprime tous les enfants avec. Donc il faut recharger cette partie de l'arbre.

Ensuite le formulaire de mise à jour :

   $('updateForm').addEvent('submit', function(e) {
        new Event(e).stop();

        if (tree.selected != null) {
            //this.node_path.value = tree.selected.id;
            this.send({
                onComplete: function(result) {
                    // On récupère le père (pour le rechargemetn de la partie du tree)
                    if (result != 'NOK') {
                        tree.selected.text=result;
                        tree.selected.update();
                    } else {
                        node_parent = tree.selected.parent;
                        node_parent.load('/catalogue/xml/?node_path='+node_parent.id);
                    }
                }
            });
        }
    });

Ici il y a un petit truc qui fait echo au côté serveur : nous avons vu précedemment que la fonction côté serveur renvoyait le nouveau nom en cas de réussite et "NOK" en cas d'échec. Donc ici, si la valeur retournée est différente de "NOK", on met à jour le noeud mootree avec cette valeur, mais si le retour est "NOK", nous ne sommes pas certain si c'était une erreur ou si l'utilisateur voulait vraiment renommer son noeud en "NOK", donc on recharge cette partie de l'arbre depuis le serveur, car c'est toujours le serveur qui a raison.
On voit ici les limites de ne renvoyer qu'une seule valeur à l'appel AJAX (qui d'ailleurs est un nom abusif dans ce cas :-)). Dans une version plus évolué de ce programme, il faudra passer à un retour JSON ou XML et bien gérer tous les cas d'erreurs côté client.

Enfin le formulaire de rechargement :


Le principe est simple : on recharge la partie de l'arbre qui est sous l'élément sélectionné :

    $('reloadForm').addEvent('submit', function(e) {
        new Event(e).stop();

        if (tree.selected != null) {
            node = tree.selected;
            node.load('/catalogue/xml/?node_path='+node.id);
        }

    });

Telechargement et Live Démo


Le serveur de chiroux.com n'est pas -encore- compatible python / Django, donc pour héberger la démo j'ai choisi de le mettre sur alwaysdata suite aux conseils de jehaisleprintemps.

Vous pouvez donc trouver la démo ici. Amusez vous bien.

L'admin est également disponible en suivant ce lien. Le login et pass sont : admin/admin

Vous pouvez télécharger l'archive d'un site django complet qui inclu cette seule application 'catalogue' ci-dessous (archive extraite du site de démo) :
FilenameFilesizeDate
. catalogue_django_v0.1.zip 80.83 kB 2008-08-11




Ecrivez un commentaire

  • Les champs obligatoires sont marques d'une *.

Si vous avez du mal a lire le code, cliquez sur l'image du code pour en generer une nouvelle.
Code de securite:
 
Merwok
Posts: 2
Comment
Re: Gérer une arborescence d'un catalogue en utilisant Django
Reply #3 on : Sat March 15, 2008, 20:04:36
« […] il faudrait bien tout de même définir des vues pour chaque ressource et ses méthodes pour chaque action, dont l'action DELETE. On aurait bien un traitement du DELETE dans une view »

Justement, non. Un URI sert à identifier une ressource, pas à indiquer l’action à effectuer sur cette ressource (cette action est exprimée par le verbe HTTP).

Amicalement, Merwok
Thomas
Posts: 1
Comment
Re: Gérer une arborescence d'un catalogue en utilisant Django
Reply #2 on : Sat March 01, 2008, 11:07:28
Merci Merwok pour ton commentaire.
La logique métier du delete n'est pas gérée dans la vue mais bien dans le model (comment traiter les noeuds fils, etc..). Je ne pense donc pas que le modèle MVC soit malmené (la vue finalement ne fait qu'appeller la méthode delete du modèle).

Par contre je suis entièrement d'accord avec toi sur ta réflexion autour de REST. Pour tout te dire, la logique REST m'était étrangère au moment de cet article (en août dernier), mais je me rattrape (et j'ai justement lu l'excellent bouquin 'services web restful' de Richardson et Ruby. Et si je devais refaire ce système d'arborescence, je ne le referais effectivement plus de la même façon :-) (mais il faudrait bien tout de même définir des vues pour chaque ressource et ses méthodes pour chaque action, dont l'action DELETE. On aurait bien un traitement du DELETE dans une view.

Merci aussi pour tes remarques sur la forme, ce n'était en effet pas bon mon objectif de traiter le côté habillage client, mais ce n'est pas une excuse pour faire du mauvais html :-)

Thomas.
Merwok
Posts: 2
Comment
Re: Gérer une arborescence d'un catalogue en utilisant Django
Reply #1 on : Sat March 01, 2008, 02:36:24
Bonjour

Ne semble-t-il pas bizarre qu’une vue soit responsable de supprimer une ressource? Autrement dit, n’y a-t-il pas dans ce code mélange entre vue et contrôleur ? Je n’ai jamais utilisé Django mais j’en ai lu la documentation, et la séparation des couches diverses est un principe de base.

Si on suit REST (c’est-à-dire si on applique HTTP comme cela était prévu), les URI ne font qu’identifier des ressources, les actions étant exprimées par les verbes HTTP (par exemple une requête HTTP DELETE sur /catalogue/42 effacerait l’entrée 42, un PUT sur le même URI crérait l’entrée, etc.). De nombreux articles détaillent bien cela, je conseille en particulier les colonnes « The Restful Web » de Joe Gregorio et « Dive Into XML » de Mark Pilgrim sur XML.com. Suivre ce modèle architectural simplifie la vie du programmeur, qui a moins de choses à coder, ou du moins code de façon plus claire, et permet divers bonus de HTTP comme la mise en cache (si le logiciel serveur est bien écrit).

Bon je sais, vu les limitations des navigateurs actuels, il faut prévoir une API alternative avec des formulaires HTML et du code pour convertir côté serveur ces POST en requêtes spécifiques, mais l’exemple d’Atom (le protocole, pas le format) montre qu’il est possible de dépasser le couple HTML—navigateur en promouvant un format mieux pensé et des clients plus malins.

Je n’ai pas pris le temps de lire tout le code mais j’ai remarqué au passage une erreur HTML : il y a des éléments form qui ont des éléments inline comme enfants, quelques fieldset permettraient de respecter la norme, d’ajouter un peu de style et d’augmenter l’ergonomie. Si on me demandait mon avis j’enlèverais aussi tous les br.

J’espère ne pas être flou ou péremptoire. Je repasserai voir s’il y a des demandes de précision. Cordialement, Merwok