"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/roundup/cgi/templating.py" (1 Jul 2020, 127357 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "templating.py": 1.6.1_vs_2.0.0.

    1 """Implements the API used in the HTML templating for the web interface.
    2 """
    3 
    4 todo = """
    5 - Document parameters to Template.render() method
    6 - Add tests for Loader.load() method
    7 - Most methods should have a "default" arg to supply a value
    8   when none appears in the hyperdb or request.
    9 - Multilink property additions: change_note and new_upload
   10 - Add class.find() too
   11 - NumberHTMLProperty should support numeric operations
   12 - LinkHTMLProperty should handle comparisons to strings (cf. linked name)
   13 - HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
   14   '''Set the request's view arguments to the given values when no
   15      values are found in the CGI environment.
   16   '''
   17 - have menu() methods accept filtering arguments
   18 """
   19 
   20 __docformat__ = 'restructuredtext'
   21 
   22 # List of schemes that are not rendered as links in rst and markdown.
   23 _disable_url_schemes = [ 'javascript' ]
   24 
   25 import base64, cgi, re, os.path, mimetypes, csv, string
   26 import calendar
   27 import textwrap
   28 import time, hashlib
   29 
   30 from roundup.anypy.html import html_escape
   31 
   32 from roundup.anypy import urllib_
   33 from roundup import hyperdb, date, support
   34 from roundup import i18n
   35 from roundup.i18n import _
   36 from roundup.anypy.strings import is_us, b2s, s2b, us2s, s2u, u2s, StringIO
   37 
   38 from .KeywordsExpr import render_keywords_expression_editor
   39 
   40 from roundup.cgi.timestamp import pack_timestamp
   41 
   42 import roundup.anypy.random_ as random_
   43 try:
   44     import cPickle as pickle
   45 except ImportError:
   46     import pickle
   47 try:
   48     from StructuredText.StructuredText import HTML as StructuredText
   49 except ImportError:
   50     try: # older version
   51         import StructuredText
   52     except ImportError:
   53         StructuredText = None
   54 try:
   55     from docutils.core import publish_parts as ReStructuredText
   56 except ImportError:
   57     ReStructuredText = None
   58 try:
   59     from itertools import zip_longest
   60 except ImportError:
   61     from itertools import izip_longest as zip_longest
   62 
   63 from roundup.exceptions import RoundupException
   64 
   65 def _import_markdown2():
   66     try:
   67         import markdown2, re
   68         class Markdown(markdown2.Markdown):
   69             # don't allow disabled protocols in links
   70             _safe_protocols = re.compile('(?!' + ':|'.join([re.escape(s) for s in _disable_url_schemes]) + ':)', re.IGNORECASE)
   71 
   72         markdown = lambda s: Markdown(safe_mode='escape', extras={ 'fenced-code-blocks' : True }).convert(s)
   73     except ImportError:
   74         markdown = None
   75 
   76     return markdown
   77 
   78 def _import_markdown():
   79     try:
   80         from markdown import markdown as markdown_impl
   81         from markdown.extensions import Extension as MarkdownExtension
   82         from markdown.treeprocessors import Treeprocessor
   83 
   84         class RestrictLinksProcessor(Treeprocessor):
   85             def run(self, root):
   86                 for el in root.iter('a'):
   87                     if 'href' in el.attrib:
   88                         url = el.attrib['href'].lstrip(' \r\n\t\x1a\0').lower()
   89                         for s in _disable_url_schemes:
   90                             if url.startswith(s + ':'):
   91                                 el.attrib['href'] = '#'
   92 
   93         # make sure any HTML tags get escaped and some links restricted
   94         class SafeHtml(MarkdownExtension):
   95             def extendMarkdown(self, md, md_globals=None):
   96                 if hasattr(md.preprocessors, 'deregister'):
   97                     md.preprocessors.deregister('html_block')
   98                 else:
   99                     del md.preprocessors['html_block']
  100                 if hasattr(md.inlinePatterns, 'deregister'):
  101                     md.inlinePatterns.deregister('html')
  102                 else:
  103                     del md.inlinePatterns['html']
  104 
  105                 if hasattr(md.preprocessors, 'register'):
  106                     md.treeprocessors.register(RestrictLinksProcessor(), 'restrict_links', 0)
  107                 else:
  108                     md.treeprocessors['restrict_links'] = RestrictLinksProcessor()
  109 
  110         markdown = lambda s: markdown_impl(s, extensions=[SafeHtml(), 'fenced_code'])
  111     except ImportError:
  112         markdown = None
  113 
  114     return markdown
  115 
  116 def _import_mistune():
  117     try:
  118         import mistune
  119         mistune._scheme_blacklist = [ s + ':' for s in _disable_url_schemes ]
  120         markdown = mistune.markdown
  121     except ImportError:
  122         markdown = None
  123 
  124     return markdown
  125 
  126 markdown = _import_markdown2() or _import_markdown() or _import_mistune()
  127 
  128 # bring in the templating support
  129 from roundup.cgi import TranslationService, ZTUtils
  130 
  131 ### i18n services
  132 # this global translation service is not thread-safe.
  133 # it is left here for backward compatibility
  134 # until all Web UI translations are done via client.translator object
  135 translationService = TranslationService.get_translation()
  136 
  137 def anti_csrf_nonce(client, lifetime=None):
  138     ''' Create a nonce for defending against CSRF attack.
  139 
  140         Then it stores the nonce, the session id for the user
  141         and the user id in the one time key database for use
  142         by the csrf validator that runs in the client::inner_main
  143         module/function.
  144     '''
  145     otks=client.db.getOTKManager()
  146     key = b2s(base64.b32encode(random_.token_bytes(40)))
  147 
  148     while otks.exists(key):
  149         key = b2s(base64.b32encode(random_.token_bytes(40)))
  150 
  151     # lifetime is in minutes.
  152     if lifetime is None:
  153         lifetime = client.db.config['WEB_CSRF_TOKEN_LIFETIME']
  154 
  155     # offset to time.time is calculated as:
  156     #  default lifetime is 1 week after __timestamp.
  157     # That's the cleanup period hardcoded in otk.clean().
  158     # If a user wants a 10 minute lifetime calculate
  159     # 10 minutes newer than 1 week ago.
  160     #   lifetime - 10080 (number of minutes in a week)
  161     # convert to seconds and add (possible negative number)
  162     # to current time (time.time()).
  163     ts = time.time() + ((lifetime - 10080) * 60)
  164     otks.set(key, uid=client.db.getuid(),
  165              sid=client.session_api._sid,
  166              __timestamp=ts )
  167     otks.commit()
  168     return key
  169 
  170 ### templating
  171 
  172 class NoTemplate(RoundupException):
  173     pass
  174 
  175 class Unauthorised(RoundupException):
  176     def __init__(self, action, klass, translator=None):
  177         self.action = action
  178         self.klass = klass
  179         if translator:
  180             self._ = translator.gettext
  181         else:
  182             self._ = TranslationService.get_translation().gettext
  183     def __str__(self):
  184         return self._('You are not allowed to %(action)s '
  185             'items of class %(class)s') % {
  186             'action': self.action, 'class': self.klass}
  187 
  188 
  189 # --- Template Loader API
  190 
  191 class LoaderBase:
  192     """ Base for engine-specific template Loader class."""
  193     def __init__(self, dir):
  194         # loaders are given the template directory as a first argument
  195         pass
  196 
  197     def precompile(self):
  198         """ This method may be called when tracker is loaded to precompile
  199             templates that support this ability.
  200         """
  201         pass
  202 
  203     def load(self, tplname):
  204         """ Load template and return template object with render() method.
  205 
  206             "tplname" is a template name. For filesystem loaders it is a
  207             filename without extensions, typically in the "classname.view"
  208             format.
  209         """
  210         raise NotImplementedError
  211 
  212     def check(self, name):
  213         """ Check if template with the given name exists. Should return
  214             false if template can not be found.
  215         """
  216         raise NotImplementedError
  217 
  218 class TALLoaderBase(LoaderBase):
  219     """ Common methods for the legacy TAL loaders."""
  220 
  221     def __init__(self, dir):
  222         self.dir = dir
  223 
  224     def _find(self, name):
  225         """ Find template, return full path and filename of the
  226             template if it is found, None otherwise."""
  227         realsrc = os.path.realpath(self.dir)
  228         for extension in ['', '.html', '.xml']:
  229             f = name + extension
  230             src = os.path.join(realsrc, f)
  231             realpath = os.path.realpath(src)
  232             if not realpath.startswith(realsrc):
  233                 return # will raise invalid template
  234             if os.path.exists(src):
  235                 return (src, f)
  236 
  237     def check(self, name):
  238         return bool(self._find(name))
  239 
  240     def precompile(self):
  241         """ Precompile templates in load directory by loading them """
  242         for filename in os.listdir(self.dir):
  243             # skip subdirs
  244             if os.path.isdir(filename):
  245                 continue
  246 
  247             # skip files without ".html" or ".xml" extension - .css, .js etc.
  248             for extension in '.html', '.xml':
  249                 if filename.endswith(extension):
  250                     break
  251             else:
  252                 continue
  253 
  254             # remove extension
  255             filename = filename[:-len(extension)]
  256             self.load(filename)
  257 
  258     def __getitem__(self, name):
  259         """Special method to access templates by loader['name']"""
  260         try:
  261             return self.load(name)
  262         except NoTemplate as message:
  263             raise KeyError(message)
  264 
  265 class MultiLoader(LoaderBase):
  266     def __init__(self):
  267         self.loaders = []
  268 
  269     def add_loader(self, loader):
  270         self.loaders.append(loader)
  271       
  272     def check(self, name):
  273         for l in self.loaders:
  274             if l.check(name):
  275                 return True
  276 
  277     def load(self, name):    
  278         for l in self.loaders:
  279             if l.check(name):
  280                 return l.load(name)
  281 
  282     def __getitem__(self, name):
  283         """Needed for TAL templates compatibility"""
  284         # [ ] document root and helper templates
  285         try:
  286             return self.load(name)
  287         except NoTemplate as message:
  288             raise KeyError(message)
  289         
  290 
  291 class TemplateBase:
  292     content_type = 'text/html'
  293 
  294 
  295 def get_loader(dir, template_engine):
  296 
  297     # Support for multiple engines using fallback mechanizm
  298     # meaning that if first engine can't find template, we
  299     # use the second
  300 
  301     engines = template_engine.split(',')
  302     engines = [x.strip() for x in engines]
  303     ml = MultiLoader()
  304 
  305     for engine_name in engines:
  306         if engine_name == 'chameleon':
  307             from .engine_chameleon import Loader
  308         elif engine_name == 'jinja2':
  309             from .engine_jinja2 import Jinja2Loader as Loader
  310         elif engine_name == 'zopetal':
  311             from .engine_zopetal import Loader
  312         else:
  313             raise Exception('Unknown template engine "%s"' % engine_name)
  314         ml.add_loader(Loader(dir))
  315     
  316     if len(engines) == 1:
  317         return ml.loaders[0]
  318     else:
  319         return ml
  320 
  321 # --/ Template Loader API
  322 
  323 
  324 def context(client, template=None, classname=None, request=None):
  325     """Return the rendering context dictionary
  326 
  327     The dictionary includes following symbols:
  328 
  329     *context*
  330      this is one of three things:
  331 
  332      1. None - we're viewing a "home" page
  333      2. The current class of item being displayed. This is an HTMLClass
  334         instance.
  335      3. The current item from the database, if we're viewing a specific
  336         item, as an HTMLItem instance.
  337 
  338     *request*
  339       Includes information about the current request, including:
  340 
  341        - the url
  342        - the current index information (``filterspec``, ``filter`` args,
  343          ``properties``, etc) parsed out of the form.
  344        - methods for easy filterspec link generation
  345        - *user*, the current user node as an HTMLItem instance
  346        - *form*, the current CGI form information as a FieldStorage
  347 
  348     *config*
  349       The current tracker config.
  350 
  351     *db*
  352       The current database, used to access arbitrary database items.
  353 
  354     *utils*
  355       This is an instance of client.instance.TemplatingUtils, which is
  356       optionally defined in the tracker interfaces module and defaults to
  357       TemplatingUtils class in this file.
  358 
  359     *templates*
  360       Access to all the tracker templates by name.
  361       Used mainly in *use-macro* commands.
  362 
  363     *template*
  364       Current rendering template.
  365 
  366     *true*
  367       Logical True value.
  368 
  369     *false*
  370       Logical False value.
  371 
  372     *i18n*
  373       Internationalization service, providing string translation
  374       methods ``gettext`` and ``ngettext``.
  375 
  376     """
  377 
  378     # if template, classname and/or request are not passed explicitely,
  379     # compute form client
  380     if template is None:
  381         template = client.template
  382     if classname is None:
  383         classname = client.classname
  384     if request is None:
  385         request = HTMLRequest(client)
  386 
  387     c = {
  388          'context': None,
  389          'options': {},
  390          'nothing': None,
  391          'request': request,
  392          'db': HTMLDatabase(client),
  393          'config': client.instance.config,
  394          'tracker': client.instance,
  395          'utils': client.instance.TemplatingUtils(client),
  396          'templates': client.instance.templates,
  397          'template': template,
  398          'true': 1,
  399          'false': 0,
  400          'i18n': client.translator
  401     }
  402     # add in the item if there is one
  403     if client.nodeid:
  404         c['context'] = HTMLItem(client, classname, client.nodeid,
  405             anonymous=1)
  406     elif classname in client.db.classes:
  407         c['context'] = HTMLClass(client, classname, anonymous=1)
  408     return c
  409 
  410 class HTMLDatabase:
  411     """ Return HTMLClasses for valid class fetches
  412     """
  413     def __init__(self, client):
  414         self._client = client
  415         self._ = client._
  416         self._db = client.db
  417 
  418         # we want config to be exposed
  419         self.config = client.db.config
  420 
  421     def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
  422         # check to see if we're actually accessing an item
  423         m = desre.match(item)
  424         if m:
  425             cl = m.group('cl')
  426             self._client.db.getclass(cl)
  427             return HTMLItem(self._client, cl, m.group('id'))
  428         else:
  429             self._client.db.getclass(item)
  430             return HTMLClass(self._client, item)
  431 
  432     def __getattr__(self, attr):
  433         try:
  434             return self[attr]
  435         except KeyError:
  436             raise AttributeError(attr)
  437 
  438     def classes(self):
  439         l = sorted(self._client.db.classes.keys())
  440         m = []
  441         for item in l:
  442             m.append(HTMLClass(self._client, item))
  443         return m
  444 
  445 num_re = re.compile(r'^-?\d+$')
  446 
  447 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
  448     """ "fail_ok" should be specified if we wish to pass through bad values
  449         (most likely form values that we wish to represent back to the user)
  450         "do_lookup" is there for preventing lookup by key-value (if we
  451         know that the value passed *is* an id)
  452     """
  453     cl = db.getclass(prop.classname)
  454     l = []
  455     for entry in ids:
  456         # Do not look up numeric IDs if try_id_parsing
  457         if prop.try_id_parsing and num_re.match(entry):
  458             l.append(entry)
  459             continue
  460         if do_lookup:
  461             try:
  462                 item = cl.lookup(entry)
  463             except (TypeError, KeyError):
  464                 pass
  465             else:
  466                 l.append(item)
  467                 continue
  468         # if fail_ok, ignore lookup error
  469         # otherwise entry must be existing object id rather than key value
  470         if fail_ok:
  471             l.append(entry)
  472     return l
  473 
  474 def lookupKeys(linkcl, key, ids, num_re=num_re):
  475     """ Look up the "key" values for "ids" list - though some may already
  476     be key values, not ids.
  477     """
  478     l = []
  479     for entry in ids:
  480         if num_re.match(entry):
  481             try:
  482                 label = linkcl.get(entry, key)
  483             except IndexError:
  484                 # fall back to id if illegal (avoid template crash)
  485                 label = entry
  486             # fall back to designator if label is None
  487             if label is None: label = '%s%s'%(linkcl.classname, entry)
  488             l.append(label)
  489         else:
  490             l.append(entry)
  491     return l
  492 
  493 def _set_input_default_args(dic):
  494     # 'text' is the default value anyway --
  495     # but for CSS usage it should be present
  496     dic.setdefault('type', 'text')
  497     # useful e.g for HTML LABELs:
  498     if 'id' not in dic:
  499         try:
  500             if dic['text'] in ('radio', 'checkbox'):
  501                 dic['id'] = '%(name)s-%(value)s' % dic
  502             else:
  503                 dic['id'] = dic['name']
  504         except KeyError:
  505             pass
  506 
  507 def html4_cgi_escape_attrs(**attrs):
  508     ''' Boolean attributes like 'disabled', 'required'
  509         are represented without a value. E.G.
  510         <input required ..> not <input required="required" ...>
  511         The latter is xhtml. Recognize booleans by:
  512           value is None
  513         Code can use None to indicate a pure boolean.
  514     '''
  515     return ' '.join(['%s="%s"'%(k,html_escape(str(v), True)) 
  516                          if v != None else '%s'%(k)
  517                          for k,v in sorted(attrs.items())])
  518 
  519 def xhtml_cgi_escape_attrs(**attrs):
  520     ''' Boolean attributes like 'disabled', 'required'
  521         are represented with a value that is the same as
  522         the attribute name.. E.G.
  523            <input required="required" ...> not <input required ..>
  524         The latter is html4 or 5. Recognize booleans by:
  525           value is None
  526         Code can use None to indicate a pure boolean.
  527     '''
  528     return ' '.join(['%s="%s"'%(k,html_escape(str(v), True))
  529                          if v != None else '%s="%s"'%(k,k)
  530                          for k,v in sorted(attrs.items())])
  531 
  532 def input_html4(**attrs):
  533     """Generate an 'input' (html4) element with given attributes"""
  534     _set_input_default_args(attrs)
  535     return '<input %s>'%html4_cgi_escape_attrs(**attrs)
  536 
  537 def input_xhtml(**attrs):
  538     """Generate an 'input' (xhtml) element with given attributes"""
  539     _set_input_default_args(attrs)
  540     return '<input %s/>'%xhtml_cgi_escape_attrs(**attrs)
  541 
  542 class HTMLInputMixin(object):
  543     """ requires a _client property """
  544     def __init__(self):
  545         html_version = 'html4'
  546         if hasattr(self._client.instance.config, 'HTML_VERSION'):
  547             html_version = self._client.instance.config.HTML_VERSION
  548         if html_version == 'xhtml':
  549             self.input = input_xhtml
  550             self.cgi_escape_attrs=xhtml_cgi_escape_attrs
  551         else:
  552             self.input = input_html4
  553             self.cgi_escape_attrs=html4_cgi_escape_attrs
  554         # self._context is used for translations.
  555         # will be initialized by the first call to .gettext()
  556         self._context = None
  557 
  558     def gettext(self, msgid):
  559         """Return the localized translation of msgid"""
  560         if self._context is None:
  561             self._context = context(self._client)
  562         return self._client.translator.translate(domain="roundup",
  563             msgid=msgid, context=self._context)
  564 
  565     _ = gettext
  566 
  567 class HTMLPermissions(object):
  568 
  569     def view_check(self):
  570         """ Raise the Unauthorised exception if the user's not permitted to
  571             view this class.
  572         """
  573         if not self.is_view_ok():
  574             raise Unauthorised("view", self._classname,
  575                 translator=self._client.translator)
  576 
  577     def edit_check(self):
  578         """ Raise the Unauthorised exception if the user's not permitted to
  579             edit items of this class.
  580         """
  581         if not self.is_edit_ok():
  582             raise Unauthorised("edit", self._classname,
  583                 translator=self._client.translator)
  584 
  585     def retire_check(self):
  586         """ Raise the Unauthorised exception if the user's not permitted to
  587             retire items of this class.
  588         """
  589         if not self.is_retire_ok():
  590             raise Unauthorised("retire", self._classname,
  591                 translator=self._client.translator)
  592 
  593 
  594 class HTMLClass(HTMLInputMixin, HTMLPermissions):
  595     """ Accesses through a class (either through *class* or *db.<classname>*)
  596     """
  597     def __init__(self, client, classname, anonymous=0):
  598         self._client = client
  599         self._ = client._
  600         self._db = client.db
  601         self._anonymous = anonymous
  602 
  603         # we want classname to be exposed, but _classname gives a
  604         # consistent API for extending Class/Item
  605         self._classname = self.classname = classname
  606         self._klass = self._db.getclass(self.classname)
  607         self._props = self._klass.getprops()
  608 
  609         HTMLInputMixin.__init__(self)
  610 
  611     def is_edit_ok(self):
  612         """ Is the user allowed to Create the current class?
  613         """
  614         perm = self._db.security.hasPermission
  615         return perm('Web Access', self._client.userid) and perm('Create',
  616             self._client.userid, self._classname)
  617 
  618     def is_retire_ok(self):
  619         """ Is the user allowed to retire items of the current class?
  620         """
  621         perm = self._db.security.hasPermission
  622         return perm('Web Access', self._client.userid) and perm('Retire',
  623             self._client.userid, self._classname)
  624 
  625     def is_restore_ok(self):
  626         """ Is the user allowed to restore retired items of the current class?
  627         """
  628         perm = self._db.security.hasPermission
  629         return perm('Web Access', self._client.userid) and perm('Restore',
  630             self._client.userid, self._classname)
  631 
  632     def is_view_ok(self):
  633         """ Is the user allowed to View the current class?
  634         """
  635         perm = self._db.security.hasPermission
  636         return perm('Web Access', self._client.userid) and perm('View',
  637             self._client.userid, self._classname)
  638 
  639     def is_only_view_ok(self):
  640         """ Is the user only allowed to View (ie. not Create) the current class?
  641         """
  642         return self.is_view_ok() and not self.is_edit_ok()
  643 
  644     def __repr__(self):
  645         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
  646 
  647     def __getitem__(self, item):
  648         """ return an HTMLProperty instance
  649         """
  650 
  651         # we don't exist
  652         if item == 'id':
  653             return None
  654 
  655         # get the property
  656         try:
  657             prop = self._props[item]
  658         except KeyError:
  659             raise KeyError('No such property "%s" on %s'%(item, self.classname))
  660 
  661         # look up the correct HTMLProperty class
  662         for klass, htmlklass in propclasses:
  663             if not isinstance(prop, klass):
  664                 continue
  665             value = prop.get_default_value()
  666             return htmlklass(self._client, self._classname, None, prop, item,
  667                 value, self._anonymous)
  668 
  669         # no good
  670         raise KeyError(item)
  671 
  672     def __getattr__(self, attr):
  673         """ convenience access """
  674         try:
  675             return self[attr]
  676         except KeyError:
  677             raise AttributeError(attr)
  678 
  679     def designator(self):
  680         """ Return this class' designator (classname) """
  681         return self._classname
  682 
  683     def getItem(self, itemid, num_re=num_re):
  684         """ Get an item of this class by its item id.
  685         """
  686         # make sure we're looking at an itemid
  687         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
  688             itemid = self._klass.lookup(itemid)
  689 
  690         return HTMLItem(self._client, self.classname, itemid)
  691 
  692     def properties(self, sort=1, cansearch=True):
  693         """ Return HTMLProperty for allowed class' properties.
  694 
  695             To return all properties call it with cansearch=False
  696             and it will return properties the user is unable to
  697             search.
  698         """
  699         l = []
  700         canSearch=self._db.security.hasSearchPermission
  701         userid=self._client.userid
  702         for name, prop in self._props.items():
  703             if cansearch and \
  704                not canSearch(userid, self._classname, name):
  705                 continue
  706             for klass, htmlklass in propclasses:
  707                 if isinstance(prop, klass):
  708                     value = prop.get_default_value()
  709                     l.append(htmlklass(self._client, self._classname, '',
  710                                        prop, name, value, self._anonymous))
  711         if sort:
  712             l.sort(key=lambda a:a._name)
  713         return l
  714 
  715     def list(self, sort_on=None):
  716         """ List all items in this class.
  717         """
  718         # get the list and sort it nicely
  719         l = self._klass.list()
  720         keyfunc = make_key_function(self._db, self._classname, sort_on)
  721         l.sort(key=keyfunc)
  722 
  723         # check perms
  724         check = self._client.db.security.hasPermission
  725         userid = self._client.userid
  726         if not check('Web Access', userid):
  727             return []
  728 
  729         l = [HTMLItem(self._client, self._classname, id) for id in l
  730             if check('View', userid, self._classname, itemid=id)]
  731 
  732         return l
  733 
  734     def csv(self):
  735         """ Return the items of this class as a chunk of CSV text.
  736         """
  737         props = self.propnames()
  738         s = StringIO()
  739         writer = csv.writer(s)
  740         writer.writerow(props)
  741         check = self._client.db.security.hasPermission
  742         userid = self._client.userid
  743         if not check('Web Access', userid):
  744             return ''
  745         for nodeid in self._klass.list():
  746             l = []
  747             for name in props:
  748                 # check permission to view this property on this item
  749                 if not check('View', userid, itemid=nodeid,
  750                         classname=self._klass.classname, property=name):
  751                     raise Unauthorised('view', self._klass.classname,
  752                         translator=self._client.translator)
  753                 value = self._klass.get(nodeid, name)
  754                 if value is None:
  755                     l.append('')
  756                 elif isinstance(value, type([])):
  757                     l.append(':'.join(map(str, value)))
  758                 else:
  759                     l.append(str(self._klass.get(nodeid, name)))
  760             writer.writerow(l)
  761         return s.getvalue()
  762 
  763     def propnames(self):
  764         """ Return the list of the names of the properties of this class.
  765         """
  766         idlessprops = sorted(self._klass.getprops(protected=0).keys())
  767         return ['id'] + idlessprops
  768 
  769     def filter(self, request=None, filterspec={}, sort=[], group=[]):
  770         """ Return a list of items from this class, filtered and sorted
  771             by the current requested filterspec/filter/sort/group args
  772 
  773             "request" takes precedence over the other three arguments.
  774         """
  775         security = self._db.security
  776         userid = self._client.userid
  777         if request is not None:
  778             # for a request we asume it has already been
  779             # security-filtered
  780             filterspec = request.filterspec
  781             sort = request.sort
  782             group = request.group
  783         else:
  784             cn = self.classname
  785             filterspec = security.filterFilterspec(userid, cn, filterspec)
  786             sort = security.filterSortspec(userid, cn, sort)
  787             group = security.filterSortspec(userid, cn, group)
  788 
  789         check = security.hasPermission
  790         if not check('Web Access', userid):
  791             return []
  792 
  793         l = [HTMLItem(self._client, self.classname, id)
  794              for id in self._klass.filter(None, filterspec, sort, group)
  795              if check('View', userid, self.classname, itemid=id)]
  796         return l
  797 
  798     def classhelp(self, properties=None, label=''"(list)", width='500',
  799             height='600', property='', form='itemSynopsis',
  800             pagesize=50, inputtype="checkbox", html_kwargs={},
  801             group='', sort=None, filter=None):
  802         """Pop up a javascript window with class help
  803 
  804         This generates a link to a popup window which displays the
  805         properties indicated by "properties" of the class named by
  806         "classname". The "properties" should be a comma-separated list
  807         (eg. 'id,name,description'). Properties defaults to all the
  808         properties of a class (excluding id, creator, created and
  809         activity).
  810 
  811         You may optionally override the label displayed, the width,
  812         the height, the number of items per page and the field on which
  813         the list is sorted (defaults to username if in the displayed
  814         properties).
  815 
  816         With the "filter" arg it is possible to specify a filter for
  817         which items are supposed to be displayed. It has to be of
  818         the format "<field>=<values>;<field>=<values>;...".
  819 
  820         The popup window will be resizable and scrollable.
  821 
  822         If the "property" arg is given, it's passed through to the
  823         javascript help_window function.
  824 
  825         You can use inputtype="radio" to display a radio box instead
  826         of the default checkbox (useful for entering Link-properties)
  827 
  828         If the "form" arg is given, it's passed through to the
  829         javascript help_window function. - it's the name of the form
  830         the "property" belongs to.
  831         """
  832         if properties is None:
  833             properties = sorted(self._klass.getprops(protected=0).keys())
  834             properties = ','.join(properties)
  835         if sort is None:
  836             if 'username' in properties.split( ',' ):
  837                 sort = 'username'
  838             else:
  839                 sort = self._klass.orderprop()
  840         sort = '&amp;@sort=' + sort
  841         if group :
  842             group = '&amp;@group=' + group
  843         if property:
  844             property = '&amp;property=%s'%property
  845         if form:
  846             form = '&amp;form=%s'%form
  847         if inputtype:
  848             type= '&amp;type=%s'%inputtype
  849         if filter:
  850             filterprops = filter.split(';')
  851             filtervalues = []
  852             names = []
  853             for x in filterprops:
  854                 (name, values) = x.split('=')
  855                 names.append(name)
  856                 filtervalues.append('&amp;%s=%s' % (name, urllib_.quote(values)))
  857             filter = '&amp;@filter=%s%s' % (','.join(names), ''.join(filtervalues))
  858         else:
  859            filter = ''
  860         help_url = "%s?@startwith=0&amp;@template=help&amp;"\
  861                    "properties=%s%s%s%s%s%s&amp;@pagesize=%s%s" % \
  862                    (self.classname, properties, property, form, type,
  863                    group, sort, pagesize, filter)
  864         onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
  865                   (help_url, width, height)
  866         return '<a class="classhelp" data-helpurl="%s" data-width="%s" data-height="%s" href="%s" onclick="%s" %s>%s</a>' % \
  867                (help_url, width, height,
  868                 help_url, onclick, self.cgi_escape_attrs(**html_kwargs),
  869                 self._(label))
  870 
  871     def submit(self, label=''"Submit New Entry", action="new", html_kwargs={}):
  872         """ Generate a submit button (and action hidden element)
  873 
  874             "html_kwargs" specified additional html args for the
  875             generated html <select>
  876 
  877         Generate nothing if we're not editable.
  878         """
  879         if not self.is_edit_ok():
  880             return ''
  881 
  882         return \
  883             self.input(type="submit", name="submit_button",
  884                               value=self._(label), **html_kwargs) + \
  885             '\n' + \
  886             self.input(type="hidden", name="@csrf",
  887                               value=anti_csrf_nonce(self._client)) + \
  888             '\n' + \
  889             self.input(type="hidden", name="@action", value=action)
  890 
  891     def history(self):
  892         if not self.is_view_ok():
  893             return self._('[hidden]')
  894         return self._('New node - no history')
  895 
  896     def renderWith(self, name, **kwargs):
  897         """ Render this class with the given template.
  898         """
  899         # create a new request and override the specified args
  900         req = HTMLRequest(self._client)
  901         req.classname = self.classname
  902         req.update(kwargs)
  903 
  904         # new template, using the specified classname and request
  905         # [ ] this code is too similar to client.renderContext()
  906         tplname = self._client.selectTemplate(self.classname, name)
  907         pt = self._client.instance.templates.load(tplname)
  908 
  909         # use our fabricated request
  910         args = {
  911             'ok_message': self._client._ok_message,
  912             'error_message': self._client._error_message
  913         }
  914         return pt.render(self._client, self.classname, req, **args)
  915 
  916 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
  917     """ Accesses through an *item*
  918     """
  919     def __init__(self, client, classname, nodeid, anonymous=0):
  920         self._client = client
  921         self._db = client.db
  922         self._classname = classname
  923         self._nodeid = nodeid
  924         self._klass = self._db.getclass(classname)
  925         self._props = self._klass.getprops()
  926 
  927         # do we prefix the form items with the item's identification?
  928         self._anonymous = anonymous
  929 
  930         HTMLInputMixin.__init__(self)
  931 
  932     def is_edit_ok(self):
  933         """ Is the user allowed to Edit this item?
  934         """
  935         perm = self._db.security.hasPermission
  936         return perm('Web Access', self._client.userid) and perm('Edit',
  937             self._client.userid, self._classname, itemid=self._nodeid)
  938 
  939     def is_retire_ok(self):
  940         """ Is the user allowed to Reture this item?
  941         """
  942         perm = self._db.security.hasPermission
  943         return perm('Web Access', self._client.userid) and perm('Retire',
  944             self._client.userid, self._classname, itemid=self._nodeid)
  945 
  946     def is_restore_ok(self):
  947         """ Is the user allowed to restore this item?
  948         """
  949         perm = self._db.security.hasPermission
  950         return perm('Web Access', self._client.userid) and perm('Restore',
  951             self._client.userid, self._classname, itemid=self._nodeid)
  952 
  953     def is_view_ok(self):
  954         """ Is the user allowed to View this item?
  955         """
  956         perm = self._db.security.hasPermission
  957         if perm('Web Access', self._client.userid) and perm('View',
  958                 self._client.userid, self._classname, itemid=self._nodeid):
  959             return 1
  960         return self.is_edit_ok()
  961 
  962     def is_only_view_ok(self):
  963         """ Is the user only allowed to View (ie. not Edit) this item?
  964         """
  965         return self.is_view_ok() and not self.is_edit_ok()
  966 
  967     def __repr__(self):
  968         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
  969             self._nodeid)
  970 
  971     def __getitem__(self, item):
  972         """ return an HTMLProperty instance
  973             this now can handle transitive lookups where item is of the
  974             form x.y.z
  975         """
  976         if item == 'id':
  977             return self._nodeid
  978 
  979         items = item.split('.', 1)
  980         has_rest = len(items) > 1
  981 
  982         # get the property
  983         prop = self._props[items[0]]
  984 
  985         if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
  986             raise KeyError(item)
  987 
  988         # get the value, handling missing values
  989         value = None
  990         if int(self._nodeid) > 0:
  991             value = self._klass.get(self._nodeid, items[0], None)
  992         if value is None:
  993             if isinstance(prop, hyperdb.Multilink):
  994                 value = []
  995 
  996         # look up the correct HTMLProperty class
  997         htmlprop = None
  998         for klass, htmlklass in propclasses:
  999             if isinstance(prop, klass):
 1000                 htmlprop = htmlklass(self._client, self._classname,
 1001                     self._nodeid, prop, items[0], value, self._anonymous)
 1002         if htmlprop is not None:
 1003             if has_rest:
 1004                 if isinstance(htmlprop, MultilinkHTMLProperty):
 1005                     return [h[items[1]] for h in htmlprop]
 1006                 return htmlprop[items[1]]
 1007             return htmlprop
 1008 
 1009         raise KeyError(item)
 1010 
 1011     def __getattr__(self, attr):
 1012         """ convenience access to properties """
 1013         try:
 1014             return self[attr]
 1015         except KeyError:
 1016             raise AttributeError(attr)
 1017 
 1018     def designator(self):
 1019         """Return this item's designator (classname + id)."""
 1020         return '%s%s'%(self._classname, self._nodeid)
 1021 
 1022     def is_retired(self):
 1023         """Is this item retired?"""
 1024         return self._klass.is_retired(self._nodeid)
 1025 
 1026     def submit(self, label=''"Submit Changes", action="edit", html_kwargs={}):
 1027         """Generate a submit button.
 1028 
 1029             "html_kwargs" specified additional html args for the
 1030             generated html <select>
 1031 
 1032         Also sneak in the lastactivity and action hidden elements.
 1033         """
 1034         return \
 1035             self.input(type="submit", name="submit_button",
 1036                        value=self._(label), **html_kwargs) + \
 1037             '\n' + \
 1038             self.input(type="hidden", name="@lastactivity",
 1039                        value=self.activity.local(0)) + \
 1040             '\n' + \
 1041             self.input(type="hidden", name="@csrf",
 1042                        value=anti_csrf_nonce(self._client)) + \
 1043             '\n' + \
 1044             self.input(type="hidden", name="@action", value=action)
 1045 
 1046     def journal(self, direction='descending'):
 1047         """ Return a list of HTMLJournalEntry instances.
 1048         """
 1049         # XXX do this
 1050         return []
 1051 
 1052     def history(self, direction='descending', dre=re.compile(r'^\d+$'),
 1053                 limit=None, showall=False ):
 1054         """Create an html view of the journal for the item.
 1055 
 1056            Display property changes for all properties that does not have quiet set.
 1057            If showall=True then all properties regardless of quiet setting will be
 1058            shown.
 1059         """
 1060         if not self.is_view_ok():
 1061             return self._('[hidden]')
 1062 
 1063         # get the journal, sort and reverse
 1064         history = self._klass.history(self._nodeid, skipquiet=(not showall))
 1065         history.sort(key=lambda a: a[:3])
 1066         history.reverse()
 1067 
 1068         # restrict the volume
 1069         if limit:
 1070             history = history[:limit]
 1071 
 1072         timezone = self._db.getUserTimezone()
 1073         l = []
 1074         current = {}
 1075         comments = {}
 1076         for id, evt_date, user, action, args in history:
 1077             date_s = str(evt_date.local(timezone)).replace("."," ")
 1078             arg_s = ''
 1079             if action in ['link', 'unlink'] and type(args) == type(()):
 1080                 if len(args) == 3:
 1081                     linkcl, linkid, key = args
 1082                     arg_s += '<a rel="nofollow noopener" href="%s%s">%s%s %s</a>'%(linkcl, linkid,
 1083                         linkcl, linkid, key)
 1084                 else:
 1085                     arg_s = str(args)
 1086             elif type(args) == type({}):
 1087                 cell = []
 1088                 for k in args.keys():
 1089                     # try to get the relevant property and treat it
 1090                     # specially
 1091                     try:
 1092                         prop = self._props[k]
 1093                     except KeyError:
 1094                         prop = None
 1095                     if prop is None:
 1096                         # property no longer exists
 1097                         comments['no_exist'] = self._(
 1098                             "<em>The indicated property no longer exists</em>")
 1099                         cell.append(self._('<em>%s: %s</em>\n')
 1100                             % (self._(k), str(args[k])))
 1101                         continue
 1102 
 1103                     # load the current state for the property (if we
 1104                     # haven't already)
 1105                     if k not in current:
 1106                         val = self[k]
 1107                         if not isinstance(val, HTMLProperty):
 1108                             current[k] = None
 1109                         else:
 1110                             current[k] = val.plain(escape=1)
 1111                             # make link if hrefable
 1112                             if (isinstance(prop, hyperdb.Link)):
 1113                                 classname = prop.classname
 1114                                 try:
 1115                                     template = self._client.selectTemplate(classname, 'item')
 1116                                     if template.startswith('_generic.'):
 1117                                         raise NoTemplate('not really...')
 1118                                 except NoTemplate:
 1119                                     pass
 1120                                 else:
 1121                                     linkid = self._klass.get(self._nodeid, k, None)
 1122                                     current[k] = '<a rel="nofollow noopener" href="%s%s">%s</a>'%(
 1123                                         classname, linkid, current[k])
 1124 
 1125                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
 1126                             isinstance(prop, hyperdb.Link)):
 1127                         # figure what the link class is
 1128                         classname = prop.classname
 1129                         try:
 1130                             linkcl = self._db.getclass(classname)
 1131                         except KeyError:
 1132                             labelprop = None
 1133                             comments[classname] = self._(
 1134                                 "The linked class %(classname)s no longer exists"
 1135                             ) % locals()
 1136                         labelprop = linkcl.labelprop(1)
 1137                         try:
 1138                             template = self._client.selectTemplate(classname,
 1139                                'item')
 1140                             if template.startswith('_generic.'):
 1141                                 raise NoTemplate('not really...')
 1142                             hrefable = 1
 1143                         except NoTemplate:
 1144                             hrefable = 0
 1145 
 1146                     if isinstance(prop, hyperdb.Multilink) and args[k]:
 1147                         ml = []
 1148                         for linkid in args[k]:
 1149                             if isinstance(linkid, type(())):
 1150                                 sublabel = linkid[0] + ' '
 1151                                 linkids = linkid[1]
 1152                             else:
 1153                                 sublabel = ''
 1154                                 linkids = [linkid]
 1155                             subml = []
 1156                             for linkid in linkids:
 1157                                 # We're seeing things like
 1158                                 # {'nosy':['38', '113', None, '82']} in the wild
 1159                                 if linkid is None :
 1160                                     continue
 1161                                 label = classname + linkid
 1162                                 # if we have a label property, try to use it
 1163                                 # TODO: test for node existence even when
 1164                                 # there's no labelprop!
 1165                                 try:
 1166                                     if labelprop is not None and \
 1167                                             labelprop != 'id':
 1168                                         label = linkcl.get(linkid, labelprop)
 1169                                         label = html_escape(label)
 1170                                 except IndexError:
 1171                                     comments['no_link'] = self._(
 1172                                         "<strike>The linked node"
 1173                                         " no longer exists</strike>")
 1174                                     subml.append('<strike>%s</strike>'%label)
 1175                                 else:
 1176                                     if hrefable:
 1177                                         subml.append('<a rel="nofollow noopener" '
 1178                                                      'href="%s%s">%s</a>'%(
 1179                                             classname, linkid, label))
 1180                                     elif label is None:
 1181                                         subml.append('%s%s'%(classname,
 1182                                             linkid))
 1183                                     else:
 1184                                         subml.append(label)
 1185                             ml.append(sublabel + ', '.join(subml))
 1186                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
 1187                     elif isinstance(prop, hyperdb.Link) and args[k]:
 1188                         label = classname + args[k]
 1189                         # if we have a label property, try to use it
 1190                         # TODO: test for node existence even when
 1191                         # there's no labelprop!
 1192                         if labelprop is not None and labelprop != 'id':
 1193                             try:
 1194                                 label = html_escape(linkcl.get(args[k],
 1195                                     labelprop))
 1196                             except IndexError:
 1197                                 comments['no_link'] = self._(
 1198                                     "<strike>The linked node"
 1199                                     " no longer exists</strike>")
 1200                                 cell.append(' <strike>%s</strike>,\n'%label)
 1201                                 # "flag" this is done .... euwww
 1202                                 label = None
 1203                         if label is not None:
 1204                             if hrefable:
 1205                                 old = '<a ref="nofollow noopener" href="%s%s">%s</a>'%(classname,
 1206                                     args[k], label)
 1207                             else:
 1208                                 old = label;
 1209                             cell.append('%s: %s' % (self._(k), old))
 1210                             if k in current and current[k] is not None:
 1211                                 cell[-1] += ' -> %s'%current[k]
 1212                                 current[k] = old
 1213 
 1214                     elif isinstance(prop, hyperdb.Date) and args[k]:
 1215                         if args[k] is None:
 1216                             d = ''
 1217                         else:
 1218                             d = date.Date(args[k],
 1219                                 translator=self._client).local(timezone)
 1220                         cell.append('%s: %s'%(self._(k), str(d)))
 1221                         if k in current and current[k] is not None:
 1222                             cell[-1] += ' -> %s' % current[k]
 1223                             current[k] = str(d)
 1224 
 1225                     elif isinstance(prop, hyperdb.Interval) and args[k]:
 1226                         val = str(date.Interval(args[k],
 1227                             translator=self._client))
 1228                         cell.append('%s: %s'%(self._(k), val))
 1229                         if k in current and current[k] is not None:
 1230                             cell[-1] += ' -> %s'%current[k]
 1231                             current[k] = val
 1232 
 1233                     elif isinstance(prop, hyperdb.String) and args[k]:
 1234                         val = html_escape(args[k])
 1235                         cell.append('%s: %s'%(self._(k), val))
 1236                         if k in current and current[k] is not None:
 1237                             cell[-1] += ' -> %s'%current[k]
 1238                             current[k] = val
 1239 
 1240                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
 1241                         val = args[k] and ''"Yes" or ''"No"
 1242                         cell.append('%s: %s'%(self._(k), val))
 1243                         if k in current and current[k] is not None:
 1244                             cell[-1] += ' -> %s'%current[k]
 1245                             current[k] = val
 1246 
 1247                     elif isinstance(prop, hyperdb.Password) and args[k] is not None:
 1248                         val = args[k].dummystr()
 1249                         cell.append('%s: %s'%(self._(k), val))
 1250                         if k in current and current[k] is not None:
 1251                             cell[-1] += ' -> %s'%current[k]
 1252                             current[k] = val
 1253 
 1254                     elif not args[k]:
 1255                         if k in current and current[k] is not None:
 1256                             cell.append('%s: %s'%(self._(k), current[k]))
 1257                             current[k] = '(no value)'
 1258                         else:
 1259                             cell.append(self._('%s: (no value)')%self._(k))
 1260 
 1261                     else:
 1262                         cell.append('%s: %s'%(self._(k), str(args[k])))
 1263                         if k in current and current[k] is not None:
 1264                             cell[-1] += ' -> %s'%current[k]
 1265                             current[k] = str(args[k])
 1266 
 1267                 arg_s = '<br />'.join(cell)
 1268             else:
 1269                 if action in ( 'retired', 'restored' ):
 1270                     # args = None for these actions
 1271                     pass
 1272                 else:
 1273                     # unknown event!!
 1274                     comments['unknown'] = self._(
 1275                         "<strong><em>This event %s is not handled"
 1276                         " by the history display!</em></strong>"%action)
 1277                     arg_s = '<strong><em>' + str(args) + '</em></strong>'
 1278 
 1279             date_s = date_s.replace(' ', '&nbsp;')
 1280             # if the user's an itemid, figure the username (older journals
 1281             # have the username)
 1282             if dre.match(user):
 1283                 user = self._db.user.get(user, 'username')
 1284             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
 1285                 date_s, html_escape(user), self._(action), arg_s))
 1286         if comments:
 1287             l.append(self._(
 1288                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
 1289         for entry in comments.values():
 1290             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
 1291 
 1292         if direction == 'ascending':
 1293             l.reverse()
 1294 
 1295         l[0:0] = ['<table class="history table table-condensed table-striped">'
 1296              '<tr><th colspan="4" class="header">',
 1297              self._('History'),
 1298              '</th></tr><tr>',
 1299              self._('<th>Date</th>'),
 1300              self._('<th>User</th>'),
 1301              self._('<th>Action</th>'),
 1302              self._('<th>Args</th>'),
 1303             '</tr>']
 1304         l.append('</table>')
 1305         return '\n'.join(l)
 1306 
 1307     def renderQueryForm(self):
 1308         """ Render this item, which is a query, as a search form.
 1309         """
 1310         # create a new request and override the specified args
 1311         req = HTMLRequest(self._client)
 1312         req.classname = self._klass.get(self._nodeid, 'klass')
 1313         name = self._klass.get(self._nodeid, 'name')
 1314         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
 1315             '&@queryname=%s'%urllib_.quote(name))
 1316 
 1317         # new template, using the specified classname and request
 1318         # [ ] the custom logic for search page doesn't belong to
 1319         #     generic templating module (techtonik)
 1320         tplname = self._client.selectTemplate(req.classname, 'search')
 1321         pt = self._client.instance.templates.load(tplname)
 1322         # The context for a search page should be the class, not any
 1323         # node.
 1324         self._client.nodeid = None
 1325 
 1326         # use our fabricated request
 1327         return pt.render(self._client, req.classname, req)
 1328 
 1329     def download_url(self):
 1330         """ Assume that this item is a FileClass and that it has a name
 1331         and content. Construct a URL for the download of the content.
 1332         """
 1333         name = self._klass.get(self._nodeid, 'name')
 1334         url = '%s%s/%s'%(self._classname, self._nodeid, name)
 1335         return urllib_.quote(url)
 1336 
 1337     def copy_url(self, exclude=("messages", "files")):
 1338         """Construct a URL for creating a copy of this item
 1339 
 1340         "exclude" is an optional list of properties that should
 1341         not be copied to the new object.  By default, this list
 1342         includes "messages" and "files" properties.  Note that
 1343         "id" property cannot be copied.
 1344 
 1345         """
 1346         exclude = ("id", "activity", "actor", "creation", "creator") \
 1347             + tuple(exclude)
 1348         query = {
 1349             "@template": "item",
 1350             "@note": self._("Copy of %(class)s %(id)s") % {
 1351                 "class": self._(self._classname), "id": self._nodeid},
 1352         }
 1353         for name in self._props.keys():
 1354             if name not in exclude:
 1355                 prop = self._props[name]
 1356                 if not isinstance(prop, hyperdb.Multilink):
 1357                     query[name] = self[name].plain()
 1358                 else:
 1359                     query[name] = ",".join(self._klass.get(self._nodeid, name))
 1360 
 1361         return self._classname + "?" + "&".join(
 1362             ["%s=%s" % (key, urllib_.quote(value))
 1363                 for key, value in query.items()])
 1364 
 1365 class _HTMLUser(_HTMLItem):
 1366     """Add ability to check for permissions on users.
 1367     """
 1368     _marker = []
 1369     def hasPermission(self, permission, classname=_marker,
 1370             property=None, itemid=None):
 1371         """Determine if the user has the Permission.
 1372 
 1373         The class being tested defaults to the template's class, but may
 1374         be overidden for this test by suppling an alternate classname.
 1375         """
 1376         if classname is self._marker:
 1377             classname = self._client.classname
 1378         return self._db.security.hasPermission(permission,
 1379             self._nodeid, classname, property, itemid)
 1380 
 1381     def hasRole(self, *rolenames):
 1382         """Determine whether the user has any role in rolenames."""
 1383         return self._db.user.has_role(self._nodeid, *rolenames)
 1384 
 1385 def HTMLItem(client, classname, nodeid, anonymous=0):
 1386     if classname == 'user':
 1387         return _HTMLUser(client, classname, nodeid, anonymous)
 1388     else:
 1389         return _HTMLItem(client, classname, nodeid, anonymous)
 1390 
 1391 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
 1392     """ String, Integer, Number, Date, Interval HTMLProperty
 1393 
 1394         Has useful attributes:
 1395 
 1396          _name  the name of the property
 1397          _value the value of the property if any
 1398 
 1399         A wrapper object which may be stringified for the plain() behaviour.
 1400     """
 1401     def __init__(self, client, classname, nodeid, prop, name, value,
 1402             anonymous=0):
 1403         self._client = client
 1404         self._db = client.db
 1405         self._ = client._
 1406         self._classname = classname
 1407         self._nodeid = nodeid
 1408         self._prop = prop
 1409         self._value = value
 1410         self._anonymous = anonymous
 1411         self._name = name
 1412         if not anonymous:
 1413             if nodeid:
 1414                 self._formname = '%s%s@%s'%(classname, nodeid, name)
 1415             else:
 1416                 # This case occurs when creating a property for a
 1417                 # non-anonymous class.
 1418                 self._formname = '%s@%s'%(classname, name)
 1419         else:
 1420             self._formname = name
 1421 
 1422         # If no value is already present for this property, see if one
 1423         # is specified in the current form.
 1424         form = self._client.form
 1425         try:
 1426             is_in = self._formname in form
 1427         except TypeError:
 1428             is_in = False
 1429         if is_in and (not self._value or self._client.form_wins):
 1430             if isinstance(prop, hyperdb.Multilink):
 1431                 value = lookupIds(self._db, prop,
 1432                                   handleListCGIValue(form[self._formname]),
 1433                                   fail_ok=1)
 1434             elif isinstance(prop, hyperdb.Link):
 1435                 value = form.getfirst(self._formname).strip()
 1436                 if value:
 1437                     value = lookupIds(self._db, prop, [value],
 1438                                       fail_ok=1)[0]
 1439                 else:
 1440                     value = None
 1441             else:
 1442                 value = form.getfirst(self._formname).strip() or None
 1443             self._value = value
 1444 
 1445         HTMLInputMixin.__init__(self)
 1446 
 1447     def __repr__(self):
 1448         classname = self.__class__.__name__
 1449         return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
 1450                                       self._prop, self._value)
 1451     def __str__(self):
 1452         return self.plain()
 1453     def __lt__(self, other):
 1454         if isinstance(other, HTMLProperty):
 1455             return self._value < other._value
 1456         return self._value < other
 1457     def __le__(self, other):
 1458         if isinstance(other, HTMLProperty):
 1459             return self._value <= other._value
 1460         return self._value <= other
 1461     def __eq__(self, other):
 1462         if isinstance(other, HTMLProperty):
 1463             return self._value == other._value
 1464         return self._value == other
 1465     def __ne__(self, other):
 1466         if isinstance(other, HTMLProperty):
 1467             return self._value != other._value
 1468         return self._value != other
 1469     def __gt__(self, other):
 1470         if isinstance(other, HTMLProperty):
 1471             return self._value > other._value
 1472         return self._value > other
 1473     def __ge__(self, other):
 1474         if isinstance(other, HTMLProperty):
 1475             return self._value >= other._value
 1476         return self._value >= other
 1477 
 1478     def __bool__(self):
 1479         return not not self._value
 1480     # Python 2 compatibility:
 1481     __nonzero__ = __bool__
 1482 
 1483     def isset(self):
 1484         """Is my _value not None?"""
 1485         return self._value is not None
 1486 
 1487     def is_edit_ok(self):
 1488         """Should the user be allowed to use an edit form field for this
 1489         property. Check "Create" for new items, or "Edit" for existing
 1490         ones.
 1491         """
 1492         perm = self._db.security.hasPermission
 1493         userid = self._client.userid
 1494         if self._nodeid:
 1495             if not perm('Web Access', userid):
 1496                 return False
 1497             return perm('Edit', userid, self._classname, self._name,
 1498                 self._nodeid)
 1499         return perm('Create', userid, self._classname, self._name) or \
 1500             perm('Register', userid, self._classname, self._name)
 1501 
 1502     def is_view_ok(self):
 1503         """ Is the user allowed to View the current class?
 1504         """
 1505         perm = self._db.security.hasPermission
 1506         if perm('Web Access',  self._client.userid) and perm('View',
 1507                 self._client.userid, self._classname, self._name, self._nodeid):
 1508             return 1
 1509         return self.is_edit_ok()
 1510 
 1511 class StringHTMLProperty(HTMLProperty):
 1512     hyper_re = re.compile(r'''(
 1513         (?P<url>
 1514          (
 1515           (ht|f)tp(s?)://                   # protocol
 1516           ([\w]+(:\w+)?@)?                  # username/password
 1517           ([\w\-]+)                         # hostname
 1518           ((\.[\w-]+)+)?                    # .domain.etc
 1519          |                                  # ... or ...
 1520           ([\w]+(:\w+)?@)?                  # username/password
 1521           www\.                             # "www."
 1522           ([\w\-]+\.)+                      # hostname
 1523           [\w]{2,5}                         # TLD
 1524          )
 1525          (:[\d]{1,5})?                     # port
 1526          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
 1527         )|
 1528         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
 1529         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
 1530     )''', re.X | re.I)
 1531     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
 1532 
 1533     # disable rst directives that have security implications
 1534     rst_defaults = {'file_insertion_enabled': 0,
 1535                     'raw_enabled': 0,
 1536                     '_disable_config': 1}
 1537 
 1538     valid_schemes = { }
 1539 
 1540     def _hyper_repl(self, match):
 1541         if match.group('url'):
 1542             return self._hyper_repl_url(match, '<a href="%s" rel="nofollow noopener">%s</a>%s')
 1543         elif match.group('email'):
 1544             return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
 1545         elif len(match.group('id')) < 10:
 1546             return self._hyper_repl_item(match,
 1547                 '<a href="%(cls)s%(id)s">%(item)s</a>')
 1548         else:
 1549             # just return the matched text
 1550             return match.group(0)
 1551 
 1552     def _hyper_repl_url(self, match, replacement):
 1553         u = s = match.group('url')
 1554         if not self.protocol_re.search(s):
 1555             u = 'http://' + s
 1556         end = ''
 1557         if '&gt;' in s:
 1558             # catch an escaped ">" in the URL
 1559             pos = s.find('&gt;')
 1560             end = s[pos:]
 1561             u = s = s[:pos]
 1562         if s.endswith(tuple('.,;:!')):
 1563             # don't include trailing punctuation
 1564             end = s[-1:] + end
 1565             u = s = s[:-1]
 1566         if ')' in s and s.count('(') != s.count(')'):
 1567             # don't include extraneous ')' in the link
 1568             pos = s.rfind(')')
 1569             end = s[pos:] + end
 1570             u = s = s[:pos]
 1571         return replacement % (u, s, end)
 1572 
 1573     def _hyper_repl_email(self, match, replacement):
 1574         s = match.group('email')
 1575         return replacement % (s, s)
 1576 
 1577     def _hyper_repl_item(self, match, replacement):
 1578         item = match.group('item')
 1579         cls = match.group('class').lower()
 1580         id = match.group('id')
 1581         try:
 1582             # make sure cls is a valid tracker classname
 1583             cl = self._db.getclass(cls)
 1584             if not cl.hasnode(id):
 1585                 return item
 1586             return replacement % locals()
 1587         except KeyError:
 1588             return item
 1589 
 1590 
 1591     def _hyper_repl_rst(self, match):
 1592         if match.group('url'):
 1593             s = match.group('url')
 1594             return '`%s <%s>`_'%(s, s)
 1595         elif match.group('email'):
 1596             s = match.group('email')
 1597             return '`%s <mailto:%s>`_'%(s, s)
 1598         elif len(match.group('id')) < 10:
 1599             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
 1600         else:
 1601             # just return the matched text
 1602             return match.group(0)
 1603 
 1604     def _hyper_repl_markdown(self, match):
 1605         if match.group('id') and len(match.group('id')) < 10:
 1606             return self._hyper_repl_item(match,'[%(item)s](%(cls)s%(id)s)')
 1607         else:
 1608             # just return the matched text
 1609             return match.group(0)
 1610 
 1611     def url_quote(self):
 1612         """ Return the string in plain format but escaped for use in a url """
 1613         return urllib_.quote(self.plain())
 1614 
 1615     def hyperlinked(self):
 1616         """ Render a "hyperlinked" version of the text """
 1617         return self.plain(hyperlink=1)
 1618 
 1619     def plain(self, escape=0, hyperlink=0):
 1620         """Render a "plain" representation of the property
 1621 
 1622         - "escape" turns on/off HTML quoting
 1623         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
 1624           addresses and designators
 1625         """
 1626         if not self.is_view_ok():
 1627             return self._('[hidden]')
 1628 
 1629         if self._value is None:
 1630             return ''
 1631         if escape:
 1632             s = html_escape(str(self._value))
 1633         else:
 1634             s = str(self._value)
 1635         if hyperlink:
 1636             # no, we *must* escape this text
 1637             if not escape:
 1638                 s = html_escape(s)
 1639             s = self.hyper_re.sub(self._hyper_repl, s)
 1640         return s
 1641 
 1642     def wrapped(self, escape=1, hyperlink=1):
 1643         """Render a "wrapped" representation of the property.
 1644 
 1645         We wrap long lines at 80 columns on the nearest whitespace. Lines
 1646         with no whitespace are not broken to force wrapping.
 1647 
 1648         Note that unlike plain() we default wrapped() to have the escaping
 1649         and hyperlinking turned on since that's the most common usage.
 1650 
 1651         - "escape" turns on/off HTML quoting
 1652         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
 1653           addresses and designators
 1654         """
 1655         if not self.is_view_ok():
 1656             return self._('[hidden]')
 1657 
 1658         if self._value is None:
 1659             return ''
 1660         s = '\n'.join(textwrap.wrap(str(self._value), 80))
 1661         if escape:
 1662             s = html_escape(s)
 1663         if hyperlink:
 1664             # no, we *must* escape this text
 1665             if not escape:
 1666                 s = html_escape(s)
 1667             s = self.hyper_re.sub(self._hyper_repl, s)
 1668         return s
 1669 
 1670     def stext(self, escape=0, hyperlink=1):
 1671         """ Render the value of the property as StructuredText.
 1672 
 1673             This requires the StructureText module to be installed separately.
 1674         """
 1675         if not self.is_view_ok():
 1676             return self._('[hidden]')
 1677 
 1678         s = self.plain(escape=escape, hyperlink=hyperlink)
 1679         if not StructuredText:
 1680             return s
 1681         return StructuredText(s,level=1,header=0)
 1682 
 1683     def rst(self, hyperlink=1):
 1684         """ Render the value of the property as ReStructuredText.
 1685 
 1686             This requires docutils to be installed separately.
 1687         """
 1688         if not self.is_view_ok():
 1689             return self._('[hidden]')
 1690 
 1691         if not ReStructuredText:
 1692             return self.plain(escape=0, hyperlink=hyperlink)
 1693         s = self.plain(escape=0, hyperlink=0)
 1694         if hyperlink:
 1695             s = self.hyper_re.sub(self._hyper_repl_rst, s)
 1696 
 1697         # disable javascript and possibly other url schemes from working
 1698         from docutils.utils.urischemes import schemes
 1699         for sch in _disable_url_schemes:
 1700             # I catch KeyError but reraise if scheme didn't exist.
 1701             # Safer to fail if a disabled scheme isn't found. It may
 1702             # be a typo that keeps a bad scheme enabled. But this
 1703             # function can be called multiple times. On the first call
 1704             # the key will be deleted. On the second call the schemes
 1705             # variable isn't re-initialized so the key is missing
 1706             # causing a KeyError. So see if we removed it (and entered
 1707             # it into valid_schemes). If we didn't raise KeyError.
 1708             try:
 1709                 del(schemes[sch])
 1710                 self.valid_schemes[sch] = True
 1711             except KeyError:
 1712                 if sch in self.valid_schemes:
 1713                     pass
 1714                 else:
 1715                     raise
 1716                 
 1717         return u2s(ReStructuredText(s, writer_name="html",
 1718                        settings_overrides=self.rst_defaults)["html_body"])
 1719 
 1720     def markdown(self, hyperlink=1):
 1721         """ Render the value of the property as markdown.
 1722 
 1723             This requires markdown2 or markdown to be installed separately.
 1724         """
 1725         if not self.is_view_ok():
 1726             return self._('[hidden]')
 1727 
 1728         if not markdown:
 1729             return self.plain(escape=0, hyperlink=hyperlink)
 1730         s = self.plain(escape=0, hyperlink=0)
 1731         if hyperlink:
 1732             s = self.hyper_re.sub(self._hyper_repl_markdown, s)
 1733         return u2s(markdown(s2u(s)))
 1734 
 1735     def field(self, **kwargs):
 1736         """ Render the property as a field in HTML.
 1737 
 1738             If not editable, just display the value via plain().
 1739         """
 1740         if not self.is_edit_ok():
 1741             return self.plain(escape=1)
 1742 
 1743         value = self._value
 1744         if value is None:
 1745             value = ''
 1746 
 1747         kwargs.setdefault("size", 30)
 1748         kwargs.update({"name": self._formname, "value": value})
 1749         return self.input(**kwargs)
 1750 
 1751     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
 1752         """ Render a multiline form edit field for the property.
 1753 
 1754             If not editable, just display the plain() value in a <pre> tag.
 1755         """
 1756         if not self.is_edit_ok():
 1757             return '<pre>%s</pre>'%self.plain()
 1758 
 1759         if self._value is None:
 1760             value = ''
 1761         else:
 1762             value = html_escape(str(self._value))
 1763 
 1764             value = '&quot;'.join(value.split('"'))
 1765         name = self._formname
 1766         passthrough_args = self.cgi_escape_attrs(**kwargs)
 1767         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
 1768                 ' rows="%(rows)s" cols="%(cols)s">'
 1769                  '%(value)s</textarea>') % locals()
 1770 
 1771     def email(self, escape=1):
 1772         """ Render the value of the property as an obscured email address
 1773         """
 1774         if not self.is_view_ok():
 1775             return self._('[hidden]')
 1776 
 1777         if self._value is None:
 1778             value = ''
 1779         else:
 1780             value = str(self._value)
 1781         split = value.split('@')
 1782         if len(split) == 2:
 1783             name, domain = split
 1784             domain = ' '.join(domain.split('.')[:-1])
 1785             name = name.replace('.', ' ')
 1786             value = '%s at %s ...'%(name, domain)
 1787         else:
 1788             value = value.replace('.', ' ')
 1789         if escape:
 1790             value = html_escape(value)
 1791         return value
 1792 
 1793 class PasswordHTMLProperty(HTMLProperty):
 1794     def plain(self, escape=0):
 1795         """ Render a "plain" representation of the property
 1796         """
 1797         if not self.is_view_ok():
 1798             return self._('[hidden]')
 1799 
 1800         if self._value is None:
 1801             return ''
 1802         try:
 1803             value = self._value.dummystr()
 1804         except AttributeError:
 1805             value = self._('[hidden]')
 1806         if escape:
 1807             value = html_escape(value)
 1808         return value
 1809 
 1810     def field(self, size=30, **kwargs):
 1811         """ Render a form edit field for the property.
 1812 
 1813             If not editable, just display the value via plain().
 1814         """
 1815         if not self.is_edit_ok():
 1816             return self.plain(escape=1)
 1817 
 1818         return self.input(type="password", name=self._formname, size=size,
 1819                           **kwargs)
 1820 
 1821     def confirm(self, size=30):
 1822         """ Render a second form edit field for the property, used for
 1823             confirmation that the user typed the password correctly. Generates
 1824             a field with name "@confirm@name".
 1825 
 1826             If not editable, display nothing.
 1827         """
 1828         if not self.is_edit_ok():
 1829             return ''
 1830 
 1831         return self.input(type="password",
 1832             name="@confirm@%s"%self._formname,
 1833             id="%s-confirm"%self._formname,
 1834             size=size)
 1835 
 1836 class NumberHTMLProperty(HTMLProperty):
 1837     def plain(self, escape=0):
 1838         """ Render a "plain" representation of the property
 1839         """
 1840         if not self.is_view_ok():
 1841             return self._('[hidden]')
 1842 
 1843         if self._value is None:
 1844             return ''
 1845 
 1846         return str(self._value)
 1847 
 1848     def field(self, size=30, **kwargs):
 1849         """ Render a form edit field for the property.
 1850 
 1851             If not editable, just display the value via plain().
 1852         """
 1853         if not self.is_edit_ok():
 1854             return self.plain(escape=1)
 1855 
 1856         value = self._value
 1857         if value is None:
 1858             value = ''
 1859 
 1860         return self.input(name=self._formname, value=value, size=size,
 1861                           **kwargs)
 1862 
 1863     def __int__(self):
 1864         """ Return an int of me
 1865         """
 1866         return int(self._value)
 1867 
 1868     def __float__(self):
 1869         """ Return a float of me
 1870         """
 1871         return float(self._value)
 1872 
 1873 class IntegerHTMLProperty(HTMLProperty):
 1874     def plain(self, escape=0):
 1875         """ Render a "plain" representation of the property
 1876         """
 1877         if not self.is_view_ok():
 1878             return self._('[hidden]')
 1879 
 1880         if self._value is None:
 1881             return ''
 1882 
 1883         return str(self._value)
 1884 
 1885     def field(self, size=30, **kwargs):
 1886         """ Render a form edit field for the property.
 1887 
 1888             If not editable, just display the value via plain().
 1889         """
 1890         if not self.is_edit_ok():
 1891             return self.plain(escape=1)
 1892 
 1893         value = self._value
 1894         if value is None:
 1895             value = ''
 1896 
 1897         return self.input(name=self._formname, value=value, size=size,
 1898                           **kwargs)
 1899 
 1900     def __int__(self):
 1901         """ Return an int of me
 1902         """
 1903         return int(self._value)
 1904 
 1905 
 1906 class BooleanHTMLProperty(HTMLProperty):
 1907     def plain(self, escape=0):
 1908         """ Render a "plain" representation of the property
 1909         """
 1910         if not self.is_view_ok():
 1911             return self._('[hidden]')
 1912 
 1913         if self._value is None:
 1914             return ''
 1915         return self._value and self._("Yes") or self._("No")
 1916 
 1917     def field(self, labelfirst=False, y_label=None, n_label=None,
 1918               u_label=None, **kwargs):
 1919         """ Render a form edit field for the property
 1920 
 1921             If not editable, just display the value via plain().
 1922 
 1923             In addition to being able to set arbitrary html properties
 1924             using prop=val arguments, the thre arguments:
 1925 
 1926               y_label, n_label, u_label let you control the labels
 1927               associated with the yes, no (and optionally unknown/empty)
 1928               values.
 1929 
 1930            Also the labels can be placed before the radiobuttons by setting
 1931            labelfirst=True.
 1932         """
 1933         if not self.is_edit_ok():
 1934             return self.plain(escape=1)
 1935 
 1936         value = self._value
 1937         if is_us(value):
 1938             value = value.strip().lower() in ('checked', 'yes', 'true',
 1939                 'on', '1')
 1940 
 1941         if ( not y_label ):
 1942             y_label = '<label class="rblabel" for="%s_%s">'%(self._formname, 'yes')
 1943             y_label += self._('Yes')
 1944             y_label += '</label>'
 1945 
 1946         if ( not n_label ):
 1947             n_label = '<label class="rblabel" for="%s_%s">'%(self._formname, 'no')
 1948             n_label += self._('No')
 1949             n_label += '</label>'
 1950 
 1951         checked = value and "checked" or ""
 1952         if value:
 1953             y_rb = self.input(type="radio", name=self._formname, value="yes",
 1954                 checked="checked", id="%s_%s"%(self._formname, 'yes'), **kwargs)
 1955 
 1956             n_rb =self.input(type="radio", name=self._formname,  value="no",
 1957                            id="%s_%s"%(self._formname, 'no'), **kwargs)
 1958         else:
 1959             y_rb = self.input(type="radio", name=self._formname, value="yes",
 1960                               id="%s_%s"%(self._formname, 'yes'), **kwargs)
 1961 
 1962             n_rb = self.input(type="radio", name=self._formname,  value="no",
 1963                 checked="checked", id="%s_%s"%(self._formname, 'no'), **kwargs)
 1964 
 1965         if ( u_label ):
 1966             if (u_label is True): # it was set via u_label=True
 1967                 u_label = '' # make it empty but a string not boolean
 1968             u_rb = self.input(type="radio", name=self._formname,  value="",
 1969                 id="%s_%s"%(self._formname, 'unk'), **kwargs)
 1970         else:
 1971             # don't generate a trivalue radiobutton.
 1972             u_label = ''
 1973             u_rb=''
 1974             
 1975         if ( labelfirst ):
 1976             s = u_label + u_rb + y_label + y_rb + n_label + n_rb
 1977         else:
 1978             s = u_label + u_rb +y_rb + y_label +  n_rb + n_label
 1979 
 1980         return s
 1981 
 1982 class DateHTMLProperty(HTMLProperty):
 1983 
 1984     _marker = []
 1985 
 1986     def __init__(self, client, classname, nodeid, prop, name, value,
 1987             anonymous=0, offset=None):
 1988         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
 1989                 value, anonymous=anonymous)
 1990         if self._value and not is_us(self._value):
 1991             self._value.setTranslator(self._client.translator)
 1992         self._offset = offset
 1993         if self._offset is None :
 1994             self._offset = self._prop.offset (self._db)
 1995 
 1996     def plain(self, escape=0):
 1997         """ Render a "plain" representation of the property
 1998         """
 1999         if not self.is_view_ok():
 2000             return self._('[hidden]')
 2001 
 2002         if self._value is None:
 2003             return ''
 2004         if self._offset is None:
 2005             offset = self._db.getUserTimezone()
 2006         else:
 2007             offset = self._offset
 2008         try:
 2009             return str(self._value.local(offset))
 2010         except AttributeError:
 2011             # not a date value, e.g. from unsaved form data
 2012             return str(self._value)
 2013 
 2014     def now(self, str_interval=None):
 2015         """ Return the current time.
 2016 
 2017             This is useful for defaulting a new value. Returns a
 2018             DateHTMLProperty.
 2019         """
 2020         if not self.is_view_ok():
 2021             return self._('[hidden]')
 2022 
 2023         ret = date.Date('.', translator=self._client)
 2024 
 2025         if is_us(str_interval):
 2026             sign = 1
 2027             if str_interval[0] == '-':
 2028                 sign = -1
 2029                 str_interval = str_interval[1:]
 2030             interval = date.Interval(str_interval, translator=self._client)
 2031             if sign > 0:
 2032                 ret = ret + interval
 2033             else:
 2034                 ret = ret - interval
 2035 
 2036         return DateHTMLProperty(self._client, self._classname, self._nodeid,
 2037             self._prop, self._formname, ret)
 2038 
 2039     def field(self, size=30, default=None, format=_marker, popcal=True,
 2040               **kwargs):
 2041         """Render a form edit field for the property
 2042 
 2043         If not editable, just display the value via plain().
 2044 
 2045         If "popcal" then include the Javascript calendar editor.
 2046         Default=yes.
 2047 
 2048         The format string is a standard python strftime format string.
 2049         """
 2050         if not self.is_edit_ok():
 2051             if format is self._marker:
 2052                 return self.plain(escape=1)
 2053             else:
 2054                 return self.pretty(format)
 2055 
 2056         value = self._value
 2057 
 2058         if value is None:
 2059             if default is None:
 2060                 raw_value = None
 2061             else:
 2062                 if is_us(default):
 2063                     raw_value = date.Date(default, translator=self._client)
 2064                 elif isinstance(default, date.Date):
 2065                     raw_value = default
 2066                 elif isinstance(default, DateHTMLProperty):
 2067                     raw_value = default._value
 2068                 else:
 2069                     raise ValueError(self._('default value for '
 2070                         'DateHTMLProperty must be either DateHTMLProperty '
 2071                         'or string date representation.'))
 2072         elif is_us(value):
 2073             # most likely erroneous input to be passed back to user
 2074             value = us2s(value)
 2075             s = self.input(name=self._formname, value=value, size=size,
 2076                               **kwargs)
 2077             if popcal:
 2078                 s += self.popcal()
 2079             return s
 2080         else:
 2081             raw_value = value
 2082 
 2083         if raw_value is None:
 2084             value = ''
 2085         elif is_us(raw_value):
 2086             if format is self._marker:
 2087                 value = raw_value
 2088             else:
 2089                 value = date.Date(raw_value).pretty(format)
 2090         else:
 2091             if self._offset is None :
 2092                 offset = self._db.getUserTimezone()
 2093             else :
 2094                 offset = self._offset
 2095             value = raw_value.local(offset)
 2096             if format is not self._marker:
 2097                 value = value.pretty(format)
 2098 
 2099         s = self.input(name=self._formname, value=value, size=size,
 2100                        **kwargs)
 2101         if popcal:
 2102             s += self.popcal()
 2103         return s
 2104 
 2105     def reldate(self, pretty=1):
 2106         """ Render the interval between the date and now.
 2107 
 2108             If the "pretty" flag is true, then make the display pretty.
 2109         """
 2110         if not self.is_view_ok():
 2111             return self._('[hidden]')
 2112 
 2113         if not self._value:
 2114             return ''
 2115 
 2116         # figure the interval
 2117         interval = self._value - date.Date('.', translator=self._client)
 2118         if pretty:
 2119             return interval.pretty()
 2120         return str(interval)
 2121 
 2122     def pretty(self, format=_marker):
 2123         """ Render the date in a pretty format (eg. month names, spaces).
 2124 
 2125             The format string is a standard python strftime format string.
 2126             Note that if the day is zero, and appears at the start of the
 2127             string, then it'll be stripped from the output. This is handy
 2128             for the situation when a date only specifies a month and a year.
 2129         """
 2130         if not self.is_view_ok():
 2131             return self._('[hidden]')
 2132 
 2133         if self._offset is None:
 2134             offset = self._db.getUserTimezone()
 2135         else:
 2136             offset = self._offset
 2137 
 2138         if not self._value:
 2139             return ''
 2140         elif format is not self._marker:
 2141             return self._value.local(offset).pretty(format)
 2142         else:
 2143             return self._value.local(offset).pretty()
 2144 
 2145     def local(self, offset):
 2146         """ Return the date/time as a local (timezone offset) date/time.
 2147         """
 2148         if not self.is_view_ok():
 2149             return self._('[hidden]')
 2150 
 2151         return DateHTMLProperty(self._client, self._classname, self._nodeid,
 2152             self._prop, self._formname, self._value, offset=offset)
 2153 
 2154     def popcal(self, width=300, height=200, label="(cal)",
 2155             form="itemSynopsis"):
 2156         """Generate a link to a calendar pop-up window.
 2157 
 2158         item: HTMLProperty e.g.: context.deadline
 2159         """
 2160         if self.isset():
 2161             date = "&date=%s"%self._value
 2162         else :
 2163             date = ""
 2164 
 2165         data_attr = {
 2166             "data-calurl": '%s?@template=calendar&amp;property=%s&amp;form=%s%s' % (self._classname, self._name, form, date),
 2167             "data-width": width,
 2168             "data-height": height
 2169         }
 2170         
 2171         return ('<a class="classhelp" %s href="javascript:help_window('
 2172             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
 2173             '">%s</a>'%(self.cgi_escape_attrs(**data_attr),self._classname, self._name, form, date, width,
 2174             height, label))
 2175 
 2176 class IntervalHTMLProperty(HTMLProperty):
 2177     def __init__(self, client, classname, nodeid, prop, name, value,
 2178             anonymous=0):
 2179         HTMLProperty.__init__(self, client, classname, nodeid, prop,
 2180             name, value, anonymous)
 2181         if self._value and not is_us(self._value):
 2182             self._value.setTranslator(self._client.translator)
 2183 
 2184     def plain(self, escape=0):
 2185         """ Render a "plain" representation of the property
 2186         """
 2187         if not self.is_view_ok():
 2188             return self._('[hidden]')
 2189 
 2190         if self._value is None:
 2191             return ''
 2192         return str(self._value)
 2193 
 2194     def pretty(self):
 2195         """ Render the interval in a pretty format (eg. "yesterday")
 2196         """
 2197         if not self.is_view_ok():
 2198             return self._('[hidden]')
 2199 
 2200         return self._value.pretty()
 2201 
 2202     def field(self, size=30, **kwargs):
 2203         """ Render a form edit field for the property
 2204 
 2205             If not editable, just display the value via plain().
 2206         """
 2207         if not self.is_edit_ok():
 2208             return self.plain(escape=1)
 2209 
 2210         value = self._value
 2211         if value is None:
 2212             value = ''
 2213 
 2214         return self.input(name=self._formname, value=value, size=size,
 2215                           **kwargs)
 2216 
 2217 class LinkHTMLProperty(HTMLProperty):
 2218     """ Link HTMLProperty
 2219         Include the above as well as being able to access the class
 2220         information. Stringifying the object itself results in the value
 2221         from the item being displayed. Accessing attributes of this object
 2222         result in the appropriate entry from the class being queried for the
 2223         property accessed (so item/assignedto/name would look up the user
 2224         entry identified by the assignedto property on item, and then the
 2225         name property of that user)
 2226     """
 2227     def __init__(self, *args, **kw):
 2228         HTMLProperty.__init__(self, *args, **kw)
 2229         # if we're representing a form value, then the -1 from the form really
 2230         # should be a None
 2231         if str(self._value) == '-1':
 2232             self._value = None
 2233 
 2234     def __getattr__(self, attr):
 2235         """ return a new HTMLItem """
 2236         if not self._value:
 2237             # handle a special page templates lookup
 2238             if attr == '__render_with_namespace__':
 2239                 def nothing(*args, **kw):
 2240                     return ''
 2241                 return nothing
 2242             msg = self._('Attempt to look up %(attr)s on a missing value')
 2243             return MissingValue(msg%locals())
 2244         i = HTMLItem(self._client, self._prop.classname, self._value)
 2245         return getattr(i, attr)
 2246 
 2247     def __getitem__(self, item):
 2248         """Explicitly define __getitem__ -- this used to work earlier
 2249            due to __getattr__ returning the __getitem__ of HTMLItem -- this
 2250            lookup doesn't work for new-style classes.
 2251         """
 2252         if not self._value:
 2253             msg = self._('Attempt to look up %(item)s on a missing value')
 2254             return MissingValue(msg%locals())
 2255         i = HTMLItem(self._client, self._prop.classname, self._value)
 2256         return i[item]
 2257 
 2258     def plain(self, escape=0):
 2259         """ Render a "plain" representation of the property
 2260         """
 2261         if not self.is_view_ok():
 2262             return self._('[hidden]')
 2263 
 2264         if self._value is None:
 2265             return ''
 2266         linkcl = self._db.classes[self._prop.classname]
 2267         k = linkcl.labelprop(1)
 2268         if num_re.match(self._value):
 2269             try:
 2270                 value = str(linkcl.get(self._value, k))
 2271             except IndexError:
 2272                 value = self._value
 2273         else :
 2274             value = self._value
 2275         if escape:
 2276             value = html_escape(value)
 2277         return value
 2278 
 2279     def field(self, showid=0, size=None, **kwargs):
 2280         """ Render a form edit field for the property
 2281 
 2282             If not editable, just display the value via plain().
 2283         """
 2284         if not self.is_edit_ok():
 2285             return self.plain(escape=1)
 2286 
 2287         # edit field
 2288         linkcl = self._db.getclass(self._prop.classname)
 2289         if self._value is None:
 2290             value = ''
 2291         else:
 2292             k = linkcl.getkey()
 2293             idparse = self._prop.try_id_parsing
 2294             if k and num_re.match(self._value):
 2295                 try :
 2296                     value = linkcl.get(self._value, k)
 2297                 except IndexError :
 2298                     if idparse :
 2299                         raise
 2300                     value = ''
 2301             else:
 2302                 value = self._value
 2303         return self.input(name=self._formname, value=value, size=size,
 2304                           **kwargs)
 2305 
 2306     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
 2307              sort_on=None, html_kwargs={}, translate=True, showdef=None, **conditions):
 2308         """ Render a form select list for this property
 2309 
 2310             "size" is used to limit the length of the list labels
 2311             "height" is used to set the <select> tag's "size" attribute
 2312             "showid" includes the item ids in the list labels
 2313             "value" specifies which item is pre-selected
 2314             "additional" lists properties which should be included in the
 2315                 label
 2316             "sort_on" indicates the property to sort the list on as
 2317                 (direction, property) where direction is '+' or '-'. A
 2318                 single string with the direction prepended may be used.
 2319                 For example: ('-', 'order'), '+name'.
 2320             "html_kwargs" specified additional html args for the
 2321             generated html <select>
 2322             "translate" indicates if we should do translation of labels
 2323             using gettext -- this is often desired (e.g. for status
 2324             labels) but sometimes not.
 2325             "showdef" marks the default value with the string passed
 2326                 as the showdef argument. It is appended to the selected
 2327                 value so the user can reset the menu to the original value.
 2328                 Note that the marker may be removed if the length of
 2329                 the option label and the marker exceed the size.
 2330 
 2331             The remaining keyword arguments are used as conditions for
 2332             filtering the items in the list - they're passed as the
 2333             "filterspec" argument to a Class.filter() call.
 2334 
 2335             If not editable, just display the value via plain().
 2336         """
 2337         if not self.is_edit_ok():
 2338             return self.plain(escape=1)
 2339 
 2340         # Since None indicates the default, we need another way to
 2341         # indicate "no selection".  We use -1 for this purpose, as
 2342         # that is the value we use when submitting a form without the
 2343         # value set.
 2344         if value is None:
 2345             value = self._value
 2346         elif value == '-1':
 2347             value = None
 2348 
 2349         linkcl = self._db.getclass(self._prop.classname)
 2350         l = ['<select %s>'%self.cgi_escape_attrs(name = self._formname,
 2351                                             **html_kwargs)]
 2352         k = linkcl.labelprop(1)
 2353         s = ''
 2354         if value is None:
 2355             s = 'selected="selected" '
 2356         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
 2357 
 2358         if sort_on is not None:
 2359             if not isinstance(sort_on, tuple):
 2360                 if sort_on[0] in '+-':
 2361                     sort_on = (sort_on[0], sort_on[1:])
 2362                 else:
 2363                     sort_on = ('+', sort_on)
 2364         else:
 2365             sort_on = ('+', linkcl.orderprop())
 2366 
 2367         options = [opt
 2368             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
 2369             if self._db.security.hasPermission("View", self._client.userid,
 2370                 linkcl.classname, itemid=opt)]
 2371 
 2372         # make sure we list the current value if it's retired
 2373         if value and value not in options:
 2374             options.insert(0, value)
 2375 
 2376         if additional:
 2377             additional_fns = []
 2378             props = linkcl.getprops()
 2379             for propname in additional:
 2380                 prop = props[propname]
 2381                 if isinstance(prop, hyperdb.Link):
 2382                     cl = self._db.getclass(prop.classname)
 2383                     labelprop = cl.labelprop()
 2384                     fn = lambda optionid, cl=cl, linkcl=linkcl, \
 2385                                 propname=propname, labelprop=labelprop: \
 2386                             cl.get(linkcl.get(optionid, propname), labelprop)
 2387                 else:
 2388                     fn = lambda optionid, linkcl=linkcl, propname=propname: \
 2389                             linkcl.get(optionid, propname)
 2390                 additional_fns.append(fn)
 2391 
 2392         for optionid in options:
 2393             # get the option value, and if it's None use an empty string
 2394             option = linkcl.get(optionid, k) or ''
 2395 
 2396             # figure if this option is selected
 2397             s = ''
 2398             # record the marker for the selected item if requested
 2399             selected_mark=''
 2400 
 2401             if value in [optionid, option]:
 2402                 s = 'selected="selected" '
 2403                 if ( showdef ):
 2404                     selected_mark = showdef
 2405 
 2406             # figure the label
 2407             if showid:
 2408                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
 2409             elif not option:
 2410                 lab = '%s%s'%(self._prop.classname, optionid)
 2411             else:
 2412                 lab = option
 2413 
 2414             lab = lab + selected_mark
 2415             # truncate if it's too long
 2416             if size is not None and len(lab) > size:
 2417                 lab = lab[:size-3] + '...'
 2418             if additional:
 2419                 m = []
 2420                 for fn in additional_fns:
 2421                     m.append(str(fn(optionid)))
 2422                 lab = lab + ' (%s)'%', '.join(m)
 2423 
 2424             # and generate
 2425             tr = str
 2426             if translate:
 2427                 tr = self._
 2428             lab = html_escape(tr(lab))
 2429             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
 2430         l.append('</select>')
 2431         return '\n'.join(l)
 2432 #    def checklist(self, ...)
 2433 
 2434 
 2435 
 2436 class MultilinkHTMLProperty(HTMLProperty):
 2437     """ Multilink HTMLProperty
 2438 
 2439         Also be iterable, returning a wrapper object like the Link case for
 2440         each entry in the multilink.
 2441     """
 2442     def __init__(self, *args, **kwargs):
 2443         HTMLProperty.__init__(self, *args, **kwargs)
 2444         if self._value:
 2445             display_value = lookupIds(self._db, self._prop, self._value,
 2446                 fail_ok=1, do_lookup=False)
 2447             keyfun = make_key_function(self._db, self._prop.classname)
 2448             # sorting fails if the value contains
 2449             # items not yet stored in the database
 2450             # ignore these errors to preserve user input
 2451             try:
 2452                 display_value.sort(key=keyfun)
 2453             except:
 2454                 pass
 2455             self._value = display_value
 2456 
 2457     def __len__(self):
 2458         """ length of the multilink """
 2459         return len(self._value)
 2460 
 2461     def __getattr__(self, attr):
 2462         """ no extended attribute accesses make sense here """
 2463         raise AttributeError(attr)
 2464 
 2465     def viewableGenerator(self, values):
 2466         """Used to iterate over only the View'able items in a class."""
 2467         check = self._db.security.hasPermission
 2468         userid = self._client.userid
 2469         classname = self._prop.classname
 2470         if check('Web Access', userid):
 2471             for value in values:
 2472                 if check('View', userid, classname,
 2473                          itemid=value,
 2474                          property=self._db.getclass(classname).labelprop(default_to_id=1)):
 2475                     yield HTMLItem(self._client, classname, value)
 2476 
 2477     def __iter__(self):
 2478         """ iterate and return a new HTMLItem
 2479         """
 2480         return self.viewableGenerator(self._value)
 2481 
 2482     def reverse(self):
 2483         """ return the list in reverse order
 2484         """
 2485         l = self._value[:]
 2486         l.reverse()
 2487         return self.viewableGenerator(l)
 2488 
 2489     def sorted(self, property, reverse=False):
 2490         """ Return this multilink sorted by the given property """
 2491         value = list(self.__iter__())
 2492         value.sort(key=lambda a:a[property], reverse=reverse)
 2493         return value
 2494 
 2495     def __contains__(self, value):
 2496         """ Support the "in" operator. We have to make sure the passed-in
 2497             value is a string first, not a HTMLProperty.
 2498         """
 2499         return str(value) in self._value
 2500 
 2501     def isset(self):
 2502         """Is my _value not []?"""
 2503         return self._value != []
 2504 
 2505     def plain(self, escape=0):
 2506         """ Render a "plain" representation of the property
 2507         """
 2508         if not self.is_view_ok():
 2509             return self._('[hidden]')
 2510 
 2511         linkcl = self._db.classes[self._prop.classname]
 2512         k = linkcl.labelprop(1)
 2513         labels = []
 2514         for v in self._value:
 2515             if num_re.match(v):
 2516                 try:
 2517                     label = linkcl.get(v, k)
 2518                 except IndexError:
 2519                     label = None
 2520                 # fall back to designator if label is None
 2521                 if label is None: label = '%s%s'%(self._prop.classname, k)
 2522             else:
 2523                 label = v
 2524             labels.append(label)
 2525         value = ', '.join(labels)
 2526         if escape:
 2527             value = html_escape(value)
 2528         return value
 2529 
 2530     def field(self, size=30, showid=0, **kwargs):
 2531         """ Render a form edit field for the property
 2532 
 2533             If not editable, just display the value via plain().
 2534         """
 2535         if not self.is_edit_ok():
 2536             return self.plain(escape=1)
 2537 
 2538         linkcl = self._db.getclass(self._prop.classname)
 2539 
 2540         if 'value' not in kwargs:
 2541             value = self._value[:]
 2542             # map the id to the label property
 2543             if not linkcl.getkey():
 2544                 showid=1
 2545             if not showid:
 2546                 k = linkcl.labelprop(1)
 2547                 value = lookupKeys(linkcl, k, value)
 2548             value = ','.join(value)
 2549             kwargs["value"] = value
 2550 
 2551         return self.input(name=self._formname, size=size, **kwargs)
 2552 
 2553     def menu(self, size=None, height=None, showid=0, additional=[],
 2554              value=None, sort_on=None, html_kwargs={}, translate=True,
 2555              **conditions):
 2556         """ Render a form <select> list for this property.
 2557 
 2558             "size" is used to limit the length of the list labels
 2559             "height" is used to set the <select> tag's "size" attribute
 2560             "showid" includes the item ids in the list labels
 2561             "additional" lists properties which should be included in the
 2562                 label
 2563             "value" specifies which item is pre-selected
 2564             "sort_on" indicates the property to sort the list on as
 2565                 (direction, property) where direction is '+' or '-'. A
 2566                 single string with the direction prepended may be used.
 2567                 For example: ('-', 'order'), '+name'.
 2568 
 2569             The remaining keyword arguments are used as conditions for
 2570             filtering the items in the list - they're passed as the
 2571             "filterspec" argument to a Class.filter() call.
 2572 
 2573             If not editable, just display the value via plain().
 2574         """
 2575         if not self.is_edit_ok():
 2576             return self.plain(escape=1)
 2577 
 2578         if value is None:
 2579             value = self._value
 2580         # When rendering from form contents, 'value' may contain
 2581         # elements starting '-' from '- no selection -' having been
 2582         # selected on a previous form submission.
 2583         value = [v for v in value if not v.startswith('-')]
 2584 
 2585         linkcl = self._db.getclass(self._prop.classname)
 2586 
 2587         if sort_on is not None:
 2588             if not isinstance(sort_on, tuple):
 2589                 if sort_on[0] in '+-':
 2590                     sort_on = (sort_on[0], sort_on[1:])
 2591                 else:
 2592                     sort_on = ('+', sort_on)
 2593         else:
 2594             sort_on = ('+', linkcl.orderprop())
 2595 
 2596         options = [opt
 2597             for opt in linkcl.filter(None, conditions, sort_on)
 2598             if self._db.security.hasPermission("View", self._client.userid,
 2599                 linkcl.classname, itemid=opt)]
 2600 
 2601         # make sure we list the current values if they're retired
 2602         for val in value:
 2603             if val not in options:
 2604                 options.insert(0, val)
 2605 
 2606         if not height:
 2607             height = len(options)
 2608             if value:
 2609                 # The "no selection" option.
 2610                 height += 1
 2611             height = min(height, 7)
 2612         l = ['<select multiple %s>'%self.cgi_escape_attrs(name = self._formname,
 2613                                                      size = height,
 2614                                                      **html_kwargs)]
 2615         k = linkcl.labelprop(1)
 2616 
 2617         if value:  # FIXME '- no selection -' mark for translation
 2618             l.append('<option value="%s">- no selection -</option>'
 2619                      % ','.join(['-' + v for v in value]))
 2620 
 2621         if additional:
 2622             additional_fns = []
 2623             props = linkcl.getprops()
 2624             for propname in additional:
 2625                 prop = props[propname]
 2626                 if isinstance(prop, hyperdb.Link):
 2627                     cl = self._db.getclass(prop.classname)
 2628                     labelprop = cl.labelprop()
 2629                     fn = lambda optionid, cl=cl, linkcl=linkcl, \
 2630                                 propname=propname, labelprop=labelprop: \
 2631                             cl.get(linkcl.get(optionid, propname), labelprop)
 2632                 else:
 2633                     fn = lambda optionid, linkcl=linkcl, propname=propname: \
 2634                             linkcl.get(optionid, propname)
 2635                 additional_fns.append(fn)
 2636 
 2637         for optionid in options:
 2638             # get the option value, and if it's None use an empty string
 2639             option = linkcl.get(optionid, k) or ''
 2640 
 2641             # figure if this option is selected
 2642             s = ''
 2643             if optionid in value or option in value:
 2644                 s = 'selected="selected" '
 2645 
 2646             # figure the label
 2647             if showid:
 2648                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
 2649             else:
 2650                 lab = option
 2651             # truncate if it's too long
 2652             if size is not None and len(lab) > size:
 2653                 lab = lab[:size-3] + '...'
 2654             if additional:
 2655                 m = []
 2656                 for fn in additional_fns:
 2657                     m.append(str(fn(optionid)))
 2658                 lab = lab + ' (%s)'%', '.join(m)
 2659 
 2660             # and generate
 2661             tr = str
 2662             if translate:
 2663                 tr = self._
 2664             lab = html_escape(tr(lab))
 2665             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
 2666                 lab))
 2667         l.append('</select>')
 2668         return '\n'.join(l)
 2669 
 2670 
 2671 # set the propclasses for HTMLItem
 2672 propclasses = [
 2673     (hyperdb.String, StringHTMLProperty),
 2674     (hyperdb.Number, NumberHTMLProperty),
 2675     (hyperdb.Integer, IntegerHTMLProperty),
 2676     (hyperdb.Boolean, BooleanHTMLProperty),
 2677     (hyperdb.Date, DateHTMLProperty),
 2678     (hyperdb.Interval, IntervalHTMLProperty),
 2679     (hyperdb.Password, PasswordHTMLProperty),
 2680     (hyperdb.Link, LinkHTMLProperty),
 2681     (hyperdb.Multilink, MultilinkHTMLProperty),
 2682 ]
 2683 
 2684 def register_propclass(prop, cls):
 2685     for index,propclass in enumerate(propclasses):
 2686         p, c = propclass
 2687         if prop == p:
 2688             propclasses[index] = (prop, cls)
 2689             break
 2690     else:
 2691         propclasses.append((prop, cls))
 2692 
 2693 
 2694 def make_key_function(db, classname, sort_on=None):
 2695     """Make a sort key function for a given class.
 2696 
 2697     The list being sorted may contain mixed ids and labels.
 2698     """
 2699     linkcl = db.getclass(classname)
 2700     if sort_on is None:
 2701         sort_on = linkcl.orderprop()
 2702     def keyfunc(a):
 2703         if num_re.match(a):
 2704             a = linkcl.get(a, sort_on)
 2705         return a
 2706     return keyfunc
 2707 
 2708 def handleListCGIValue(value):
 2709     """ Value is either a single item or a list of items. Each item has a
 2710         .value that we're actually interested in.
 2711     """
 2712     if isinstance(value, type([])):
 2713         return [value.value for value in value]
 2714     else:
 2715         value = value.value.strip()
 2716         if not value:
 2717             return []
 2718         return [v.strip() for v in value.split(',')]
 2719 
 2720 class HTMLRequest(HTMLInputMixin):
 2721     """The *request*, holding the CGI form and environment.
 2722 
 2723     - "form" the CGI form as a cgi.FieldStorage
 2724     - "env" the CGI environment variables
 2725     - "base" the base URL for this instance
 2726     - "user" a HTMLItem instance for this user
 2727     - "language" as determined by the browser or config
 2728     - "classname" the current classname (possibly None)
 2729     - "template" the current template (suffix, also possibly None)
 2730 
 2731     Index args:
 2732 
 2733     - "columns" dictionary of the columns to display in an index page
 2734     - "show" a convenience access to columns - request/show/colname will
 2735       be true if the columns should be displayed, false otherwise
 2736     - "sort" index sort column (direction, column name)
 2737     - "group" index grouping property (direction, column name)
 2738     - "filter" properties to filter the index on
 2739     - "filterspec" values to filter the index on
 2740     - "search_text" text to perform a full-text search on for an index
 2741     """
 2742     def __repr__(self):
 2743         return '<HTMLRequest %r>'%self.__dict__
 2744 
 2745     def __init__(self, client):
 2746         # _client is needed by HTMLInputMixin
 2747         self._client = self.client = client
 2748 
 2749         # easier access vars
 2750         self.form = client.form
 2751         self.env = client.env
 2752         self.base = client.base
 2753         self.user = HTMLItem(client, 'user', client.userid)
 2754         self.language = client.language
 2755 
 2756         # store the current class name and action
 2757         self.classname = client.classname
 2758         self.nodeid = client.nodeid
 2759         self.template = client.template
 2760 
 2761         # the special char to use for special vars
 2762         self.special_char = '@'
 2763 
 2764         HTMLInputMixin.__init__(self)
 2765 
 2766         self._post_init()
 2767 
 2768     def current_url(self):
 2769         url = self.base
 2770         if self.classname:
 2771             url += self.classname
 2772             if self.nodeid:
 2773                 url += self.nodeid
 2774         args = {}
 2775         if self.template:
 2776             args['@template'] = self.template
 2777         return self.indexargs_url(url, args)
 2778 
 2779     def _parse_sort(self, var, name):
 2780         """ Parse sort/group options. Append to var
 2781         """
 2782         fields = []
 2783         dirs = []
 2784         for special in '@:':
 2785             idx = 0
 2786             key = '%s%s%d'%(special, name, idx)
 2787             while self._form_has_key(key):
 2788                 self.special_char = special
 2789                 fields.append(self.form.getfirst(key))
 2790                 dirkey = '%s%sdir%d'%(special, name, idx)
 2791                 if dirkey in self.form:
 2792                     dirs.append(self.form.getfirst(dirkey))
 2793                 else:
 2794                     dirs.append(None)
 2795                 idx += 1
 2796                 key = '%s%s%d'%(special, name, idx)
 2797             # backward compatible (and query) URL format
 2798             key = special + name
 2799             dirkey = key + 'dir'
 2800             if self._form_has_key(key) and not fields:
 2801                 fields = handleListCGIValue(self.form[key])
 2802                 if dirkey in self.form:
 2803                     dirs.append(self.form.getfirst(dirkey))
 2804             if fields: # only try other special char if nothing found
 2805                 break
 2806 
 2807         # sometimes requests come in without a class
 2808         # chances are they won't have any filter params,
 2809         # in that case anyway but...
 2810         if self.classname:
 2811             cls = self.client.db.getclass(self.classname)
 2812         for f, d in zip_longest(fields, dirs):
 2813             if f.startswith('-'):
 2814                 dir, propname = '-', f[1:]
 2815             elif d:
 2816                 dir, propname = '-', f
 2817             else:
 2818                 dir, propname = '+', f
 2819             # if no classname, just append the propname unchecked.
 2820             # this may be valid for some actions that bypass classes.
 2821             if self.classname and cls.get_transitive_prop(propname) is None:
 2822                 self.client.add_error_message("Unknown %s property %s"%(name, propname))
 2823             else:
 2824                 var.append((dir, propname))
 2825 
 2826     def _form_has_key(self, name):
 2827         try:
 2828             return name in self.form
 2829         except TypeError:
 2830             pass
 2831         return False
 2832 
 2833     def _post_init(self):
 2834         """ Set attributes based on self.form
 2835         """
 2836         # extract the index display information from the form
 2837         self.columns = []
 2838         for name in ':columns @columns'.split():
 2839             if self._form_has_key(name):
 2840                 self.special_char = name[0]
 2841                 self.columns = handleListCGIValue(self.form[name])
 2842                 break
 2843         self.show = support.TruthDict(self.columns)
 2844         security = self._client.db.security
 2845         userid = self._client.userid
 2846 
 2847         # sorting and grouping
 2848         self.sort = []
 2849         self.group = []
 2850         self._parse_sort(self.sort, 'sort')
 2851         self._parse_sort(self.group, 'group')
 2852         self.sort = security.filterSortspec(userid, self.classname, self.sort)
 2853         self.group = security.filterSortspec(userid, self.classname, self.group)
 2854 
 2855         # filtering
 2856         self.filter = []
 2857         for name in ':filter @filter'.split():
 2858             if self._form_has_key(name):
 2859                 self.special_char = name[0]
 2860                 self.filter = handleListCGIValue(self.form[name])
 2861 
 2862         self.filterspec = {}
 2863         db = self.client.db
 2864         if self.classname is not None:
 2865             cls = db.getclass (self.classname)
 2866             for name in self.filter:
 2867                 if not self._form_has_key(name):
 2868                     continue
 2869                 prop = cls.get_transitive_prop (name)
 2870                 fv = self.form[name]
 2871                 if (isinstance(prop, hyperdb.Link) or
 2872                         isinstance(prop, hyperdb.Multilink)):
 2873                     self.filterspec[name] = lookupIds(db, prop,
 2874                         handleListCGIValue(fv))
 2875                 else:
 2876                     if isinstance(fv, type([])):
 2877                         self.filterspec[name] = [v.value for v in fv]
 2878                     elif name == 'id':
 2879                         # special case "id" property
 2880                         self.filterspec[name] = handleListCGIValue(fv)
 2881                     else:
 2882                         self.filterspec[name] = fv.value
 2883         self.filterspec = security.filterFilterspec(userid, self.classname,
 2884             self.filterspec)
 2885 
 2886         # full-text search argument
 2887         self.search_text = None
 2888         for name in ':search_text @search_text'.split():
 2889             if self._form_has_key(name):
 2890                 self.special_char = name[0]
 2891                 self.search_text = self.form.getfirst(name)
 2892 
 2893         # pagination - size and start index
 2894         # figure batch args
 2895         self.pagesize = 50
 2896         for name in ':pagesize @pagesize'.split():
 2897             if self._form_has_key(name):
 2898                 self.special_char = name[0]
 2899                 try:
 2900                     self.pagesize = int(self.form.getfirst(name))
 2901                 except ValueError:
 2902                     # not an integer - ignore
 2903                     pass
 2904 
 2905         self.startwith = 0
 2906         for name in ':startwith @startwith'.split():
 2907             if self._form_has_key(name):
 2908                 self.special_char = name[0]
 2909                 try:
 2910                     self.startwith = int(self.form.getfirst(name))
 2911                 except ValueError:
 2912                     # not an integer - ignore
 2913                     pass
 2914 
 2915         # dispname
 2916         if self._form_has_key('@dispname'):
 2917             self.dispname = self.form.getfirst('@dispname')
 2918         else:
 2919             self.dispname = None
 2920 
 2921     def updateFromURL(self, url):
 2922         """ Parse the URL for query args, and update my attributes using the
 2923             values.
 2924         """
 2925         env = {'QUERY_STRING': url}
 2926         self.form = cgi.FieldStorage(environ=env)
 2927 
 2928         self._post_init()
 2929 
 2930     def update(self, kwargs):
 2931         """ Update my attributes using the keyword args
 2932         """
 2933         self.__dict__.update(kwargs)
 2934         if 'columns' in kwargs:
 2935             self.show = support.TruthDict(self.columns)
 2936 
 2937     def description(self):
 2938         """ Return a description of the request - handle for the page title.
 2939         """
 2940         s = [self.client.db.config.TRACKER_NAME]
 2941         if self.classname:
 2942             if self.client.nodeid:
 2943                 s.append('- %s%s'%(self.classname, self.client.nodeid))
 2944             else:
 2945                 if self.template == 'item':
 2946                     s.append('- new %s'%self.classname)
 2947                 elif self.template == 'index':
 2948                     s.append('- %s index'%self.classname)
 2949                 else:
 2950                     s.append('- %s %s'%(self.classname, self.template))
 2951         else:
 2952             s.append('- home')
 2953         return ' '.join(s)
 2954 
 2955     def __str__(self):
 2956         d = {}
 2957         d.update(self.__dict__)
 2958         f = ''
 2959         for k in self.form.keys():
 2960             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
 2961         d['form'] = f
 2962         e = ''
 2963         for k,v in self.env.items():
 2964             e += '\n     %r=%r'%(k, v)
 2965         d['env'] = e
 2966         return """
 2967 form: %(form)s
 2968 base: %(base)r
 2969 classname: %(classname)r
 2970 template: %(template)r
 2971 columns: %(columns)r
 2972 sort: %(sort)r
 2973 group: %(group)r
 2974 filter: %(filter)r
 2975 search_text: %(search_text)r
 2976 pagesize: %(pagesize)r
 2977 startwith: %(startwith)r
 2978 env: %(env)s
 2979 """%d
 2980 
 2981     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
 2982             filterspec=1, search_text=1, exclude=[]):
 2983         """ return the current index args as form elements
 2984 
 2985             This routine generates an html form with hidden elements.
 2986             If you want to have visible form elements in your tal/jinja
 2987             generated templates use the exclude aray to list the names for
 2988             these elements. This wll prevent the function from creating
 2989             these elements in its output.
 2990         """
 2991         l = []
 2992         sc = self.special_char
 2993         def add(k, v):
 2994             l.append(self.input(type="hidden", name=k, value=v))
 2995         if columns and self.columns:
 2996             add(sc+'columns', ','.join(self.columns))
 2997         if sort:
 2998             val = []
 2999             for dir, attr in self.sort:
 3000                 if dir == '-':
 3001                     val.append('-'+attr)
 3002                 else:
 3003                     val.append(attr)
 3004             add(sc+'sort', ','.join (val))
 3005         if group:
 3006             val = []
 3007             for dir, attr in self.group:
 3008                 if dir == '-':
 3009                     val.append('-'+attr)
 3010                 else:
 3011                     val.append(attr)
 3012             add(sc+'group', ','.join (val))
 3013         if filter and self.filter:
 3014             add(sc+'filter', ','.join(self.filter))
 3015         if self.classname and filterspec:
 3016             cls = self.client.db.getclass(self.classname)
 3017             for k,v in self.filterspec.items():
 3018                 if k in exclude:
 3019                     continue
 3020                 if type(v) == type([]):
 3021                     # id's are stored as strings but should be treated
 3022                     # as integers in lists.
 3023                     if (isinstance(cls.get_transitive_prop(k), hyperdb.String)
 3024                         and k != 'id'):
 3025                         add(k, ' '.join(v))
 3026                     else:
 3027                         add(k, ','.join(v))
 3028                 else:
 3029                     add(k, v)
 3030         if search_text and self.search_text:
 3031             add(sc+'search_text', self.search_text)
 3032         add(sc+'pagesize', self.pagesize)
 3033         add(sc+'startwith', self.startwith)
 3034         return '\n'.join(l)
 3035 
 3036     def indexargs_url(self, url, args):
 3037         """ Embed the current index args in a URL
 3038 
 3039             If the value of an arg (in args dict) is None,
 3040             the argument is excluded from the url. If you want
 3041             an empty value use an empty string '' as the value.
 3042             Use this in templates to conditionally
 3043             include an arg if it is set to a value. E.G.
 3044             {..., '@queryname': request.dispname or None, ...}
 3045             will include @queryname in the url if there is a
 3046             dispname otherwise the parameter will be omitted
 3047             from the url.
 3048         """
 3049         q = urllib_.quote
 3050         sc = self.special_char
 3051         l = ['%s=%s'%(k,is_us(v) and q(v) or v)
 3052              for k,v in args.items() if v != None ]
 3053         # pull out the special values (prefixed by @ or :)
 3054         specials = {}
 3055         for key in args.keys():
 3056             if key[0] in '@:':
 3057                 specials[key[1:]] = args[key]
 3058 
 3059         # ok, now handle the specials we received in the request
 3060         if self.columns and 'columns' not in specials:
 3061             l.append(sc+'columns=%s'%(','.join(self.columns)))
 3062         if self.sort and 'sort' not in specials:
 3063             val = []
 3064             for dir, attr in self.sort:
 3065                 if dir == '-':
 3066                     val.append('-'+attr)
 3067                 else:
 3068                     val.append(attr)
 3069             l.append(sc+'sort=%s'%(','.join(val)))
 3070         if self.group and 'group' not in specials:
 3071             val = []
 3072             for dir, attr in self.group:
 3073                 if dir == '-':
 3074                     val.append('-'+attr)
 3075                 else:
 3076                     val.append(attr)
 3077             l.append(sc+'group=%s'%(','.join(val)))
 3078         if self.filter and 'filter' not in specials:
 3079             l.append(sc+'filter=%s'%(','.join(self.filter)))
 3080         if self.search_text and 'search_text' not in specials:
 3081             l.append(sc+'search_text=%s'%q(self.search_text))
 3082         if 'pagesize' not in specials:
 3083             l.append(sc+'pagesize=%s'%self.pagesize)
 3084         if 'startwith' not in specials:
 3085             l.append(sc+'startwith=%s'%self.startwith)
 3086 
 3087         # finally, the remainder of the filter args in the request
 3088         if self.classname and self.filterspec:
 3089             cls = self.client.db.getclass(self.classname)
 3090             for k,v in self.filterspec.items():
 3091                 if k not in args:
 3092                     if type(v) == type([]):
 3093                         prop = cls.get_transitive_prop(k)
 3094                         if k != 'id' and isinstance(prop, hyperdb.String):
 3095                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
 3096                         else:
 3097                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
 3098                     else:
 3099                         l.append('%s=%s'%(k, q(v)))
 3100         return '%s?%s'%(url, '&'.join(l))
 3101     indexargs_href = indexargs_url
 3102 
 3103     def base_javascript(self):
 3104         return """
 3105 <script nonce="%s" type="text/javascript">
 3106 submitted = false;
 3107 function submit_once() {
 3108     if (submitted) {
 3109         alert("Your request is being processed.\\nPlease be patient.");
 3110         return false;
 3111     }
 3112     submitted = true;
 3113     return true;
 3114 }
 3115 
 3116 function help_window(helpurl, width, height) {
 3117     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
 3118     HelpWin.focus ()
 3119 }
 3120 </script>
 3121 """%(self._client.client_nonce,self.base)
 3122 
 3123     def batch(self, permission='View'):
 3124         """ Return a batch object for results from the "current search"
 3125         """
 3126         check = self._client.db.security.hasPermission
 3127         userid = self._client.userid
 3128         if not check('Web Access', userid):
 3129             return Batch(self.client, [], self.pagesize, self.startwith,
 3130                 classname=self.classname)
 3131 
 3132         filterspec = self.filterspec
 3133         sort = self.sort
 3134         group = self.group
 3135 
 3136         # get the list of ids we're batching over
 3137         klass = self.client.db.getclass(self.classname)
 3138         if self.search_text:
 3139             matches = self.client.db.indexer.search(
 3140                 [u2s(w.upper()) for w in re.findall(
 3141                     r'(?u)\b\w{2,25}\b',
 3142                     s2u(self.search_text, "replace")
 3143                 )], klass)
 3144         else:
 3145             matches = None
 3146 
 3147         # filter for visibility
 3148         l = [id for id in klass.filter(matches, filterspec, sort, group)
 3149             if check(permission, userid, self.classname, itemid=id)]
 3150 
 3151         # return the batch object, using IDs only
 3152         return Batch(self.client, l, self.pagesize, self.startwith,
 3153             classname=self.classname)
 3154 
 3155 # extend the standard ZTUtils Batch object to remove dependency on
 3156 # Acquisition and add a couple of useful methods
 3157 class Batch(ZTUtils.Batch):
 3158     """ Use me to turn a list of items, or item ids of a given class, into a
 3159         series of batches.
 3160 
 3161         ========= ========================================================
 3162         Parameter  Usage
 3163         ========= ========================================================
 3164         sequence  a list of HTMLItems or item ids
 3165         classname if sequence is a list of ids, this is the class of item
 3166         size      how big to make the sequence.
 3167         start     where to start (0-indexed) in the sequence.
 3168         end       where to end (0-indexed) in the sequence.
 3169         orphan    if the next batch would contain less items than this
 3170                   value, then it is combined with this batch
 3171         overlap   the number of items shared between adjacent batches
 3172         ========= ========================================================
 3173 
 3174         Attributes: Note that the "start" attribute, unlike the
 3175         argument, is a 1-based index (I know, lame).  "first" is the
 3176         0-based index.  "length" is the actual number of elements in
 3177         the batch.
 3178 
 3179         "sequence_length" is the length of the original, unbatched, sequence.
 3180     """
 3181     def __init__(self, client, sequence, size, start, end=0, orphan=0,
 3182             overlap=0, classname=None):
 3183         self.client = client
 3184         self.last_index = self.last_item = None
 3185         self.current_item = None
 3186         self.classname = classname
 3187         self.sequence_length = len(sequence)
 3188         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
 3189             overlap)
 3190 
 3191     # overwrite so we can late-instantiate the HTMLItem instance
 3192     def __getitem__(self, index):
 3193         if index < 0:
 3194             if index + self.end < self.first: raise IndexError(index)
 3195             return self._sequence[index + self.end]
 3196 
 3197         if index >= self.length:
 3198             raise IndexError(index)
 3199 
 3200         # move the last_item along - but only if the fetched index changes
 3201         # (for some reason, index 0 is fetched twice)
 3202         if index != self.last_index:
 3203             self.last_item = self.current_item
 3204             self.last_index = index
 3205 
 3206         item = self._sequence[index + self.first]
 3207         if self.classname:
 3208             # map the item ids to instances
 3209             item = HTMLItem(self.client, self.classname, item)
 3210         self.current_item = item
 3211         return item
 3212 
 3213     def propchanged(self, *properties):
 3214         """ Detect if one of the properties marked as being a group
 3215             property changed in the last iteration fetch
 3216         """
 3217         # we poke directly at the _value here since MissingValue can screw
 3218         # us up and cause Nones to compare strangely
 3219         if self.last_item is None:
 3220             return 1
 3221         for property in properties:
 3222             if property == 'id' or property.endswith ('.id')\
 3223                or isinstance (self.last_item[property], list):
 3224                 if (str(self.last_item[property]) !=
 3225                     str(self.current_item[property])):
 3226                     return 1
 3227             else:
 3228                 if (self.last_item[property]._value !=
 3229                     self.current_item[property]._value):
 3230                     return 1
 3231         return 0
 3232 
 3233     # override these 'cos we don't have access to acquisition
 3234     def previous(self):
 3235         if self.start == 1:
 3236             return None
 3237         return Batch(self.client, self._sequence, self.size,
 3238             self.first - self._size + self.overlap, 0, self.orphan,
 3239             self.overlap)
 3240 
 3241     def next(self):
 3242         try:
 3243             self._sequence[self.end]
 3244         except IndexError:
 3245             return None
 3246         return Batch(self.client, self._sequence, self.size,
 3247             self.end - self.overlap, 0, self.orphan, self.overlap)
 3248 
 3249 class TemplatingUtils:
 3250     """ Utilities for templating
 3251     """
 3252     def __init__(self, client):
 3253         self.client = client
 3254     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
 3255         return Batch(self.client, sequence, size, start, end, orphan,
 3256             overlap)
 3257 
 3258     def anti_csrf_nonce(self, lifetime=None):
 3259         return anti_csrf_nonce(self.client, lifetime=lifetime)
 3260 
 3261     def timestamp(self):
 3262         return pack_timestamp()
 3263     
 3264     def url_quote(self, url):
 3265         """URL-quote the supplied text."""
 3266         return urllib_.quote(url)
 3267 
 3268     def html_quote(self, html):
 3269         """HTML-quote the supplied text."""
 3270         return html_escape(html)
 3271 
 3272     def __getattr__(self, name):
 3273         """Try the tracker's templating_utils."""
 3274         if not hasattr(self.client.instance, 'templating_utils'):
 3275             # backwards-compatibility
 3276             raise AttributeError(name)
 3277         if name not in self.client.instance.templating_utils:
 3278             raise AttributeError(name)
 3279         return self.client.instance.templating_utils[name]
 3280 
 3281     def keywords_expressions(self, request):
 3282         return render_keywords_expression_editor(request)
 3283 
 3284     def html_calendar(self, request):
 3285         """Generate a HTML calendar.
 3286 
 3287         `request`  the roundup.request object
 3288                    - @template : name of the template
 3289                    - form      : name of the form to store back the date
 3290                    - property  : name of the property of the form to store
 3291                                  back the date
 3292                    - date      : current date
 3293                    - display   : when browsing, specifies year and month
 3294 
 3295         html will simply be a table.
 3296         """
 3297         tz = request.client.db.getUserTimezone()
 3298         current_date = date.Date(".").local(tz)
 3299         date_str  = request.form.getfirst("date", current_date)
 3300         display   = request.form.getfirst("display", date_str)
 3301         template  = request.form.getfirst("@template", "calendar")
 3302         form      = request.form.getfirst("form")
 3303         property  = request.form.getfirst("property")
 3304         curr_date = ""
 3305         try:
 3306             # date_str and display can be set to an invalid value
 3307             # if user submits a value like "d4" and gets an edit error.
 3308             # If either or both invalid just ignore that we can't parse it
 3309             # and assign them to today.
 3310             curr_date = date.Date(date_str) # to highlight
 3311             display   = date.Date(display)  # to show
 3312         except ValueError:
 3313             # we couldn't parse the date
 3314             # just let the calendar display
 3315             curr_date = current_date
 3316             display = current_date
 3317         day       = display.day
 3318 
 3319         # for navigation
 3320         try:
 3321             date_prev_month = display + date.Interval("-1m")
 3322         except ValueError:
 3323             date_prev_month = None
 3324         try:
 3325             date_next_month = display + date.Interval("+1m")
 3326         except ValueError:
 3327             date_next_month = None
 3328         try:
 3329             date_prev_year = display + date.Interval("-1y")
 3330         except ValueError:
 3331             date_prev_year = None
 3332         try:
 3333             date_next_year = display + date.Interval("+1y")
 3334         except ValueError:
 3335             date_next_year = None
 3336 
 3337         res = []
 3338 
 3339         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
 3340                     (request.classname, template, property, form, curr_date)
 3341 
 3342         # navigation
 3343         # month
 3344         res.append('<table class="calendar"><tr><td>')
 3345         res.append(' <table width="100%" class="calendar_nav"><tr>')
 3346         link = "&display=%s"%date_prev_month
 3347         if date_prev_month:
 3348             res.append('  <td><a href="%s&display=%s">&lt;</a></td>'
 3349                     % (base_link, date_prev_month))
 3350         else:
 3351             res.append('  <td></td>')
 3352         res.append('  <td>%s</td>'%calendar.month_name[display.month])
 3353         if date_next_month:
 3354             res.append('  <td><a href="%s&display=%s">&gt;</a></td>'
 3355                     % (base_link, date_next_month))
 3356         else:
 3357             res.append('  <td></td>')
 3358         # spacer
 3359         res.append('  <td width="100%"></td>')
 3360         # year
 3361         if date_prev_year:
 3362             res.append('  <td><a href="%s&display=%s">&lt;</a></td>'
 3363                     % (base_link, date_prev_year))
 3364         else:
 3365             res.append('  <td></td>')
 3366         res.append('  <td>%s</td>'%display.year)
 3367         if date_next_year:
 3368             res.append('  <td><a href="%s&display=%s">&gt;</a></td>'
 3369                     % (base_link, date_next_year))
 3370         else:
 3371             res.append('  <td></td>')
 3372         res.append(' </tr></table>')
 3373         res.append(' </td></tr>')
 3374 
 3375         # the calendar
 3376         res.append(' <tr><td><table class="calendar_display">')
 3377         res.append('  <tr class="weekdays">')
 3378         for day in calendar.weekheader(3).split():
 3379             res.append('   <td>%s</td>'%day)
 3380         res.append('  </tr>')
 3381         for week in calendar.monthcalendar(display.year, display.month):
 3382             res.append('  <tr>')
 3383             for day in week:
 3384                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
 3385                      "if ('createEvent' in document) { var evt = document.createEvent('HTMLEvents'); evt.initEvent('change', true, true); form[field].dispatchEvent(evt); } else { form[field].fireEvent('onchange'); }" \
 3386                      "window.close ();"%(display.year, display.month, day)
 3387                 if (day == curr_date.day and display.month == curr_date.month
 3388                         and display.year == curr_date.year):
 3389                     # highlight
 3390                     style = "today"
 3391                 else :
 3392                     style = ""
 3393                 if day:
 3394                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
 3395                         style, link, day))
 3396                 else :
 3397                     res.append('   <td></td>')
 3398             res.append('  </tr>')
 3399         res.append('</table></td></tr></table>')
 3400         return "\n".join(res)
 3401 
 3402 class MissingValue(object):
 3403     def __init__(self, description, **kwargs):
 3404         self.__description = description
 3405         for key, value in kwargs.items():
 3406             self.__dict__[key] = value
 3407 
 3408     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
 3409     def __getattr__(self, name):
 3410         # This allows assignments which assume all intermediate steps are Null
 3411         # objects if they don't exist yet.
 3412         #
 3413         # For example (with just 'client' defined):
 3414         #
 3415         # client.db.config.TRACKER_WEB = 'BASE/'
 3416         self.__dict__[name] = MissingValue(self.__description)
 3417         return getattr(self, name)
 3418 
 3419     def __getitem__(self, key): return self
 3420     def __bool__(self): return False
 3421     # Python 2 compatibility:
 3422     __nonzero__ = __bool__
 3423     def __contains__(self, key): return False
 3424     def __eq__(self, rhs): return False
 3425     def __ne__(self, rhs): return False
 3426     def __str__(self): return '[%s]'%self.__description
 3427     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
 3428         self.__description)
 3429     def gettext(self, str): return str
 3430     _ = gettext
 3431 
 3432 # vim: set et sts=4 sw=4 :