Changeset - 3ada2f409c1c
[Not reviewed]
default
0 8 2
Marcin Kuzminski - 16 years ago 2010-04-08 01:50:46
marcin@python-blog.com
Added sqlalchemy support
made models for database
changed views to handle sqlalchemy
10 files changed with 146 insertions and 30 deletions:
0 comments (0 inline, 0 general)
development.ini
Show inline comments
 
################################################################################
 
################################################################################
 
# pylons_app - Pylons environment configuration                                #
 
#                                                                              # 
 
# The %(here)s variable will be replaced with the parent directory of this file#
 
################################################################################
 

	
 
[DEFAULT]
 
debug = true
 
############################################
 
## Uncomment and replace with the address ##
 
## which should receive any error reports ##
 
############################################
 
#email_to = marcin.kuzminski@etelko.pl
 
#smtp_server = mail.etelko.pl
 
#error_email_from = paste_error@localhost
 
#smtp_username = 
 
#smtp_password = 
 
#error_message = 'mercurial crash !'
 

	
 
[server:main]
 
use = egg:Paste#http
 
host = 127.0.0.1
 
port = 5000
 

	
 
[app:main]
 
use = egg:pylons_app
 
full_stack = true
 
static_files = true
 
lang=en
 
cache_dir = %(here)s/data
 
repos_name = etelko
 

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
beaker.cache.data_dir=/tmp/cache/data
 
beaker.cache.lock_dir=/tmp/cache/lock
 
beaker.cache.regions=short_term
 
beaker.cache.short_term.type=memory
 
beaker.cache.short_term.expire=3600
 
    
 
################################################################################
 
## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*  ##
 
## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
 
## execute malicious code after an exception is raised.                       ##
 
################################################################################
 
#set debug = false
 

	
 
##################################
 
###       LOGVIEW CONFIG       ###
 
##################################
 
logview.sqlalchemy = #faa
 
logview.pylons.templating = #bfb
 
logview.pylons.util = #eee
 

	
 
#########################################################
 
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 
#########################################################
 
sqlalchemy.db1.url = sqlite:///%(here)s/auth.sqlite
 
#sqlalchemy.db1.echo = True
 
#sqlalchemy.db1.pool_recycle = 3600
 
sqlalchemy.convert_unicode = true
 

	
 
################################
 
### LOGGING CONFIGURATION   ####
 
################################
 
[loggers]
 
keys = root, routes, pylons_app, sqlalchemy
 

	
 
[handlers]
 
keys = console
 

	
 
[formatters]
 
keys = generic
 

	
 
#############
 
## LOGGERS ##
 
#############
 
[logger_root]
 
level = NOTSET
 
handlers = console
 

	
 
[logger_routes]
 
level = INFO
 
handlers = console
 
qualname = routes.middleware
 
# "level = DEBUG" logs the route matched and routing variables.
 

	
 
[logger_pylons_app]
 
level = DEBUG
 
handlers = console
 
qualname = pylons_app
 

	
 

	
 
[logger_sqlalchemy]
 
level = DEBUG
 
handlers = console
 
qualname = sqlalchemy.engine
 

	
 
##############
 
## HANDLERS ##
 
##############
 

	
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = NOTSET
 
formatter = generic
 

	
 
################
 
## FORMATTERS ##
 
################
 

	
 
[formatter_generic]
 
format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
production.ini
Show inline comments
 
################################################################################
 
################################################################################
 
# pylons_app - Pylons environment configuration                                #
 
#                                                                              # 
 
# The %(here)s variable will be replaced with the parent directory of this file#
 
################################################################################
 

	
 
[DEFAULT]
 
debug = true
 
############################################
 
## Uncomment and replace with the address ##
 
## which should receive any error reports ##
 
############################################
 
#email_to = marcin.kuzminski@etelko.pl
 
#smtp_server = mail.etelko.pl
 
#error_email_from = paste_error@localhost
 
#smtp_username = 
 
#smtp_password = 
 
