jeudi 11 avril 2013

[ web2py ] Mettre en place l'envoi multiple de fichiers en AJAX avec le plugin jQuery-file-upload, en moins de 10 minutes

L'objectif de cet article en moins de 2 minutes

Vous souhaitez proposer sur votre site web l'envoi (upload) multiple de fichiers à vos visiteurs. Pour rendre l'utilisation de votre services plus transparent et plus rapide, vous souhaitez faire usage de l'AJAX (envoi des données de façon asynchrone).

Un plugin très efficace a été développé dans ce but : jQuery-file-upload (une démonstration est disponible à cette adresse). La liste des caractéstiques de ce plugin est très complète :

  • Envoi multiple de fichiers
  • Support du Drag & Drop
  • Bar de progession de l'envoi
  • Possibilité d'annuler l'upload en cours
  • Possibilité de reprendre un upload
  • Possiblité de découper les gros fichiers pour l'envoi
  • Redimensionnement des images du côté du client
  • Prévisualisation des images envoyées
  • Aucun plugin navigateur requis (Comme Adobe Flash)
  • Supporte l'envoi des fichiers sur un domaine différent
  • Compatible avec n'importe quelle application web côté serveur, quelle que soit le langage

Ce dernier point nous intéresse pour ce qui suit !

Votre langage de développement favori est Python (heureux soyez-vous!), et parmi les framework de développement web Python, vous avez choisi web2py (commode, performant, pas trop contraignant : bon choix également!).

Nous en venons à la question existentielle a laquelle tentera de répondre cet article :

Comment -diable- intégrer jQuery-file-upload à une application web2py ?Il ne me reste que 8 minutes pour vous donner la solution !

Terminer son café qui a déjà trop refroidi : 1 minute

Profitons-en pour télécharger tout le matériel nécessaire à notre affaire, et mettre en place l'environnement nécessaire !
  1. On commence par télécharger le plugin et si ce n'est pas fait web2py 
  2. On créée une nouvelle application web2py (ou on applique les prochains changements directement à son application si on est téméraire). Passons là-dessus, la création d'une application web2py n'est pas l'objet de ce tutoriel.

La création du modèle dans web2py qui recevra les fichiers : 1 minute

Poursuivons les opérations par créer le modèle qui représentera dans la base de données les fichiers envoyés sur le serveur grâce à jQuery-file-upload.

Vous le faites comme vous les souhaitez, mais basiquement, il nous faut un champ de type upload que je nomme doc, et comme je souhaite que mes utilisateurs envoient des images, je créée un autre champ de type upload qui s'appelle thumb, les fluent in english auront compris que c'est pour stocker le thumbnail de l'image : sa miniature. On prévoit un dernier champ, sizeImg, qui représente la taille de l'image originale.

Les options suivantes sont disponibles et sont bien pratiques :
autodelete, si une ligne dans la base de données est effacée, les fichiers stockés sur le disque seront automatiquement effacés en même temps
uploadseparate, les fichiers envoyés sur le serveurs sont répartis en plusieurs répertoires (et non stockés dans un unique répertoire "uploads") pour améliorer les performances. La documentation de web2py indique que celles-ci sont dégradées au-delà de 1000 fichiers dans le même répertoire. Libre à vous de passer ce paramètre à "False" si vous estimez que ce nombre restera sous les 1000, et ainsi simplifier l'architecture de votre application.

Au final, dans le modèle, on doit se retrouver avec quelque chose de similaire à :

Files = db.define_table('files',
 Field('doc', 'upload', autodelete=True),
 Field('thumb', 'upload', autodelete=True),
 Field('sizeFile', 'float'),
 Field('sessionId', 'string'),)

from smarthumb import SMARTHUMB
box = (200, 200)
Files.thumb.compute = lambda row: SMARTHUMB(row.doc, box)

