domingo, 15 de mayo de 2011

Programación Web con Python

Programación Web en Python

Con Pyramid, SQLAlchemy y Genshi


Autor: @jobliz, producido para @tecnoyucas

Última Revisión: Abril del 2011






ÍJustificación


Índice General



1. Introducción a los términos del tutorial

2. Introducción al tutorial (límites y propósito)

3. Pasos a seguir

3.1. Preparando las herramientas

3.2. ¡Hola mundo!

3.3. Ejemplo del Tutorial: Aplicación sencilla para guardar marcadores.





1. Introducción a los términos del tutorial


Los elementos conceptuales de éste tutorial son:

*

Python: Es un lenguaje de programación de alto nivel que puede ser utilizado en un gran número de áreas, entre ellas las aplicaciones de escritorio, aplicaciones web, scripts de configuración e incluso videojuegos. Fué diseñado desde un principio para que su código fuente resulte fácil de leer y entender, aunque esto no limita en ningún sentido sus capacidades.

*

Object-Relational-Mapping (ORM, o “Mapeado Objeto Relación”): Es una técnica de programación que permite acceder a las bases de datos definiendo objetos y llamando a sus métodos. Las dos principales ventajas de éste estilo de programación es que la lógica interna del sistema ORM viene protegida contra diversos tipos de ataque a la base de datos, y que se evita tener que escribir comandos SQL a mano.

*

Sistema de plantillas: Es el lenguaje de marcado que define la estructura de las páginas web, procesando los archivos donde se combina HTML con la lógica de programación necesaria para hacer dinámico al sitio web. En Python existe un gran número de sistemas de plantillas, que serán mostrados posteriormente.

*