#error_message = 'mercurial crash !'
 

	
 
[server:main]
 
use = egg:Paste#http
 
host = 127.0.0.1
 
port = 8001
 

	
 
[app:main]
 
use = egg:pylons_app
 
full_stack = true
 
static_files = true
 
lang=en
 
cache_dir = %(here)s/data
 
repos_name = etelko
 

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
beaker.cache.data_dir=/tmp/cache/data
 
beaker.cache.lock_dir=/tmp/cache/lock
 
beaker.cache.regions=short_term
 
beaker.cache.short_term.type=memory
 
beaker.cache.short_term.expire=3600
 
    
 
################################################################################
 
## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*  ##
 
## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
 
## execute malicious code after an exception is raised.                       ##
 
################################################################################
 
#set debug = false
 

	
 
##################################
 
###       LOGVIEW CONFIG       ###
 
##################################
 
logview.sqlalchemy = #faa
 
logview.pylons.templating = #bfb
 
logview.pylons.util = #eee
 

	
 
#########################################################
 
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 
#########################################################
 
sqlalchemy.db1.url = sqlite:///%(here)s/auth.sqlite
 
#sqlalchemy.db1.echo = True
 
#sqlalchemy.db1.pool_recycle = 3600
 
sqlalchemy.convert_unicode = true
 

	
 
################################
 
### LOGGING CONFIGURATION   ####
 
################################
 
[loggers]
 
keys = root, routes, pylons_app, sqlalchemy
 

	
 
[handlers]
 
keys = console
 

	
 
[formatters]
 
keys = generic
 

	
 
#############
 
## LOGGERS ##
 
#############
 
[logger_root]
 
level = INFO
 
handlers = console
 

	
 
[logger_routes]
 
level = INFO
 
handlers = console
 
qualname = routes.middleware
 
# "level = DEBUG" logs the route matched and routing variables.
 

	
 
[logger_pylons_app]
 
level = DEBUG
 
handlers = console
 
qualname = pylons_app
 

	
 

	
 
[logger_sqlalchemy]
 
level = DEBUG
 
handlers = console
 
qualname = sqlalchemy.engine
 

	
 
##############
 
## HANDLERS ##
 
##############
 

	
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = NOTSET
 
formatter = generic
 

	
 
################
 
## FORMATTERS ##
 
################
 

	
 
[formatter_generic]
 
format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
pylons_app/config/environment.py
Show inline comments
 
"""Pylons environment configuration"""
 
import logging
 
import os
 

	
 
from mako.lookup import TemplateLookup
 
from pylons.configuration import PylonsConfig
 
from pylons.error import handle_mako_error
 
from sqlalchemy import engine_from_config
 

	
 
import pylons_app.lib.app_globals as app_globals
 
import pylons_app.lib.helpers
 
from pylons_app.config.routing import make_map
 
from pylons_app.model import init_model
 

	
 
log = logging.getLogger(__name__)
 

	
 
def load_environment(global_conf, app_conf):
 
    """Configure the Pylons environment via the ``pylons.config``
 
    object
 
    """
 
    config = PylonsConfig()
 
    
 
    # Pylons paths
 
    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
    paths = dict(root=root,
 
                 controllers=os.path.join(root, 'controllers'),
 
                 static_files=os.path.join(root, 'public'),
 
                 templates=[os.path.join(root, 'templates')])
 

	
 
    # Initialize config with the basic options
 
    config.init_app(global_conf, app_conf, package='pylons_app', paths=paths)
 

	
 
    config['routes.map'] = make_map(config)
 
    config['pylons.app_globals'] = app_globals.Globals(config)
 
    config['pylons.h'] = pylons_app.lib.helpers
 
    
 
    # Setup cache object as early as possible
 
    import pylons
 
    pylons.cache._push_object(config['pylons.app_globals'].cache)
 
    
 

	
 
    # Create the Mako TemplateLookup, with the default auto-escaping
 
    config['pylons.app_globals'].mako_lookup = TemplateLookup(
 
        directories=paths['templates'],
 
        error_handler=handle_mako_error,
 
        module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
 
        input_encoding='utf-8', default_filters=['escape'],
 
        imports=['from webhelpers.html import escape'])
 

	
 
    #sets the c attribute access when don't existing attribute ar accessed
 
    config['pylons.strict_tmpl_context'] = False
 
    
 
    #MULTIPLE DB configs
 
    # Setup the SQLAlchemy database engine
 
