Doff, el Framework Desconectado

En capítulos anteriores se mencionó que la creación de un framework generalmente surge de la identificación de objetos reusables en el desarrollo de software. Posteriormente los objetos identificados constituyen componentes que forman parte de una arquitectura. A éstos se accede mediante una API específica y se añade o modifica su funcionalidad mediante configuración o extensión. Esta forma de obtener un framework implica pasar por varias etapas de maduración en el desarrollo, pero requiere de un punto de partida esencial que es justamente un proyecto que oriente la identificación de las partes reusables.

En el desarrollo del framework desconectado de esta tesina, si bien se contaba con Django como base, se detectó que muchos aspectos no estaban claramente definidos y algunos componentes carecían de sentido en el contexto del navegador. Por esta razón se realizaron dos proyectos [*] que sirvieron para identificar las piezas necesarias del framework y mejorar el enfoque de Protopy, además de permitir crear nuevos componentes reusables.

El framework desconectado, de manera similar a Protopy, se denominó Doff en base a su padre y a la tarea que cumple, tomando la d de Django y off de offline (desconectado). Su nombre significa, por lo tanto, Django Desconectado:

d jango + off line = doff
[*]Inicialmente se creó una aplicación de blog, debido a la popularidad de este tipo de aplicaciones, y luego “salesman” (o agente de ventas viajante) para explotar las posibilidades de contar con una aplicación web desconectada.

En este capítulo se realiza un análisis de los elementos migrados y las modificaciones que se definieron para ejecutar un proyecto en un ambiente desconectado. En el capítulo siguiente se presenta el trabajo realizado para lograr la interacción con un proyecto existente.

Migración de Componentes Básicos

Doff se inició como un conjunto de módulos para Protopy. Estos módulos implementaban diferentes componentes de Django sobre el cliente. Debido a que los módulos de JavaScript son recursos estáticos, se publicaron mediante un servidor de archivos muy simple llamado Aspen, que no requería intervención de Django [AspenWebServer09].

Como se mencionó en el capítulo introductorio, los componentes que requerían una migración directa a JavaScript eran el ORM (django.db.models) y el sistema de plantillas (django.templates). Las vistas y el sistema de URLs deberían ser adaptadas para el contexto del navegador.

[AspenWebServer09]Lawrence Akka, Christopher Baus, Chris Beaven, Steven Brown, Chad Whitacre, Servidor Web Aspen, ultimo acceso Diciembre 2008, http://www.zetadev.com/software/aspen/

El primer componente de Django migrado fue django.db.models, el cual se realizó simplificando algunos aspectos, como el soporte para múltiples bases de datos ya que Gears sólo provee SQLite. La tarea de migración ayudó a perfeccionar el sistema de tipos (clases) y el de módulos. El paquete resultante fue doff.db.models y, gracias a implementar progresivamente la misma API que Django, fue posible realizar el mismo manejo de datos en el cliente y en el servidor.

Las pruebas y depuración del proyecto se realizaron sobre Firebug, cargando Protopy en un archivo estático y realizando los require() correspondientes en la consola. Por ejemplo:

// En la consola de Firebug
>>> require('doff.db.models.base');

El siguiente componente en ser analizado y migrado fue el sistema de plantillas (django.templates). Si bien la librería de JavaScript Dojo provee un sistema de manejo de plantillas basado en la sintaxis de Django [DojoLibDjangoTmpl09], se optó por portar directamente la implementación de Django ya que Dojo no está basado en el sistema de módulos de Protopy. Esta funcionalidad se situó en el módulo doff.templates.

[DojoLibDjangoTmpl09]The Dojo Foundation, Documetnación sobre los templates de Django portados a Dojo, ultimo acceso Septiembre de 2009, http://www.dojotoolkit.org/book/dojo-book-0-9/part-5-dojox/dojox-dtl/

Definición del Proyecto

Luego de haber alcanzado un nivel de funcionalidad básico en doff.db.models y doff.templates se definió la interacción básica con Django, mediante la cual un proyecto en línea sería capaz de transferirse al cliente. Para este fin se creó una aplicación Django llamada offline cuyo objetivo inicial fue publicar estáticamente el código JavaScript de Protopy y Doff.

El siguiente paso fue definir un proyecto Django desconectado, es decir, un proyecto Doff. Se buscó el mayor grado de similitud posible con Django, donde un proyecto es un paquete Python (con los módulos settings.py, manage.py y urls.py) y cada aplicación un paquete dentro de la proyecto o localizable en el PYTHONPATH (una aplicación cuenta con los módulos models.py y views.py).