Vous aurez remarqué les 3 dernières lignes qui contiennent smarthumb (qui provient d'une recette web2py : Generate a thumbnail that fits in a box).

Ce module est utilisé pour générer des miniatures pour les images. On place le fichier sous le répertoire "modules" de votre application. L'avantage de ce module est double : il redimensionne l'image, mais en conservant ses proportions ("crop"). Ces lignes placées sous le modèle permettent d'automatiquement appeler le module lors de l'envoi d'une image et de la redimensionner.

On aurait pu utiliser la fonction thumbnail de PIL (la célèbre librairie de manipulation d'images en Python). Je l'expliquerai un autre jour.

Vite le temps passe ! Que doit-on faire avec le contrôleur ? En 2 minutes !

Dans le contrôleur, il nous faut générer les fonctions qui seront en contact avec le formulaire d'upload du plugin, via AJAX.

Nous avons donc besoin pour faire correctement fonctionner jQuery-file-upload :
  • Du "handler" qui reçoit chaque fichier envoyé par le plugin (en AJAX donc), et qui envoi comme réponse une chaine de caractère au format JSON, requise par le plugin et qui contient les informations telles que : l'adresse du fichier, sa taille, l'adresse de la miniature de l'image et l'adresse pour supprimer le fichier envoyé.
  • Il nous faut donc également une fonction qui prend en charge la suppression d'un fichier envoyé
  • Et pour finir, une fonction qui prend en charge le téléchargement du fichier ou de sa miniature. Et là bonheur ! Web2py l'a déjà prévu avec la fonction "download" dans le contrôleur.
Nous avons donc les lignes suivantes à ajouter au contrôleur :
def upload_file():
        """
        File upload handler for the ajax form of the plugin jquery-file-upload
        Return the response in JSON required by the plugin
        """
        try:
            # Get the file from the form
            f = request.vars['files[]']
            
            # Store file
            id = db.files.insert(doc = db.files.doc.store(f.file, f.filename))
            
            # Compute size of the file and update the record
            record = db.files[id]
            path_list = []
            path_list.append(request.folder)
            path_list.append('uploads')
            path_list.append(record['doc'])
            size =  shutil.os.path.getsize(shutil.os.path.join(*path_list))
            File = db(db.files.id==id).select()[0]
            db.files[id] = dict(sizeFile=size)
            db.files[id] = dict(sessionId=response.session_id)
            
            res = dict(files=[{"name": str(f.filename), "size": size, "url": URL(f='download', args=[File['doc']]), "thumbnail_url": URL(f='download', args=[File['thumb']]), "delete_url": URL(f='delete_file', args=[File['doc']]), "delete_type": "DELETE" }])
            
            return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

        except:
            return dict(message=T('Upload error'))


def delete_file():
        """
        Delete an uploaded file
        """
        try:
            name = request.args[0]
            db(db.files.doc==name).delete()
            return dict(message=T('File deleted'))
        except:
            return dict(message=T('Deletion error'))


def upload():
        return dict()

La fonction upload est uniquement là pour afficher la vue d'exemple.

Il ne nous reste plus qu'à créer la vue ! 4 minutes devraient suffire !


On créée simplement le fichier /views/default/upload.html en relation avec le contrôleur upload pour le front-end du formulaire d'envoi des fichiers. Le formulaire est directement inspiré du modèle proposé par la démonstration du plugin.

Maintenant occupons-nous des fichiers statiques.

  • On crée sous le répertoire static le répertoire jQuery-File-Upload qui contient les fichiers CSS, javascript et les images du plugin. De fait, on copie le contenu des répertoires js, css et img du plugin vers le répertoire que nous venons de créer.
  • On modifie alors toutes les URL faisant référence à ces fichiers statiques dans le fichier upload.html avec la syntaxe de web2py ( {{=URL... )
Le code de cette partie est trop long, vous le trouverez dans mon dépôt bitbucket.

Pour finir, on s'occupe du fichier main.js dans les fichiers statiques du plugin :

On peut le nettoyer pour faire disparaitre les spécificités de la démo du plugin.
Et on ajoute dans ce fichier l'URL de la fonction du contrôleur qui reçoit les fichiers depuis le formulaire (AJAX handler)

On obtient alors le fichier main.js suivant :
$(function () {
    'use strict';

    // Initialize the jQuery File Upload widget:
    $('#fileupload').fileupload({
        // Uncomment the following to send cross-domain cookies:
        //xhrFields: {withCredentials: true},
        url: 'http://127.0.0.1:8000/multiupload_module/default/upload_file',
        autoUpload: 'True',
        maxNumberOfFiles: 3,
        maxFileSize: 2500000,
        acceptFileTypes: '/(\.|\/)(gif|jpe?g|png)$/i',
    });

    // Enable iframe cross-domain access via redirect option:
    $('#fileupload').fileupload(
        'option',
        'redirect',
        window.location.href.replace(
            /\/[^\/]*$/,
            '/cors/result.html?%s'
        )
    );

        // Load existing files:
        $.ajax({
                // Uncomment the following to send cross-domain cookies:
                //xhrFields: {withCredentials: true},
                url: $('#fileupload').fileupload('option', 'url'),
                dataType: 'json',
                context: $('#fileupload')[0]
        }).done(function (result) {
                $(this).fileupload('option', 'done')
                        .call(this, null, {result: result});
        });

});

On lance alors notre serveur qui héberge web2py, on se dirige vers la vue upload, et joie et bonheur, ça fonctionne !

Epilogue (non chronométré)

Si ça ne fonctionne pas (et même si ça fonctionne), vous pouvez jeter un oeil à mon dépôt qui contient une application web2py complète et fonctionnelle avec toutes les modifications apportées qui sont décrites dans cet article. Il n'y a que ces modifications, donc coper-coller la solution ne devrait pas être trop compliqué !

1 commentaire:

  1. Bonjour, serait-il possible d'avoir une copie de l'application web2py de démo avec le plugin file uplod ?. olituks@gmail.com

    RépondreSupprimer