Chapter 12: Componentes y agregados

Componentes y Agregados (plugin)

component
plugin

Los componentes y los agregados o plugin son características relativamente novedosas en web2py, y existen diferencias entre los desarrolladores sobre qué son o qué deberían ser. La confusión deriva mayormente de los distintos usos de esos términos en otros proyectos de software y del hecho de que los desarrolladores todavía se encuentran en la tarea de definir sus especificaciones.

Sin embargo, el soporte de plugin es una característica importante y debemos establecer ciertas definiciones. Estas definiciones no tienen la intención de cerrar la discusión. Sólo deben mantener cierta coherencia con los patrones de programación que vamos a detallar en este capítulo.

Necesitamos resolver dos problemas:

  • Cómo construir aplicaciones modulares que minimicen la carga del servidor y maximicen la reutilización del código?
  • Cómo podemos distribuir piezas de código siguiendo de alguna forma el estilo plugin-and-play?

Componentes es la solución para el primer problema; plugin es la solución del segundo.

Componentes

load
LOAD
Ajax

Un componente es una parte funcionalmente autónoma de una página web.

Un componente puede estar compuesto de módulos, controladores y vistas, pero no hay requisitos estrictos salvo que, cuando se incrustan en una página web, deben localizarse por medio de una etiqueta html (por ejemplo un DIV, un SPAN o un IFRAME) y debe realizar sus tareas en forma independiente del resto de la página. Tenemos especial interés en aquellos componentes que se carguen en la página y que se comuniquen con el controlador a través de Ajax.

Un ejemplo de componente es un "componente para comentarios" que se incluye en un DIV y muestra los comentarios de usuarios y un formulario para publicar un comentario. Cuando el formulario se envía, se transmite al servidor por medio de Ajax, la lista se actualiza, y el comentario se almacena del lado del servidor en la base de datos. El contenido del DIV se refresca sin la actualización del resto de la página.

La función LOAD de web2py hace fácil la tarea sin conocimiento específico de JavaScript/Ajax o programación.

Nuestra meta es ser capaces de desarrollar aplicaciones web por ensamblado de componentes en los diseños de página.

Consideremos una simple app de web2py, "prueba", que extiende la app de andamiaje por defecto con el modelo personalizado en el archivo "models/db_comentario.py":

db.define_table('comentario',
   Field('cuerpo','text',label='Tu comentario'),
   Field('publicado_en','datetime',default=request.now),
   Field('publicado_por',db.auth_user,default=auth.user_id))
db.comentario.publicado_en.writable=db.comentario.publicado_en.readable=False
db.comentario.publicado_por.writable=db.comentario.publicado_por.readable=False

una acción en "controllers/comentarios.py"

@auth.requires_login()
def publicar():
    return dict(formulario=crud.create(db.comentario),
                comentarios=db(db.comentario).select())

y su correspondiente vista "views/comentarios/publicar.html"

{{extend 'layout.html'}}
{{for comentario in comentarios:}}
<div class="comentario">
  El {{=comentario.publicado_en}} {{=comentario.publicado_por.first_name}}
  dice <span class="comentario_cuerpo">{{=comentario.cuerpo}}</span>
</div>
{{pass}}
{{=formulario}}

Puedes acceder a él como de costumbre con:

http://127.0.0.1:8000/prueba/comentarios/publicar

imagen

Hasta aquí no hay nada de especial en esta acción, pero podemos convertirla en un componente definiendo una nueva vista con la extensión ".load" que no extiende el diseño de página o layout.

Entonces creamos una vista "views/comentarios/publicar.load":

