jeudi 11 avril 2013

[Web2py] Implement multiple files upload using AJAX with jQuery-file-upload plugin in less than 10 minutes

This is a translation of an original article in french

The objective of this article in less than 2 minutes

You want to offer on your website to upload multiple files. To make use of your service more transparently and faster, you want to use AJAX (sending data asynchronously).

A very effective plugin has been developed for this purpose:  jQuery-file-upload (a demonstration is available at this link). The list of features of this plugin is very complete:

    • Sending multiple files
    • Support Drag & Drop
    • Bar progession sending
    • Option to cancel the upload in progress
    • Ability to resume an upload
    • Large files can be uploaded in smaller chunks
    • Resizing images on the client side
    • Preview images sent
    • No browser plugin required (like Adobe Flash)
    • Supports sending files to a different domain
    • Compatible with any web application server side, regardless of the language

      This last point interest us for the following!

      Your favorite development language is Python (lucky you are!), And among the Python web development framework, you chose web2py (convenient, efficient, not too restrictive: good choice too!).

      We come to the existential question to which this article will attempt to answer:
      How to integrate jQuery-file-upload in a web2py application ? I only have 8 minutes to give you the solution!

      Finish his coffee that has cooled too much: 1 minute

      During the time we finish the coffe, we can download all the material necessary for our business and implement the necessary environment!
      1. We begin by  downloading the plugin and if it is not done web2py 
      2. We create a new web2py application (or upcoming changes are applied directly to the application if we are foolhardy). We do it quickly, creating an application web2py is not the purpose of this tutorial.

      The creation of the model that will receive uploaded files : 1 minute

      We continue operations by creating the model that represent the database files sent to the server using jQuery-file-upload.

      You do as you like, but basically, we need a field of type "upload" that I call "doc", as I want my users send images, I created another field of type "upload" called "thumb", you will understand that this is to store thumbnail image. We create a final field "sizeImg", which represents the size of the original image.

      The following options are available and are very useful:
      • autodelete, if a row in the database is deleted, the files on the disk will be automatically deleted at the same time
      • uploadseparate, files sent to the server are divided into several directories (and not stored in a single directory "uploads") to improve performance. Web2py documentation indicates that they are degraded beyond 1000 files in the same directory. Feel free to pass this parameter to "False" if you think that this number will remain below 1000, and thus simplify the architecture of your application.

      Finally, in the model, we should end up with something similar to:

      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)
      

      You'll notice the last 3 lines that contain smarthumb (which comes from a recipe web2py: Generate a thumbnail that fits in a box).

      This module is used to generate thumbnails for images. We place the file in the "modules" directory of your application. The advantage of this module is twofold: it resizes the image, but keep its proportions ("crop"). These lines under the model are used to automatically call the module when an image is sent and resize it.

      We could use the thumbnail of PIL (the famous library of image manipulation in Python). I'll explain it another day.

      Time flies! What should we do with the controller? In 2 minutes!

      In the controller, we need to generate functions that will be called by the upload plugin via AJAX.

      What we need to properly operate jQuery-file-upload:
      • The "handler" which receives each file sent by the plugin (so AJAX), and sends response as a string in JSON format, required by the plugin, that contains information such as the address of the file, its size, the address of the thumbnail image and the address to delete the uploaded file.
      • We need a function that also supports the deletion of a file sent.
      • Finally, a function that supports the download of the file or its thumbnail. And there happiness! Web2py has already planned with the "download" function in the controller.
      So we add the following lines to the controller:

      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()
      
      
      Upload function is only there to show the view as an example.

      It remains us to create the view! 4 minutes should be enough!

      We just create the file /views/default/upload.html in connection with the upload controller for front-end form for sending files. The form is directly inspired by the model proposed by the demonstration of the plugin.
      Now let's see the static files.

      • A directory "jQuery-File-Upload" is created under the static directory of web2py, that will contains CSS, javascript and images of the plugin. Then, we copy the contents of directories js, css and img of the plugin to the directory that we just created.
      • We then modify any URLs referring to these static files in Upload.html with the web2py syntax ({{URL ...)
      The code for this part is too long, you will find it in my  repository bitbucket.

      Finally, we take care of main.js file in static files of the plugin:

      • We can delete the specific lines of the demo of the plugin.
      • And add the URL to the controller function that will receive the files from the form (The AJAX handler URL)

      We obtain the following file main.js:
      $(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});
              });
      
      });
      

      We then launched our server hosting web2py, we headed for the upload, and joy and happiness, it works!

      Epilogue (untimed)

      If it does not work (and even if it works), you can take a look at my repository that contains a web2py application complete and functional with all the changes that are described in this article. There is only these changes, so copy-paste the solution should not be too complicated!

      [ 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é !