#    if config['debug']:
 
#        #use query time debugging.
 
#        from pylons_app.lib.timer_proxy import TimerProxy
 
#        sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.',
 
#                                                            proxy=TimerProxy())
 
#    else:
 
#        sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
 
    if config['debug']:
 
        #use query time debugging.
 
        from pylons_app.lib.timerproxy import TimerProxy
 
        sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.',
 
                                                            proxy=TimerProxy())
 
    else:
 
        sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
 

	
 
    #init_model(sa_engine_db1)
 
    init_model(sa_engine_db1)
 

	
 
    # CONFIGURATION OPTIONS HERE (note: all config options will override
 
    # any Pylons config options)
 
    
 
    return config
pylons_app/config/middleware.py
Show inline comments
 
"""Pylons middleware initialization"""
 
from beaker.middleware import SessionMiddleware
 
from paste.cascade import Cascade
 
from paste.registry import RegistryManager
 
from paste.urlparser import StaticURLParser
 
from paste.deploy.converters import asbool
 
from pylons.middleware import ErrorHandler, StatusCodeRedirect
 
from pylons.wsgiapp import PylonsApp
 
from routes.middleware import RoutesMiddleware
 
from paste.auth.basic import AuthBasicHandler
 
from pylons_app.config.environment import load_environment
 
from pylons_app.lib.auth import authfunc 
 

	
 
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
 
    """Create a Pylons WSGI application and return it
 

	
 
    ``global_conf``
 
        The inherited configuration for this application. Normally from
 
        the [DEFAULT] section of the Paste ini file.
 

	
 
    ``full_stack``
 
        Whether or not this application provides a full WSGI stack (by
 
        default, meaning it handles its own exceptions and errors).
 
        Disable full_stack when this application is "managed" by
 
        another WSGI middleware.
 

	
 
    ``app_conf``
 
        The application's local configuration. Normally specified in
 
        the [app:<name>] section of the Paste ini file (where <name>
 
        defaults to main).
 

	
 
    """
 
    # Configure the Pylons environment
 
    config = load_environment(global_conf, app_conf)
 

	
 

	
 
    # The Pylons WSGI app
 
    app = PylonsApp(config=config)
 

	
 
    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
 

	
 
    # Routing/Session/Cache Middleware
 
    app = RoutesMiddleware(app, config['routes.map'])
 
    app = SessionMiddleware(app, config)
 
    app = AuthBasicHandler(app, config['repos_name'] + ' mercurial repository', authfunc)
 
    
 
    if asbool(full_stack):
 
        # Handle Python exceptions
 
        app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
 

	
 
        # Display error documents for 401, 403, 404 status codes (and
 
        # 500 when debug is disabled)
 
        if asbool(config['debug']):
 
            #don't handle 404, since mercurial does it for us.
 
            app = StatusCodeRedirect(app, [400, 401, 403, 500])
 
            app = StatusCodeRedirect(app, [400, 401, 403])
 
        else:
 
            app = StatusCodeRedirect(app, [400, 401, 403, 500])
 
    
 
    # Establish the Registry for this application
 
    app = RegistryManager(app)
 

	
 
    if asbool(static_files):
 
        # Serve static files
 
        static_app = StaticURLParser(config['pylons.paths']['static_files'])
 
        app = Cascade([static_app, app])
 
    
 
        app.config = config
 

	
 
    return app
 

	
pylons_app/controllers/repos.py
Show inline comments
 
import logging
 

	
 