MVC (Modelo-Vista-Controlador): Es un patrón de arquitectura de software que divide las aplicaciones en tres capas fundamentales, ampliamente utilizado en el desarrollo de sitios web: Modelo (el código que accede únicamente a la base de datos), Vista (las funciones que están relacionadas con la interfaz gráfica, y Controlador (la lógica interna de la aplicación, que une al modelo con la vista).









2. Introducción al tutorial (límites y propósito)


En ésta sección quisiera explicar las intenciones y los límites de éste tutorial. Para empezar, es necesario aclarar que para Python existe un grán número de frameworks, no sólo el que se va a utilizar en éste tutorial. Ninguno de éstos es realmente “mejor” o “peor” que otro, sino que funcionan de maneras distintas. Cada uno posee diferentes comunidades y recomendaciones, por lo que se recomienda investigar sobre cada uno eventualmente. Entre los frameworks que existen para desarrollar aplicaciones web en Python se encuentran:

*

http://bfg.repoze.org/

*

http://www.cherrypy.org/

*

http://www.djangoproject.com/

*

http://pylonshq.com/

*

http://docs.pylonsproject.org/docs/pyramid.html

*

http://turbogears.org/

*

http://webpy.org/

*

http://www.web2py.com/

*

http://www.zope.org/






Para éste tutorial vamos a utilizar Pyramid, una fusión bastante reciente (Febrero del 2011) de los frameworks BFG y Pylons, que ya se encuentra en su versión estable 1.0. Éste es uno de los más flexibles del grupo, ya que no impone restricciones a la hora de escojer componentes, y tiene muchas maneras de expandirse para proyectos grandes.


Éste tutorial toma de tres fuentes fundamentales:


Al inicio del tutorial, luego de preparar las herramientas y el entorno, se mostrará un programa “hola mundo” de diez líneas de código que configura el servidor y sirve una página index con texto plano. Luego, se describe como realizar una pequeña aplicación para almacenar marcadores (bookmarks), donde aparece cómo:

  • Utilizar un sistema de plantillas para crear páginas dinámicas.

  • Crear formularios y obtener los datos que envíe el usuario.

  • Diseñar nuestra base de datos y acceder a ella.

  • Crear las páginas necesarias para ver los marcadores y etiquetas.


El formato de cada sección del tutorial es código:anotaciones: Primero se mostrará en su totalidad el código de cada parte, y debajo se explicará su funcionamiento.


Pre-requisitos


Éste tutorial necesita para ser comprendido por lo menos saber como cambiar de directorio en la línea de comandos y algo de experiencia con algún tipo de lenguaje de programación. Éste no necesariamente tiene que ser Python, porque su sintaxis básica es lo suficientemente sencilla como para aprenderse en el momento si se conocen los fundamentos (condicionales, variables, funciones, etc) de otro lenguaje. Como no se asume experiencia previa en Python se explicarán algunas convenciones del lenguaje a medida que vayan apareciendo.

Para realizar éste tutorial se recomienda disponer de un ordenador con algún tipo de sistema UNIX/Linux instalado, dado que éstos ya vienen con Python preinstalado y su cónsola es más versátil. Sin embargo también es posible realizar éste tutorial en Windows, sólo que para llamar a los programas se haría necesario escribir su ruta completa para poder ejecutarlos por el shell de MS-DOS.



3. Pasos a seguir


3.1. Prepararando las herramientas


Antes de empezar a programar vamos a tomar un paso extra que es recomendado antes de desarrollar aplicaciones web en Python: Vamos a crear primero un entorno virtual para trabajar, para que los cambios y programas que descarguemos no afecten a nuestra instalación principal de Python. El programa para hacerlo se llama “virtualenv”, y puede ser obtenido de dos maneras: Descargando el archivo de internet a la carpeta donde vayamos a trabajar, o instalándolo de la manera “convencional” de nuestro sistema (apt-get en las distros de Linux derivadas de Debian, por ejemplo).


$ sudo apt-get install python-virtualenv


Sin embargo, también se puede descargar un único archivo desde Internet y copiarlo en el directorio donde vayas a trabajar. Para hacerlo, se puede bajar de:


pylonsbook.com/virtualenv.py


Éste archivo lo que hace es copiar todo el contenido de nuestra instalación de Python en otra carpeta, para que utilizemos esa en vez de la convencional. Para utilizar éste archivo se usaría el comando:


$ python virtualenv.py --no-site-packages env


Ésto creará en la carpeta donde estés un directorio “env” donde se encontrará el entorno virtual. Dentro estará una copia del intérprete de Python, que será la que utilizaremos de ahora en adelante para ejecutar los comandos. Para hacerlo, sin embargo, hay que llamar a éste intérprete en vez del convencional, y para no tener que estar escribiendo todo el camino completo hacia el ejecutable vamos a hacer que el entorno virtual se integre con nuestra línea de comandos ejecutando:

$ source env/bin/activate


De ahora en adelante, verás que tu línea de comandos tiene anexado “(env)” al principio, indicando que estás utilizando el entorno virtual. Todos los comandos que ejecutes de ahora en adelante, y sobre todo, los programas que instales desde el Python Package Index, usarán el entorno virtual y no tu instalación global de Python. Para salir del entorno virtual basta con que ejecutes:


$ deactivate


Una vez con el entorno virtual instalado y activado, una de las maneras más rápida y efectiva de instalar Pyramid es recurriendo al Python Package Index (PyPI). Éste es el sitio web donde se encuentra hospedado prácticamente todo el software que se ha escrito en éste lenguaje, y que puede ser instalado en nuestra PC con el comando easy_install. Es una especie de repositorio exclusivo para las librerías de Python, que permite instalarlas independientemente del sistema operativo que se posea. Para instalar pyramid basta con escribir entonces:


(env)$ easy_install pyramid


El programa easy_install entonces buscará automáticamente en el Python Package Index el programa que tenga el nombre indicado (pyramid en éste caso) y lo descargará e instalará en nuestro sistema junto con todos los paquetes que necesite. Aquí estamos asumiento que se hará dentro del entorno virtual env creado anteriormente, si quieres ejecutar el comando easy_install fuera del entorno virtual en tu instalación principal de python necesitarás permisos de superusuario con sudo o con su.


La descarga desde Internet y su instalación no deberían durar mucho, como máximo tendrás que esperar 5 minutos aproximadamente con una conexión banda ancha. Con Pyramid una vez instalado, ahora en la próxima sección vamos a hacer una aplicación web de “hola mundo”, y posteriormente un sistema básico para ver marcadores (bookmarks) y sus etiquetas guardados en una base de datos.








3.2. ¡Hola mundo!


Una vez instalado Pyramid ya podemos pasar a probarlo. Creamos un archivo “holamundo.py”, y en él escribimos el siguiente código:


holamundo.py

---------------------------------------------------
from paste.httpserver import serve
from pyramid.config import Configurator
from pyramid.response import Response

def hello_world(request):
return Response(“Hola mundo!”)

if __name__ == "__main__":
config = Configurator()
config.add_view(hello_world)
app = config.make_wsgi_app()

serve(app, host='0.0.0.0')

---------------------------------------------------


Descripción del código:


  • Las tres líneas del principio son los “imports”, las llamadas que hace el código para poder utilizar las distintas partes de pyramid. En la primera línea se importa la función “serve” del módulo “paste”, que es la que se encarga de ejecutar el servidor web. Los otros imports son la clase “Configurator”, que nos permite realizar las configuraciones y producir el objeto final de nuestra aplicación, y la clase “Response”, que encapsula el envío de información desde Pyramid hasta el navegador del usuario.

  • La función “hello_world” es una “vista” sencilla, que muestra una página con el texto plano “Hola Mundo”. Al igual que toda vista recibe como parámetro un objeto request, que más adelante se verá que contiene la información referida a la petición (GET, POST) que el usuario realizó a nuestra aplicación web. Sú única línea retorna una cadena de texto dentro de un objeto Response, que mostrará al usuario en su navegador el texto “Hola mundo!”.



  • La tercera y última parte del “hola mundo” inicia con if __name__ == "__main__" , una técnica de Python para hacer que el código que esté debajo del if sólo se ejecute si se está llamando directamente a ese archivo. Luego se crea el objeto configurator, y se le añade a través de su método add_view la vista “hello_world”, la que hicimos antes. Éste método sólo sirve para establecer direcciones que no reciben variables, por lo que más adelante usaremos serán las rutas. Ya que no indicamos más detalles para la vista, ésta pasa a ser la página index principal del sitio web. El siguiente paso es crear nuestra aplicación (en la variable app) a través del método make_wsgi_app del configurador, que luego pasamos a la función serve como primer parámetro, seguido de la cadena '0.0.0.0' para inicializar el servidor con nuestro sitio web.



Para poner a correr nuestro código, simplemente tenemos que pasar el script “holamundo.py” al intérprete de Python:


(env)jose@hq:~/codigo/holamundo$ python holamundo.py


Una vez que el servidor haya sido inicializado, mostrará una respuesta en la línea de comandos:


serving on 0.0.0.0:8080 view at http://127.0.0.1:8080


Para visualizar el “hola mundo” en el navegador puedes visitar cualquiera de las direcciones:


http://127.0.0.1:8080

http://localhost:8080/


3.3. Ejemplo del Tutorial: Aplicación sencilla para ver marcadores (bookmarks)



Para mostrar más capacidades que las que permite el simple pero limitado programa “hola mundo” vamos a crear un pequeño sitio web donde se almacenen marcadores y se les ponga etiquetas para categorizarlos. Ello nos permirá hacer cosas más útiles que simplemente mostrar texto sin formato. Para éste tutorial se dependerá, además de Pyramid, de dos módulos adicionales:


  • SQLAlchemy: Un sistema ORM, para un acceso más sencillo a la base de datos.

  • Genshi: Un motor de plantillas, para definir páginas dinámicas.



3.3.1. Instalando y configurando el motor ORM SQLAlchemy.



Para acceder a nuestra base de datos vamos a utilizar SQLAlchemy, una capa de abstracción que nos permite trabajar con cualquier base de datos. Por comodidad, para éste tutorial vamos a utilizar SQLite, que ya viene incluída en las instalaciones más recientes de Python.


Para instalarlo, basta con usar easy_install:


(env)$ easy_install pyramid


Ahora se mostrará a continuación el código donde se definen las tablas-objeto de la base de datos y los parámetros de conexión:




models.py

---------------------------------------------------

from sqlalchemy import create_engine, Table, Column, String, Integer, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

bookmark_tag = Table("bookmark_tag", Base.metadata,
Column('bookmark_id', Integer, ForeignKey("bookmarks.id")),
Column("tag_id", Integer, ForeignKey("tags.id"))
)

class Bookmark(Base):
__tablename__ = "bookmarks"
id = Column(Integer, primary_key=True)
title = Column(String)
link = Column(String)

tags = relationship("Tag", secondary=bookmark_tag, backref="bookmarks")

def __init__(self, title, link):
self.title = title
self.link = link

def __repr__(self):
return "<Bookmark('%s')>" % (self.title)

class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True)
name = Column(String)

