viernes, 19 de octubre de 2012

File Uploads en Yii Framework (con "drag and drop")



A continuación presentaré tres alternativas para subir archivos a tu aplicación web: Basica,  Coco y YiiFileManagerFilePicker , ambas tienen sus propios beneficios y sus diferencias. 

Adicionalmente me gustaría presentar el artículo en donde muestro cómo recibir archivos por una vía remota (desde otra aplicación), está relacionado con la subida de archivos porque las personas siempre piensan que la única vía de subir archivos es mediante un widget y la realidad muestra que tu aplicación pudiera proveer un mecanismo para recibir archivos por un canal http.


La manera estandar de subir archivos en Yii Framework

Necesitas leer acerca de estas dos clase: CUploadedFile  y CFileValidator,  la primera es para manejar el archivo subido, y la segunda es para validar el archivo que esta siendo subido.  CUploadedFile se encarga de lidiar con el estandar $_FILES de php.

Paso 1, El modelo para subir un archivo:


2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// protected/models/ImportEmployeesForm.php
class ImportEmployeesForm extends CFormModel {
    public $the_file;
    public function rules()
    {
        return array(
            array('the_file', 'file', 'allowEmpty'=>false,
                'types'=>'csv',
                'maxSize'=>array(1024 * 2000),
                'message'=>'Solo se admiten archivos de texto con extensión CSV'),
        );
    }
    public function attributeLabels(){
        return array(
            "the_file"=>"Archivo a Importar",
        );
    }
}



Paso 2, La vista que aloja al formulario:




<?php $form=$this->beginWidget('bootstrap.widgets.TbActiveForm', array(
    'id'=>'importemployees-form',
    'enableAjaxValidation'=>false,
    'type'=>'horizontal',
    'htmlOptions'=>array("enctype"=>"multipart/form-data"),
)); ?>
    <div class='file-uploader'>
        <?php echo $form->fileFieldRow($model,'the_file'); ?>
    </div>
    <div class='buttons'>
        <?php
        $this->widget('bootstrap.widgets.TbButton', array(
            'buttonType'=>'submit',
            'label'=>'Subir Archivo',
            'type'=>'primary', // null, 'primary', 'info', 'success', 'warning', 'danger' or 'inverse'
            'size'=>'large', // null, 'large', 'small' or 'mini'
            'htmlOptions'=>array(
            ),
        ));
        ?>
    </div>
<?php $this->endWidget(); ?>

Paso 3, La controladora que usa al modelo y la vista para descargar y procesar el archivo subido.

public function actionImportEmployees(){
   $model = new ImportEmployeesForm();
   if(isset($_POST["ImportEmployeesForm"])){
       $model->attributes = $_POST["ImportEmployeesForm"];
       $model->the_file = CUploadedFile::getInstance($model, "the_file");
       if($model->validate()){ // aqui entra CFileValidator
          $filename = $model->the_file->tempName;
          //hacer algo con el archivo recien subido...
       }
    }
    $this->render("importemployees",array('model'=>$model));
}

A continuación otras opciones mas avanzadas:

Coco
Es un widget para implementar en tu formulario que mostrará un multi-file-uploader ajax-based, para recibir los archivos subidos deberás indicarle al widget la ubicación de un método en alguna clase el cual recibirá los archivos. En el caso particular de coco la funcionalidad es heredada de un componente preexistente fabricado por valums/fileuploader.



el widget coco ofrece el boton "find & upload", la lista de archivos bajo el botón aparece cuando se han ido subiendo archivos. permite drag&drop.



YiiFileManagerFilePicker
Es mas avanzado que Coco, también es un multi-file-uploader, también ofrece un widget al igual que Coco, pero agrega mas funcionalidad y extensibilidad, por ejemplo:

1. El usuario tiene un explorador de sus archivos subidos con capacidad de renombrar archivos, eliminarlos, seleccionar varios.


2. La presentación del widget depende de tu diseño, tu fabricas el layout del widget incrustándole las piezas html que el widget pide pero con el estilo y ubicación que tu necesitas.

3. El control del widget se hace desde una sub clase que alojas en tus componentes, donde harás los cambios de código necesarios sin alterar al core del widget.

