Chapter 12: Componentes y agregados
Componentes y Agregados (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
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
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:
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:
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
yf
son el controlador y la función que queremos utilizar respectivamente. args
yvars
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 enf='index.load'
.target
es elid
del DIV de destino (donde se incrustará el componente). Si no se especifica se generará unid
aleatorio.ajax
debería establecerse comoTrue
si el DIV se debe completar a través de Ajax y comoFalse
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 valorTrue
siajax=True
.url
, si se especifica, sobrescribe los valores parac
,f
,args
,vars
, yextension
y carga el componente ubicado enurl
. 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 encontent=IMG(...)
.- Se pueden ingresar atributos
**attr
adicionales para elDIV
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
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='[email protected]',
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
:{{=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
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.
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:
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 unplugin_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_
nombrePlugin
Nombre_
Regla 3:
Los modelos de plugin pueden definir únicamente variables con nombres que comiencen con
session.plugin_
nombresession.Plugin
Nombre
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
llamadaauth
- una instancia de
Crud
llamadacrud
- una instancia de
Service
llamadaservice
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.
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
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
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
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:
- 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
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.
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
He aquí una iniciación a la sintaxis markmin:
markmin | html |
# 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)
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 :code
lenguaje
``
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:
(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
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:
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:
## 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.
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:
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 tablarecord_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 tablamessage
es el mensaje a mostrarse después de la creación del registronext
es la redirección al aceptar el formulario, por ejemplo: "pagina/inicio/[id]"readonly_fields
es una lista de valores separados por coma indicando camposhidden_fields
es una lista separada por coma de campos ocultosdefault_fields
es una lista de valores de campo por defectocampo=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 tablarecord_id
es el registro a actualizar o{{=request.args(-1)}}
message
es el mensaje a mostrarse después de la actualización del registronext
es la redirección al actualizar, por ejemplo: "pagina/inicio/[id]"readonly_fields
es una lista de valores separados por coma indicando camposhidden_fields
es una lista separada por coma de campos ocultosdefault_fields
es una lista de valores de campo por defectocampo=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 tablaquery_field
yquery_value
si se especifican, se filtrarán los registros de acuerdo con la consultaquery_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 tablafields
es una lista de valores separados por coma con los campos a mostrar
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 tablafieldname
,fieldvalue
son filtros opcionales:fieldname==fieldvalue
col_widths
son los anchos de cada columnacolnames
es una lista de las columnas que se deben mostrar_id
es el "id" del elemento TABLE que contiene el jqGridfields
es una lista de columnas a mostrarcol_width
es el ancho por defecto de las columnasheight
es el alto del jqGridwidth
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(expression)
Usa la API de Google charting para incrustar LaTeX
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 comanames
es una lista de etiquetas separada por coma (una por ítem de datos)width
es el ancho de la imagenheight
es la altura de la imagenalign
especifica la alineación de la imagen
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 comanames
es una lista de etiquetas separadas por coma (una por ítem de datos)width
es el ancho de la imagenheight
es el alto de la imagenalign
determina la alineación de la imagen
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 tablafield
es el campo upload en la tabla que contiene las imágenestransition
determina el tipo de transición, por ejemplo fundido, etc.width
es el ancho de la imagenheight
es el alto de la imagen
youtube
youtube(code, width=400, height=250)
Incrusta un video de YouTube (por código)
code
es el código del videowidth
es el ancho de la imagenheight
es la altura de la imagen
vimeo
vimeo(code, width=400, height=250)
Embebe un video de Vimeo (por código)
code
es el código del videowidth
es el ancho de la imagenheight
es el alto de la imagen
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 videowidth
es el ancho de la imagenheight
es el alto de la imagen
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 tablarecord_id
es el id del registro
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 tablarecord_id
es el id del registro
tag_cloud
tag_cloud()
Agrega una nube de etiquetas o tag cloud
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 tablawidth
es el ancho del mapaheight
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(src, width=400, height=300)
Incrusta una página con <iframe></iframe>
load_url
load_url(src)
Carga el contenido de un url usando la función LOAD
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='[email protected]',
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(...)