En Doff, un proyecto se definió como un paquete Protopy con un módulo settings.js con la configuración del proyecto. El módulo de proyecto Django manage.py carece de sentido en el navegador, ya que no se cuenta con una interfaz de comandos en JavaScript, por lo que se implementó la clase doff.core.project.Project. Esta clase define una serie de métodos para interactuar con el proyecto, que cumplen algunas de las funciones del módulo manange.py, y realizar tareas necesarias en el ambiente desconectado. El módulo urls.js se reservó para un análisis posterior.

La clase Project posee los siguientes métodos:

  • bootstrap()

    Se encarga de iniciar el manejo de URLs por parte del proyecto, de mostrar la página inicial y ceder el control de la navegación al framework desconectado.

  • install()

    Se encarga de persistir el framework y el proyecto en el almacenamiento local del cliente. También crea la base de datos, tarea que en la aplicación en línea se realiza mediante la invocación de la orden manage.py syncdb.

  • uninstall()

    Remueve la base de datos, el proyecto y el código del framework del cliente.

Un proyecto se crea de la siguiente manera:

<script type="text/javascript;version=1.7">
    require('doff.core.project', 'new_project');

    var agentes = new_project('agentes', '/off/agentes');
    agentes.bootstrap();
</script>

El primer argumento es el nombre del proyecto y el segundo es la URL que se agrega al sys.path de Protopy, definiendo de esta manera un nuevo paquete en Protopy en el ámbito de nombres global.

Las aplicaciones de un proyecto Doff se definieron como paquetes de Protopy que se encuentran dentro del proyecto. Constan de los módulos views.js y models.js.

Los siguientes apartados tratan sobre las implementaciones de la API del ORM, plantillas, vistas y formularios. Además se hace un análisis de una tarea fundamental del Project que es emular HTTP.

Modelos

Doff implementa la misma API que Django de definición y consultas, realizando adaptaciones a la sintaxis de JavaScript y a la librería Protopy.

Cada modelo es representado por un clase de Protopy que extiende de doff.db.models.base.Model.

A continuación se ejemplifica la implementación en el cliente del módulo models.js de una aplicación que tiene las entidades libro, autor y editor:

// Cargar el módulo del ORM
var models = require('doff.db.models.base');

// La clase editor (Publisher) extiende de models.Model y por lo tanto
// se crea como tabla en la base de datos
var Publisher = type('Publisher', [ models.Model ], {
    name: new models.CharField({ maxlength: 30 }),
    address: new models.CharField({ maxlength: 50 }),
    city: new models.CharField({ maxlength: 60 }),
    state_province: new models.CharField({ maxlength: 30 }),
    country: new models.CharField({ maxlength: 50 }),
    website: new models.URLField()
});

// La clase autor (Author) extiende de models.Model y por lo tanto
// se crea como tabla en la base de datos
var Author = type('Author', [ models.Model ], {
    salutation: new models.CharField({ maxlength: 10 }),
    first_name: new models.CharField({ maxlength: 30 }),
    last_name: new models.CharField({ maxlength: 40 }),
    email: new models.EmailField(),
    headshot: new models.ImageField({ upload_to: '/tmp' })
});

// La clase libro (Book) extiende de models.Model y por lo tanto
// se crea como tabla en la base de datos
var Book = type('Book', [ models.Model ], {
    title: new models.CharField({ maxlength: 100 }),
    authors: new models.ManyToManyField(Author),
    publisher: new models.ForeignKey(Publisher),
    publication_date: new models.DateField()
});

Cada modelo se corresponde con una tabla única de la base de datos, y cada atributo de un modelo, con una columna en esa tabla. El nombre de atributo corresponde al nombre de columna y el tipo de campo, al tipo de columna. Por ejemplo, el modelo Publisher es equivalente a la siguiente tabla:

CREATE TABLE "books_publisher" (
    "id" serial NOT NULL PRIMARY KEY,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);

La excepción a la regla de una clase por tabla es el caso de las relaciones muchos a muchos. Por ejemplo, si un libro tiene uno o más autores y un autor es autor de uno o más libros entonces, en la base de datos, existe una tabla adicional para manejar esta relación. En el modelo Book tiene un ManyToManyField llamado authors, que no existe como columna en la tabla de la base de datos, para representar este situación.

No se define explícitamente una clave primaria en ninguno de estos modelos. A no ser que se le indique lo contrario, Doff dará automáticamente a cada modelo un campo de clave primaria entera autoincremental llamado id.