4. Gracias al subclassing (lee el punto 3) puedes oir eventos, controlar mejor qué ha subido el usuario, qué quiere subir, tipos de archivos admitidos, tamaños, mimetypes y muy importante: control basado en el estado del usuario.

5. Permite aplicar visores para los distintos tipos de archivo, pudiendo tu exportar una URL de algún archivo de usuario para ser visto desde fuera de tu aplicación.

 
Aquí el widget es presentado en modo embedido (como parte de la página) pero puede presentarse también en modo "dialog box" (como una ventana flotante que aparece cuando se requiera)



USANDO COCO FILE UPLOADER

Ir al sitio web de Coco


Se hace en dos partes:

Primero: Pones el widget en la vista o formulario en donde lo requieras.
Segundo: en algun controller pones un action fijo en cualquier controller.

  1. DESCARGA O CLONA COCO.

    Si no usas GIT simplemente copia el contenido de la extension directamente dentro de 'extensions', si usas GIT haz lo siguiente:

    cd /home/blabla/myapp/protected
    mkdir extensions
    cd extensions
    git clone https://bitbucket.org/christiansalazarh/coco.git
  2. SETUP EN CONFIG/MAIN

    Edita tu archivo /protected/config/main.php
    'import'=>array(
            'application.models.*',
            'application.components.*',
            'application.extensions.coco.*',            // <------agrega esto
        ),

  3. CONECTA A "COCO" A TU APLICACION CON UN ACTION ESTATICO

    Edita protected/controllers/siteController.php (aunque puedes usar otra).
    Este action solo es requerido una vez para todo el proyecto !!
    y agregale lo siguiente (coloreado) :

    public function actions()
        {
            return array(
                'captcha'=>array(
                    'class'=>'CCaptchaAction',
                    'backColor'=>0xFFFFFF,
                ),
                'page'=>array(
                    'class'=>'CViewAction',
                ),
                'coco'=>array(
                    'class'=>'CocoAction',
                ),
            );
        }
  4. INSERTA EL WIDGET EN UNA VISTA

    widget('ext.coco.CocoWidget'
            ,array(
                'id'=>'cocowidget1',
                'onCompleted'=>'function(id,filename,jsoninfo){  }',
                'onCancelled'=>'function(id,filename){ alert("cancelled"); }',
                'onMessage'=>'function(m){ alert(m); }',
                'allowedExtensions'=>array('jpeg','jpg','gif','png'),
                'sizeLimit'=>2000000,
                'uploadDir' => 'assets/',
                // para recibir el archivo subido:
                'receptorClassName'=>'application.models.MyModel',
                'methodName'=>'onFileUploaded',
                'userdata'=>$model->primaryKey,
            ));
       ?>
    

¿ CÓMO FUNCIONA COCO ?
  1. Cuando alguien visite la vista en donde insertaste el Widget verás que aparece un botón con el texto que pusiste en el argumento "buttonText", el cual por defecto dice: "Find & Upload".
  2. Alguien podrá arrastrar un archivo a ese botón o podrá darle clic y éste le presentará una caja de selección de archivo a subir.
  3. El usuario envia el archivo, y coco internamente invoca a tu action: index.php?r=site/coco con algunos argumentos. Va a transferir a ese action el archivo a subir mediante llamadas ajax.  Tu no haces nada en este punto, solo mirar como coco lo hace, incluso te presentará una caja de progreso cancelable.
  4. Cuando el action determina que el archivo fue subido (lo sube en la carpeta que tu indicas en el atributo "uploadDir") entonces, hara una de estas dos cosas:

    a) Si no has configurado los argumentos: receptorClassName y methodName entonces Coco dejará el archivo en el directorio assets (o donde tu digas en "uploadDir") y solo recibiras una notificacion via ajax en el metodo javascript que has definido en "onCompleted".

    b) Si has configurado los argumentos: receptorClassName y methodName, entonces coco desde el lado del servidor creara una instancia de la clase que tu configures en "receptorClassName" y luego invocará un método: aquel que pusiste en "methodName".  En ese método podrás recibir la notificacion del archivo subido.

    Por tanto, para que esta opción B funcione deberás crear una clase asi:

    // creas la clase en: protected/models/MyModel.php  (o donde quieras)
    class MyModel {

        public function onFileUploaded($fullFileName,$userdata) {
            // userdata es el mismo valor que pusiste en config/main
            // fullFileName es la ruta del archivo listo para leer.
        }
    }

    y los argumentos a pasarle al widget serían:
                'receptorClassName'=>'application.models.MyModel',
                'methodName'=>'onFileUploaded',