from pylons import request, response, session, tmpl_context as c, url, app_globals as g
 
from pylons.controllers.util import abort, redirect
 
from pylons_app.lib import auth
 
from pylons_app.lib.base import BaseController, render
 

	
 
from pylons_app.model import meta
 
from pylons_app.model.db import Users, UserLogs
 
log = logging.getLogger(__name__)
 

	
 
class ReposController(BaseController):
 
    """REST Controller styled on the Atom Publishing Protocol"""
 
    # To properly map this controller, ensure your config/routing.py
 
    # file has a resource setup:
 
    #     map.resource('repo', 'repos')
 
    def __before__(self):
 
        c.staticurl = g.statics
 
        c.admin_user = session.get('admin_user')
 
        c.admin_username = session.get('admin_username')
 
        
 
        self.sa = meta.Session
 
                
 
    def index(self, format='html'):
 
        """GET /repos: All items in the collection"""
 
        # url('repos')
 
        return render('/repos.html')
 
    
 
    def create(self):
 
        """POST /repos: Create a new item"""
 
        # url('repos')
 

	
 
    def new(self, format='html'):
 
        """GET /repos/new: Form to create a new item"""
 
        # url('new_repo')
 

	
 
    def update(self, id):
 
        """PUT /repos/id: Update an existing item"""
 
        # Forms posted to this method should contain a hidden field:
 
        #    <input type="hidden" name="_method" value="PUT" />
 
        # Or using helpers:
 
        #    h.form(url('repo', id=ID),
 
        #           method='put')
 
        # url('repo', id=ID)
 

	
 
    def delete(self, id):
 
        """DELETE /repos/id: Delete an existing item"""
 
        # Forms posted to this method should contain a hidden field:
 
        #    <input type="hidden" name="_method" value="DELETE" />
 
        # Or using helpers:
 
        #    h.form(url('repo', id=ID),
 
        #           method='delete')
 
        # url('repo', id=ID)
 

	
 
    def show(self, id, format='html'):
 
        """GET /repos/id: Show a specific item"""
 
        # url('repo', id=ID)
 
        return render('/repos_show.html')
 
    def edit(self, id, format='html'):
 
        """GET /repos/id/edit: Form to edit an existing item"""
 
        # url('edit_repo', id=ID)
pylons_app/controllers/users.py
Show inline comments
 
import logging
 

	
 
from pylons import request, response, session, tmpl_context as c, url, app_globals as g
 
from pylons.controllers.util import abort, redirect
 

	
 
from pylons_app.lib.base import BaseController, render
 
from pylons_app.lib import auth
 
from formencode import htmlfill
 
from pylons_app.model import meta
 
from pylons_app.model.db import Users, UserLogs
 
log = logging.getLogger(__name__)
 

	
 