Para activar los modelos en el proyecto, la aplicación que los contiene debe estar incluida en la lista de aplicaciones instaladas de Doff. En consonancia con Django, esta configuración se realiza en el módulo settings.js, en la variable de configuración INSTALLED_APPS. Suponiendo que este archivo se encuentre en bookstore/models.js, el nombre de la aplicación sería bookstore.

Cuando el usuario instala el proyecto para la operación desconectada en su navegador, el sistema recorre las aplicaciones en INSTALLED_APPS y genera el SQL para cada modelo, creando las tablas en la base de datos.

Una vez que se crea el modelo, Doff provee automáticamente una API JavaScript de alto nivel, muy similar a la de Django, para trabajar con estos modelos.

Además de implementar la API de consultas, el desarrollador puede realizar consultas en SQL sobre la base de datos del cliente mediante las herramientas de desarrollador provistas por Doff.

Inserción y Modificación de Datos

En el siguiente ejemplo se muestra la forma de realizar una inserción de una fila en la base de datos:

>>> require('books.models', 'Publisher');
>>> p1 = new Publisher({ name: 'Addison-Wesley',
            address: '75 Arlington Street',
    city: 'Boston', state_province: 'MA', country: 'U.S.A.',
    website: 'http://www.apress.com/'});
// Guardado de la instancia
>>> p1.save();

Primero se crea una instancia del modelo pasando argumentos nombrados y luego se llama al método save() de la instancia. Esto genera internamente la siguiente sentencia SQL:

INSERT INTO books_publisher
(name , address, city, state_province, country,
website)
VALUES
('Addison-Wesley', '75 Arlington Street', 'Boston', 'MA', 'U.S.A.',
'http://www.apress.com/');

En el caso de Publisher se usa una clave primaria auto incremental id, por lo tanto la llamada inicial a save() hace una cosa más: calcula el valor de la clave primaria para el registro y lo establece como el valor del atributo id de la instancia.

Las subsecuentes llamadas a save() guardarán el registro en su lugar, sin crear un nuevo registro (es decir, ejecutarán la sentencia SQL UPDATE en lugar de un INSERT).

Por ejemplo:

>>> p1.name = "Pearson Education"
>>> p1.save()

se traduce en la siguiente secuencia SQL, asumiendo que el id asignado sea 1:

UPDATE books_publisher
SET name = "Pearson Education"
WHERE id = 1;

Recuperación de Datos

La forma de seleccionar y filtrar los datos es la utilización de Managers o administradores de consultas.

Todos los modelos automáticamente obtienen un administrador objects que debe ser usado cada vez que se quiera consultar sobre una instancia del modelo. El método all() es un método del administrador objects que retorna todas las filas de la base de datos. Al igual que en Django, el valor retornado es un QuerySet. Por ejemplo:

// Recuperación mediante el Manager
>>> publisher_list = Publisher.objects.all();
// Mostrar el resultado de la consulta
>>> print(array(publisher_list));
[<Publisher: Publisher object>]

En el ejemplo la línea Publisher.objects.all() solicita al administrador objects de Publisher que obtenga todos los registros. Internamente esto genera una consulta SQL:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher;

En la última línea del ejemplo anterior, se ve que en el listado no aparece una representación adecuada de la instancia del editor. Esto responde a que en la definición del modelo no se incluyó un método de pasaje a cadena, o __str__. Se puede implementar este método para mejorar la depuración.

En el caso de requerir una fila o conjunto en particular, se utiliza el método filter. Por ejemplo:

>>> Publisher.objects.filter({name : "Apress Publishing"})
[<Publisher: Apress Publishing>]

filter() toma argumentos del tipo arreglo asociativo que son traducidos en las cláusulas SQL WHERE apropiadas, concatenando con el operador AND en el caso de tener más de un parámetro. El ejemplo anterior se traduce en la siguiente consulta:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE name = 'Apress Publishing';

Las claves de los parámetros del arreglo asociativo de filter pueden contener subargumentos separados por dobles guiones bajos, por ejemplo:

>>> Publisher.objects.filter({name__contains : "press"})
[<Publisher: Apress Publishing>]

que busca los editores cuyo nombre contenga la palabra “press”; lo que se traduce en la sentencia SQL:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE name LIKE '%press%';

Existen otros tipos de subargumentos de búsqueda, incluyendo icontains (LIKE no sensible a diferencias de mayúsculas/minúsculas), startswith (busca al comienzo), y endswith (busca al final) y range (consultas SQL BETWEEN). En el apéndice sobre Doff se describen en detalle todos esos tipos de búsqueda.

Cuando se requiere obtener un único objeto dada una condición, los administradores de consultas brindan el método get():

>>> Publisher.objects.get({name : "Apress Publishing"})
<Publisher: Apress Publishing>