OTRAS OPCIONES DEL WIDGET:

'buttonText'=>'Find & Upload',
'dropFilesText'=>'Drop Files Here !',
'htmlOptions'=>array('style'=>'width: 300px;'),
'defaultControllerName'=>'site',
'defaultActionName'=>'coco',


USANDO YIIFILEMANAGERFILEPICKER FILE UPLOADER & FILE EXPLORER

Ir al sitio web de YiiFileManagerFilePicker

dependencia:  la extensión YiiFileManager  (es muy simple de instalar)

1. puedes descargar la extensión para Yii Framework o clonarla de aquí:

cd yourapp/protected/extensions/
git clone https://bitbucket.org/christiansalazarh/yiifilemanagerfilepicker.git

2. En tu archivo protected/config/main.php agregarás:

'import'=>array(
    'application.models.*',
    'application.components.*',
    'application.extensions.yiifilemanager.*',
    'application.extensions.yiifilemanagerfilepicker.*', // <<--THIS
), 

3. En algún controller (por defecto siteController) debes crear un action que implemente la clase YiiFileManagerFilePickerAction como indico aca, es para que el widget pueda comunicarse con la aplicación.

class SiteController extends Controller {
    public function actions()
    {
    return array(
        'captcha'=>array(
        'class'=>'CCaptchaAction','backColor'=>0xFFFFFF,),
        'page'=>array('class'=>'CViewAction',),
        'yiifilemanagerfilepicker'=>array(
    'class'=>
        'ext.yiifilemanagerfilepicker.YiiFileManagerFilePickerAction'),
    );
    }
...
}
 
4. Debes crear la clase que dará vida al widget. Creala dentro de tus componentes, para facilitar las cosas ya hay una clase lista para ser copiada y usada como patrón, por favor no uses la clase directamente desde la extensión, haz una copia en tus componentes. Los detalles de la clase los puedes conseguir en el instructivo README.md dentro de la extensión.


#copiala desde aqui:
'protected/extensions/yiifilemanagerfilepicker/demo-component/MyYiiFileManViewer.php'

#a tu propia aplicacion:
'protected/components/MyYiiFileManViewer.php'



5. El widget requiere algunos iconos, la extensión trae algunos predefinidos:

# copiar los iconos a tu aplicacion desde aqui:
'yourapp/protected/extensions/yiifilemanagerfilepicker/demo-images'  

# a esta ubicacion:
'tuaplicacion/images/'

6. En alguna vista insertas el codigo HTML que el widget usará para presentar en el la información, aqui puedes dar el formato html requerido, clases CSS etc, pero respetando lo que aquí se presenta:

<div>Select a Background image: <a href='#' id='file-picker'>click here</a>
    <img src='' width='50%' id='selected-image' />
</div>
<div id='file-picker-viewer'>
    <div class='body'></div>
    <hr/>
    <div id='myuploader'>
        <label rel='pin'><b>Upload Files
            <img style='float: left;' src='images/pin.png'></b></label>
        <br/>
        <div class='files'></div>
        <div class='progressbar'>
            <div style='float: left;'>
                Uploading your file(s), please wait...</div>
            <img style='float: left;' src='images/progressbar.gif' />
            <div style=
                'float: left; margin-right:10px;'class='progress'>
            </div>
            <img style='float: left;' class='canceljob' 
                src='images/delete.png' title='cancel the upload'/>
        </div>
    </div>
    <hr/>
    <button id='select_file' class='ok_button'>Select File(s)</button>
    <button id='delete_file' class='delete_button'>
        Delete Selected File(s)</button>
    <button id='close_window' class='cancel_button'>Close Window
        </button>
</div>
<hr/>Logger:<br/><div id='logger'></div>

7. En la misma vista anterior (paso 6) insertas el widget. 
Cuando el usuario hace click en el elemento html #file-picker (en el paso6) entonces aparecerá un file-chooser











