Commit f577a9e1 by Julien Muchembled

Merge ZMySQLDDA product into ZMySQLDA

parent ccfbfcbc
......@@ -67,7 +67,7 @@ def findMessageListFromPythonInProduct(function_name_list):
'ERP5Form', 'ERP5OOo', 'ERP5Security', 'ERP5VCS',
'ERP5SyncML', 'ERP5Type', 'ERP5Wizard', 'ERP5Workflow',
'HBTreeFolder2', 'MailTemplates', 'TimerService',
'ZMySQLDA', 'ZMySQLDDA', 'ZSQLCatalog',
'ZMySQLDA', 'ZSQLCatalog',
)
result = []
def findStaticMessage(file_path):
......
......@@ -1890,11 +1890,12 @@ class ERP5Generator(PortalGenerator):
if p.erp5_sql_deferred_connection_type == \
'Z MySQL Deferred Database Connection':
if not p.hasObject('erp5_sql_deferred_connection'):
addSQLConnection = p.manage_addProduct['ZMySQLDDA'].\
manage_addZMySQLDeferredConnection
addSQLConnection = p.manage_addProduct['ZMySQLDA'].\
manage_addZMySQLConnection
addSQLConnection('erp5_sql_deferred_connection',
'ERP5 SQL Server Deferred Connection',
p.erp5_sql_deferred_connection_string)
p.erp5_sql_deferred_connection_string,
deferred=True)
elif p.erp5_sql_deferred_connection_type == 'Z Gadfly':
pass
......
......@@ -38,7 +38,6 @@ From ERP5 subversion repository:
* ERP5SyncML
* ERP5Type
* ZSQLCatalog
* ZMySQLDDA
Installation
============
......
......@@ -98,17 +98,24 @@ from App.special_dtml import HTMLFile
from App.ImageFile import ImageFile
from DateTime import DateTime
from . import DABase
from .db import DB
from .db import DB, DeferredDB
SHARED_DC_ZRDB_LOCATION = os.path.dirname(Shared.DC.ZRDB.__file__)
manage_addZMySQLConnectionForm=HTMLFile('connectionAdd',globals())
def manage_addZMySQLConnection(self, id, title,
connection_string,
check=None, REQUEST=None):
"""Add a DB connection to a folder"""
connection = Connection(id, title, connection_string)
def manage_addZMySQLConnection(self, id, title, connection_string,
check=None, deferred=False, REQUEST=None):
"""Add a MySQL connection to a folder.
Arguments:
REQUEST -- The current request
title -- The title of the ZMySQLDA Connection (string)
id -- The id of the ZMySQLDA Connection (string)
connection_string -- see connectionAdd.dtml
"""
cls = DeferredConnection if deferred else Connection
connection = cls(id, title, connection_string)
self._setObject(id, connection)
if check:
connection.connect(connection_string)
......@@ -119,7 +126,8 @@ def manage_addZMySQLConnection(self, id, title,
database_connection_pool = defaultdict(WeakKeyDictionary)
class Connection(DABase.Connection):
" "
"""MySQL Connection Object
"""
database_type=database_type
id='%s_database_connection' % database_type
meta_type=title='Z %s Database Connection' % database_type
......@@ -164,6 +172,28 @@ class Connection(DABase.Connection):
return connection.string_literal(v)
class DeferredConnection(Connection):
"""
Experimental MySQL DA which implements
deferred SQL code execution to reduce locking issues
"""
meta_type=title='Z %s Deferred Database Connection' % database_type
def factory(self): return DeferredDB
# BBB: Allow loading of deferred connections that were created
# before the merge of ZMySQLDDA into ZMySQLDA.
import sys, imp
m = 'Products.ZMySQLDDA'
assert m not in sys.modules, "please remove obsolete ZMySQLDDA product"
sys.modules[m] = imp.new_module(m)
m += '.DA'
sys.modules[m] = m = imp.new_module(m)
m.DeferredConnection = DeferredConnection
del m
meta_types=(
{'name':'Z %s Database Connection' % database_type,
'action':'manage_addZ%sConnectionForm' % database_type,
......
......@@ -100,7 +100,3 @@ def initialize(context):
constructors=(DA.manage_addZMySQLConnectionForm,
DA.manage_addZMySQLConnection),
)
context.registerHelp()
context.registerHelpTitle('ZMySQLDA')
......@@ -28,6 +28,12 @@
<input type="TEXT" name="connection_string" size="40">
</td>
</tr>
<tr>
<th align="LEFT" valign="TOP">Create Deferred Connection</th>
<td align="LEFT" valign="TOP">
<input name="deferred:int" type="CHECKBOX" value="1">
</td>
</tr>
<tr>
<th align="LEFT" valign="TOP">Connect immediately</th>
<td align="LEFT" valign="TOP">
......
......@@ -165,6 +165,7 @@ def ord_or_None(s):
return ord(s)
class DB(TM):
"""This is the ZMySQLDA Database Connection Object."""
conv=conversions.copy()
conv[FIELD_TYPE.LONG] = int
......@@ -253,6 +254,7 @@ class DB(TM):
def tables(self, rdb=0,
_care=('TABLE', 'VIEW')):
"""Returns a list of tables in the current database."""
r=[]
a=r.append
result = self._query("SHOW TABLES")
......@@ -263,6 +265,7 @@ class DB(TM):
return r
def columns(self, table_name):
"""Returns a list of column descriptions for 'table_name'."""
try:
c = self._query('SHOW COLUMNS FROM %s' % table_name)
except Exception:
......@@ -360,6 +363,7 @@ class DB(TM):
return self.db.store_result()
def query(self, query_string, max_rows=1000):
"""Execute 'query_string' and return at most 'max_rows'."""
self._use_TM and self._register()
desc = None
# XXX deal with a typical mistake that the user appends
......@@ -394,6 +398,7 @@ class DB(TM):
return self.db.string_literal(s)
def _begin(self, *ignored):
"""Begin a transaction (when TM is enabled)."""
try:
self._transaction_begun = True
# Ping the database to reconnect if connection was closed.
......@@ -408,6 +413,7 @@ class DB(TM):
raise
def _finish(self, *ignored):
"""Commit a transaction (when TM is enabled)."""
if not self._transaction_begun:
return
self._transaction_begun = False
......@@ -417,6 +423,7 @@ class DB(TM):
self._query("COMMIT")
def _abort(self, *ignored):
"""Rollback a transaction (when TM is enabled)."""
if not self._transaction_begun:
return
self._transaction_begun = False
......@@ -427,3 +434,44 @@ class DB(TM):
else:
LOG('ZMySQLDA', ERROR, "aborting when non-transactional")
class DeferredDB(DB):
"""
An experimental MySQL DA which implements deferred execution
of SQL code in order to reduce locks and provide better behaviour
with MyISAM non transactional tables
"""
def __init__(self, *args, **kw):
DB.__init__(self, *args, **kw)
assert self._use_TM
self._sql_string_list = []
def query(self,query_string, max_rows=1000):
self._register()
for qs in query_string.split('\0'):
qs = qs.strip()
if qs:
if qs[:6].upper() == "SELECT":
raise NotSupportedError(
"can not SELECT in deferred connections")
self._sql_string_list.append(qs)
return (),()
def _begin(self, *ignored):
# The Deferred DB instance is sometimes used for several
# transactions, so it is required to clear the sql_string_list
# each time a transaction starts
del self._sql_string_list[:]
def _finish(self, *ignored):
# BUG: It's wrong to execute queries here because tpc_finish must not
# fail. Consider moving them to commit, tpc_vote or in an
# after-commit hook.
if self._sql_string_list:
DB._begin(self)
for qs in self._sql_string_list:
self._query(qs)
del self._sql_string_list[:]
DB._finish(self)
_abort = _begin
def manage_addZMySQLConnection(self, id, title,
connection_string,
check=None, REQUEST=None):
"""Add a MySQL connection to a folder.
Arguments:
REQUEST -- The current request
title -- The title of the ZMySQLDA Connection (string)
id -- The id of the ZMySQLDA Connection (string)
connection_string -- The connection string is of the form:
'[*lock] [+/-][database][@host[:port]] [user [password [unix_socket]]]'
or typically:
'database user password'
to use a MySQL server on localhost via the standard UNIX
socket. Only specify host if the server is on a remote
system. You can use a non-standard port, if necessary. If the
UNIX socket is in a non-standard location, you can specify the
full path to it after the password. Hint: To use a
non-standard port on the local system, use 127.0.0.1 for the
host instead of localhost.
Either a database or a host or both must be specified.
A '-' in front of the database tells ZMySQLDA to not use
Zope's Transaction Manager, even if the server supports
transactions. A '+' in front of the database tells ZMySQLDA
that it must use transactions; an exception will be raised if
they are not supported by the server. If neither '-' or '+'
are present, then transactions will be enabled if the server
supports them. If you are using non-transaction safe tables
(TSTs) on a server that supports TSTs, use '-'. If you require
transactions, use '+'. If you aren't sure, don't use either.
*lock at the begining of the connection string means to
psuedo-transactional. When the transaction begins, it will
acquire a lock on the server named lock (i.e. MYLOCK). When
the transaction commits, the lock will be released. If the
transaction is aborted and restarted, which can happen due to
a ConflictError, you'll get an error in the logs, and
inconsistent data. In this respect, it's equivalent to
transactions turned off.
Transactions are highly recommended. Using a named lock in
conjunctions with transactions is probably pointless.
"""
class Connection:
"""MySQL Connection Object"""
__constructor__ = manage_addZMySQLConnection
def manage_addZMySQLConnection(self, id, title,
connection_string,
check=None, REQUEST=None):
"""Add a MySQL connection to a folder.
Arguments:
REQUEST -- The current request
title -- The title of the ZMySQLDA Connection (string)
id -- The id of the ZMySQLDA Connection (string)
connection_string -- The connection string is of the form:
'database[@host[:port]] [user [password [unix_socket]]]'
or typically:
'database user password'
to use a MySQL server on localhost via the standard UNIX socket.
Only specify host if the server is on a remote system. You can
use a non-standard port, if necessary. If the UNIX socket is in
a non-standard location, you can specify the full path to it
after the password.
"""
class Connection:
"""MySQL Connection Object"""
__constructor__ = manage_addZMySQLConnection
class DB:
"""This is the ZMySQLDA Database Connection Object."""
def __init__(self,connection):
"""
connection
blah blah
"""
def tables(self, rdb=0,
_care=('TABLE', 'VIEW')):
"""Returns a list of tables in the current database."""
def columns(self, table_name):
"""Returns a list of column descriptions for 'table_name'."""
def query(self,query_string, max_rows=1000):
"""Execute 'query_string' and return at most 'max_rows'."""
def _begin(self, *ignored):
"""Begin a transaction (when TM is enabled)."""
def _finish(self, *ignored):
"""Commit a transaction (when TM is enabled)."""
def _abort(self, *ignored):
"""Rollback a transaction (when TM is enabled)."""
......@@ -30,7 +30,6 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from AccessControl.SecurityManagement import newSecurityManager
from _mysql_exceptions import OperationalError
from Products.ZMySQLDA.db import hosed_connection
from thread import get_ident
from zLOG import LOG
UNCONNECTED_STATE = 0
......@@ -97,22 +96,16 @@ class TestDeferredConnection(ERP5TypeTestCase):
Revert monkeypatching done on db.
"""
connection.__class__._forceReconnection = connection.__class__.original_forceReconnection
delattr(connection.__class__, 'original_forceReconnection')
del connection.__class__.original_forceReconnection
mysql_class = connection.db.__class__
mysql_class.query = mysql_class.original_query
delattr(mysql_class, 'original_query')
del mysql_class.original_query
def getDeferredConnection(self):
"""
Return site's deferred connection object.
"""
deferred = self.getPortal().erp5_sql_deferred_connection
deferred_connection = getattr(deferred, '_v_database_connection', None)
if deferred_connection is None:
deferred.connect(deferred.connection_string)
deferred_connection = getattr(deferred, '_v_database_connection')
deferred_connection.tables() # Dummy access to force actual connection.
return deferred_connection._pool_get(get_ident())
return self.portal.erp5_sql_deferred_connection()
def test_00_basicReplaceQuery(self):
"""
......@@ -151,7 +144,7 @@ class TestDeferredConnection(ERP5TypeTestCase):
self.fail()
finally:
self.abort()
delattr(connection, '_query')
del connection._query
self.unmonkeypatchConnection(connection)
def test_02_disconnectionRobustness(self):
......
Z MySQL Deferred DA Releases
2.0.9
Initial Release
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
# Copyright (c) Nexedi SARL 2004. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
database_type='MySQL'
import os.path
from db import ThreadedDeferredDB
import Shared.DC.ZRDB
import DABase
from App.Dialogs import MessageDialog
from App.special_dtml import HTMLFile
from App.ImageFile import ImageFile
from ExtensionClass import Base
from DateTime import DateTime
from thread import allocate_lock
SHARED_DC_ZRDB_LOCATION = os.path.dirname(Shared.DC.ZRDB.__file__)
manage_addZMySQLDeferredConnectionForm=HTMLFile('deferredConnectionAdd',globals())
def manage_addZMySQLDeferredConnection(self, id, title,
connection_string,
check=None, REQUEST=None):
"""Add a DB connection to a folder"""
self._setObject(id, DeferredConnection(id, title, connection_string, check))
if REQUEST is not None: return self.manage_main(self,REQUEST)
# Connection Pool for connections to MySQL.
database_connection_pool_lock = allocate_lock()
database_connection_pool = {}
class DeferredConnection(DABase.Connection):
"""
Experimental MySQL DA which implements
deferred SQL code execution to reduce locking issues
"""
database_type=database_type
id='%s_database_connection' % database_type
meta_type=title='Z %s Deferred Database Connection' % database_type
icon='misc_/Z%sDDA/conn' % database_type
manage_properties=HTMLFile('connectionEdit', globals())
def factory(self): return ThreadedDeferredDB
def connect(self, s):
try:
database_connection_pool_lock.acquire()
self._v_connected = ''
pool_key = self.getPhysicalPath()
connection = database_connection_pool.get(pool_key)
if connection is not None and connection._connection == s:
self._v_database_connection = connection
else:
if connection is not None:
connection.closeConnection()
ThreadedDeferredDB = self.factory()
database_connection_pool[pool_key] = ThreadedDeferredDB(s)
self._v_database_connection = database_connection_pool[pool_key]
# XXX If date is used as such, it can be wrong because an existing
# connection may be reused. But this is suposedly only used as a
# marker to know if connection was successfull.
self._v_connected = DateTime()
finally:
database_connection_pool_lock.release()
return self
def sql_quote__(self, v, escapes={}):
return self._v_database_connection.string_literal(v)
classes=('DA.DeferredConnection')
meta_types=(
{'name':'Z %s Deferred Database Connection' % database_type,
'action':'manage_addZ%sDeferredConnectionForm' % database_type,
},
)
folder_methods={
'manage_addZMySQLDeferredConnection':
manage_addZMySQLDeferredConnection,
'manage_addZMySQLDeferredConnectionForm':
manage_addZMySQLDeferredConnectionForm,
}
__ac_permissions__=(
('Add Z MySQL Database Connections',
('manage_addZMySQLDeferredConnectionForm',
'manage_addZMySQLDeferredConnection')),
)
misc_={'conn': ImageFile(
os.path.join(SHARED_DC_ZRDB_LOCATION,'www','DBAdapterFolder_icon.gif'))}
for icon in ('table', 'view', 'stable', 'what',
'field', 'text','bin','int','float',
'date','time','datetime'):
misc_[icon]=ImageFile(os.path.join('icons','%s.gif') % icon, globals())
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
__doc__='''Database Connection
$Id: DABase.py,v 1.5 2001/08/17 02:17:38 adustman Exp $'''
__version__='$Revision: 1.5 $'[11:-2]
import Shared.DC.ZRDB.Connection, sys
from App.special_dtml import HTMLFile
from ExtensionClass import Base
import Acquisition
class Connection(Shared.DC.ZRDB.Connection.Connection):
_isAnSQLConnection=1
manage_options=Shared.DC.ZRDB.Connection.Connection.manage_options+(
{'label': 'Browse', 'action':'manage_browse'},
# {'label': 'Design', 'action':'manage_tables'},
)
manage_tables=HTMLFile('tables',globals())
manage_browse=HTMLFile('browse',globals())
info=None
def tpValues(self):
#if hasattr(self, '_v_tpValues'): return self._v_tpValues
r=[]
# self._v_tables=tables=TableBrowserCollection()
#tables=tables.__dict__
c=self._v_database_connection
try:
for d in c.tables(rdb=0):
try:
name=d['TABLE_NAME']
b=TableBrowser()
b.__name__=name
b._d=d
b._c=c
#b._columns=c.columns(name)
b.icon=table_icons.get(d['TABLE_TYPE'],'text')
r.append(b)
# tables[name]=b
except:
# print d['TABLE_NAME'], sys.exc_type, sys.exc_value
pass
finally: pass #print sys.exc_type, sys.exc_value
#self._v_tpValues=r
return r
def __getitem__(self, name):
if name=='tableNamed':
if not hasattr(self, '_v_tables'): self.tpValues()
return self._v_tables.__of__(self)
raise KeyError, name
def manage_wizard(self, tables):
" "
def manage_join(self, tables, select_cols, join_cols, REQUEST=None):
"""Create an SQL join"""
def manage_insert(self, table, cols, REQUEST=None):
"""Create an SQL insert"""
def manage_update(self, table, keys, cols, REQUEST=None):
"""Create an SQL update"""
class TableBrowserCollection(Acquisition.Implicit):
"Helper class for accessing tables via URLs"
class Browser(Base):
def __getattr__(self, name):
try: return self._d[name]
except KeyError: raise AttributeError, name
class values:
def len(self): return 1
def __getitem__(self, i):
try: return self._d[i]
except AttributeError: pass
self._d=self._f()
return self._d[i]
class TableBrowser(Browser, Acquisition.Implicit):
icon='what'
Description=check=''
info=HTMLFile('table_info',globals())
menu=HTMLFile('table_menu',globals())