Mercurial > p > roundup > code
diff roundup/cgi/templating.py @ 7045:ca90f7270cd4 issue2550923_computed_property
merge from main tip. This should fix test failure in Markdown2TestCase.test_string_markdown_code_block_attribute by merging html diff method used in tests.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Mon, 07 Nov 2022 22:58:38 -0500 |
| parents | e1588ae185dc 22183e7d1443 |
| children | 82bbb95e5690 |
line wrap: on
line diff
--- a/roundup/cgi/templating.py Thu Apr 21 16:54:17 2022 -0400 +++ b/roundup/cgi/templating.py Mon Nov 07 22:58:38 2022 -0500 @@ -1,7 +1,7 @@ """Implements the API used in the HTML templating for the web interface. """ -todo = """ +__todo__ = """ - Document parameters to Template.render() method - Add tests for Loader.load() method - Most methods should have a "default" arg to supply a value @@ -19,35 +19,26 @@ __docformat__ = 'restructuredtext' -# List of schemes that are not rendered as links in rst and markdown. -_disable_url_schemes = [ 'javascript', 'data' ] - -import base64, cgi, re, os.path, mimetypes, csv, string import calendar +import cgi +import csv +import os.path +import re import textwrap -import time, hashlib - -from roundup.anypy.html import html_escape - -from roundup.anypy import urllib_ + from roundup import hyperdb, date, support -from roundup import i18n -from roundup.i18n import _ -from roundup.anypy.strings import is_us, b2s, s2b, us2s, s2u, u2s, StringIO - -from .KeywordsExpr import render_keywords_expression_editor - +from roundup.anypy import urllib_ +from roundup.anypy.html import html_escape +from roundup.anypy.strings import is_us, us2s, s2u, u2s, StringIO +from roundup.cgi import TranslationService, ZTUtils from roundup.cgi.timestamp import pack_timestamp - -import roundup.anypy.random_ as random_ -try: - import cPickle as pickle -except ImportError: - import pickle +from roundup.exceptions import RoundupException +from .KeywordsExpr import render_keywords_expression_editor + try: from StructuredText.StructuredText import HTML as StructuredText except ImportError: - try: # older version + try: # older version import StructuredText except ImportError: StructuredText = None @@ -60,27 +51,35 @@ except ImportError: from itertools import izip_longest as zip_longest -from roundup.exceptions import RoundupException + +# List of schemes that are not rendered as links in rst and markdown. +_disable_url_schemes = ['javascript', 'data'] + def _import_markdown2(): try: - import markdown2, re + import markdown2 + import re + 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) + _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 } + 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) + 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 @@ -130,25 +129,26 @@ 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)) + 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_link, escape - mistune._scheme_blacklist = [ s + ':' for s in _disable_url_schemes ] + mistune._scheme_blacklist = [s + ':' for s in _disable_url_schemes] class LinkRendererWithRel(Renderer): ''' Rendering class that sets the rel="nofollow noreferer" @@ -161,7 +161,8 @@ text = link = escape_link(link) if is_email: link = 'mailto:%s' % link - return '<a href="%(href)s">%(text)s</a>' % { 'href': link, 'text': text } + return '<a href="%(href)s">%(text)s</a>' % { + 'href': link, 'text': text} return '<a href="%(href)s" rel="%(rel)s">%(href)s</a>' % { 'rel': self.rel_value, 'href': escape_link(link)} @@ -173,7 +174,7 @@ 'content': escape(content), 'href': escape_link(link), 'rel': self.rel_value, - 'title': escape(title) if title else '', + 'title': escape(title) if title else '', } if title: @@ -183,27 +184,20 @@ return '<a href="%(href)s" rel="%(rel)s">%(content)s</a>' % values def _options(config): - options = {'renderer': LinkRendererWithRel(escape = True)} + 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)) + 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() -# bring in the templating support -from roundup.cgi import TranslationService, ZTUtils - -### i18n services -# this global translation service is not thread-safe. -# it is left here for backward compatibility -# until all Web UI translations are done via client.translator object -translationService = TranslationService.get_translation() def anti_csrf_nonce(client, lifetime=None): ''' Create a nonce for defending against CSRF attack. @@ -213,36 +207,26 @@ by the csrf validator that runs in the client::inner_main module/function. ''' - otks=client.db.getOTKManager() - key = b2s(base64.b32encode(random_.token_bytes(40))) - - while otks.exists(key): - key = b2s(base64.b32encode(random_.token_bytes(40))) - + otks = client.db.getOTKManager() + key = otks.getUniqueKey() # lifetime is in minutes. if lifetime is None: lifetime = client.db.config['WEB_CSRF_TOKEN_LIFETIME'] - # offset to time.time is calculated as: - # default lifetime is 1 week after __timestamp. - # That's the cleanup period hardcoded in otk.clean(). - # If a user wants a 10 minute lifetime calculate - # 10 minutes newer than 1 week ago. - # lifetime - 10080 (number of minutes in a week) - # convert to seconds and add (possible negative number) - # to current time (time.time()). - ts = time.time() + ((lifetime - 10080) * 60) + ts = otks.lifetime(lifetime * 60) otks.set(key, uid=client.db.getuid(), sid=client.session_api._sid, - __timestamp=ts ) + __timestamp=ts) otks.commit() return key -### templating +# templating + class NoTemplate(RoundupException): pass + class Unauthorised(RoundupException): def __init__(self, action, klass, translator=None): self.action = action @@ -251,10 +235,11 @@ 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} + 'items of class %(class)s') % { + 'action': self.action, 'class': self.klass} # --- Template Loader API @@ -286,6 +271,7 @@ """ raise NotImplementedError + class TALLoaderBase(LoaderBase): """ Common methods for the legacy TAL loaders.""" @@ -301,7 +287,7 @@ src = os.path.join(realsrc, f) realpath = os.path.realpath(src) if not realpath.startswith(realsrc): - return # will raise invalid template + return # will raise invalid template if os.path.exists(src): return (src, f) @@ -333,22 +319,23 @@ 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 l in self.loaders: - if l.check(name): + for loader in self.loaders: + if loader.check(name): return True - def load(self, name): - for l in self.loaders: - if l.check(name): - return l.load(name) + 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""" @@ -357,7 +344,7 @@ return self.load(name) except NoTemplate as message: raise KeyError(message) - + class TemplateBase: content_type = 'text/html' @@ -383,7 +370,7 @@ else: raise Exception('Unknown template engine "%s"' % engine_name) ml.add_loader(Loader(dir)) - + if len(engines) == 1: return ml.loaders[0] else: @@ -473,11 +460,12 @@ # add in the item if there is one if client.nodeid: c['context'] = HTMLItem(client, classname, client.nodeid, - anonymous=1) + 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 """ @@ -507,14 +495,16 @@ raise AttributeError(attr) def classes(self): - l = sorted(self._client.db.classes.keys()) + class_keys = sorted(self._client.db.classes.keys()) m = [] - for item in l: + 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) @@ -542,6 +532,7 @@ 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. @@ -555,12 +546,14 @@ # 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) + 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 @@ -575,6 +568,7 @@ except KeyError: pass + def html4_cgi_escape_attrs(**attrs): ''' Boolean attributes like 'disabled', 'required' are represented without a value. E.G. @@ -583,9 +577,10 @@ value is None Code can use None to indicate a pure boolean. ''' - return ' '.join(['%s="%s"'%(k,html_escape(str(v), True)) - if v != None else '%s'%(k) - for k,v in sorted(attrs.items())]) + 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' @@ -596,19 +591,22 @@ value is None Code can use None to indicate a pure boolean. ''' - return ' '.join(['%s="%s"'%(k,html_escape(str(v), True)) - if v != None else '%s="%s"'%(k,k) - for k,v in sorted(attrs.items())]) + 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 '<input %s>'%html4_cgi_escape_attrs(**attrs) + return '<input %s>' % html4_cgi_escape_attrs(**attrs) + def input_xhtml(**attrs): """Generate an 'input' (xhtml) element with given attributes""" _set_input_default_args(attrs) - return '<input %s/>'%xhtml_cgi_escape_attrs(**attrs) + return '<input %s/>' % xhtml_cgi_escape_attrs(**attrs) + class HTMLInputMixin(object): """ requires a _client property """ @@ -618,10 +616,10 @@ html_version = self._client.instance.config.HTML_VERSION if html_version == 'xhtml': self.input = input_xhtml - self.cgi_escape_attrs=xhtml_cgi_escape_attrs + self.cgi_escape_attrs = xhtml_cgi_escape_attrs else: self.input = input_html4 - self.cgi_escape_attrs=html4_cgi_escape_attrs + 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 @@ -630,11 +628,12 @@ """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) + return self._client.translator.translate( + domain="roundup", msgid=msgid, context=self._context) _ = gettext + class HTMLPermissions(object): def view_check(self): @@ -643,7 +642,7 @@ """ if not self.is_view_ok(): raise Unauthorised("view", self._classname, - translator=self._client.translator) + translator=self._client.translator) def edit_check(self): """ Raise the Unauthorised exception if the user's not permitted to @@ -651,7 +650,7 @@ """ if not self.is_edit_ok(): raise Unauthorised("edit", self._classname, - translator=self._client.translator) + translator=self._client.translator) def retire_check(self): """ Raise the Unauthorised exception if the user's not permitted to @@ -659,7 +658,7 @@ """ if not self.is_retire_ok(): raise Unauthorised("retire", self._classname, - translator=self._client.translator) + translator=self._client.translator) class HTMLClass(HTMLInputMixin, HTMLPermissions): @@ -683,29 +682,29 @@ """ 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) + 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) + 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) + 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) + 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? @@ -713,7 +712,7 @@ return self.is_view_ok() and not self.is_edit_ok() def __repr__(self): - return '<HTMLClass(0x%x) %s>'%(id(self), self.classname) + return '<HTMLClass(0x%x) %s>' % (id(self), self.classname) def __getitem__(self, item): """ return an HTMLProperty instance @@ -727,14 +726,15 @@ try: prop = self._props[item] except KeyError: - raise KeyError('No such property "%s" on %s'%(item, self.classname)) + 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) + None, self._anonymous) # no good raise KeyError(item) @@ -767,8 +767,8 @@ search. """ l = [] - canSearch=self._db.security.hasSearchPermission - userid=self._client.userid + 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): @@ -779,16 +779,16 @@ l.append(htmlklass(self._client, self._classname, '', prop, name, value, self._anonymous)) if sort: - l.sort(key=lambda a:a._name) + 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 - l = self._klass.list() + class_list = self._klass.list() keyfunc = make_key_function(self._db, self._classname, sort_on) - l.sort(key=keyfunc) + class_list.sort(key=keyfunc) # check perms check = self._client.db.security.hasPermission @@ -796,10 +796,11 @@ if not check('Web Access', userid): return [] - l = [HTMLItem(self._client, self._classname, id) for id in l - if check('View', userid, self._classname, itemid=id)] - - return l + class_list = [HTMLItem(self._client, self._classname, id) + for id in class_list if + check('View', userid, self._classname, itemid=id)] + + return class_list def csv(self): """ Return the items of this class as a chunk of CSV text. @@ -817,9 +818,9 @@ 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): + classname=self._klass.classname, property=name): raise Unauthorised('view', self._klass.classname, - translator=self._client.translator) + translator=self._client.translator) value = self._klass.get(nodeid, name) if value is None: l.append('') @@ -860,15 +861,15 @@ if not check('Web Access', userid): return [] - l = [HTMLItem(self._client, self.classname, id) - for id in self._klass.filter(None, filterspec, sort, group) - if check('View', userid, self.classname, itemid=id)] - return l + filtered = [HTMLItem(self._client, self.classname, id) + for id in self._klass.filter(None, filterspec, sort, group) + if check('View', userid, self.classname, itemid=id)] + 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): + 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 @@ -903,19 +904,19 @@ properties = sorted(self._klass.getprops(protected=0).keys()) properties = ','.join(properties) if sort is None: - if 'username' in properties.split( ',' ): + if 'username' in properties.split(','): sort = 'username' else: sort = self._klass.orderprop() sort = '&@sort=' + sort - if group : + if group: group = '&@group=' + group if property: - property = '&property=%s'%property + property = '&property=%s' % property if form: - form = '&form=%s'%form + form = '&form=%s' % form if inputtype: - type= '&type=%s'%inputtype + type = '&type=%s' % inputtype if filter: filterprops = filter.split(';') filtervalues = [] @@ -926,17 +927,19 @@ filtervalues.append('&%s=%s' % (name, urllib_.quote(values))) filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues)) else: - filter = '' + 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) + group, sort, pagesize, filter) onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \ (help_url, width, height) - return '<a class="classhelp" data-helpurl="%s" data-width="%s" data-height="%s" href="%s" onclick="%s" %s>%s</a>' % \ - (help_url, width, height, - help_url, onclick, self.cgi_escape_attrs(**html_kwargs), - self._(label)) + return ('<a class="classhelp" data-helpurl="%s" ' + 'data-width="%s" data-height="%s" href="%s" ' + 'onclick="%s" %s>%s</a>') % ( + 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) @@ -951,10 +954,10 @@ return \ self.input(type="submit", name="submit_button", - value=self._(label), **html_kwargs) + \ + value=self._(label), **html_kwargs) + \ '\n' + \ self.input(type="hidden", name="@csrf", - value=anti_csrf_nonce(self._client)) + \ + value=anti_csrf_nonce(self._client)) + \ '\n' + \ self.input(type="hidden", name="@action", value=action) @@ -983,6 +986,7 @@ } return pt.render(self._client, self.classname, req, **args) + class _HTMLItem(HTMLInputMixin, HTMLPermissions): """ Accesses through an *item* """ @@ -1003,29 +1007,32 @@ """ Is the user allowed to Edit this item? """ perm = self._db.security.hasPermission - return perm('Web Access', self._client.userid) and perm('Edit', - self._client.userid, self._classname, itemid=self._nodeid) + return perm('Web Access', self._client.userid) and perm( + 'Edit', self._client.userid, self._classname, itemid=self._nodeid) def is_retire_ok(self): """ Is the user allowed to Reture this item? """ perm = self._db.security.hasPermission - return perm('Web Access', self._client.userid) and perm('Retire', - self._client.userid, self._classname, itemid=self._nodeid) + return perm('Web Access', self._client.userid) and perm( + 'Retire', self._client.userid, self._classname, + itemid=self._nodeid) def is_restore_ok(self): """ Is the user allowed to restore this item? """ perm = self._db.security.hasPermission - return perm('Web Access', self._client.userid) and perm('Restore', - self._client.userid, self._classname, itemid=self._nodeid) + return perm('Web Access', self._client.userid) and perm( + 'Restore', self._client.userid, self._classname, + itemid=self._nodeid) def is_view_ok(self): """ Is the user allowed to View this item? """ perm = self._db.security.hasPermission - if perm('Web Access', self._client.userid) and perm('View', - self._client.userid, self._classname, itemid=self._nodeid): + if perm('Web Access', self._client.userid) and perm( + 'View', self._client.userid, self._classname, + itemid=self._nodeid): return 1 return self.is_edit_ok() @@ -1035,8 +1042,8 @@ return self.is_view_ok() and not self.is_edit_ok() def __repr__(self): - return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname, - self._nodeid) + return '<HTMLItem(0x%x) %s %s>' % (id(self), self._classname, + self._nodeid) def __getitem__(self, item): """ return an HTMLProperty instance @@ -1071,7 +1078,8 @@ for klass, htmlklass in propclasses: if isinstance(prop, klass): htmlprop = htmlklass(self._client, self._classname, - self._nodeid, prop, items[0], value, self._anonymous) + self._nodeid, prop, items[0], + value, self._anonymous) if htmlprop is not None: if has_rest: if isinstance(htmlprop, MultilinkHTMLProperty): @@ -1090,7 +1098,7 @@ def designator(self): """Return this item's designator (classname + id).""" - return '%s%s'%(self._classname, self._nodeid) + return '%s%s' % (self._classname, self._nodeid) def is_retired(self): """Is this item retired?""" @@ -1123,7 +1131,7 @@ return [] def history(self, direction='descending', dre=re.compile(r'^\d+$'), - limit=None, showall=False ): + limit=None, showall=False): """Create an html view of the journal for the item. Display property changes for all properties that does not have quiet set. @@ -1138,7 +1146,7 @@ # function. Reset it to original value on return. orig_form_wins = self._client.form_wins self._client.form_wins = False - + # get the journal, sort and reverse history = self._klass.history(self._nodeid, skipquiet=(not showall)) history.sort(key=lambda a: a[:3]) @@ -1152,17 +1160,17 @@ l = [] current = {} comments = {} - for id, evt_date, user, action, args in history: - date_s = str(evt_date.local(timezone)).replace("."," ") + for _id, evt_date, user, action, args in history: + date_s = str(evt_date.local(timezone)).replace(".", " ") arg_s = '' - if action in ['link', 'unlink'] and type(args) == type(()): + if action in ['link', 'unlink'] and isinstance(args, tuple): if len(args) == 3: linkcl, linkid, key = args - arg_s += '<a rel="nofollow noopener" href="%s%s">%s%s %s</a>'%(linkcl, linkid, - linkcl, linkid, key) + arg_s += '<a rel="nofollow noopener" href="%s%s">%s%s %s</a>' % ( + linkcl, linkid, linkcl, linkid, key) else: arg_s = str(args) - elif type(args) == type({}): + elif isinstance(args, dict): cell = [] for k in args.keys(): # try to get the relevant property and treat it @@ -1176,7 +1184,7 @@ comments['no_exist'] = self._( "<em>The indicated property no longer exists</em>") cell.append(self._('<em>%s: %s</em>\n') - % (self._(k), str(args[k]))) + % (self._(k), str(args[k]))) continue # load the current state for the property (if we @@ -1198,11 +1206,11 @@ pass else: linkid = self._klass.get(self._nodeid, k, None) - current[k] = '<a rel="nofollow noopener" href="%s%s">%s</a>'%( + current[k] = '<a rel="nofollow noopener" href="%s%s">%s</a>' % ( classname, linkid, current[k]) if args[k] and (isinstance(prop, hyperdb.Multilink) or - isinstance(prop, hyperdb.Link)): + isinstance(prop, hyperdb.Link)): # figure what the link class is classname = prop.classname try: @@ -1215,7 +1223,7 @@ labelprop = linkcl.labelprop(1) try: template = self._client.selectTemplate(classname, - 'item') + 'item') if template.startswith('_generic.'): raise NoTemplate('not really...') hrefable = 1 @@ -1235,7 +1243,7 @@ for linkid in linkids: # We're seeing things like # {'nosy':['38', '113', None, '82']} in the wild - if linkid is None : + if linkid is None: continue label = classname + linkid # if we have a label property, try to use it @@ -1244,27 +1252,29 @@ try: if labelprop is not None and \ labelprop != 'id': - label = linkcl.get(linkid, labelprop, - default=self._( - "[label is missing]")) + label = linkcl.get( + linkid, labelprop, + default=self._( + "[label is missing]")) label = html_escape(label) except IndexError: comments['no_link'] = self._( "<strike>The linked node" " no longer exists</strike>") - subml.append('<strike>%s</strike>'%label) + subml.append('<strike>%s</strike>' % label) else: if hrefable: - subml.append('<a rel="nofollow noopener" ' - 'href="%s%s">%s</a>'%( - classname, linkid, label)) + subml.append( + '<a rel="nofollow noopener" ' + 'href="%s%s">%s</a>' % ( + classname, linkid, label)) elif label is None: - subml.append('%s%s'%(classname, - linkid)) + subml.append('%s%s' % (classname, + linkid)) else: subml.append(label) ml.append(sublabel + ', '.join(subml)) - cell.append('%s:\n %s'%(self._(k), ', '.join(ml))) + cell.append('%s:\n %s' % (self._(k), ', '.join(ml))) elif isinstance(prop, hyperdb.Link) and args[k]: label = classname + args[k] # if we have a label property, try to use it @@ -1272,90 +1282,92 @@ # there's no labelprop! if labelprop is not None and labelprop != 'id': try: - label = html_escape(linkcl.get(args[k], - labelprop, default=self._( - "[label is missing]"))) + label = html_escape( + linkcl.get(args[k], + labelprop, default=self._( + "[label is missing]"))) except IndexError: comments['no_link'] = self._( "<strike>The linked node" " no longer exists</strike>") - cell.append(' <strike>%s</strike>,\n'%label) + cell.append(' <strike>%s</strike>,\n' % label) # "flag" this is done .... euwww label = None if label is not None: if hrefable: - old = '<a rel="nofollow noopener" href="%s%s">%s</a>'%(classname, - args[k], label) + old = '<a rel="nofollow noopener" href="%s%s">%s</a>' % ( + classname, args[k], label) else: - old = label; + old = label cell.append('%s: %s' % (self._(k), old)) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = old elif isinstance(prop, hyperdb.Date) and args[k]: if args[k] is None: d = '' else: - d = date.Date(args[k], + d = date.Date( + args[k], translator=self._client).local(timezone) - cell.append('%s: %s'%(self._(k), str(d))) + cell.append('%s: %s' % (self._(k), str(d))) if k in current and current[k] is not None: cell[-1] += ' -> %s' % current[k] current[k] = str(d) elif isinstance(prop, hyperdb.Interval) and args[k]: val = str(date.Interval(args[k], - translator=self._client)) - cell.append('%s: %s'%(self._(k), val)) + translator=self._client)) + cell.append('%s: %s' % (self._(k), val)) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = val elif isinstance(prop, hyperdb.String) and args[k]: val = html_escape(args[k]) - cell.append('%s: %s'%(self._(k), val)) + cell.append('%s: %s' % (self._(k), val)) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = val elif isinstance(prop, hyperdb.Boolean) and args[k] is not None: val = args[k] and ''"Yes" or ''"No" - cell.append('%s: %s'%(self._(k), val)) + cell.append('%s: %s' % (self._(k), val)) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = val elif isinstance(prop, hyperdb.Password) and args[k] is not None: val = args[k].dummystr() - cell.append('%s: %s'%(self._(k), val)) + cell.append('%s: %s' % (self._(k), val)) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = val elif not args[k]: if k in current and current[k] is not None: - cell.append('%s: %s'%(self._(k), current[k])) + cell.append('%s: %s' % (self._(k), current[k])) current[k] = '(no value)' else: - cell.append(self._('%s: (no value)')%self._(k)) + cell.append(self._('%s: (no value)') % self._(k)) else: - cell.append('%s: %s'%(self._(k), str(args[k]))) + cell.append('%s: %s' % (self._(k), str(args[k]))) if k in current and current[k] is not None: - cell[-1] += ' -> %s'%current[k] + cell[-1] += ' -> %s' % current[k] current[k] = str(args[k]) arg_s = '<br />'.join(cell) else: - if action in ( 'retired', 'restored' ): + if action in ('retired', 'restored'): # args = None for these actions pass else: # unknown event!! comments['unknown'] = self._( "<strong><em>This event %s is not handled" - " by the history display!</em></strong>"%action) + " by the history display!</em></strong>" % action) arg_s = '<strong><em>' + str(args) + '</em></strong>' date_s = date_s.replace(' ', ' ') @@ -1363,30 +1375,30 @@ # have the username) if dre.match(user): user = self._db.user.get(user, 'username') - l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%( + l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % ( date_s, html_escape(user), self._(action), arg_s)) if comments: l.append(self._( '<tr><td colspan=4><strong>Note:</strong></td></tr>')) for entry in comments.values(): - l.append('<tr><td colspan=4>%s</td></tr>'%entry) + l.append('<tr><td colspan=4>%s</td></tr>' % entry) if direction == 'ascending': l.reverse() l[0:0] = ['<table class="history table table-condensed table-striped">' - '<tr><th colspan="4" class="header">', - self._('History'), - '</th></tr><tr>', - self._('<th>Date</th>'), - self._('<th>User</th>'), - self._('<th>Action</th>'), - self._('<th>Args</th>'), - '</tr>'] + '<tr><th colspan="4" class="header">', + self._('History'), + '</th></tr><tr>', + self._('<th>Date</th>'), + self._('<th>User</th>'), + self._('<th>Action</th>'), + self._('<th>Args</th>'), + '</tr>'] l.append('</table>') self._client.form_wins = orig_form_wins - + return '\n'.join(l) def renderQueryForm(self): @@ -1397,7 +1409,7 @@ req.classname = self._klass.get(self._nodeid, 'klass') name = self._klass.get(self._nodeid, 'name') req.updateFromURL(self._klass.get(self._nodeid, 'url') + - '&@queryname=%s'%urllib_.quote(name)) + '&@queryname=%s' % urllib_.quote(name)) # new template, using the specified classname and request # [ ] the custom logic for search page doesn't belong to @@ -1416,7 +1428,7 @@ and content. Construct a URL for the download of the content. """ name = self._klass.get(self._nodeid, 'name') - url = '%s%s/%s'%(self._classname, self._nodeid, name) + url = '%s%s/%s' % (self._classname, self._nodeid, name) return urllib_.quote(url) def copy_url(self, exclude=("messages", "files")): @@ -1447,12 +1459,14 @@ ["%s=%s" % (key, urllib_.quote(value)) for key, value in query.items()]) + class _HTMLUser(_HTMLItem): """Add ability to check for permissions on users. """ _marker = [] + def hasPermission(self, permission, classname=_marker, - property=None, itemid=None): + property=None, itemid=None): """Determine if the user has the Permission. The class being tested defaults to the template's class, but may @@ -1460,19 +1474,21 @@ """ if classname is self._marker: classname = self._client.classname - return self._db.security.hasPermission(permission, - self._nodeid, classname, property, itemid) + return self._db.security.hasPermission( + permission, self._nodeid, classname, property, itemid) def hasRole(self, *rolenames): """Determine whether the user has any role in rolenames.""" return self._db.user.has_role(self._nodeid, *rolenames) + def HTMLItem(client, classname, nodeid, anonymous=0): if classname == 'user': return _HTMLUser(client, classname, nodeid, anonymous) else: return _HTMLItem(client, classname, nodeid, anonymous) + class HTMLProperty(HTMLInputMixin, HTMLPermissions): """ String, Integer, Number, Date, Interval HTMLProperty @@ -1484,7 +1500,7 @@ A wrapper object which may be stringified for the plain() behaviour. """ def __init__(self, client, classname, nodeid, prop, name, value, - anonymous=0): + anonymous=0): self._client = client self._db = client.db self._ = client._ @@ -1496,11 +1512,11 @@ self._name = name if not anonymous: if nodeid: - self._formname = '%s%s@%s'%(classname, nodeid, name) + self._formname = '%s%s@%s' % (classname, nodeid, name) else: # This case occurs when creating a property for a # non-anonymous class. - self._formname = '%s@%s'%(classname, name) + self._formname = '%s@%s' % (classname, name) else: self._formname = name @@ -1535,30 +1551,37 @@ def __repr__(self): classname = self.__class__.__name__ - return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname, - self._prop, self._value) + return '<%s(0x%x) %s %r %r>' % (classname, id(self), self._formname, + self._prop, self._value) + def __str__(self): return self.plain() + def __lt__(self, other): if isinstance(other, HTMLProperty): return self._value < other._value return self._value < other + def __le__(self, other): if isinstance(other, HTMLProperty): return self._value <= other._value return self._value <= other + def __eq__(self, other): if isinstance(other, HTMLProperty): return self._value == other._value return self._value == other + def __ne__(self, other): if isinstance(other, HTMLProperty): return self._value != other._value return self._value != other + def __gt__(self, other): if isinstance(other, HTMLProperty): return self._value > other._value return self._value > other + def __ge__(self, other): if isinstance(other, HTMLProperty): return self._value >= other._value @@ -1584,7 +1607,7 @@ if not perm('Web Access', userid): return False return perm('Edit', userid, self._classname, self._name, - self._nodeid) + self._nodeid) return perm('Create', userid, self._classname, self._name) or \ perm('Register', userid, self._classname, self._name) @@ -1592,11 +1615,13 @@ """ Is the user allowed to View the current class? """ perm = self._db.security.hasPermission - if perm('Web Access', self._client.userid) and perm('View', - self._client.userid, self._classname, self._name, self._nodeid): + if perm('Web Access', self._client.userid) and perm( + 'View', self._client.userid, self._classname, + self._name, self._nodeid): return 1 return self.is_edit_ok() + class StringHTMLProperty(HTMLProperty): hyper_re = re.compile(r'''( (?P<url> @@ -1624,7 +1649,7 @@ 'raw_enabled': 0, '_disable_config': 1} - valid_schemes = { } + valid_schemes = {} def _hyper_repl(self, match): if match.group('url'): @@ -1632,8 +1657,8 @@ elif match.group('email'): return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>') elif len(match.group('id')) < 10: - return self._hyper_repl_item(match, - '<a href="%(cls)s%(id)s%(fragment)s">%(item)s</a>') + return self._hyper_repl_item( + match, '<a href="%(cls)s%(id)s%(fragment)s">%(item)s</a>') else: # just return the matched text return match.group(0) @@ -1669,7 +1694,7 @@ id = match.group('id') fragment = match.group('fragment') if fragment is None: - fragment="" + fragment = "" try: # make sure cls is a valid tracker classname cl = self._db.getclass(cls) @@ -1679,16 +1704,15 @@ except KeyError: return item - def _hyper_repl_rst(self, match): if match.group('url'): s = match.group('url') - return '`%s <%s>`_'%(s, s) + return '`%s <%s>`_' % (s, s) elif match.group('email'): s = match.group('email') - return '`%s <mailto:%s>`_'%(s, s) + return '`%s <mailto:%s>`_' % (s, s) elif len(match.group('id')) < 10: - return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_') + return self._hyper_repl_item(match, '`%(item)s <%(cls)s%(id)s>`_') else: # just return the matched text return match.group(0) @@ -1724,12 +1748,12 @@ if end < len(match.string): suffix = match.string[end] if (prefix, suffix) in {('[', ']')}: - if match.string[end+1] == '(': # find following ( + if match.string[end+1] == '(': # find following ( return match.group(0) - if (prefix, suffix) in {('(',')')}: + if (prefix, suffix) in {('(', ')')}: if match.string[start-1] == ']': return match.group(0) - return self._hyper_repl_item(match,'[%(item)s](%(cls)s%(id)s)') + return self._hyper_repl_item(match, '[%(item)s](%(cls)s%(id)s)') else: # just return the matched text return match.group(0) @@ -1804,7 +1828,7 @@ s = self.plain(escape=escape, hyperlink=hyperlink) if not StructuredText: return s - return StructuredText(s,level=1,header=0) + return StructuredText(s, level=1, header=0) def rst(self, hyperlink=1): """ Render the value of the property as ReStructuredText. @@ -1839,9 +1863,10 @@ pass else: raise - - return u2s(ReStructuredText(s, writer_name="html", - settings_overrides=self.rst_defaults)["html_body"]) + + return u2s(ReStructuredText( + s, writer_name="html", + settings_overrides=self.rst_defaults)["html_body"]) def markdown(self, hyperlink=1): """ Render the value of the property as markdown. @@ -1884,7 +1909,7 @@ If not editable, just display the plain() value in a <pre> tag. """ if not self.is_edit_ok(): - return '<pre>%s</pre>'%self.plain() + return '<pre>%s</pre>' % self.plain() if self._value is None: value = '' @@ -1896,7 +1921,7 @@ passthrough_args = self.cgi_escape_attrs(**kwargs) return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"' ' rows="%(rows)s" cols="%(cols)s">' - '%(value)s</textarea>') % locals() + '%(value)s</textarea>') % locals() def email(self, escape=1): """ Render the value of the property as an obscured email address @@ -1913,7 +1938,7 @@ name, domain = split domain = ' '.join(domain.split('.')[:-1]) name = name.replace('.', ' ') - value = '%s at %s ...'%(name, domain) + value = '%s at %s ...' % (name, domain) else: value = value.replace('.', ' ') if escape: @@ -1986,9 +2011,10 @@ return '' return self.input(type="password", - name="@confirm@%s"%self._formname, - id="%s-confirm"%self._formname, - size=size) + name="@confirm@%s" % self._formname, + id="%s-confirm" % self._formname, + size=size) + class NumberHTMLProperty(HTMLProperty): def plain(self, escape=0): @@ -2002,6 +2028,21 @@ return str(self._value) + def pretty(self, format="%0.3f"): + '''Pretty print number using printf format specifier. + + If value is not convertable, returns str(_value) or "" + if None. + ''' + try: + return format % self._value + except TypeError: + value = self._value + if value is None: + return '' + else: + return str(value) + def field(self, size=30, **kwargs): """ Render a form edit field for the property. @@ -2027,6 +2068,7 @@ """ return float(self._value) + class IntegerHTMLProperty(HTMLProperty): def plain(self, escape=0): """ Render a "plain" representation of the property @@ -2093,62 +2135,68 @@ value = self._value if is_us(value): value = value.strip().lower() in ('checked', 'yes', 'true', - 'on', '1') - - if ( not y_label ): - y_label = '<label class="rblabel" for="%s_%s">'%(self._formname, 'yes') + 'on', '1') + + if (not y_label): + y_label = '<label class="rblabel" for="%s_%s">' % ( + self._formname, 'yes') y_label += self._('Yes') y_label += '</label>' - if ( not n_label ): - n_label = '<label class="rblabel" for="%s_%s">'%(self._formname, 'no') + if (not n_label): + n_label = '<label class="rblabel" for="%s_%s">' % ( + self._formname, 'no') n_label += self._('No') n_label += '</label>' checked = value and "checked" or "" if value: y_rb = self.input(type="radio", name=self._formname, value="yes", - checked="checked", id="%s_%s"%(self._formname, 'yes'), **kwargs) - - n_rb =self.input(type="radio", name=self._formname, value="no", - id="%s_%s"%(self._formname, 'no'), **kwargs) + checked="checked", id="%s_%s" % ( + self._formname, 'yes'), **kwargs) + + n_rb = self.input(type="radio", name=self._formname, value="no", + id="%s_%s" % ( + self._formname, 'no'), **kwargs) else: y_rb = self.input(type="radio", name=self._formname, value="yes", - id="%s_%s"%(self._formname, 'yes'), **kwargs) + id="%s_%s" % (self._formname, 'yes'), **kwargs) n_rb = self.input(type="radio", name=self._formname, value="no", - checked="checked", id="%s_%s"%(self._formname, 'no'), **kwargs) - - if ( u_label ): - if (u_label is True): # it was set via u_label=True - u_label = '' # make it empty but a string not boolean + checked="checked", id="%s_%s" % ( + self._formname, 'no'), **kwargs) + + if (u_label): + if (u_label is True): # it was set via u_label=True + u_label = '' # make it empty but a string not boolean u_rb = self.input(type="radio", name=self._formname, value="", - id="%s_%s"%(self._formname, 'unk'), **kwargs) + id="%s_%s" % (self._formname, 'unk'), **kwargs) else: # don't generate a trivalue radiobutton. u_label = '' - u_rb='' - - if ( labelfirst ): + u_rb = '' + + if (labelfirst): s = u_label + u_rb + y_label + y_rb + n_label + n_rb else: - s = u_label + u_rb +y_rb + y_label + n_rb + n_label + s = u_label + u_rb + y_rb + y_label + n_rb + n_label return s + class DateHTMLProperty(HTMLProperty): _marker = [] def __init__(self, client, classname, nodeid, prop, name, value, - anonymous=0, offset=None): + anonymous=0, offset=None): HTMLProperty.__init__(self, client, classname, nodeid, prop, name, - value, anonymous=anonymous) + value, anonymous=anonymous) if self._value and not is_us(self._value): self._value.setTranslator(self._client.translator) self._offset = offset - if self._offset is None : - self._offset = self._prop.offset (self._db) + if self._offset is None: + self._offset = self._prop.offset(self._db) def plain(self, escape=0): """ Render a "plain" representation of the property @@ -2191,7 +2239,7 @@ ret = ret - interval return DateHTMLProperty(self._client, self._classname, self._nodeid, - self._prop, self._formname, ret) + self._prop, self._formname, ret) def field(self, size=30, default=None, format=_marker, popcal=True, **kwargs): @@ -2223,14 +2271,15 @@ elif isinstance(default, DateHTMLProperty): raw_value = default._value else: - raise ValueError(self._('default value for ' + raise ValueError(self._( + 'default value for ' 'DateHTMLProperty must be either DateHTMLProperty ' 'or string date representation.')) elif is_us(value): # most likely erroneous input to be passed back to user value = us2s(value) s = self.input(name=self._formname, value=value, size=size, - **kwargs) + **kwargs) if popcal: s += self.popcal() return s @@ -2245,9 +2294,9 @@ else: value = date.Date(raw_value).pretty(format) else: - if self._offset is None : + if self._offset is None: offset = self._db.getUserTimezone() - else : + else: offset = self._offset value = raw_value.local(offset) if format is not self._marker: @@ -2310,35 +2359,39 @@ return self._('[hidden]') return DateHTMLProperty(self._client, self._classname, self._nodeid, - self._prop, self._formname, self._value, offset=offset) + self._prop, self._formname, self._value, + offset=offset) def popcal(self, width=300, height=200, label="(cal)", - form="itemSynopsis"): + form="itemSynopsis"): """Generate a link to a calendar pop-up window. item: HTMLProperty e.g.: context.deadline """ if self.isset(): - date = "&date=%s"%self._value - else : + date = "&date=%s" % self._value + else: date = "" data_attr = { - "data-calurl": '%s?@template=calendar&property=%s&form=%s%s' % (self._classname, self._name, form, date), + "data-calurl": '%s?@template=calendar&property=%s&form=%s%s' % ( + self._classname, self._name, form, date), "data-width": width, "data-height": height } - + return ('<a class="classhelp" %s href="javascript:help_window(' - "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)" - '">%s</a>'%(self.cgi_escape_attrs(**data_attr),self._classname, self._name, form, date, width, - height, label)) + "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)" + '">%s</a>' % (self.cgi_escape_attrs(**data_attr), + self._classname, self._name, form, date, width, + height, label)) + class IntervalHTMLProperty(HTMLProperty): def __init__(self, client, classname, nodeid, prop, name, value, - anonymous=0): + anonymous=0): HTMLProperty.__init__(self, client, classname, nodeid, prop, - name, value, anonymous) + name, value, anonymous) if self._value and not is_us(self._value): self._value.setTranslator(self._client.translator) @@ -2375,6 +2428,7 @@ return self.input(name=self._formname, value=value, size=size, **kwargs) + class LinkHTMLProperty(HTMLProperty): """ Link HTMLProperty Include the above as well as being able to access the class @@ -2401,7 +2455,7 @@ return '' return nothing msg = self._('Attempt to look up %(attr)s on a missing value') - return MissingValue(msg%locals()) + return MissingValue(msg % locals()) i = HTMLItem(self._client, self._prop.classname, self._value) return getattr(i, attr) @@ -2412,7 +2466,7 @@ """ if not self._value: msg = self._('Attempt to look up %(item)s on a missing value') - return MissingValue(msg%locals()) + return MissingValue(msg % locals()) i = HTMLItem(self._client, self._prop.classname, self._value) return i[item] @@ -2432,7 +2486,7 @@ default=self._("[label is missing]"))) except IndexError: value = self._value - else : + else: value = self._value if escape: value = html_escape(value) @@ -2454,10 +2508,10 @@ k = linkcl.getkey() idparse = self._prop.try_id_parsing if k and num_re.match(self._value): - try : + try: value = linkcl.get(self._value, k) - except IndexError : - if idparse : + except IndexError: + if idparse: raise value = '' else: @@ -2509,13 +2563,14 @@ value = None linkcl = self._db.getclass(self._prop.classname) - l = ['<select %s>'%self.cgi_escape_attrs(name = self._formname, - **html_kwargs)] + html = ['<select %s>' % self.cgi_escape_attrs(name=self._formname, + **html_kwargs)] k = linkcl.labelprop(1) s = '' if value is None: s = 'selected="selected" ' - l.append(self._('<option %svalue="-1">- no selection -</option>')%s) + html.append(self._( + '<option %svalue="-1">- no selection -</option>') % s) if sort_on is not None: if not isinstance(sort_on, tuple): @@ -2527,9 +2582,11 @@ sort_on = ('+', linkcl.orderprop()) options = [opt - for opt in linkcl.filter(None, conditions, sort_on, (None, None)) - if self._db.security.hasPermission("View", self._client.userid, - linkcl.classname, itemid=opt)] + for opt in linkcl.filter( + None, conditions, sort_on, (None, None)) + if self._db.security.hasPermission( + "View", self._client.userid, linkcl.classname, + itemid=opt)] # make sure we list the current value if it's retired if value and value not in options: @@ -2565,18 +2622,18 @@ # figure if this option is selected s = '' # record the marker for the selected item if requested - selected_mark='' + selected_mark = '' if value in [optionid, option]: s = 'selected="selected" ' - if ( showdef ): + if (showdef): selected_mark = showdef # figure the label if showid: - lab = '%s%s: %s'%(self._prop.classname, optionid, option) + lab = '%s%s: %s' % (self._prop.classname, optionid, option) elif not option: - lab = '%s%s'%(self._prop.classname, optionid) + lab = '%s%s' % (self._prop.classname, optionid) else: lab = option @@ -2588,20 +2645,21 @@ m = [] for fn in additional_fns: m.append(str(fn(optionid))) - lab = lab + ' (%s)'%', '.join(m) + lab = lab + ' (%s)' % ', '.join(m) # and generate tr = str if translate: tr = self._ lab = html_escape(tr(lab)) - l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab)) - l.append('</select>') - return '\n'.join(l) + html.append( + '<option %svalue="%s">%s</option>' % (s, optionid, lab)) + html.append('</select>') + return '\n'.join(html) + # def checklist(self, ...) - class MultilinkHTMLProperty(HTMLProperty): """ Multilink HTMLProperty @@ -2612,7 +2670,7 @@ HTMLProperty.__init__(self, *args, **kwargs) if self._value: display_value = lookupIds(self._db, self._prop, self._value, - fail_ok=1, do_lookup=False) + fail_ok=1, do_lookup=False) keyfun = make_key_function(self._db, self._prop.classname) # sorting fails if the value contains # items not yet stored in the database @@ -2651,22 +2709,22 @@ def reverse(self): """ return the list in reverse order """ - l = self._value[:] - l.reverse() - return self.viewableGenerator(l) + mylist = self._value[:] + mylist.reverse() + return self.viewableGenerator(mylist) def sorted(self, property, reverse=False, NoneFirst=False): - """ Return this multilink sorted by the given property + """ Return this multilink sorted by the given property Set Nonefirst to True to sort None/unset property before a property with a valid value. - + """ # use 2 if NoneFirst is False to sort None last # 0 to sort to sort None first # 1 is used to sort the integer values. - NoneCode = (2,0)[NoneFirst] + NoneCode = (2, 0)[NoneFirst] value = list(self.__iter__()) @@ -2679,7 +2737,7 @@ if type(prop) in [hyperdb.Link, hyperdb.Multilink]: orderprop = value[0]._db.getclass(prop.classname).orderprop() sort_by_link = True - else: + else: orderprop = property sort_by_link = False @@ -2698,7 +2756,7 @@ else: val = prop._value - if val is None: # verify orderprop is set to a value + if val is None: # verify orderprop is set to a value return (NoneCode, None) return (1, val) # val should be base python type @@ -2732,7 +2790,8 @@ except IndexError: label = None # fall back to designator if label is None - if label is None: label = '%s%s'%(self._prop.classname, k) + if label is None: + label = '%s%s' % (self._prop.classname, k) else: label = v labels.append(label) @@ -2755,7 +2814,7 @@ value = self._value[:] # map the id to the label property if not linkcl.getkey(): - showid=1 + showid = 1 if not showid: k = linkcl.labelprop(1) value = lookupKeys(linkcl, k, value) @@ -2808,9 +2867,10 @@ sort_on = ('+', linkcl.orderprop()) options = [opt - for opt in linkcl.filter(None, conditions, sort_on) - if self._db.security.hasPermission("View", self._client.userid, - linkcl.classname, itemid=opt)] + for opt in linkcl.filter(None, conditions, sort_on) + if self._db.security.hasPermission( + "View", self._client.userid, linkcl.classname, + itemid=opt)] # make sure we list the current values if they're retired for val in value: @@ -2823,14 +2883,13 @@ # The "no selection" option. height += 1 height = min(height, 7) - l = ['<select multiple %s>'%self.cgi_escape_attrs(name = self._formname, - size = height, - **html_kwargs)] + html = ['<select multiple %s>' % self.cgi_escape_attrs( + name=self._formname, size=height, **html_kwargs)] k = linkcl.labelprop(1) if value: # FIXME '- no selection -' mark for translation - l.append('<option value="%s">- no selection -</option>' - % ','.join(['-' + v for v in value])) + html.append('<option value="%s">- no selection -</option>' + % ','.join(['-' + v for v in value])) if additional: additional_fns = [] @@ -2859,7 +2918,7 @@ # figure the label if showid: - lab = '%s%s: %s'%(self._prop.classname, optionid, option) + lab = '%s%s: %s' % (self._prop.classname, optionid, option) else: lab = option # truncate if it's too long @@ -2869,17 +2928,17 @@ m = [] for fn in additional_fns: m.append(str(fn(optionid))) - lab = lab + ' (%s)'%', '.join(m) + lab = lab + ' (%s)' % ', '.join(m) # and generate tr = str if translate: tr = self._ lab = html_escape(tr(lab)) - l.append('<option %svalue="%s">%s</option>'%(s, optionid, - lab)) - l.append('</select>') - return '\n'.join(l) + html.append('<option %svalue="%s">%s</option>' % (s, optionid, + lab)) + html.append('</select>') + return '\n'.join(html) # set the propclasses for HTMLItem @@ -2896,8 +2955,9 @@ (hyperdb.Multilink, MultilinkHTMLProperty), ] + def register_propclass(prop, cls): - for index,propclass in enumerate(propclasses): + for index, propclass in enumerate(propclasses): p, c = propclass if prop == p: propclasses[index] = (prop, cls) @@ -2914,12 +2974,17 @@ linkcl = db.getclass(classname) if sort_on is None: sort_on = linkcl.orderprop() + def keyfunc(a): if num_re.match(a): a = linkcl.get(a, sort_on) + # In Python3 we may not compare strings and None + if a is None: + return '' return a return keyfunc + def handleListCGIValue(value): """ Value is either a single item or a list of items. Each item has a .value that we're actually interested in. @@ -2932,6 +2997,7 @@ return [] return [v.strip() for v in value.split(',')] + class HTMLRequest(HTMLInputMixin): """The *request*, holding the CGI form and environment. @@ -2955,7 +3021,7 @@ - "search_text" text to perform a full-text search on for an index """ def __repr__(self): - return '<HTMLRequest %r>'%self.__dict__ + return '<HTMLRequest %r>' % self.__dict__ def __init__(self, client): # _client is needed by HTMLInputMixin @@ -2998,17 +3064,17 @@ dirs = [] for special in '@:': idx = 0 - key = '%s%s%d'%(special, name, idx) + key = '%s%s%d' % (special, name, idx) while self._form_has_key(key): self.special_char = special fields.append(self.form.getfirst(key)) - dirkey = '%s%sdir%d'%(special, name, idx) + dirkey = '%s%sdir%d' % (special, name, idx) if dirkey in self.form: dirs.append(self.form.getfirst(dirkey)) else: dirs.append(None) idx += 1 - key = '%s%s%d'%(special, name, idx) + key = '%s%s%d' % (special, name, idx) # backward compatible (and query) URL format key = special + name dirkey = key + 'dir' @@ -3016,7 +3082,7 @@ fields = handleListCGIValue(self.form[key]) if dirkey in self.form: dirs.append(self.form.getfirst(dirkey)) - if fields: # only try other special char if nothing found + if fields: # only try other special char if nothing found break # sometimes requests come in without a class @@ -3034,7 +3100,8 @@ # if no classname, just append the propname unchecked. # this may be valid for some actions that bypass classes. if self.classname and cls.get_transitive_prop(propname) is None: - self.client.add_error_message("Unknown %s property %s"%(name, propname)) + self.client.add_error_message("Unknown %s property %s" % ( + name, propname)) else: var.append((dir, propname)) @@ -3077,16 +3144,16 @@ self.filterspec = {} db = self.client.db if self.classname is not None: - cls = db.getclass (self.classname) + cls = db.getclass(self.classname) for name in self.filter: if not self._form_has_key(name): continue - prop = cls.get_transitive_prop (name) + prop = cls.get_transitive_prop(name) fv = self.form[name] if (isinstance(prop, hyperdb.Link) or isinstance(prop, hyperdb.Multilink)): self.filterspec[name] = lookupIds(db, prop, - handleListCGIValue(fv)) + handleListCGIValue(fv)) else: if isinstance(fv, type([])): self.filterspec[name] = [v.value for v in fv] @@ -3096,7 +3163,7 @@ else: self.filterspec[name] = fv.value self.filterspec = security.filterFilterspec(userid, self.classname, - self.filterspec) + self.filterspec) # full-text search argument self.search_text = None @@ -3155,14 +3222,14 @@ s = [self.client.db.config.TRACKER_NAME] if self.classname: if self.client.nodeid: - s.append('- %s%s'%(self.classname, self.client.nodeid)) + s.append('- %s%s' % (self.classname, self.client.nodeid)) else: if self.template == 'item': - s.append('- new %s'%self.classname) + s.append('- new %s' % self.classname) elif self.template == 'index': - s.append('- %s index'%self.classname) + s.append('- %s index' % self.classname) else: - s.append('- %s %s'%(self.classname, self.template)) + s.append('- %s %s' % (self.classname, self.template)) else: s.append('- home') return ' '.join(s) @@ -3172,11 +3239,11 @@ d.update(self.__dict__) f = '' for k in self.form.keys(): - f += '\n %r=%r'%(k,handleListCGIValue(self.form[k])) + f += '\n %r=%r' % (k, handleListCGIValue(self.form[k])) d['form'] = f e = '' - for k,v in self.env.items(): - e += '\n %r=%r'%(k, v) + for k, v in self.env.items(): + e += '\n %r=%r' % (k, v) d['env'] = e return """ form: %(form)s @@ -3191,10 +3258,10 @@ pagesize: %(pagesize)r startwith: %(startwith)r env: %(env)s -"""%d +""" % d def indexargs_form(self, columns=1, sort=1, group=1, filter=1, - filterspec=1, search_text=1, exclude=[]): + filterspec=1, search_text=1, exclude=[]): """ return the current index args as form elements This routine generates an html form with hidden elements. @@ -3203,10 +3270,11 @@ these elements. This wll prevent the function from creating these elements in its output. """ - l = [] + html = [] sc = self.special_char + def add(k, v): - l.append(self.input(type="hidden", name=k, value=v)) + html.append(self.input(type="hidden", name=k, value=v)) if columns and self.columns: add(sc+'columns', ','.join(self.columns)) if sort: @@ -3216,7 +3284,7 @@ val.append('-'+attr) else: val.append(attr) - add(sc+'sort', ','.join (val)) + add(sc+'sort', ','.join(val)) if group: val = [] for dir, attr in self.group: @@ -3224,19 +3292,19 @@ val.append('-'+attr) else: val.append(attr) - add(sc+'group', ','.join (val)) + add(sc+'group', ','.join(val)) if filter and self.filter: add(sc+'filter', ','.join(self.filter)) if self.classname and filterspec: cls = self.client.db.getclass(self.classname) - for k,v in self.filterspec.items(): + for k, v in self.filterspec.items(): if k in exclude: continue - if type(v) == type([]): + if isinstance(v, list): # id's are stored as strings but should be treated # as integers in lists. if (isinstance(cls.get_transitive_prop(k), hyperdb.String) - and k != 'id'): + and k != 'id'): add(k, ' '.join(v)) else: add(k, ','.join(v)) @@ -3246,7 +3314,7 @@ add(sc+'search_text', self.search_text) add(sc+'pagesize', self.pagesize) add(sc+'startwith', self.startwith) - return '\n'.join(l) + return '\n'.join(html) def indexargs_url(self, url, args): """ Embed the current index args in a URL @@ -3263,8 +3331,8 @@ """ q = urllib_.quote sc = self.special_char - l = ['%s=%s'%(k,is_us(v) and q(v) or v) - for k,v in args.items() if v != None ] + l = ['%s=%s' % (k, is_us(v) and q(v) or v) + for k, v in args.items() if v is not None] # pull out the special values (prefixed by @ or :) specials = {} for key in args.keys(): @@ -3273,7 +3341,7 @@ # ok, now handle the specials we received in the request if self.columns and 'columns' not in specials: - l.append(sc+'columns=%s'%(','.join(self.columns))) + l.append(sc+'columns=%s' % (','.join(self.columns))) if self.sort and 'sort' not in specials: val = [] for dir, attr in self.sort: @@ -3281,7 +3349,7 @@ val.append('-'+attr) else: val.append(attr) - l.append(sc+'sort=%s'%(','.join(val))) + l.append(sc+'sort=%s' % (','.join(val))) if self.group and 'group' not in specials: val = [] for dir, attr in self.group: @@ -3289,30 +3357,32 @@ val.append('-'+attr) else: val.append(attr) - l.append(sc+'group=%s'%(','.join(val))) + l.append(sc+'group=%s' % (','.join(val))) if self.filter and 'filter' not in specials: - l.append(sc+'filter=%s'%(','.join(self.filter))) + l.append(sc+'filter=%s' % (','.join(self.filter))) if self.search_text and 'search_text' not in specials: - l.append(sc+'search_text=%s'%q(self.search_text)) + l.append(sc+'search_text=%s' % q(self.search_text)) if 'pagesize' not in specials: - l.append(sc+'pagesize=%s'%self.pagesize) + l.append(sc+'pagesize=%s' % self.pagesize) if 'startwith' not in specials: - l.append(sc+'startwith=%s'%self.startwith) + l.append(sc+'startwith=%s' % self.startwith) # finally, the remainder of the filter args in the request if self.classname and self.filterspec: cls = self.client.db.getclass(self.classname) - for k,v in self.filterspec.items(): + for k, v in self.filterspec.items(): if k not in args: - if type(v) == type([]): + if isinstance(v, list): prop = cls.get_transitive_prop(k) if k != 'id' and isinstance(prop, hyperdb.String): - l.append('%s=%s'%(k, '%20'.join([q(i) for i in v]))) + l.append('%s=%s' % ( + k, '%20'.join([q(i) for i in v]))) else: - l.append('%s=%s'%(k, ','.join([q(i) for i in v]))) + l.append('%s=%s' % ( + k, ','.join([q(i) for i in v]))) else: - l.append('%s=%s'%(k, q(v))) - return '%s?%s'%(url, '&'.join(l)) + l.append('%s=%s' % (k, q(v))) + return '%s?%s' % (url, '&'.join(l)) indexargs_href = indexargs_url def base_javascript(self): @@ -3333,7 +3403,7 @@ HelpWin.focus () } </script> -"""%(self._client.client_nonce,self.base) +""" % (self._client.client_nonce, self.base) def batch(self, permission='View'): """ Return a batch object for results from the "current search" @@ -3342,7 +3412,7 @@ userid = self._client.userid if not check('Web Access', userid): return Batch(self.client, [], self.pagesize, self.startwith, - classname=self.classname) + classname=self.classname) filterspec = self.filterspec sort = self.sort @@ -3370,12 +3440,13 @@ matches = None # filter for visibility - l = [id for id in klass.filter(matches, filterspec, sort, group) - if check(permission, userid, self.classname, itemid=id)] + allowed = [id for id in klass.filter(matches, filterspec, sort, group) + if check(permission, userid, self.classname, itemid=id)] # return the batch object, using IDs only - return Batch(self.client, l, self.pagesize, self.startwith, - classname=self.classname) + return Batch(self.client, allowed, self.pagesize, self.startwith, + classname=self.classname) + # extend the standard ZTUtils Batch object to remove dependency on # Acquisition and add a couple of useful methods @@ -3404,19 +3475,20 @@ "sequence_length" is the length of the original, unbatched, sequence. """ def __init__(self, client, sequence, size, start, end=0, orphan=0, - overlap=0, classname=None): + overlap=0, classname=None): self.client = client self.last_index = self.last_item = None self.current_item = None self.classname = classname self.sequence_length = len(sequence) ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan, - overlap) + overlap) # overwrite so we can late-instantiate the HTMLItem instance def __getitem__(self, index): if index < 0: - if index + self.end < self.first: raise IndexError(index) + if index + self.end < self.first: + raise IndexError(index) return self._sequence[index + self.end] if index >= self.length: @@ -3444,14 +3516,14 @@ if self.last_item is None: return 1 for property in properties: - if property == 'id' or property.endswith ('.id')\ - or isinstance (self.last_item[property], list): + if property == 'id' or property.endswith('.id')\ + or isinstance(self.last_item[property], list): if (str(self.last_item[property]) != - str(self.current_item[property])): + str(self.current_item[property])): return 1 else: if (self.last_item[property]._value != - self.current_item[property]._value): + self.current_item[property]._value): return 1 return 0 @@ -3460,8 +3532,8 @@ if self.start == 1: return None return Batch(self.client, self._sequence, self.size, - self.first - self._size + self.overlap, 0, self.orphan, - self.overlap) + self.first - self._size + self.overlap, 0, self.orphan, + self.overlap) def next(self): try: @@ -3469,23 +3541,25 @@ except IndexError: return None return Batch(self.client, self._sequence, self.size, - self.end - self.overlap, 0, self.orphan, self.overlap) + self.end - self.overlap, 0, self.orphan, self.overlap) + class TemplatingUtils: """ Utilities for templating """ def __init__(self, client): self.client = client + def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): return Batch(self.client, sequence, size, start, end, orphan, - overlap) + overlap) def anti_csrf_nonce(self, lifetime=None): return anti_csrf_nonce(self.client, lifetime=lifetime) def timestamp(self): return pack_timestamp() - + def url_quote(self, url): """URL-quote the supplied text.""" return urllib_.quote(url) @@ -3521,25 +3595,25 @@ """ tz = request.client.db.getUserTimezone() current_date = date.Date(".").local(tz) - date_str = request.form.getfirst("date", current_date) - display = request.form.getfirst("display", date_str) - template = request.form.getfirst("@template", "calendar") - form = request.form.getfirst("form") - property = request.form.getfirst("property") + date_str = request.form.getfirst("date", current_date) + display = request.form.getfirst("display", date_str) + template = request.form.getfirst("@template", "calendar") + form = request.form.getfirst("form") + property = request.form.getfirst("property") curr_date = "" try: # date_str and display can be set to an invalid value # if user submits a value like "d4" and gets an edit error. # If either or both invalid just ignore that we can't parse it # and assign them to today. - curr_date = date.Date(date_str) # to highlight - display = date.Date(display) # to show + curr_date = date.Date(date_str) # to highlight + display = date.Date(display) # to show except ValueError: # we couldn't parse the date # just let the calendar display curr_date = current_date display = current_date - day = display.day + day = display.day # for navigation try: @@ -3568,16 +3642,16 @@ # month res.append('<table class="calendar"><tr><td>') res.append(' <table width="100%" class="calendar_nav"><tr>') - link = "&display=%s"%date_prev_month + link = "&display=%s" % date_prev_month if date_prev_month: res.append(' <td><a href="%s&display=%s"><</a></td>' - % (base_link, date_prev_month)) + % (base_link, date_prev_month)) else: res.append(' <td></td>') - res.append(' <td>%s</td>'%calendar.month_name[display.month]) + res.append(' <td>%s</td>' % calendar.month_name[display.month]) if date_next_month: res.append(' <td><a href="%s&display=%s">></a></td>' - % (base_link, date_next_month)) + % (base_link, date_next_month)) else: res.append(' <td></td>') # spacer @@ -3585,13 +3659,13 @@ # year if date_prev_year: res.append(' <td><a href="%s&display=%s"><</a></td>' - % (base_link, date_prev_year)) + % (base_link, date_prev_year)) else: res.append(' <td></td>') - res.append(' <td>%s</td>'%display.year) + res.append(' <td>%s</td>' % display.year) if date_next_year: res.append(' <td><a href="%s&display=%s">></a></td>' - % (base_link, date_next_year)) + % (base_link, date_next_year)) else: res.append(' <td></td>') res.append(' </tr></table>') @@ -3601,29 +3675,30 @@ res.append(' <tr><td><table class="calendar_display">') res.append(' <tr class="weekdays">') for day in calendar.weekheader(3).split(): - res.append(' <td>%s</td>'%day) + res.append(' <td>%s</td>' % day) res.append(' </tr>') for week in calendar.monthcalendar(display.year, display.month): res.append(' <tr>') for day in week: link = "javascript:form[field].value = '%d-%02d-%02d'; " \ "if ('createEvent' in document) { var evt = document.createEvent('HTMLEvents'); evt.initEvent('change', true, true); form[field].dispatchEvent(evt); } else { form[field].fireEvent('onchange'); }" \ - "window.close ();"%(display.year, display.month, day) + "window.close ();" % (display.year, display.month, day) if (day == curr_date.day and display.month == curr_date.month and display.year == curr_date.year): # highlight style = "today" - else : + else: style = "" if day: - res.append(' <td class="%s"><a href="%s">%s</a></td>'%( + res.append(' <td class="%s"><a href="%s">%s</a></td>' % ( style, link, day)) - else : + else: res.append(' <td></td>') res.append(' </tr>') res.append('</table></td></tr></table>') return "\n".join(res) + class MissingValue(object): def __init__(self, description, **kwargs): self.__description = description @@ -3631,6 +3706,7 @@ self.__dict__[key] = value def __call__(self, *args, **kwargs): return MissingValue(self.__description) + def __getattr__(self, name): # This allows assignments which assume all intermediate steps are Null # objects if they don't exist yet. @@ -3648,9 +3724,10 @@ def __contains__(self, key): return False def __eq__(self, rhs): return False def __ne__(self, rhs): return False - def __str__(self): return '[%s]'%self.__description - def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self), - self.__description) + def __str__(self): return '[%s]' % self.__description + def __repr__(self): return '<MissingValue 0x%x "%s">' % ( + id(self), self.__description) + def gettext(self, str): return str _ = gettext