def __init__(self, name):
self.name = name

def __repr__(self):
return "<Tag('%s')>" % self.name

location = "sqlite:///" + "".join(os.path.abspath("data.db"))
engine = create_engine(location)
Base.metadata.create_all(engine)
SessionMaker = sessionmaker(bind=engine)
---------------------------------------------------


  • Las líneas de imports en éste caso llaman a varias partes de SQLAlchemy, cómo su definición de Tabla (Table) y columna (Column), y los tipos de datos que vamos a usar (String, Integer), al igual que la relación de claves foráneas (ForeignKey) y otras definiciones de relaciones necesarias para vincular una tabla con otra (relationship y backref). Aparte de éstas definiciones también se importa el objeto que creará nuestras conexiones a la base de datos (sessionmaker), y una clase principal de la cual derivaremos nuestras objetos (declarative_base). La única vez que se usará Table será para definir la tabla intermedia que permitirá la relacion “muchas-a-muchas” entre Bookmark y Tag, porque ésta tabla no es en sí misma un objeto. Tag y Bookmark sí lo son, y por eso en vez de definirse como “Table” para mayor comodidad heredan del objeto declarative_base.

  • La parte principal del código se encuentra en las definiciones de la clase Tag (etiqueta) y Bookmark (marcador). Cada una de esas clases representa a su respectiva tabla en la base de datos, y define tanto su nombre (en la variable __tablename__) como sus campos (lo que se encuentra después). Además de ésto ambas también implementan su constructor (__init__) para crear los objetos que las representan. El método __repr__, por su parte, es el que procesa la representación en forma de cadena de cada instancia, que se verá en la próxima sección.

  • La relación entre las tablas Bookmark y Tag es una relación “muchas a muchas”, y cómo tal necesita de una tabla adicional para poder funcionar (bookmark_tag), que está compuesta únicamente por dos claves foráneas y que también recibe la información de Base. Para que los objetos sepan de ésta relación, en la clase Bookmark se crea un objeto relationship que recibe como primer parámetro el nombre de la clase a la que se está relacionando (“Tag”) , como variable “secundary” la tabla bookmark_tag, y como referencia trasversa (backref) su propio nombre de tabla (“bookmarks”).

  • El resultado del proceso anterior crea una de las principales ventajas de SQLAlchemy, que es encapsular cada registro de las tablas en un objeto que está “consciente” de a cuales otros objetos está relacionado, o a cuales pertenece. Por ejemplo, cuando llamemos a la base de datos para obtener un marcador no hará falta hacer otra llamada para conocer sus etiquetas, sino que éstas estarán en una lista dentro de su atributo “tags”.