class UsersController(BaseController):
 
    """REST Controller styled on the Atom Publishing Protocol"""
 
    # To properly map this controller, ensure your config/routing.py
 
    # file has a resource setup:
 
    #     map.resource('user', 'users')
 
    def __before__(self):
 
        c.staticurl = g.statics
 
        c.admin_user = session.get('admin_user')
 
        c.admin_username = session.get('admin_username')
 
        self.conn, self.cur = auth.get_sqlite_conn_cur()
 
        self.sa = meta.Session
 
        
 
    def index(self, format='html'):
 
        """GET /users: All items in the collection"""
 
        # url('users')
 
        
 
        self.cur.execute('SELECT * FROM users')
 
        c.users_list = self.cur.fetchall()        
 
        c.users_list = self.sa.query(Users).all()     
 
        return render('/users.html')
 
    
 
    def create(self):
 
        """POST /users: Create a new item"""
 
        # url('users')
 

	
 
    def new(self, format='html'):
 
        """GET /users/new: Form to create a new item"""
 
        # url('new_user')
 

	
 
    def update(self, id):
 
        """PUT /users/id: Update an existing item"""
 
        # Forms posted to this method should contain a hidden field:
 
        #    <input type="hidden" name="_method" value="PUT" />
 
        # Or using helpers:
 
        #    h.form(url('user', id=ID),
 
        #           method='put')
 
        # url('user', id=ID)
 

	
 
    def delete(self, id):
 
        """DELETE /users/id: Delete an existing item"""
 
        # Forms posted to this method should contain a hidden field:
 
        #    <input type="hidden" name="_method" value="DELETE" />
 
        # Or using helpers:
 
        #    h.form(url('user', id=ID),
 
        #           method='delete')
 
        # url('user', id=ID)
 
        try:
 
            self.cur.execute("DELETE FROM users WHERE user_id=?", (id,))
 
            self.conn.commit()
 
            self.sa.delete(self.sa.query(Users).get(id))
 
            self.sa.commit()
 
        except:
 
            self.conn.rollback()
 
            self.sa.rollback()
 
            raise
 
        return redirect(url('users'))
 
        
 
    def show(self, id, format='html'):
 
        """GET /users/id: Show a specific item"""
 
        # url('user', id=ID)
 
        self.cur.execute("SELECT * FROM users WHERE user_id=?", (id,))
 
        ret = self.cur.fetchone()
 
        c.user_name = ret[1]
 
        return render('/users_show.html')
 
        c.user = self.sa.query(Users).get(id)
 

	
 
        return htmlfill.render(
 
            render('/users_show.html'),
 
            defaults=c.user.__dict__,
 
            encoding="UTF-8",
 
            force_defaults=False
 
        )        
 
    
 
    def edit(self, id, format='html'):
 
        """GET /users/id/edit: Form to edit an existing item"""
 
        # url('edit_user', id=ID)
pylons_app/lib/timerproxy.py
Show inline comments
 
new file 100644
 
from sqlalchemy.interfaces import ConnectionProxy
 
import time
 
import logging
 
log = logging.getLogger(__name__)
 

	
 
class TimerProxy(ConnectionProxy):
 
    def cursor_execute(self, execute, cursor, statement, parameters, context, executemany):
 
        now = time.time()
 
        try:
 
            log.info(">>>>> STARTING QUERY >>>>>")
 
            return execute(cursor, statement, parameters, context)
 
        finally:
 
            total = time.time() - now
 
            log.info("Query: %s" % statement % parameters)
 
            log.info("<<<<< TOTAL TIME: %f <<<<<" % total)
pylons_app/model/db.py
Show inline comments
 
new file 100644
 
from sqlalchemy.ext.declarative import declarative_base
 
from sqlalchemy.orm import relation, backref
 
from sqlalchemy import ForeignKey, Column, Table, Sequence
 
from sqlalchemy.types import *
 
from sqlalchemy.databases.sqlite import *
 
from pylons_app.model.meta import Base
 

	
 

	
 