En lugar de un QuerySet, este método retorna un objeto individual. Debido a eso, una consulta cuyo resultado sean múltiples objetos o ningún objeto causará una excepción.

Generalmente se necesita que los resultados de los QuerySet tengan un orden determinado por algún campo. Para esto se dispone del método order_by(). Por ejemplo, para un listado de editores ordenados por el campo nombre se debe hacer:

>>> Publisher.objects.order_by("name")
[<Publisher: Addison-Wesley>,
    <Publisher: Apress Publishing>,
    <Publisher: O'Reilly>]

El ejemplo anterior es similar a la invocación de all(), pero la consulta SQL incluye un parámetro extra:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
ORDER BY name;

El ordenamiento puede realizarse por cualquier campo ordenable (cadenas, enteros, fechas, etc.) y además por múltiples campos. Si se antepone al nombre del campo un guión, el ordenamiento es inverso, por ejemplo:

>>> Publisher.objects.order_by("-name")
[<Publisher: O'Reilly>,
    <Publisher: Apress Publishing>,
    <Publisher: Addison-Wesley>]

Cuando el ordenamiento es frecuente, puede incluirse por defecto en la definición del modelo mediante la clase interna Meta:

var Publisher = type('Publisher', [ models.Model ], {
    name: new models.CharField({ maxlength: 30 }),
    //...

    // Clase interna
    Meta: {'ordering': ['name']}
});

Las búsquedas y ordenamientos, por ser métodos que devuelven QuerySet, se pueden encadenar, como se muestra a continuación:

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name")
[<Publisher: O'Reilly>,
    <Publisher: Apress Publishing>,
    <Publisher: Addison-Wesley>]

Cuando se requiere acceder a un registro en particular, se puede usar la siguiente sintaxis:

>>> Publisher.objects.all().get(0) // Primer elemento del QuerySet
<Publisher: Addison-Wesley>

Esto se traduce en la siguiente sentencia SQL:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
ORDER BY name
LIMIT 1;

También se puede utilizar la sintaxis de rebanadas de JavaScript slice(inicio, [fin]), por ejemplo:

// Retorna a partir del primero
>>> Publisher.objects.all().slice(1)
[<Publisher: Apress Publishing>, <Publisher: O'Reilly>]
// Retorna entre las posiciones 1 y 3
>>> Publisher.objects.all().slice(1,3)
[<Publisher: Apress Publishing>]

que genera el siguiente SQL respectivamente:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
OFFSET 1;

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
OFFSET 1 LIMIT 1;

Eliminación de Datos

Para eliminar objetos, simplemente se debe llamar al método delete() sobre una instancia:

>>> p = Publisher.objects.get({ name: "Addison-Wesley" });
>>> p.delete();
>>> array(Publisher.objects.all());
[]

que se traduce en el siguiente SQL:

DELETE FROM books_publisher
WHERE name = 'Adison-Wesley';

Los QuerySet pueden recibir también el método delete() y esto se traduce en la eliminación de todas las filas afectadas por el QuerySet. Por ejemplo:

>>> publishers = Publisher.objects.all();
>>> publishers.delete();
>>> array(Publisher.objects.all());
[]

que se traduce en el siguiente SQL:

DELETE FROM books_publisher;

Plantillas

Doff brinda soporte al sistema de plantillas de Django. Las plantillas escritas para una aplicación en línea son reutilizables sin modificaciones en la aplicación desconectada.

Existen, sin embargo, algunos factores a tener en cuenta a la hora de realizar plantillas para aplicaciones desconectadas. Cuando una aplicación se ejecuta de manera desconectada, el navegador sólo realiza una carga de documento y la navegación se basa en manipulaciones de DOM. Cada vez que se carga una página, en realidad se suprime la anterior y se anexa la nueva en los elementos fake-body y fake-header. Este trabajo es realizado por Doff, con ayuda del paquete dom de Protopy.

A continuación se realiza un breve análisis de la API de plantillas y su utilización en Doff.

Creación

Doff implementa la clase Template para crear plantillas, que se encuentra en el módulo doff.template.base. El argumento para la instanciación del objeto es el texto en crudo de la plantilla. En el siguiente ejemplo t es un objeto Template listo para ser procesado:

>>> require('doff.template.base', 'Template');
>>> t = new Template("Mi nombre es {{ name }}.");
>>> print(t);

En caso de encontrar errores en el análisis de la plantilla, la instanciación de Template falla. Los errores contemplados son:

  • Bloques de etiquetas inválidos
  • Argumentos inválidos de una etiqueta válida
  • Filtros inválidos
  • Argumentos inválidos para filtros válidos
  • Sintaxis de plantilla inválida
  • Etiquetas de bloque sin cerrar (para etiquetas de bloque que requieran la etiqueta de cierre)

En todos los casos, el sistema lanza una excepción TemplateSyntaxError.

Contexto

Para obtener la salida procesada de una instancia de Template, es necesario proveer un contexto. Un contexto es simplemente un conjunto de variables y sus valores asociados. Una objeto plantilla usa las variables para rellenar la plantilla evaluando las etiquetas.

El contexto está representado en el tipo Context, el cual se encuentra en el módulo doff.template.base. La construcción del objeto toma como argumento opcional un arreglo asociativo. La llamada al método render() de la instancia de Template con el contexto como argumento “rellena” la plantilla, por ejemplo:

>>> requiere('doff.template.base', 'Context', 'Template');
// Crear plantilla
>>> t = new Template("Mi nombre es {{ name }}.")
// Crear contexto
>>> c = new Context({"name": "Pedro"})
// Procesar plantilla
>>> t.render(c)
'My name is Pedro.'

El objeto Template puede ser procesado con múltiples contextos, obteniendo así salidas diferentes para la misma plantilla. Por cuestiones de eficiencia es conveniente crear un objeto Template y luego llamar a render() sobre éste muchas veces:

# Código ineficiente
for each (var name in ['John', 'Julie', 'Pat']) {
    var t = new Template('Hello, {{ name }}');
    print(t.render(new Context({'name': name})));
}

# Código eficiente
t = new Template('Hello, {{ name }}');
for each (var name in ['John', 'Julie', 'Pat'])
    print(t.render(new Context({'name': name})));

Al igual que en Django el objeto Context puede contener variables más complejas. La forma de inspeccionar éstas es con el operador . (punto). Usando el punto se puede acceder a objetos, atributos, índices, o métodos de un objeto.

En estos casos, cuando el sistema de plantillas encuentra un punto en una variable, el orden de búsqueda es el siguiente:

  • Arreglo asociativo (por ej. foo["bar"])
  • Atributo (por ej. foo.bar)
  • Llamada de método (por ej. foo.bar())
  • Índice de arreglos (por ej. foo[bar])

Por ejemplo, ante la siguiente plantilla:

{{ x.elemento }}

al momento de ser procesada, inicialmente se comprobará si x es un arreglo asociativo y se reemplazará por el contenido de x["elemento"], si no existe la clave o no se trata de un arreglo asociativo, se intentará obtener el atributo, simplemente x.elemento. Si en la búsqueda como atributo, se encuentra un método, se lo ejecutará y se utilizará su salida para el remplazo. En el caso de no poder realizar el reemplazo por ninguna de las anteriores, buscará elemento como índice de arreglos. Tiene sentido cuando lo que se encuentra tras el punto es un número.

Si una variable no existe en el contexto, el sistema de plantillas la procesa como una cadena vacía. Es posible cambiar este comportamiento modificando el valor de la variable de configuración TEMPLATE_STRING_IF_INVALID en el módulo settings.js.

Cargador de Plantillas

Si bien en los ejemplos anteriores se mostró la API de plantillas con cadenas como argumentos, las plantillas pueden estar almacenadas en archivos. Para Doff éstos son recursos estáticos y existe un sistema para la carga similar al de Django. Para tal fin en el módulo settings.js se establece el valor TEMPLATE_URL.

Se muestra a continuación un ejemplo de una vista que retorna HTML generado por una plantilla:

// Importar las clases Template y Context
require('doff.template.base', 'Template', 'Context');
// Importar la clase HttpResponse
require('doff.utils.http', 'HttpResponse');
// Importar la clase get_template
require('doff.template.loader', 'get_template');

// Definición de una vista
function current_datetime(request) {
    // Cargar la vista
    var t = get_template('mytemplate.html');
    // Renderización sobre la variable html con el contexto con una
    // fecha
    html = t.render(new Context({'current_date': new Date()}))
    // Retornar la respuesta en una respuesta http
    return new HttpResponse(html);
}

En esta vista se utiliza la API para cargar plantillas, a la cual se accede mediante la función get_template().

Existen varios cargadores de plantillas que se pueden habilitar en el archivo de configuración. En la presente tesina se utiliza sólo el de carga de plantillas a través de URLs. Por defecto, TEMPLATE_URL es una cadena vacía, en cuyo caso la búsqueda de la plantilla se realizará en templates/ a partir de donde se instanció el proyecto.

Emulación de HTTP

En una aplicación en línea, con cada click sobre un enlace, envío de formulario o solicitud asincrónica (AJAX) se realiza un requerimiento al servidor, es decir se traduce en una solicitud HTTP.

Cuando la aplicación se encuentra desconectada, es decir, se ha instanciado el proyecto, Doff utiliza dos clases para emular HTTP y el comportamiento predefinido del navegador. Estas clases son: DOMAdapter y LocalHandler.

DOMAdapter se encarga de crear un documento falso en el DOM, un historial y administrar la interacción con el usuario, generando instancias de Request con los eventos de los enlaces y formularios, que son enviadas al LocalHandler como Request emulando peticiones HTTP.

Cuando se recibe una petición en Django, ésta es procesada por una instancia de la clase Handler. El equivalente en el proyecto desconectado es LocalHandler, que se encarga de procesar las peticiones que envía el DOMAdapter.

El LocalHandler se encarga de realizar el pasaje del Request por los diferentes middlewares, como indica la figura 2.6 del capítulo de tecnologías del servidor. En este proceso se llama a la vista.

La salida de la vista se envuelve en un objeto Response, equivalente a la respuesta HTTP, que es devuelto al DOMAdapter.

Cuando el DOMAdapter recibe la respuesta, realiza las tareas de actualización de la fachada del navegador: actualizar el documento falso, generar una entrada en el historial y conectarse a los eventos que puedan generar los links y formularios.

_svg/esquema_domadapter.png

Diagrama de comunicación entre el documento y el sistema de emulación HTTP de DOMAapter y su comunicación con el LocalHandler.

Con cada entrada en el historial se modifica el enlace (hash) de la URL. La instancia de History captura, durante la instanciación del proyecto desconectado, la URL raíz. La navegación en el proyecto desconectado se realiza en base a la URL raíz. Por ejemplo, si la URL en la que se instancia el proyecto es:

http://mi_dominio.com/base_offline/base

y se accede en la aplicación en línea a la URL:

http://mi_dominio.com/base_offline/base/ventas

el objeto History generará la URL:

http://mi_dominio.com/base_offline/base/#ventas

Los módulos antes mencionados se encuentran conectados entre sí mediante el módulo de eventos event provisto por Protopy.

Debido a que la operación con el DOM se encuentra recubierta en la operación desconectada, el código de las plantillas debe ser adaptado para trabajar con la API de Doff/Protopy. Se debe:

  • Utilizar sys.window en vez de elemento window. Éste apunta a window cuando la plantilla se procesa con conexión, y a DOMAdapter cuando se encuentra sin conexión.

  • Utilizar sys.transport en vez de XMLHttpRequest para que las llamadas asincrónicas sean correctamente enrutadas. Éste apunta a XMLHttpRequest cuando la aplicación se encuentra en línea y a una emulación que trabaja con el LocalHandler en modo desconectado.

  • Utilizar el selector CSS $$("selector") para la selección de elementos que trabajan adecuadamente en el contexto desconectado y en línea. Como es un builtin está siempre disponible. Para el caso de la selección por ID se tiene $("id") (similar a Prototype):

    var mi_boton = $('mi_boton'); // en vez de
                   // sys.window.document.getElementById('mi_boton');
    
    var mis_links = $$('a.mis_links');
    
  • Utilizar event.connect( nombre_evento, emisor, receptor, [método]) en vez de addListener sobre los HTMLElements u otros elementos del DOM. Por ejemplo:

    require('event');
    event.connect('click', $('boton'), function() {
        alert("Clickeaste mi boton");
    });
    

    Una operación muy común es ejecutar algo ante el evento de carga de página. Esto se realiza de la siguiente manera:

    require('event');
    event.connect('load', sys.window, miFuncionListener);
    
  • Evaluar el valor del contexto offline si existiese parte del template que sólo debe mostrarse en un estado de la aplicación:

    {% if offline %}
        <h1>Estoy Offline!</h1>
    {% else %}
        <h1>Estoy en línea</h1>
    {% endif %}
    

Como se ha descripto, Doff modifica el comportamiento de varios módulos de Protopy para lograr consistencia en el entorno desconectado.

Vistas

Las vistas en Doff son funciones que contienen la lógica de la aplicación. Están asociadas a URLs en el archivo urls.js (siempre que no se haya modificado el ROOT_URLCONF) del proyecto desconectado. Al igual que en Django, la variable que almacena estas asociaciones es urlpatterns. En el mapeo se pueden hacer inclusiones de módulos de URLs definidos en las aplicaciones.

Un módulo de URLs se conforma de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
require('doff.conf.urls', '*');
// El proyecto se llama Blog y la aplicación Post, por lo que las vistas
// se encuentran en el submódulo blog.post.views
require('blog.post.views');

// Definición de los patrones
var urlpatterns = patterns('',
    // Asociación por el nombre completo de la vista
    ['^/$', 'blog.post.index'],
    ['^add_tag/$', 'blog.post.views.add_tag'],
    ['^remove_tag/([A-Za-z0-9-]+)/$', 'blog.post.views.remove_tag'],
    // Asociación mediante la referencia de la función del módulo importado
    ['^add_post/$', views.add_post],
    ['^remove_post/([A-Za-z0-9-]+)/$', views.remove_post]
);

// Lo único que se publica es la variable urlpatterns
publish({
    urlpatterns: urlpatterns
});

Es importante notar que las expresiones regulares nativas de JavaScript no poseen la capacidad de recuperación de grupos nombrados, sólo recuperación posicional como se puede apreciar en las líneas de 11 y 14 del ejemplo anterior.

Una vista se conforma de manera muy similar a Django:

require('doff.utils.shortcuts', 'render_to_response', 'redirect');

function remove_tag(request, slug){
    var tag = Tag.objects.get({'slug': slug});
    tag.delete();
    return redirect('/');
}

function index(request){
    return render_to_response('mi_template.html', {
        titulo: "Bienvenido a la aplicación desconectada"
    });
}

// ... más vistas

publish({
    remove_tag: remove_tag,
    index: index,
    // ... más publicaciones
});

Las vistas reciben el objeto HTTPRequest como primer argumento, en consonancia con Django. En la aplicación sin conexión, la creación del ciclo del Request comienza con la captura del evento por parte del DOMAdapter, como se describe en la sección anterior.

Las vistas deben devolver un objeto HTTPResponse. Doff implementa algunos atajos de django.shortcuts, por ejemplo, render_to_response(nombre_template, contexto) para simplificar el proceso de construcción e interpretación de una plantilla y generación de la instancia del HTTPResponse.

Formularios

Los formularios son casi por excelencia el mecanismo de entrada para las operaciones CRUD en los sistemas de información basados en la web.

Doff posee la misma abstracción de objetos que brinda Django para facilitar la manipulación y validación de datos mediante instancias del tipo Form.

Un formulario en Doff es una clase que extiende de Form y tiene la responsabilidad de validar la entrada de datos y generar salida HTML.

Los campos del formulario se definen como atributos de la clase Form, y extienden de doff.forms.fields.base.Field o alguna de sus subclases.

Un campo tiene como atributo, de manera implícita, un Widget que se encarga de la representación en HTML del campo particular. Por ejemplo, un formulario para un autor (del ejemplo expuesto en la sección sobre modelos) se define de la siguiente manera:

var forms = require('doff.forms.base');

var AuthorForm = type('Author', [ forms.Form ], {
    salutation: new forms.CharField({ maxlength: 10 }),
    first_name: new forms.CharField({ maxlength: 30 }),
    last_name: new forms.CharField({ maxlength: 40 }),
    email: new forms.EmailField(),
    headshot: new forms.ImageField({ upload_to: '/tmp' })
});

Las instancias de Form tienen los métodos as_ul() (genera salida en forma de lista) y as_table() (genera salida en forma de tabla) que generan la salida HTML del formulario y son utilizados en las plantillas. Por ejemplo:

<html>
    <head>
        <title>Template para formulario</title>
    </head>
    <body>
        <!-- Definición del formulario -->
        <form action="" method="POST">
            <ul>
                <!-- Si el formulario  fue enviado en el contexto del
                template con el nombre "form", se puede invocar al método
                que lo muestra como ítems de una lista -->
                {{ form.as_ul }}
            </ul>
            <!-- Botón de enviar -->
            <input type="submit" value="Enviar">
        </form>
    </body>
</html>

De esta manera se muestra el formulario en la página.

Por regla general, los formularios se encuentran en un módulo forms.js, aunque pueden definirse en tiempo de ejecución para necesidades puntuales.

Las vistas que manejan formularios suelen valerse del request.method para evaluar si el formulario está siendo requerido o enviado para su validación. Por ejemplo, la siguiente vista hace uso del atributo mencionado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Funciones de ayuda
require('doff.shortcuts', 'redirect', 'render_to_response');
// Importar el formulario
require('bookstore.core.forms', 'AuthorForm');
// Importar el modelo
require('bookstore.core.models', 'Author');

function crear_autor(request) {
    // El formulario es para validación
    if (request.method == "POST") {
        var form = new AuthorForm({ data: request.POST });
        // Los datos ingresados son válidos?
        if (form.is_valid()) {
            // Se crea el autor
            var autor = new Author({data: form.data});
            autor.save();
            // Se redirecciona a una página sobre el autor
            return redirect('/autores/%d'.subs(autor.id));
        }

        // Si el formulario no es válido, cuando se muestre
        // en el template, se marcarán los errores de validación
    } else {
        // Se crea el formulario vacío
        var form = new AuthorForm();
    }

    return render_to_response("mi_formulario", {
        form: form
    });

}

Una instancia de formulario puede estar en uno de dos estados: bound (vinculado) o unbound (no vinculado). Una instancia vinculada se construye con un arreglo asociativo (como en la línea 11 del ejemplo anterior) y posee la capacidad de validar y volver a representar los datos con los cuales fue construido. Un formulario desvinculado no tiene datos asociados y simplemente tiene la utilidad de representarse en HTML.

Validación de Datos

Un formulario vinculado, tiene la responsabilidad de validar los datos con los cuales ha sido instanciado.

Para saber si un formulario está vinculado (bound) a datos válidos, se llama al método is_valid():

form = new ContactForm({ data: request.POST })
if (form.is_valid()):
    # Hace algo con los datos del formulario

Para acceder a los datos se accede directamente al request.POST, pero de esta manera no se saca provecho de la conversión que realiza Doff, por ejemplo, para el caso de las fechas. En cambio se debería utilizar form.clean_data:

if (form.is_valid()) {
    var name = form.clean_data['first_name'],
        year = form.clean_data['email'];
        //...

}

La validación se lleva a cabo llamando al método clean() de cada campo del formulario. La salida de cada una de esas llamadas completa el arreglo asociativo cleaned_data. Finalmente se ejecuta la validación integral del formulario llamando al método clean() del formulario, si existe.

Se puede generar una validación personalizada creando una subclase del campo para el cual se requiera validación extra. Existe un mecanismo abreviado que consiste en implementar el método clean_nombreCampo directamente sobre el formulario. Por ejemplo:

var AuthorForm = type('Author', [ forms.Form ], {
    salutation: new forms.CharField({ maxlength: 10 }),
    first_name: new forms.CharField({ maxlength: 30 }),
    last_name: new forms.CharField({ maxlength: 40 }),
    email: new forms.EmailField(),
    headshot: new forms.ImageField({ upload_to: '/tmp' }),
    // Validación personalizada
    clean_salutation: function () {
        var salutation = this.data['salutation'];
        if (!salutation in ['mr', 'ms', 'mrs', 'miss']) {
            throw new ValidationError("%s no es un saludo válido".subs(
                salutation);
        }
        return salutation;
    }

});

El orden de validación es el siguiente:

  1. clean() de cada campo del formulario (CharField.clean(), EmailField.clean(), etc).

    1. clean_nombreCampo sobre el formulario, si existiese.
  2. clean() del formulario.

Si en este ciclo no se captura ninguna excepción los datos son válidos. En caso contrario, el formulario almacena el mensaje de error y lo muestra adecuadamente cuando se realiza su representación.

Creación de Formularios a Partir de Modelos

La mayoría de las veces los formularios son utilizados para el ingreso de datos a los modelos, por lo que la tarea de escribir un formulario para cada modelo es muy común. Doff implementa en el módulo doff.forms.models un tipo especial de formulario que inspecciona la definición del modelo y genera automáticamente los campos del formulario. De esta manera, cualquier cambio en los campos del modelo se refleja automáticamente en los campos del formulario. Además añade un método save() que genera de manera automatizada una instancia en la base de datos del modelo asociado.

Para utilizar este tipo de formularios se debe extender de doff.forms.models.ModelForm y suministrar en el arreglo asociativo Meta el modelo al cual se asocia. Por ejemplo:

var AuthorForm = new type('AuthorForm', [ ModelForm ], {
    Meta: {model: Author}
});

Adaptando la vista del ejemplo del apartado anterior, el codigo es el siguiente:

   function crear_autor(request) {
    // El formulario es para validación
    if (request.method == "POST") {
        var form = new AuthorForm({ data: request.POST });
        // Los datos ingresados son válidos?
        if (form.is_valid()) {
            // Se crea el autor a partir del formulario
            var autor = form.save();
            // Se redericciona a una página sobre el autor
            return redirect('/autores/%d'.subs(autor.id));
        }
        // Si el formulario no es válido, cuando se muestre
        // en la plantilla, se marcarán los errores de validación
    } else {
        // Se crea el form vacío
        var form = new AuthorForm();
    }

    return render_to_response("mi_formulario", {
        form: form
    });

}