3.3.2. Utilizando la base de datos.


Antes de proceder a construir el sitio web de marcadores, vamos a llenar la base de datos con algo de información para hacer pruebas. Para ésto, nos dirigimos en el terminal al directorio donde se encuentre el archivo models.py, de donde importaremos las clases que creamos y los objetos de conexión. El intérprete de Python pasa a ser, entonces, la cónsola de la base de datos:


<intérprete de python> : (env)$ python models.py

---------------------------------------------------

importando los objetos del archivo models.py

>>> from models import *

creando los objetos de prueba

>>> twitter = Bookmark(“Twitter”, “http://twitter.com/”)
>>> social = Tag(“Social”)
>>> microblogging = Tag(“Microblogging”)

creando la conexión, asociando los objetos y guardándolos

>>> db = SessionMaker()
>>> db.add_all([social, microblogging, twitter])
>>> twitter.tags = [social, microblogging]
>>> db.commit()

realizando consultas en la base de datos para todas las etiquetas

>>> db.query(Tag).all()
[<Tag('Social'), <Tag('Microblogging')>]

realizando consultas en la base de datos para un unico elemento (el marcador)


>>> db.query(Bookmark).one()
<Bookmark('Twitter')>

---------------------------------------------------





3.3.3. Creando las vistas de control para las páginas.



views.py

---------------------------------------------------
from genshihelper import render
from models import SessionMaker, Bookmark, Tag
from pyramid.exceptions import NotFound

def index(request):
db = SessionMaker()
my_bookmarks = db.query(Bookmark).all()
return render("home.html", bookmarks=my_bookmarks)

def view_tag(request):
tagname = request.matchdict['tagname']
db = SessionMaker()
try:
tag = db.query(Tag).filter_by(name=tagname)[0]
return render("tag.html", tag=tag)
except IndexError:
raise NotFound("Tag doesn't exist.")

---------------------------------------------------



Al igual que en el ejemplo de “hola mundo” hecho al principio del tutorial, la página index de nuestro sitio web es una función que recibe un único parámetro request. Sin embargo, ésta hace más que sólo retornar una cadena. En ella, se utiliza una de las consultas que se vió anteriormente en la sección de base de datos (db.query(Bookmark).all()), y se utiliza por primera vez la función render, que recibe el nombre de archivo “home.html”, que deberá estar dentro de la carpeta “templates”.A continuación vamos a instalar el sistema de plantillas genshi y a crear ese archivo:











3.3.4 Instalando y preparando Genshi


Con respecto a los distintos sistemas de plantillas disponibles para Python, su selección depende más del gusto personal que de otra cosa. Existe un gran número de ellos, como mako, genshi o jinga2, y algunos frameworks como Django vienen con su propio sistema de plantillas incorporado. Para éste tutorial vamos a utilizar genshi, que debemos instalar en nuestro sistema (dentro del entorno virtual):


$ easy_install genshi


Ahora, en un archivo que por ahora llamaremos “rendertemplate.py”, vamos a escribir una función que nos permita utilizarlo de la manera más rápida posibe.


rendertemplate.py

---------------------------------------------------

import os.path
from genshi.template import TemplateLoader
from pyramid.response import Response

template = TemplateLoader(
os.path.join(os.path.dirname(__file__), “templates”),
auto_reload=True
)

# funcion principal
def render(filename, **content):
temp = template.load(filename)
html = temp.generate(**content).render("html", doctype="html")

---------------------------------------------------


De éste archivo la parte importante es la función “render”, que recibirá la dirección de nuestros archivos html y las variables que se van a sustituir en él. Ésta función depende del objeto TemplateLoader creado anteriormente, donde “templates” define el directorio donde estarán nuestras plantillas, que debe estar en la misma carpeta que el archivo donde se ejecute.







3.3.5. Definiendo el directorio de trabajo y el archivo de inicio.



Con el modelo para la base de datos creada, ahora podemos definir el archivo que lanzará nuestro sitio web: Crea una carpeta con el nombre que desees, supongamos que fué “tutorial”, y entra en ella a través del terminal con el entorno virtual que creastes antes activado. En ésta carpeta vamos a crear un archivo que se encargue de definir la configuración de nuestro sitio web y nada más. Las otras funciones estarán en otros archivos, para mantener más orden en el código. Lo llamaremos “main.py”, y sería:


main.py

---------------------------------------------------
# importando el servidor, configurador y nuestras vistas
from paste.httpserver import serve
from pyramid.config import Configurator
from views import *
from rendertemplate import *

if __name__ == "__main__":
config = Configurator()

# direcciones para vistas y rutas

config.add_route('index', '/', view=index)

config.add_route('tag', '/tag/{tagname}', view=view_tag)


# directorio de recursos estaticos

static_path = os.path.join(os.path.dirname(__file__), 'static')

config.add_static_view(name="static", path=static_path)

# creacion de aplicacion y activacion del servidor

app = config.make_wsgi_app()

serve(app, host='0.0.0.0')

---------------------------------------------------


# importando el servidor, configurador y nuestras vist

Aparte de los 2 primeros imports que son idénticos a los presentes en el “hola mundo”, ahora estamos llamando a todas las vistas (*) contenidas en views, que será el archivo donde vamos a definirlas en un paso más adelante. También estamos llamando a la función “render” que haremos en un momento para llamar al motor de plantillas Genshi.




Nota:

Para que la instrucción import funcione, éste tiene que llamarse views.py, y estar en el mismo directorio que main.py


# direcciones para vistas y rutas


Un concepto importante dentro de pyramid son las “rutas” (routes), que son una de las dos maneras disponibles en pyramid para definir la navegación de nuestro sitio web (la otra es “trasversal”, para quién desee buscar sobre ella). Las rutas conectan las URL’s que recibe el sitio web con cada función de vista, y también permiten recibir variables a través de ésta dirección. Se establecen con el método add_route del objeto config del mismo modo que antes hicimos antes con el método add_view, pero reciben más parámetros. Por ejemplo, cuando ponemos:


config.add_route('tag', '/tag/{tagname}', view=view_tag)


Los tres parámetros necesarios son:

  • El primer parámetro, en éste caso ‘tag’, sería el nombre que le vamos a poner a la ruta, para que otras funciones que veremos más adelante puedan encontrarla y realizar operaciones en base a ella. El nombre queda completamente a tu elección.

  • El segundo parámetro, en éste caso ‘/tag/{tagname}’, es la dirección que queremos que active ésta ruta. Los corchetes que rodean a “tagname” significan que en ese espacio puede ir cualquier tipo de texto, y que éste será pasado como una variable a la vista, que podrá ser accedida con ese nombre. Para definir la página index del sitio web basta con escribir ‘/’ en éste espacio. Entre las direcciones que ‘/tag/{tagname}’ podrían activar en nuestro sitio web podrían ser, por ejemplo:

  • view=view_tag indica que la vista (view) que debe ser llamada cuando el sitio recibe una dirección que se active con ‘/tag/{tagname}’ es view_tag, una de las vistas que definiremos dentro de un momento en views.py, de un modo similar al de la función “hello world” que hicimos al principio del tutorial.




# directorio de recursos estaticos


Los objetos Configurator poseen el método add_static_view para definir los directorios donde se almacenarán los recursos estáticos, como los archivos CSS y los scripts en javascript. Podríamos escribir ésta dirección completamente a mano, pero en caso de que pongamos la carpeta de nuestro proyecto en otro sitio ésta se haría inválida y nuestro sitio web no funcionaría. Para evitarlo, utilizamos la instruccción os.path.join(os.path.dirname(__file__), 'static'),que escribe para nosotros la ruta completa del sistema hasta la carpeta llamada “static”. Ésta lleva a la carpeta con el mismo nombre dentro de nuestro directorio de trabajo, que debemos crear. Esta dirección la almacenamos en la variable static_path. Después, al ejecutar config.add_static_view(name="static", path=static_path), estaremos indicando el directorio estático de donde podremos servir todos los archivos CSS, Javascript e imágenes, para que nuestro sitio web tenga acceso a ellos.


# creacion de aplicacion y activacion del servidor


Por último, sólo nos queda crear la instancia del objeto que representa nuestra aplicación, que se obtiene a través del método make_wsgi_app del configurador que hicimos anteriormente, que luego pasamos a la función serve junto con el puerto de red al cual debe asociarse. Con ésto, el sitio web ya puede ser ejecutado.


A continuación, se presentan las dos plantillas HTML para la vista principal y la de etiquetas:
















3.3. Plantillas html.


templates/home.html

---------------------------------------------------

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/">
<head>
<title>Yuca Piramidal</title>
<link rel="stylesheet" type="text/css" href="/static/style1.css" />
</head>
<body class="index">
<div id="header"><h1>Índice de Marcadores</h1></div>

<a class="action" href="/add_bookmark/">Agrega un marcador</a><br/>
<a class="action" href="/add_tag/">Agrega una etiqueta</a>
<br/><br/> Etiquetas:
<span py:for="tag in tags">
<a href="/tag/${tag.name}">${tag.name}</a>
</span>

Marcadores:
<ol py:if="bookmarks">
<li py:for="b in bookmarks">
<a href="${b.link}"><b>${b.title}</b></a> [
<span py:for="tag in b.tags">
<a href="/tag/${tag.name}">${tag.name}</a>
</span> ]
</li>
</ol>


<div id="footer">
<hr />
<p class="legalese">Yuca Piramidal ^_^</p>
</div>
</body>

</html>

---------------------------------------------------







templates/tag.html

---------------------------------------------------


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/">

<head>

<title>Yuca Piramidal</title>

<link rel="stylesheet" type="text/css" href="/static/style1.css" />

</head>

<body class="index">

<div id="header">

<h1>Viewing bookmarks for Tag: ${tag.name} </h1>

</div>


<ol py:if="tag">

<li py:for="bookmark in tag.bookmarks">

<a href="${bookmark.link}">${bookmark.title}</a>

</li>

</ol>


<p><a href="/add/">Add new bookmark</a></p>

</body>

</html>

---------------------------------------------------

No hay comentarios:

Publicar un comentario en la entrada