class Users(Base): 
 
    __tablename__ = 'users'
 
    __table_args__ = {'useexisting':True}
 
    user_id = Column("user_id", SLInteger(), nullable=False, unique=True, default=None, primary_key=1)
 
    username = Column("username", SLText(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
    password = Column("password", SLText(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
    active = Column("active", SLInteger(), nullable=True, unique=None, default=None)
 
    admin = Column("admin", SLInteger(), nullable=True, unique=None, default=None)
 
    
 
class UserLogs(Base): 
 
    __tablename__ = 'user_logs'
 
    __table_args__ = {'useexisting':True}
 
    id = Column("id", SLInteger(), nullable=False, unique=True, default=None, primary_key=1)
 
    user_id = Column("user_id", SLInteger(), nullable=True, unique=None, default=None)
 
    last_action = Column("last_action", SLText(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
    last_action_date = Column("last_action_date", SLDateTime(timezone=False), nullable=True, unique=None, default=None)
pylons_app/templates/users.html
Show inline comments
 
<%inherit file="base/base.html"/>
 
<%def name="title()">
 
    ${_('Repository managment')}
 
</%def>
 
<%def name="breadcrumbs()">
 
    ${h.link_to(u'Home',h.url('/'))}
 
    / 
 
    ${h.link_to(u'Admin',h.url('admin_home'))}
 
    /
 
    ${h.link_to(u'Users managment',h.url('users'))}
 
</%def>
 
<%def name="page_nav()">
 
    <li>${h.link_to(u'Home',h.url('/'))}</li>
 
    <li class="current">${_('Admin')}</li>
 
</%def>
 
<%def name="main()">
 
    <ul class="submenu">
 
        <li>
 
            ${h.link_to(u'Repos',h.url('repos'))}
 
        </li>
 
        <li class="current_submenu">
 
            ${h.link_to(u'Users',h.url('users'))}
 
        </li>
 
    </ul>
 
	<div>
 
        <h2>${_('Mercurial users')}</h2>
 
        <table>
 
         <tr>
 
            <th>Id</th>
 
            <th>Username</th>
 
            <th>Active</th>
 
            <th>Admin</th>
 
            <th>Action</th>
 
         </tr>
 
            %for i in c.users_list:
 
            %for user in c.users_list:
 
                <tr>
 
                    <td>${i[0]}</td>
 
                    <td>${h.link_to(i[1],h.url('user', id=i[0]))}</td>
 
                    <td>${i[3]}</td>
 
                    <td>${i[4]}</td>
 
                    <td>${user.user_id}</td>
 
                    <td>${h.link_to(user.username,h.url('user', id=user.user_id))}</td>
 
                    <td>${user.active}</td>
 
                    <td>${user.admin}</td>
 
                    <td>
 
	                    ${h.form(url('user', id=i[0]),method='delete')}
 
	                    ${h.form(url('user', id=user.user_id),method='delete')}
 
	                    	${h.submit('remove','remove',class_="submit")}
 
	                    ${h.end_form()}
 
        			</td>
 
                </tr>
 
            %endfor
 
        </table>        
 
    </div>
 

	
 
</%def>    
 
\ No newline at end of file
pylons_app/templates/users_show.html
Show inline comments
 
<%inherit file="base/base.html"/>
 
<%def name="title()">
 
    ${_('User c.user_name')}
 
    ${_('User')} - ${c.user.username}
 
</%def>
 
<%def name="breadcrumbs()">
 
    ${h.link_to(u'Home',h.url('/'))}
 
    / 
 
    ${h.link_to(u'Admin',h.url('admin_home'))}
 
    /
 
    ${h.link_to(u'Users',h.url('users'))}
 
</%def>
 
<%def name="page_nav()">
 
	<li>${h.link_to(u'Home',h.url('/'))}</li>
 
	<li class="current">${_('Admin')}</li>
 
</%def>
 
<%def name="main()">
 
    <ul class="submenu">
 
        <li>
 
            ${h.link_to(u'Repos',h.url('repos'))}
 
        </li>
 
        <li class="current_submenu">
 
            ${h.link_to(u'Users',h.url('users'))}
 
        </li>
 
    </ul>
 
	<div>
 
        <h2>${_('User')} - ${c.user_name}</h2>
 
        <h2>${_('User')} - ${c.user.username}</h2>
 
        ${h.form(url('user', id=c.user.user_id),method='put')}
 
        <table>
 
        	<tr>
 
        		<td>${_('Username')}</td>
 
        		<td>${h.text('username')}</td>
 
        	</tr>
 
        	<tr>
 
        		<td>${_('New password')}</td>
 
        		<td>${h.text('new_password')}</td>
 
        	</tr>
 
        	<tr>
 
        		<td>${_('Active')}</td>
 
        		<td>${h.checkbox('active')}</td>
 
        	</tr>
 
        	<tr>
 
        		<td></td>
 
        		<td>${h.submit('save','save')}</td>
 
        	</tr>
 
        	        	        	
 
        </table>
 
        	
 
        ${h.end_form()}
 
    </div>
 
</%def>    
 
\ No newline at end of file
0 comments (0 inline, 0 general)