{{#extend 'layout.html' <- observa que esto se omite!}}
{{for comentario in comentarios:}}
<div class="comentario">
  El {{=comentario.publicado_en}} {{=comentario.publicado_por.first_name}}
  dice <span class="comentario_cuerpo">{{=comentario.cuerpo}}</span>
</div>
{{pass}}
{{=formulario}}

Podemos acceder a ella por el URL

http://127.0.0.1:8000/prueba/comentarios/publicar.load

y se verá de esta forma:

imagen

Este es un componente que podemos embeber en cualquier otra página con tan solo hacer

{{=LOAD('comentarios','publicar.load',ajax=True)}}

Por ejemplo en "controllers/default.py" podemos editar

def index():
    return dict()

y en la vista correspondiente agregar el componente:

{{extend 'layout.html'}}
<p>{{='bla '*100}}</p>
{{=LOAD('comentarios','publicar.load',ajax=True)}}

Si se visita la página

http://127.0.0.1:8000/prueba/default/index

mostrará el contenido normal y el componente de comentarios:

imagen

El componente {{=LOAD(...)}} se convierte como sigue:

<script type="text/javascript"><!--
web2py_component("/prueba/comentarios/publicar.load","c282718984176")
//--></script><div id="c282718984176">loading...</div>

(el código real creado depende de las opciones pasadas a la función LOAD).

La función web2py_component(url, id) se define en "web2py_ajax.html" y se encarga de toda la magia: llama al url a través de Ajax y embebe la respuesta en el DIV con el correspondiente id; envuelve todo envío de formulario en el DIV y transmite esos formularios a través de Ajax. El target o destino de Ajax siempre es el mismo DIV.

La lista completa de argumentos del ayudante LOAD es la siguiente:

LOAD(c=None, f='index', args=[], vars={},
     extension=None, target=None,
     ajax=False, ajax_trap=False,
     url=None,user_signature=False,
     content='loading...',**attr):

Descripción:

  • los dos primeros argumentos c y f son el controlador y la función que queremos utilizar respectivamente.
  • args y vars son los argumentos y variables que queremos ingresar a la función. El primero es una lista, el segundo un diccionario.
  • extension es una extensión opcional. Observa que la extensión puede también pasarse como parte de la función como en f='index.load'.
  • target es el id del DIV de destino (donde se incrustará el componente). Si no se especifica se generará un id aleatorio.
  • ajax debería establecerse como True si el DIV se debe completar a través de Ajax y como False si el DIV tiene que completarse antes de que se devuelva la página actual (y por lo tanto, evitando la llamada a través de Ajax).
  • ajax_trap=True quiere decir que todo formulario enviado en el DIV se debe capturar y transmitir a través de Ajax, y la respuesta se debe convertir dentro del DIV. ajax_trap=False indica que los formularios se deben enviar normalmente, y por lo tanto refrescando la página completa. ajax_trap se omite y se asume el valor True si ajax=True.
  • url, si se especifica, sobrescribe los valores para c, f, args, vars, y extension y carga el componente ubicado en url. Es utilizado para cargar como componentes páginas servidas por otras aplicaciones (que pueden o no ser aplicaciones de web2py).
  • user_signature es por defecto False pero, si te has autenticado, debería ser True. Esto comprobará que el callback de ajax se ha firmado digitalmente. Esa funcionalidad está documentada en el capítulo 4.
  • content es el contenido a mostrarse mientras se realiza la llamada con ajax. Puede ser un ayudante como en content=IMG(...).
  • Se pueden ingresar atributos **attr adicionales para el DIV que contiene el componente.

1. Si no se especifica una vista .load, hay un generic.load que convierte el diccionario devuelto por la acción sin diseño de página (layout). Esto funciona mejor si el diccionario contiene un único elemento.

Si cargas un componente con LOAD que tiene una extensión .load y el controlador correspondiente redirige a otra acción (por ejemplo un formulario de autenticación), la extensión .load se propagará y el nuevo url (al cual se debe redirigir) también se carga con una extensión .load.

Si llamas a una función a través de Ajax y quieres que la acción fuerce una redirección en la página que la contiene puedes hacerlo con:

redirect(url,type='auto')

Como las solicitudes Ajax tipo POST no soportan los formularios multipart, por ejemplo subidas de archivos, los campos tipo upload no funcionarán con el componente LOAD. Esto podría engañarte y puedes llegar a pensar que funcionaría de todos modos ya que los campos upload funcionan normalmente si el POST se hace desde una vista con extensión .load del componente. En cambio, las subidas de datos con upload se hacen por medio de widget de terceros compatibles con ajax y comandos especiales de web2py para almacenamiento de archivos subidos.

Comunicación Cliente-Servidor para componentes

Cuando la acción de un componente se llama a través de Ajax, web2py pasa dos encabezados HTTP con la siguiente solicitud:

web2py-component-location
web2py-component-element

que son accesibles para la acción por las variables:

request.env.http_web2py_component_location
request.env.http_web2py_component_element

La última también es accesible por medio de:

request.cid

request.cid

La primera contiene el URL de la página que llamó a la acción del componente. La segunda contiene el id del DIV que contendrá la respuesta.

La acción del componente también puede almacenar información en dos encabezados especiales HTTP que serán interpretados por la página completa en la respuesta. Estos son:

web2py-component-flash
web2py-component-command

y se pueden establecer con:

response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'

o (si la acción fue llamada por un componente) automáticamente con:

response.flash='...'
response.js='...'

El primero contiene el texto que quieres que emerja con la respuesta El segundo contiene código JavaScript que quieres ejecutar con la respuesta. No puede contener saltos de línea.

Como ejemplo, definamos un componente para formulario de contacto en "controllers/contacto/preguntar.load" que permita al usuario hacer una pregunta. El componente enviará por correo la pregunta al administrador del sistema, devolverá un mensaje emergente "gracias" y eliminará el componente de la página que lo contiene:

def preguntar():
    formulario=SQLFORM.factory(
        Field('tu_correo',requires=IS_EMAIL()),
        Field('pregunta',requires=IS_NOT_EMPTY()))
    if formulario.process().accepted:
        if mail.send(to='admin@example.com',
                  subject='de %s' % formulario.vars.tu_correo,
                  message = formulario.vars.pregunta):
            response.flash = 'Gracias'
            response.js = "jQuery('#%s').hide()" % request.cid
        else:
            formulario.errors.tu_email = "No se pudo enviar el mail"
    return dict(formulario=formulario)

Las primeras cuatro líneas definen el formulario y lo aceptan. El objeto mail usado para el envío se define en la aplicación de andamiaje por defecto. Las últimas cuatro líneas implementan toda la lógica específica del componente al recibir los datos de encabezado de la solicitud HTTP y estableciendo el encabezado de la respuesta HTTP.

Ahora puedes embeber este formulario de contacto en cualquier página por medio de

{{=LOAD('contacto','preguntar.load',ajax=True)}}

Observa que no hemos definido una vista .load para nuestro componente preguntar. No la necesitamos porque devuelve un único objeto (formulario) y por lo tanto el "generic.load" lo manejará sin problemas. Recuerda que las vistas genéricas son una herramienta de desarrollo. En producción deberías copiar "views/generic.load" a "views/contacto/preguntar.load".

user_signature
requires_signature
Podemos bloquear el acceso a una función solicitada con Ajax con un URL firmado digitalmente utilizando el argumento user_signature:

{{=LOAD('contacto', 'preguntar.load', ajax=True, user_signature=True)}}

que agrega una firma digital al URL. La firma digital debe entonces validarse utilizando el siguiente decorador en la función callback:

@auth.requires_signature()
def preguntar(): ...

Retención o trapping de links con Ajax

A
Ajax links

Usualmente, un link no esta retenido (trapped), y al hacer clic en un link de un componente, se cargará toda la página del link. A veces necesitas que la página se descargue dentro del mismo componente. Esto se puede lograr utilizando el ayudante A:

{{=A('link a página', _href='http://example.com', cid=request.cid)}}

Si se especifica cid, la página del link se cargará con Ajax. El cid es el id del elemento html en el cual se descargará el componente de página descargado. En este caso lo configuramos como request.cid, es decir, el id del componente que incluye el link. La página solicitada del link puede ser y usualmente es un URL interno del sitio generado utilizando el comando URL.

Plugin

Un plugin o agregado es cualquier subconjunto de archivos en una aplicación.

y con esto realmente queremos significar cualquiera:

  • Un plugin no es un módulo, no es un modelo, tampoco es un controlador, ni es una vista, y de todas formas puede contener módulos, modelos, controladores y/o vistas.
  • Un plugin no necesariamente debe ser funcionalmente autónomo y puede depender de otros plugin o de código específico del usuario.
  • Un plugin no es un plugins system y por lo tanto no comprende conceptos como registro o aislamiento, si bien vamos a establecer normas para favorecer una cierto aislamiento.
  • Estamos hablando de un plugin para tu app, no de un plugin para web2py.

¿Por qué llamarlo plugin entonces? Porque provee de un mecanismo para empaquetado de subconjuntos de una aplicación y su instalación en una nueva app, es decir, conexión (plug-in) en una nueva app. Siguiendo esta definición, todo archivo en tu app puede ser manejado como plugin.

Cuando una app se distribuye, sus plugin también se empaquetan y distribuyen con ella.

En la práctica, la app admin provee de una interfaz especial para empaquetar y desempaquetar los plugin individualmente. Los archivos y carpetas de tu aplicación que tengan nombres con el prefijo plugin_nombre se pueden empaquetar separadamente en un archivo llamado:

web2py.plugin.nombre.w2p

y distribuirse en forma conjunta.

imagen

Los archivos que componen el plugin no son tratados por web2py en una forma distinta a otros archivos excepto que admin sabe por sus nombres que se supone que deben distribuirse en forma conjunta, y los muestra en una página especial:

imagen

De hecho, y siguiendo la definición anterior, estos plugin son más generales aún que aquellos reconocidos como tales por admin.

En la práctica, nos interesan únicamente dos tipos de plugin:

  • Plugin de Componentes o Component Plugins. Estos son plugin que contienen componentes según la definición de la sección previa. Un plugin de componentes puede contener uno o más de ellos. Podríamos pensar por ejemplo en un plugin_comentarios que contenga un componente comentarios como se sugiere más arriba. Otro ejemplo podría ser un plugin_etiquetado que contenga un componente etiquetado (tagging) y un componente etiquetas que comparta algunas tablas de la base de datos también definidas por el plugin.
  • Plugin de diseño de página o Layout Plugins. Estos son plugin que contiene el diseño de página y los archivos estáticos requeridos para ese diseño. Cuando se aplica uno de estos plugin, le da a la app un nuevo estilo visual.

Siguiendo las definiciones anteriores, los componentes creados en la sección anterior, por ejemplo "controllers/contact.py", ya son de hecho plugin. Podemos transferirlos de una app a otra y utilizar los componentes que definen. Todavía no son reconocidos en sí como plugin por admin porque no hay nada que los etiquete como plugin. Por lo tanto tenemos que resolver dos problemas:

  • Ponerle nombres a los archivos del plugin utilizando una convención determinada, de forma que admin pueda reconocerlos como parte del mismo plugin
  • Si el plugin tiene archivos del modelo, establecer una convención para que los objetos que define no interfieran en o contaminen el espacio de nombres y no entren en conflicto con las definiciones del resto de la app.

Supongamos que tenemos un plugin llamado nombre. Estas son las reglas que deberían seguirse:

Regla 1:

Los modelos y controladores de plugin deberían llamarse, respectivamente

  • models/plugin_nombre.py
  • controllers/plugin_nombre.py

y las vistas, módulos, archivos estáticos y los archivos en la carpeta private deberían ubicarse, respectivamente:

  • views/plugin_nombre/
  • modules/plugin_nombre/
  • static/plugin_nombre/
  • private/plugin_nombre/

Regla 2:

Los modelos pueden únicamente definir objetos con nombres que comiencen con

  • plugin_nombre
  • PluginNombre
  • _

Regla 3:

Los modelos de plugin pueden definir únicamente variables con nombres que comiencen con

  • session.plugin_nombre
  • session.PluginNombre

Regla 4:

Los plugin deberían incluir documentación y licencia. Estos deberían ubicarse en:

  • static/plugin_nombre/license.html
  • static/plugin_nombre/about.html

Regla 5:

El plugin puede únicamente depender de la existencia de objetos globales definidos en el archivo "db.py" de andamiaje, por ejemplo

  • una conexión a base de datos llamada db
  • una instancia de Auth llamada auth
  • una instancia de Crud llamada crud
  • una instancia de Service llamada service

Algunos plugin pueden ser un poco más sofisticados y tener parámetros de configuración en caso de existir más de una conexión a bases de datos.

Regla 6:

Si un plugin necesita configuración de parámetros, estos deberían establecerse a través del PluginManager según se detalla a más abajo.

PluginManager

Si se siguen las reglas anteriores podemos asegurarnos de que:

  • admin reconocerá todo archivo o carpeta de plugin_nombre como parte de una entidad autónoma.
  • no habrá conflictos entre los distintos plugin.

Las reglas recién detalladas no resuelven el problema de las dependencias y versiones de un plugin específico. Eso excede propósito de esta sección.

Plugin de componentes

component plugin

Los plugin de componente son plugin que definen componentes. Los componentes usualmente acceden a la base de datos y definen sus propios modelos.

Aquí transformamos el componente comentarios en un plugin de comentarios usando el mismo código que escribimos anteriormente, pero siguiendo las reglas especificadas para los plugin.

Primero, creamos un modelo denominado "models/plugin_comentarios.py":

db.define_table('plugin_comentarios_comentario',
   Field('cuerpo','text', label='Tu comentario'),
   Field('publicado_en', 'datetime', default=request.now),
   Field('publicado_por', db.auth_user, default=auth.user_id))
db.plugin_comentarios_comentario.publicado_en.writable=False
db.plugin_comentarios_comentario.publicado_en.readable=False
db.plugin_comentarios_comentario.publicado_por.writable=False
db.plugin_comentarios_comentario.publicado_por.readable=False

def plugin_comentarios():
    return LOAD('plugin_comentarios','publicar', ajax=True)

(observa que las últimas dos líneas definen una función que hará más simple incrustar el plugin)

El segundo paso consiste en definir un "controllers/plugin_comentarios.py"

@auth.requires_login()
def publicar():
    comentario = db.plugin_comentarios_comentario
    return dict(formulario=crud.create(comentario),
                comentarios=db(comentario).select())

Ahora

En el tercer paso creamos una vista llamada "views/plugin_comentarios/publicar.load":

{{for comentario in comentarios:}}
<div class="comentario">
  on {{=comentario.publicado_en}} {{=comentario.publicado_por.first_name}}
  says <span class="comentario_cuerpo">{{=comentario.cuerpo}}</span>
</div>
{{pass}}
{{=formulario}}

Ahora podemos usar la app admin para empaquetar el plugin para distribución. Admin guardará el plugin como:

web2py.plugin.comentarios.w2p

Podemos usar el plugin en cualquier vista con sólo instalar el plugin a través de la página diseño (edit) en admin y agregar lo siguiente a nuestras vistas

{{=plugin_comentarios()}}

Desde luego que podemos hacer más sofisticado a nuestro plugin agregando componentes que tomen parámetros y opciones de configuración. Cuanto más complicados sean los componentes, más difícil será evitar colisiones. El Plugin Manager descripto más abajo está diseñado para evitar ese problema.

Plugin Manager

La clase PluginManager está definida en gluon.tools. Antes de explicar como funciona internamente, vamos a explicar como usarla.

Vamos a tomar como ejemplo el plugin plugin_comentarios que describimos anteriormente y lo vamos a mejorar. Ahora queremos que se pueda personalizar:

db.plugin_comentarios_comentario.cuerpo.label

sin necesidad de modificar el código del plugin en sí.

Eso se puede hacer de esta manera:

Primero, reescribimos el archivo de plugin "models/plugin_comentarios.py" de esta forma:

db.define_table('plugin_comentarios_comentario',
   Field('cuerpo', 'text', label=plugin_comentarios.comentarios.cuerpo_label),
   Field('publicado_en', 'datetime', default=request.now),
   Field('publicado_por', db.auth_user, default=auth.user_id))

def plugin_comentarios()
    from gluon.tools import PluginManager
    plugins = PluginManager('comentarios', cuerpo_label='Tu comentario')

    comentario = db.plugin_comentarios_comentario
    comentario.label=plugins.comentarios.cuerpo_label
    comentario.publicado_en.writable=False
    comentario.publicado_en.readable=False
    comentario.publicado_por.writable=False
    comentario.publicado_por.readable=False
    return LOAD('plugin_comentarios', 'publicar.load', ajax=True)

Observa cómo todo el código a excepción de la definición de la tabla está encapsulado en una única función. Otro detalle a tener en cuenta es que la función crea una instancia de PluginManager.

Ahora en otro modelo en tu app, por ejemplo "models/db.py", puedes configurar este plugin como sigue:

from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comentarios.cuerpo_label = T('Publica a comentario')

La instancia plugins está creada por defecto en la app de andamiaje en "models/db.py"

El objeto PluginManager es un objeto Storage de instancia única o singleton, a nivel del hilo (thread-level) que contiene a su vez objetos Storage. Eso significa que puedes instanciar tantos como quieras en una misma aplicación pero (tengan el mismo nombre o no) se comportarán como si existiera una única instancia de la clase PluginManager.

Particularmente cada archivo de plugin puede crear su propio objeto PluginManager y registrarse con sus parámetros específicos con:

plugins = PluginManager('nombre', param1='valor', param2='valor')

Puedes sobrescribir estos parámetros en cualquier parte (por ejemplo en "models/db.py") con el código:

plugins = PluginManager()
plugins.nombre.param1 = 'otro valor'

Puedes configurar múltiples plugin en un sólo lugar.

plugins = PluginManager()
plugins.nombre.param1 = '...'
plugins.nombre.param2 = '...'
plugins.nombre.param3 = '...'
plugins.nombre.param4 = '...'
plugins.nombre.param5 = '...'

Cuando se define un plugin, el PluginManager debe recibir argumentos: el nombre del plugin y pares nombre-valor con parámetros opcionales que se establecerán por defecto. La configuración debe preceder a la definición del plugin (por ejemplo, debe incluirse en un archivo de modelo que tenga prioridad en el orden alfabético).

Plugin de diseño de página

layout plugin

Los plugin de diseño de página o layout plugin son más sencillos que los plugin de componentes porque usualmente no contienen código, sino solamente vistas y archivos estáticos. De todas formas deberían cuidarse las buenas prácticas:

Primero, crea una carpeta llamada "static/plugin_layout_nombre/" (donde nombre es el nombre de tu diseño) y copia todos tus archivos estáticos allí.

En segundo lugar, crea un archivo de diseño llamado "views/plugin_layout_nombre/layout.html" que contenga tu diseño y los link de las imágenes, CSS y archivos JavaScript en "static/plugin_layout_nombre/"

El tercer paso es modificar "views/layout.html" para que simplemente contenga:

{{extend 'plugin_layout_nombre/layout.html'}}
{{include}}

La ventaja de este diseño es que los usuarios de este plugin pueden instalar múltiples diseños y elegir cuál es el que aplicarán simplemente editando "views/layout.html". Es más, "views/layout.html" no será empaquetado por admin junto con el plugin, por lo que no hay riesgo de que el plugin sobrescriba el código del usuario en el diseño instalado anteriormente.

plugin_wiki

plugin_wiki
wiki

ACLARACIÓN: plugin_wiki sigue en etapa de desarrollo y por lo tanto no podemos prometer compatibilidad hacia atrás en el mismo nivel que para el caso de las funciones del núcleo de web2py.

plugin_wiki es un plugin con esteroides. Lo que queremos decir con eso es que define múltiples componentes y podría cambiar la forma en que desarrollas tus aplicaciones:

Puedes descargarlo desde

http://web2py.com/examples/static/web2py.plugin.wiki.w2p

La idea detrás de plugin_wiki es que la mayoría de las aplicaciones incluyen páginas semi-estáticas. Estas son páginas que no incluyen algoritmos complicados o personalizados. Contienen texto estructurado (por ejemplo una página de ayuda), imágenes, audio, video, formularios crud o un conjunto estándar de componentes (comentarios, etiquetas, planos, mapas), etc. Estas páginas pueden ser públicas, requerir autenticación o incluir otras restricciones de acceso. Pueden estar enlazadas por un menú o únicamente ser accesibles a través de un formulario ayudante. plugin_wiki provee de una forma sencilla de agregar páginas incluidas en estas categorías en una aplicación común de web2py.

En particular plugin_wiki incluye:

widget in plugin_wiki
  • Una interfaz tipo wiki que permite la inserción de páginas a tu app y la posibilidad de asociarlas a un titular o slug. Estas páginas (que denominaremos páginas wiki) registran distintas versiones y se almacenan en la base de datos.
  • Páginas públicas y privadas (con autenticación). Si una página requiere autenticación, también puede requerir que el usuario sea miembro de cierto grupo).
  • Tres niveles: 1, 2, 3. En el nivel 1, las páginas pueden únicamente incluir texto, imágenes, audio y video. En el nivel 2, las páginas pueden también incluir widget (estos son componentes según se definen en la sección anterior que se pueden embeber en páginas wiki). En el nivel 3, las páginas pueden también incluir código de plantillas de web2py.
  • La opción de editar páginas con la sintaxis markmin o en HTML usando un editor WYSIWYG (edición sobre la vista previa).
  • Una colección de widget: implementados como componentes. Incluyen documentación propia y pueden ser embebidos como componentes comunes en una vista cualquiera de app o, utilizando una sintaxis simplificada, en páginas wiki.
  • Un conjunto de páginas especiales (meta-code, meta-menu, etc.) que se pueden usar para personalizar el plugin (por ejemplo para definir código que debería correr el plugin, personalización del menú, etc.)

La app welcome junto con plugin_wiki pueden ser considerados como un entorno de desarrollo en sí, apto para la creación de aplicaciones sencillas como por ejemplo un blog.

De aquí en más vamos a asumir que se ha aplicado plugin_wiki a una copia de la app de andamiaje welcome.

Lo primero que notas luego de instalar el plugin es que agrega un nuevo ítem de menú llamado pages.

Haz clic en el ítem de menú pages y serás redirigido a la acción del plugin:

http://127.0.0.1:8000/miapp/plugin_wiki/index

imagen

La página de inicio (index) lista las páginas creadas utilizando el plugin en sí y te permite crear nuevas páginas eligiendo un slug. Prueba creando una página home. Serás redirigido a

http://127.0.0.1:8000/miapp/plugin_wiki/page/home

Haz clic en create page para editar el contenido.

imagen

Por defecto, el plugin tiene el nivel 3. Esto implica que puedes insertar widget así como también páginas con código. Por defecto usa la sintaxis markmin para la descripción del contenido de la página.

MARKMIN syntax

MARKMIN syntax

He aquí una iniciación a la sintaxis markmin:

markminhtml
# título<h1>título</h1>
## subtítulo<h2>subtítulo</h2>
### subsubtítulo<h3>subsubtítulo</h3>
**negrita**<strong>negrita</strong>
''itálica''<i>itálica</i>
http://...<a href="http://...com">http:...</a>
http://...png<img src="http://...png" />
http://...mp3<audio src="http://...mp3"></audio>
http://...mp4<video src="http://...mp4"></video>
qr:http://...<a href="http://..."><img src="qr code"/></a>
embed:http://...<iframe src="http://..."></iframe>

Observa que los link, archivos de imagen, audio y video se incrustan automáticamente. Para más información sobre la sintaxis MARKMIN, consulta el capítulo 5.

Si la página no existe, la app te solicitará que crees una.

La página de edición te permite agregar adjuntos a las páginas (por ejemplo archivos estáticos)

imagen

y puedes generar links a esos adjuntos como

[[milink nombre attachment:3.png]]

o embeberlos con

[[miimagen attachment:3.png center 200px]]

El tamaño (200px) es opcional. centro no es opcional sino que debes reemplazarlo por left o right.

Puedes embeber cuadros con citas o blockquoted text con

-----
Este es un cuadro con una cita
-----

y también tablas

-----
0 | 0 | X
0 | X | 0
X | 0 | 0
-----

y texto sin conversión (verbatim)

``
texto sin conversión
``

Además puedes agregar :class al final de ----- o ``. Para texto enmarcado y tablas se transformará según la clase de la etiqueta, por ejemplo:

-----
Prueba
-----:abc

se convierte como

<blockquote class="abc">Prueba</blockquote>

Para texto sin conversión se puede usar la clase para embeber contenido de distintos tipos.

Por ejemplo, puedes embeber código con resaltado de sintaxis si especificas el lenguaje con :codelenguaje

``
def index(): return 'hola mundo'
``:code_python

Puedes embeber widget:

``
name: nombre_del_widget
atributo1: valor1
atributo2: valor2
``:widget

Desde la página de edición puedes hacer clic en el creador de widget o widget builder para insertar widget desde una lista, en forma interactiva:

imagen

(para una lista de widget consulta la sección siguiente)

También puedes embeber una plantilla de web2py con código:

``
{{for i in range(10):}}<h1>{{=i}}</h1>{{pass}}
``:template

Permisos de página

Cuando edites una página encontrarás los siguientes campos:

  • active (por defecto True). Si una página no está activa, no estará accesible a los visitantes (incluso si es pública).
  • public (por defecto True). Si una página es pública, podrá ser visitada por usuarios no autenticados.
  • role (por defecto None). Si una página tiene un rol, será accesible únicamente por usuarios que se hayan autenticado y que sean miembros del grupo correspondiente.

Páginas especiales

menu in plugin_wiki

meta-menu contiene el menú. Si la página no existe, web2py usa response.menu, definido en "models/menu.py". El contenido de la página meta-menu sobrescribe el del menú. La sintaxis es como sigue:

Ítem 1 Nombre http://link1.com
   Submenú Ítem 11 Nombre http://link11.com
   Submenú Ítem 12 Nombre http://link12.com
   Submenú Ítem 13 Nombre http://link13.com
Ítem 2 Nombre http://link1.com
   Submenú Ítem 21 Nombre http://link21.com
      Submenú Ítem 211 Nombre http://link211.com
      Submenú Ítem 212 Nombre http://link212.com
   Submenú Ítem 22 Nombre http://link22.com
   Submenú Ítem 23 Nombre http://link23.com

donde el espaciado determina la estructura del submenú. Cada ítem se compone de el texto del ítem del menú seguido de un link. Un link puede ser page:titular. Un link con el valor None no apunta a ninguna página. Los espacios extra se omiten.

Aquí hay otro ejemplo:

Home                  page:home
Motores de búsqueda   None
   Yahoo              http://yahoo.com
   Google             http://google.com
   Bing               http://bing.com
Ayuda                 page:help

Esto se convierte de la siguiente forma:

imagen

meta-menu
meta-code
meta-header
meta-sidebar
meta-footer
meta-code es otra página especial y debe contener código de web2py. Es una extensión de tus modelos, y de hecho te permite agregar código del modelo. Se ejecuta al momento de ejecutar "models/plugin_wiki.py".

Puedes definir las tablas en meta-code.

Por ejemplo, puedes crear una simple tabla de amigos agregando lo siguiente en meta-code:

db.define_table('amigo', Field('nombre', requires=IS_NOT_EMPTY()))

y puedes crear una interfaz de administración de amigos embebiendo el siguiente código en la página que quieras:

jqGrid
CRUD

## Lista de amigos
``
name: jqgrid
table: amigo
``:widget

## Nuevo amigo
``
name: create
table: amigo
``:widget

La página tiene dos encabezados (que comienzan con #): "Lista de amigos" y "Nuevo amigo". La página contiene dos widget (bajo cada encabezado según corresponda): un widget jqgrid que crea una lista de amigos y un widget de inserción para agregar un amigo.

imagen

meta-header, meta-footer, meta-sidebar no son utilizados por el diseño de página por defecto en "welcome/views/layout.html". Si deseas usarlos, edita "layout.html" usando admin (o la consola) y agrega las siguientes etiquetas en los lugares apropiados:

{{=plugin_wiki.embed_page('meta-header') or ''}}
{{=plugin_wiki.embed_page('meta-sidebar') or ''}}
{{=plugin_wiki.embed_page('meta-footer') or ''}}

De esta forma, el contenido de esas páginas aparecerá en el encabezado, barra lateral y pie en el diseño de página.

Configuración de plugin_wiki

Como con cualquier otro plugin, en "models/db.py" puedes hacer

from gluon.tools import PluginManager
plugins = PluginManager()
plugins.wiki.editor = auth.user.email == mail.settings.sender
plugins.wiki.level = 3
plugins.wiki.mode = 'markmin' or 'html'
plugins.wiki.theme = 'ui-darkness'

donde

  • editor es True si el usuario autenticado tiene autorización para editar páginas de plugin_wiki
  • level es la permisología: 1 para editar páginas comunes, 2 para embeber widget en páginas, 3 para embeber código
  • mode determina si se debe usar un editor de "markmin" o un editor WYSIWYG de "html".
    WYSIWYG
  • theme es el nombre del estilo o theme de jQuery UI. Por defecto sólo se incluye "ui-darkness" que tiene un sistema neutral de colores.

Puedes agregar estilos aquí:

static/plugin_wiki/ui/%(estilo)s/jquery-ui-1.8.1.custom.css

Widget disponibles

Cada widget se puede incrustar tanto en páginas de plugin_wiki como en cualquier otra plantilla de app de web2py.

Por ejemplo, para embeber un video de YouTube en una página de plugin_wiki, puedes hacer

``
name: youtube
code: l7AWnfFRc7g
``:widget

o para incrustar el mismo widget en una vista de web2py, puedes hacer:

{{=plugin_wiki.widget('youtube', code='l7AWnfFRc7g')}}

En uno u otro caso, las salida es:

imagen

Los argumentos del widget que no tienen un valor asignado por defecto son obligatorios.

Esta es la lista de todos los widget actualmente disponibles:

read

read(tabla, record_id=None)

Lee y muestra un registro

  • tabla es el nombre de la tabla
  • record_id es un número de registro

create

create(tabla, message='', next='', readonly_fields='',
       hidden_fields='', default_fields='')

Muestra el formulario para crear un registro

  • tabla es el nombre de la tabla
  • message es el mensaje a mostrarse después de la creación del registro
  • next es la redirección al aceptar el formulario, por ejemplo: "pagina/inicio/[id]"
  • readonly_fields es una lista de valores separados por coma indicando campos
  • hidden_fields es una lista separada por coma de campos ocultos
  • default_fields es una lista de valores de campo por defecto campo=valor separados por coma

update

update(tabla,record_id='' ,message='', next='',
       readonly_fields='' ,hidden_fields='', default_fields='')

Displays a record update form

  • tabla es el nombre de la tabla
  • record_id es el registro a actualizar o {{=request.args(-1)}}
  • message es el mensaje a mostrarse después de la actualización del registro
  • next es la redirección al actualizar, por ejemplo: "pagina/inicio/[id]"
  • readonly_fields es una lista de valores separados por coma indicando campos
  • hidden_fields es una lista separada por coma de campos ocultos
  • default_fields es una lista de valores de campo por defecto campo=valor separados por coma

select

select(tabla, query_field='', query_value='', fields='')

Lista todos los registros de una tabla

  • tabla es el nombre de la tabla
  • query_field y query_value si se especifican, se filtrarán los registros de acuerdo con la consulta query_field == query_value
  • fields es una lista de valores separados por coma con los campos a mostrar

search

search(tabla, fields='')

Es un widget para búsqueda de registros

  • tabla es el nombre de la tabla
  • fields es una lista de valores separados por coma con los campos a mostrar

jqgrid

jqGrid
jqgrid(tabla, fieldname=None, fieldvalue=None, col_widths='', colnames=None, _id=None,fields='', col_width=80, width=700, height=300)

Incrusta un plugin jqGrid

  • tabla es el nombre de la tabla
  • fieldname, fieldvalue son filtros opcionales: fieldname==fieldvalue
  • col_widths son los anchos de cada columna
  • colnames es una lista de las columnas que se deben mostrar
  • _id es el "id" del elemento TABLE que contiene el jqGrid
  • fields es una lista de columnas a mostrar
  • col_width es el ancho por defecto de las columnas
  • height es el alto del jqGrid
  • width es el ancho del jqGrid

Una vez que ya tienes el plugin_wiki instalado, puedes fácilmente usar el jqGrid en otra vista también. Ejemplo de uso (muestra tutabla filtrada por fk_id==47):

{{=plugin_wiki.widget('jqgrid', 'tutabla', 'fk_id', 47, '70,150',
    'Id, comentarios', None,'id, notes', 80, 300, 200)}}

latex

latex
latex(expression)

Usa la API de Google charting para incrustar LaTeX

pie_chart

pie chart
pie_chart(data='1,2,3', names='a,b,c', width=300, height=150, align='center')

Incrusta un gráfico de torta o pie chart

  • data es una lista de datos separados por coma
  • names es una lista de etiquetas separada por coma (una por ítem de datos)
  • width es el ancho de la imagen
  • height es la altura de la imagen
  • align especifica la alineación de la imagen

bar_chart

bar chart
bar_chart(data='1,2,3', names='a,b,c', width=300, height=150, align='center')

Usa la API de Google charting para embeber un gráfico de barras

  • data es una lista de datos separados por coma
  • names es una lista de etiquetas separadas por coma (una por ítem de datos)
  • width es el ancho de la imagen
  • height es el alto de la imagen
  • align determina la alineación de la imagen

slideshow

slideshow
slideshow(tabla, field='image', transition='fade', width=200, height=200)

Incrusta una presentación con imágenes deslizables. Toma las imágenes de una tabla.

  • tabla es el nombre de la tabla
  • field es el campo upload en la tabla que contiene las imágenes
  • transition determina el tipo de transición, por ejemplo fundido, etc.
  • width es el ancho de la imagen
  • height es el alto de la imagen

youtube

YouTube
youtube(code, width=400, height=250)

Incrusta un video de YouTube (por código)

  • code es el código del video
  • width es el ancho de la imagen
  • height es la altura de la imagen

vimeo

Vimeo
vimeo(code, width=400, height=250)

Embebe un video de Vimeo (por código)

  • code es el código del video
  • width es el ancho de la imagen
  • height es el alto de la imagen

mediaplayer

flash mediaplayer
mediaplayer(src, width=400, height=250)

Embebe un archivo media file (por ejemplo un video de Flash o un archivo mp3)

  • src es la ubicación del video
  • width es el ancho de la imagen
  • height es el alto de la imagen

comments

comments
comments(table='None', record_id=None)

Embebe comentarios en una página

se pueden asociar a una tabla y/o registro

  • table es el nombre de la tabla
  • record_id es el id del registro

tags

tags
tags(table='None', record_id=None)

Agrega etiquetas o tags a una página

las etiquetas se pueden asociar a tablas o registros

  • table es el nombre de la tabla
  • record_id es el id del registro

tag_cloud

tag cloud
tag_cloud()

Agrega una nube de etiquetas o tag cloud

map

Google map
map(key='....', table='auth_user', width=400, height=200)

Incrusta un mapa de Google

Puede tomar puntos en el mapa de desde una tabla

  • key es la clave para acceso a la api de mapas de Google (la clave por defecto funciona con 127.0.0.1)
  • table es el nombre de la tabla
  • width es el ancho del mapa
  • height es la altura del mapa

La tabla debe tener las columnas: latitude, longitude y map_popup. Cuando se hace clic en un punto, aparecerá el mensaje de map_popup.

iframe

iframe
iframe(src, width=400, height=300)

Incrusta una página con <iframe></iframe>

load_url

load_url
load_url(src)

Carga el contenido de un url usando la función LOAD

load_action

load_action
load_action(accion, controller='', ajax=True)

Carga el contenido de URL(request.application, controller, accion) usando la función LOAD

Extendiendo los widget

Se pueden agregar widget a plugin_wiki creando el siguiente archivo de modelo llamado "models/plugin_wiki_"nombre, donde nombre'' es un nombre arbitrario y el archivo contiene algo como:

class PluginWikiWidgets(PluginWikiWidgets):
    @staticmethod
    def mi_nuevo_widget(arg1, arg2='valor', arg3='valor'):
        """
        información sobre el widget
	  """
        return "cuerpo del widget"

La primera línea indica que estás extendiendo la lista de widget. Dentro de la clase, puedes definir tantas funciones como necesites. Cada función static es un nuevo widget, salvo en el caso de funciones que comienzan con guión bajo. La función puede tomar una cantidad arbitraria de argumentos que pueden o no tener valores por defecto. El docstring de la función debe documentar la función usando la sintaxis markmin.

Cuando los widget se incrustan en páginas plugin_wiki, los argumentos se pasarán al widget como cadenas. Esto implica que la función del widget debe poder aceptar cadenas para cada argumento y eventualmente convertirlas según el tipo de representación requerida. Puedes decidir que tipo de representación de cadena debe ser - sólo asegúrate de que esté documentada en el docstring.

El widget puede devolver una cadena o ayudantes de web2py. En este último caso se convertirán a cadena usando .xml().

Observa que el nuevo widget puede acceder a cualquier variable en el espacio de nombres global.

Como ejemplo, vamos a crear un nuevo widget que muestre el formulario "contacto/preguntar" creado al inicio de este capítulo. Esto puede hacerse creando un archivo "models/plugin_wiki_contact" que contenga:

class PluginWikiWidgets(PluginWikiWidgets):
    @staticmethod
    def ask(email_label='Your email', question_label='question'):
        """
    Este plugin mostrará un formulario para contacto para que
    que el visitante pueda hacer una pregunta.
    La pregunta se te enviará por correo y el widget desaparecerá
    de la página.

    Los parámetros son:
	- email_label: la etiqueta del campo para la dirección del visitante
	- question_label: la etiqueta del campo para la pregunta

	"""
        formulario=SQLFORM.factory(
           Field('tu_email', requires=IS_EMAIL(), label=email_label),
           Field('pregunta', requires=IS_NOT_EMPTY()), label=question_label)
        if formulario.process().accepted:
           if mail.send(to='admin@example.com',
                        subject='from %s' % formulario.vars.tu_email,
                        message = formulario.vars.pregunta):
	         command="jQuery('#%s').hide()" % div_id
               response.flash = 'Gracias'
               response.js = "jQuery('#%s').hide()" % request.cid
        else:
            formulario.errors.tu_email="No se pudo enviar el correo"
        return formulario.xml()

Los widget de plugin_wiki no son convertidos por una vista a menos que el widget llame explícitamente a la función response.render(...)

 top