Este file-chooser (imagen arriba), aparecera cuando se haga click en el DIV insertado en el paso 6. Para que eso ocurra debes insertar este widget en la misma vista donde pusiste el código del paso6.

Por defecto en este caso el file-chooser aparece en forma embedida, es decir, sin mostrar un dialogbox, para hacerlo con un dialogbox se requiere un minimo de cambios expuestos en el README.md de la extensión (en ingles).



<?php
    // the widget
    //
    $this->widget('application.components.MyYiiFileManViewer'
    ,array(
        // layout selectors:
        'launch_selector'=>'#file-picker',
        'list_selector'=>'#file-picker-viewer',
        'uploader_selector' => '#myuploader',
        // messages:
        'delete_confirm_message' => 'Confirm deletion ?',
        'select_confirm_message' => 'Confirm selected items ?',
        'no_selection_message' => 'You are required to select some file',
        // events:
        'onBeforeAction'=>
            "function(viewer,action,file_ids) { return true; }",
        'onAfterAction'=>
            "function(viewer,action,file_ids, ok, response) { 
                if(action == 'select'){ 
                  // actions: select | delete
                  $.each(file_ids, function(i, item){ 
                  $('#logger').append('file_id='+item.file_id 
                  + ', <img src=\''+item.url+'&size=full\'><br/>');
                });
            }
        }",
        // 'onBeforeLaunch'=>"function(_viewer){ }",
        'onClientSideUploaderError'=>
            "function(messages){ 
                $(messages).each(function(i,m){  alert(m); }); 
            }
        ",
        'onClientUploaderProgress'=>"function(status, progress){
            $('#logger').append(
                'progress: '+status+' '+progress+'%<br/>');
            }",
        ));
?>

Basicamente el widget solo te pide los nombres de los componentes html donde se presentará al file-chooser además de las funciones de escucha de eventos.
 
Lo que hace este widget es insertar eventos jQUERY para que cuando hagas click en el lanzador del paso6 (#file-picker) entonces aparezca un selector de archivos (imagen arriba de esta nota).  

La manera en cómo tu quieres presentar el lanzador de archivos es tu asunto, el widget se enfoca en proveer funcionalidad y tu responsabilidad es la de decir cómo se presenta.

Hay algunos eventos javascript que te gustaría oir, a los cuales puedes adosar funciones JS:

onBeforeAction  #lanzado antes que se seleccionen los archivos
onAfterAction  #lanzado cuando se ha hecho click en seleccionar archivos 
onClientSideUploaderError #cuando en el browser ha ocurrido un problema
onClientUploaderProgress  #para ver el progreso de la subida
¿ Dónde controlas los tipos de archivo requerido y demás asuntos ? en tu clase 'protected/components/MyYiiFileManViewer.php', la cual hiciste en el paso4.

8. Y cómo funciona ?

Recuerdas en el paso6 este extracto de código ?
<div>Select a Background image: <a href='#' id='file-picker'>click here</a>
    <img src='' width='50%' id='selected-image' />
</div>
Eso presentará un link asi: "Select a background image: click here"

Cuando el usuario hace click entonces via jQUERY aparecerá el file-chooser apuntado por el selector jQUERY: "#file-picker" en el cual tu tienes definido el layout html a su vez renderizado por el widget del paso7.

Cuando el usuario hace selección de archivos en el file-chooser tu recibes eventos jquery, cuando ha seleccionado archivos y estos son subidos al servidor tu recibes eventos en la clase del paso4 ('protected/components/MyYiiFileManViewer.php') en donde tu aceptas y controlas.

El usuario puede guardar archivos en su propio espacio web, esto es controlado por otro componente que no he nombrado pero que forma parte de las dependencias de YiiFileManagerFilePicker: 
la extensión YiiFileManager  esta es muy simple de instalar y se encarga de darle al widget los archivos del usuario.

De nuevo, en la clase 'protected/components/MyYiiFileManViewer.php'
es donde tu manejas los archivos seleccionados o subidos.  Recuerda que el usuario puede seleccionar un archivo que previamente habia subido en otra sesión.


Links usados en este post

Cómo recibir archivos via http 

YiiFileManager (low level file manager yiiframework)

YiiFileManagerFilePicker (ajax based file uploader & file explorer yiiframework)

YiiFileManagerRemote

Coco (ajax based file uploader yiiframework)