"""Implements the API used in the HTML templating for the web interface.
"""
__todo__ = """
- Document parameters to Template.render() method
- Add tests for Loader.load() method
- Most methods should have a "default" arg to supply a value
when none appears in the hyperdb or request.
- Multilink property additions: change_note and new_upload
- Add class.find() too
- NumberHTMLProperty should support numeric operations
- LinkHTMLProperty should handle comparisons to strings (cf. linked name)
- HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
'''Set the request's view arguments to the given values when no
values are found in the CGI environment.
'''
- have menu() methods accept filtering arguments
"""
__docformat__ = 'restructuredtext'
import calendar
import csv
import logging
import os.path
import re
import textwrap
from roundup import date, hyperdb, support
from roundup.anypy import scandir_
from roundup.anypy import urllib_
from roundup.anypy.cgi_ import cgi
from roundup.anypy.html import html_escape
from roundup.anypy.strings import StringIO, is_us, s2u, u2s, us2s
from roundup.cgi import TranslationService, ZTUtils
from roundup.cgi.timestamp import pack_timestamp
from roundup.exceptions import RoundupException
from .KeywordsExpr import render_keywords_expression_editor
try:
from docutils.core import publish_parts as ReStructuredText
except ImportError:
ReStructuredText = None
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
logger = logging.getLogger('roundup.template')
# List of schemes that are not rendered as links in rst and markdown.
_disable_url_schemes = ['javascript', 'data']
def _import_markdown2():
try:
import re
import markdown2
# Note: version 2.4.9 does not work with Roundup as it breaks
# [issue1](issue1) formatted links.
# Versions 2.4.8 and 2.4.10 use different methods to filter
# allowed schemes. 2.4.8 uses a pre-compiled regexp while
# 2.4.10 uses a regexp string that it compiles.
markdown2_vi = markdown2.__version_info__
if markdown2_vi > (2, 4, 9):
# Create the filtering regexp.
# Allowed default is same as what hyper_re supports.
# pathed_schemes are terminated with ://
pathed_schemes = ['http', 'https', 'ftp', 'ftps']
# non_pathed are terminated with a :
non_pathed_schemes = ["mailto"]
for disabled in _disable_url_schemes:
try:
pathed_schemes.remove(disabled)
except ValueError: # if disabled not in list
pass
try:
non_pathed_schemes.remove(disabled)
except ValueError:
pass
re_list = []
for scheme in pathed_schemes:
re_list.append(r'(?:%s)://' % scheme)
for scheme in non_pathed_schemes:
re_list.append(r'(?:%s):' % scheme)
enabled_schemes = r"|".join(re_list)
class Markdown(markdown2.Markdown):
_safe_protocols = enabled_schemes
elif markdown2_vi == (2, 4, 9):
raise RuntimeError("Unsupported version - markdown2 v2.4.9\n")
else:
class Markdown(markdown2.Markdown):
# don't allow disabled protocols in links
_safe_protocols = re.compile('(?!' + ':|'.join([
re.escape(s) for s in _disable_url_schemes])
+ ':)', re.IGNORECASE)
def _extras(config):
extras = {'fenced-code-blocks': {}, 'nofollow': None}
if config['MARKDOWN_BREAK_ON_NEWLINE']:
extras['break-on-newline'] = True
return extras
markdown = lambda s, c: Markdown(safe_mode='escape', extras=_extras(c)).convert(s) # noqa: E731
except ImportError:
markdown = None
return markdown
def _import_markdown():
try:
from markdown import markdown as markdown_impl
from markdown.extensions import Extension as MarkdownExtension
from markdown.treeprocessors import Treeprocessor
class RestrictLinksProcessor(Treeprocessor):
def run(self, root):
for el in root.iter('a'):
if 'href' in el.attrib:
url = el.attrib['href'].lstrip(' \r\n\t\x1a\0').lower()
for s in _disable_url_schemes:
if url.startswith(s + ':'):
el.attrib['href'] = '#'
class LinkRendererWithRel(Treeprocessor):
''' Rendering class that sets the rel="nofollow noreferer"
for links. '''
rel_value = "nofollow noopener"
def run(self, root):
for el in root.iter('a'):
if 'href' in el.attrib:
url = el.get('href').lstrip(' \r\n\t\x1a\0').lower()
if not url.startswith('http'): # only add rel for absolute http url's
continue
el.set('rel', self.rel_value)
# make sure any HTML tags get escaped and some links restricted
# and rel="nofollow noopener" are added to links
class SafeHtml(MarkdownExtension):
def extendMarkdown(self, md, md_globals=None):
if hasattr(md.preprocessors, 'deregister'):
md.preprocessors.deregister('html_block')
else:
del md.preprocessors['html_block']
if hasattr(md.inlinePatterns, 'deregister'):
md.inlinePatterns.deregister('html')
else:
del md.inlinePatterns['html']
if hasattr(md.preprocessors, 'register'):
md.treeprocessors.register(RestrictLinksProcessor(), 'restrict_links', 0)
else:
md.treeprocessors['restrict_links'] = RestrictLinksProcessor()
if hasattr(md.preprocessors, 'register'):
md.treeprocessors.register(LinkRendererWithRel(), 'add_link_rel', 0)
else:
md.treeprocessors['add_link_rel'] = LinkRendererWithRel()
def _extensions(config):
extensions = [SafeHtml(), 'fenced_code']
if config['MARKDOWN_BREAK_ON_NEWLINE']:
extensions.append('nl2br')
return extensions
markdown = lambda s, c: markdown_impl(s, extensions=_extensions(c)) # noqa: E731
except ImportError:
markdown = None
return markdown
def _import_mistune():
try:
import mistune
from mistune import Renderer, escape, escape_link
mistune._scheme_blacklist = [s + ':' for s in _disable_url_schemes]
class LinkRendererWithRel(Renderer):
''' Rendering class that sets the rel="nofollow noreferer"
for links. '''
rel_value = "nofollow noopener"
def autolink(self, link, is_email=False):
''' handle style explicit links '''
text = link = escape_link(link)
if is_email:
link = 'mailto:%s' % link
return '%(text)s' % {
'href': link, 'text': text}
return '%(href)s' % {
'rel': self.rel_value, 'href': escape_link(link)}
def link(self, link, title, content):
''' handle [text](url "title") style links and Reference
links '''
values = {
'content': escape(content),
'href': escape_link(link),
'rel': self.rel_value,
'title': escape(title) if title else '',
}
if title:
return '%(content)s' % values
return '%(content)s' % values
def _options(config):
options = {'renderer': LinkRendererWithRel(escape=True)}
if config['MARKDOWN_BREAK_ON_NEWLINE']:
options['hard_wrap'] = True
return options
markdown = lambda s, c: mistune.markdown(s, **_options(c)) # noqa: E731
except ImportError:
markdown = None
return markdown
markdown = _import_markdown2() or _import_markdown() or _import_mistune()
def anti_csrf_nonce(client, lifetime=None):
''' Create a nonce for defending against CSRF attack.
Then it stores the nonce, the session id for the user
and the user id in the one time key database for use
by the csrf validator that runs in the client::inner_main
module/function.
'''
otks = client.db.getOTKManager()
key = otks.getUniqueKey()
# lifetime is in minutes.
if lifetime is None:
lifetime = client.db.config['WEB_CSRF_TOKEN_LIFETIME']
ts = otks.lifetime(lifetime * 60)
otks.set(key, uid=client.db.getuid(),
sid=client.session_api._sid,
__timestamp=ts)
otks.commit()
return key
# templating
class NoTemplate(RoundupException):
pass
class Unauthorised(RoundupException):
def __init__(self, action, klass, translator=None):
self.action = action
self.klass = klass
if translator:
self._ = translator.gettext
else:
self._ = TranslationService.get_translation().gettext
def __str__(self):
return self._('You are not allowed to %(action)s '
'items of class %(class)s') % {
'action': self.action, 'class': self.klass}
# --- Template Loader API
class LoaderBase:
""" Base for engine-specific template Loader class."""
def __init__(self, template_dir):
# loaders are given the template directory as a first argument
pass
def precompile(self):
""" This method may be called when tracker is loaded to precompile
templates that support this ability.
"""
pass
def load(self, tplname):
""" Load template and return template object with render() method.
"tplname" is a template name. For filesystem loaders it is a
filename without extensions, typically in the "classname.view"
format.
"""
raise NotImplementedError
def check(self, name):
""" Check if template with the given name exists. Should return
false if template can not be found.
"""
raise NotImplementedError
class TALLoaderBase(LoaderBase):
""" Common methods for the legacy TAL loaders."""
def __init__(self, template_dir):
self.template_dir = template_dir
def _find(self, name):
""" Find template, return full path and filename of the
template if it is found, None otherwise."""
realsrc = os.path.realpath(self.template_dir)
for extension in ['', '.html', '.xml']:
f = name + extension
src = os.path.join(realsrc, f)
realpath = os.path.realpath(src)
if not realpath.startswith(realsrc):
return None # will raise invalid template
if os.path.exists(src):
return (src, f)
return None
def check(self, name):
return bool(self._find(name))
def precompile(self):
""" Precompile templates in load directory by loading them """
for dir_entry in os.scandir(self.template_dir):
filename = dir_entry.name
# skip subdirs
if dir_entry.is_dir():
continue
# skip files without ".html" or ".xml" extension - .css, .js etc.
for extension in '.html', '.xml':
if filename.endswith(extension):
break
else:
continue
# remove extension
filename = filename[:-len(extension)]
self.load(filename)
def __getitem__(self, name):
"""Special method to access templates by loader['name']"""
try:
return self.load(name)
except NoTemplate as message:
raise KeyError(message)
class MultiLoader(LoaderBase):
def __init__(self):
self.loaders = []
def add_loader(self, loader):
self.loaders.append(loader)
def check(self, name):
for loader in self.loaders:
if loader.check(name):
return True
def load(self, name):
for loader in self.loaders:
if loader.check(name):
return loader.load(name)
def __getitem__(self, name):
"""Needed for TAL templates compatibility"""
# [ ] document root and helper templates
try:
return self.load(name)
except NoTemplate as message:
raise KeyError(message)
class TemplateBase:
content_type = 'text/html'
def get_loader(template_dir, template_engine):
# Support for multiple engines using fallback mechanizm
# meaning that if first engine can't find template, we
# use the second
engines = template_engine.split(',')
engines = [x.strip() for x in engines]
ml = MultiLoader()
for engine_name in engines:
if engine_name == 'chameleon':
from .engine_chameleon import Loader
elif engine_name == 'jinja2':
from .engine_jinja2 import Jinja2Loader as Loader
elif engine_name == 'zopetal':
from .engine_zopetal import Loader
else:
raise Exception('Unknown template engine "%s"' % engine_name)
ml.add_loader(Loader(template_dir))
if len(engines) == 1:
return ml.loaders[0]
else:
return ml
# --/ Template Loader API
def context(client, template=None, classname=None, request=None):
"""Return the rendering context dictionary
The dictionary includes following symbols:
*context*
this is one of three things:
1. None - we're viewing a "home" page
2. The current class of item being displayed. This is an HTMLClass
instance.
3. The current item from the database, if we're viewing a specific
item, as an HTMLItem instance.
*request*
Includes information about the current request, including:
- the url
- the current index information (``filterspec``, ``filter`` args,
``properties``, etc) parsed out of the form.
- methods for easy filterspec link generation
- *user*, the current user node as an HTMLItem instance
- *form*, the current CGI form information as a FieldStorage
*config*
The current tracker config.
*db*
The current database, used to access arbitrary database items.
*utils*
This is an instance of client.instance.TemplatingUtils, which is
optionally defined in the tracker interfaces module and defaults to
TemplatingUtils class in this file.
*templates*
Access to all the tracker templates by name.
Used mainly in *use-macro* commands.
*template*
Current rendering template.
*true*
Logical True value.
*false*
Logical False value.
*i18n*
Internationalization service, providing string translation
methods ``gettext`` and ``ngettext``.
"""
# if template, classname and/or request are not passed explicitely,
# compute form client
if template is None:
template = client.template
if classname is None:
classname = client.classname
if request is None:
request = HTMLRequest(client)
c = {
'context': None,
'options': {},
'nothing': None,
'request': request,
'db': HTMLDatabase(client),
'config': client.instance.config,
'tracker': client.instance,
'utils': client.instance.TemplatingUtils(client),
'templates': client.instance.templates,
'template': template,
'true': 1,
'false': 0,
'i18n': client.translator
}
# add in the item if there is one
if client.nodeid:
c['context'] = HTMLItem(client, classname, client.nodeid,
anonymous=1)
elif classname in client.db.classes:
c['context'] = HTMLClass(client, classname, anonymous=1)
return c
class HTMLDatabase:
""" Return HTMLClasses for valid class fetches
"""
def __init__(self, client):
self._client = client
self._ = client._
self._db = client.db
# we want config to be exposed
self.config = client.db.config
def __getitem__(self, item, desre=re.compile(r'(?P[a-zA-Z_]+)(?P[-\d]+)')):
# check to see if we're actually accessing an item
m = desre.match(item)
if m:
cl = m.group('cl')
self._client.db.getclass(cl)
return HTMLItem(self._client, cl, m.group('id'))
else:
self._client.db.getclass(item)
return HTMLClass(self._client, item)
def __getattr__(self, attr):
try:
return self[attr]
except KeyError:
raise AttributeError(attr)
def classes(self):
class_keys = sorted(self._client.db.classes.keys())
m = []
for item in class_keys:
m.append(HTMLClass(self._client, item))
return m
num_re = re.compile(r'^-?\d+$')
def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
""" "fail_ok" should be specified if we wish to pass through bad values
(most likely form values that we wish to represent back to the user)
"do_lookup" is there for preventing lookup by key-value (if we
know that the value passed *is* an id)
"""
cl = db.getclass(prop.classname)
l = []
for entry in ids:
# Do not look up numeric IDs if try_id_parsing
if prop.try_id_parsing and num_re.match(entry):
l.append(entry)
continue
if do_lookup:
try:
item = cl.lookup(entry)
except (TypeError, KeyError):
pass
else:
l.append(item)
continue
# if fail_ok, ignore lookup error
# otherwise entry must be existing object id rather than key value
if fail_ok:
l.append(entry)
elif entry == '@current_user' and prop.classname == 'user':
# as a special case, '@current_user' means the currently
# logged-in user
l.append(entry)
return l
def lookupKeys(linkcl, key, ids, num_re=num_re):
""" Look up the "key" values for "ids" list - though some may already
be key values, not ids.
"""
l = []
for entry in ids:
if num_re.match(entry):
try:
label = linkcl.get(entry, key)
except IndexError:
# fall back to id if illegal (avoid template crash)
label = entry
# fall back to designator if label is None
if label is None:
label = '%s%s' % (linkcl.classname, entry)
l.append(label)
else:
l.append(entry)
return l
def _set_input_default_args(dic):
# 'text' is the default value anyway --
# but for CSS usage it should be present
dic.setdefault('type', 'text')
# useful e.g for HTML LABELs:
if 'id' not in dic:
try:
if dic['text'] in ('radio', 'checkbox'):
dic['id'] = '%(name)s-%(value)s' % dic
else:
dic['id'] = dic['name']
except KeyError:
pass
def html4_cgi_escape_attrs(**attrs):
''' Boolean attributes like 'disabled', 'required'
are represented without a value. E.G.
not
The latter is xhtml. Recognize booleans by:
value is None
Code can use None to indicate a pure boolean.
'''
return ' '.join(['%s="%s"' % (k, html_escape(str(v), True))
if v is not None else '%s' % (k)
for k, v in sorted(attrs.items())])
def xhtml_cgi_escape_attrs(**attrs):
''' Boolean attributes like 'disabled', 'required'
are represented with a value that is the same as
the attribute name.. E.G.
not
The latter is html4 or 5. Recognize booleans by:
value is None
Code can use None to indicate a pure boolean.
'''
return ' '.join(['%s="%s"' % (k, html_escape(str(v), True))
if v is not None else '%s="%s"' % (k, k)
for k, v in sorted(attrs.items())])
def input_html4(**attrs):
"""Generate an 'input' (html4) element with given attributes"""
_set_input_default_args(attrs)
return '' % html4_cgi_escape_attrs(**attrs)
def input_xhtml(**attrs):
"""Generate an 'input' (xhtml) element with given attributes"""
_set_input_default_args(attrs)
return '' % xhtml_cgi_escape_attrs(**attrs)
class HTMLInputMixin(object):
""" requires a _client property """
def __init__(self):
html_version = 'html4'
if hasattr(self._client.instance.config, 'HTML_VERSION'):
html_version = self._client.instance.config.HTML_VERSION
if html_version == 'xhtml':
self.input = input_xhtml
self.cgi_escape_attrs = xhtml_cgi_escape_attrs
else:
self.input = input_html4
self.cgi_escape_attrs = html4_cgi_escape_attrs
# self._context is used for translations.
# will be initialized by the first call to .gettext()
self._context = None
def gettext(self, msgid):
"""Return the localized translation of msgid"""
if self._context is None:
self._context = context(self._client)
return self._client.translator.translate(
domain="roundup", msgid=msgid, context=self._context)
_ = gettext
class HTMLPermissions(object):
def view_check(self):
""" Raise the Unauthorised exception if the user's not permitted to
view this class.
"""
if not self.is_view_ok():
raise Unauthorised("view", self._classname,
translator=self._client.translator)
def edit_check(self):
""" Raise the Unauthorised exception if the user's not permitted to
edit items of this class.
"""
if not self.is_edit_ok():
raise Unauthorised("edit", self._classname,
translator=self._client.translator)
def retire_check(self):
""" Raise the Unauthorised exception if the user's not permitted to
retire items of this class.
"""
if not self.is_retire_ok():
raise Unauthorised("retire", self._classname,
translator=self._client.translator)
class HTMLClass(HTMLInputMixin, HTMLPermissions):
""" Accesses through a class (either through *class* or *db.*)
"""
def __init__(self, client, classname, anonymous=0):
self._client = client
self._ = client._
self._db = client.db
self._anonymous = anonymous
# we want classname to be exposed, but _classname gives a
# consistent API for extending Class/Item
self._classname = self.classname = classname
self._klass = self._db.getclass(self.classname)
self._props = self._klass.getprops()
HTMLInputMixin.__init__(self)
def is_edit_ok(self):
""" Is the user allowed to Create the current class?
"""
perm = self._db.security.hasPermission
return perm('Web Access', self._client.userid) and perm(
'Create', self._client.userid, self._classname)
def is_retire_ok(self):
""" Is the user allowed to retire items of the current class?
"""
perm = self._db.security.hasPermission
return perm('Web Access', self._client.userid) and perm(
'Retire', self._client.userid, self._classname)
def is_restore_ok(self):
""" Is the user allowed to restore retired items of the current class?
"""
perm = self._db.security.hasPermission
return perm('Web Access', self._client.userid) and perm(
'Restore', self._client.userid, self._classname)
def is_view_ok(self):
""" Is the user allowed to View the current class?
"""
perm = self._db.security.hasPermission
return perm('Web Access', self._client.userid) and perm(
'View', self._client.userid, self._classname)
def is_only_view_ok(self):
""" Is the user only allowed to View (ie. not Create) the current class?
"""
return self.is_view_ok() and not self.is_edit_ok()
def __repr__(self):
return '' % (id(self), self.classname)
def __getitem__(self, item):
""" return an HTMLProperty instance
"""
# we don't exist
if item == 'id':
return None
# get the property
try:
prop = self._props[item]
except KeyError:
raise KeyError('No such property "%s" on %s' % (item,
self.classname))
# look up the correct HTMLProperty class
for klass, htmlklass in propclasses:
if not isinstance(prop, klass):
continue
return htmlklass(self._client, self._classname, None, prop, item,
None, self._anonymous)
# no good
raise KeyError(item)
def __getattr__(self, attr):
""" convenience access """
try:
return self[attr]
except KeyError:
raise AttributeError(attr)
def designator(self):
""" Return this class' designator (classname) """
return self._classname
def getItem(self, itemid, num_re=num_re):
""" Get an item of this class by its item id.
"""
# make sure we're looking at an itemid
if not isinstance(itemid, type(1)) and not num_re.match(itemid):
itemid = self._klass.lookup(itemid)
return HTMLItem(self._client, self.classname, itemid)
def properties(self, sort=1, cansearch=True):
""" Return HTMLProperty for allowed class' properties.
To return all properties call it with cansearch=False
and it will return properties the user is unable to
search.
"""
l = []
canSearch = self._db.security.hasSearchPermission
userid = self._client.userid
for name, prop in self._props.items():
if cansearch and \
not canSearch(userid, self._classname, name):
continue
for klass, htmlklass in propclasses:
if isinstance(prop, klass):
value = prop.get_default_value()
l.append(htmlklass(self._client, self._classname, '',
prop, name, value, self._anonymous))
if sort:
l.sort(key=lambda a: a._name)
return l
def list(self, sort_on=None):
""" List all items in this class.
"""
# get the list and sort it nicely
class_list = self._klass.list()
keyfunc = make_key_function(self._db, self._classname, sort_on)
class_list.sort(key=keyfunc)
# check perms
check = self._client.db.security.hasPermission
userid = self._client.userid
if not check('Web Access', userid):
return []
class_list = [HTMLItem(self._client, self._classname, itemid)
for itemid in class_list if
check('View', userid, self._classname, itemid=itemid)]
return class_list
def csv(self):
""" Return the items of this class as a chunk of CSV text.
"""
props = self.propnames()
s = StringIO()
writer = csv.writer(s)
writer.writerow(props)
check = self._client.db.security.hasPermission
userid = self._client.userid
if not check('Web Access', userid):
return ''
for nodeid in self._klass.list():
l = []
for name in props:
# check permission to view this property on this item
if not check('View', userid, itemid=nodeid,
classname=self._klass.classname, property=name):
raise Unauthorised('view', self._klass.classname,
translator=self._client.translator)
value = self._klass.get(nodeid, name)
if value is None:
l.append('')
elif isinstance(value, type([])):
l.append(':'.join(map(str, value)))
else:
l.append(str(self._klass.get(nodeid, name)))
writer.writerow(l)
return s.getvalue()
def propnames(self):
""" Return the list of the names of the properties of this class.
"""
idlessprops = sorted(self._klass.getprops(protected=0).keys())
return ['id'] + idlessprops
def filter(self, request=None, filterspec={}, sort=[], group=[]):
""" Return a list of items from this class, filtered and sorted
by the current requested filterspec/filter/sort/group args
"request" takes precedence over the other three arguments.
"""
security = self._db.security
userid = self._client.userid
if request is not None:
# for a request we asume it has already been
# security-filtered
filterspec = request.filterspec
sort = request.sort
group = request.group
else:
cn = self.classname
filterspec = security.filterFilterspec(userid, cn, filterspec)
sort = security.filterSortspec(userid, cn, sort)
group = security.filterSortspec(userid, cn, group)
check = security.hasPermission
if not check('Web Access', userid):
return []
filtered = [HTMLItem(self._client, self.classname, itemid)
for itemid in self._klass.filter(None, filterspec,
sort, group)
if check('View', userid, self.classname, itemid=itemid)]
return filtered
def classhelp(self, properties=None, label=''"(list)", width='500',
height='600', property='', form='itemSynopsis',
pagesize=50, inputtype="checkbox", html_kwargs={},
group='', sort=None, filter=None):
"""Pop up a javascript window with class help
This generates a link to a popup window which displays the
properties indicated by "properties" of the class named by
"classname". The "properties" should be a comma-separated list
(eg. 'id,name,description'). Properties defaults to all the
properties of a class (excluding id, creator, created and
activity).
You may optionally override the label displayed, the width,
the height, the number of items per page and the field on which
the list is sorted (defaults to username if in the displayed
properties).
With the "filter" arg it is possible to specify a filter for
which items are supposed to be displayed. It has to be of
the format "=;=;...".
The popup window will be resizable and scrollable.
If the "property" arg is given, it's passed through to the
javascript help_window function.
You can use inputtype="radio" to display a radio box instead
of the default checkbox (useful for entering Link-properties)
If the "form" arg is given, it's passed through to the
javascript help_window function. - it's the name of the form
the "property" belongs to.
"""
if properties is None:
properties = sorted(self._klass.getprops(protected=0).keys())
properties = ','.join(properties)
if sort is None:
if 'username' in properties.split(','):
sort = 'username'
else:
sort = self._klass.orderprop()
sort = '&@sort=' + sort
if group:
group = '&@group=' + group
if property:
property = '&property=%s' % property
if form:
form = '&form=%s' % form
if inputtype:
type = '&type=%s' % inputtype
if filter:
filterprops = filter.split(';')
filtervalues = []
names = []
for x in filterprops:
(name, values) = x.split('=')
names.append(name)
filtervalues.append('&%s=%s' % (name, urllib_.quote(values)))
filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
else:
filter = ''
help_url = "%s?@startwith=0&@template=help&"\
"properties=%s%s%s%s%s%s&@pagesize=%s%s" % \
(self.classname, properties, property, form, type,
group, sort, pagesize, filter)
onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
(help_url, width, height)
return ('%s') % (
help_url, width, height,
help_url, onclick, self.cgi_escape_attrs(**html_kwargs),
self._(label))
def submit(self, label=''"Submit New Entry", action="new", html_kwargs={}):
""" Generate a submit button (and action hidden element)
"html_kwargs" specified additional html args for the
generated html