Code

Allow to turn off translation of generated html options in menu method
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 """Implements the API used in the HTML templating for the web interface.
4 """
6 todo = """
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 """
20 __docformat__ = 'restructuredtext'
23 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes, csv
24 import calendar, textwrap
26 from roundup import hyperdb, date, support
27 from roundup import i18n
28 from roundup.i18n import _
30 from KeywordsExpr import render_keywords_expression_editor
32 try:
33     import cPickle as pickle
34 except ImportError:
35     import pickle
36 try:
37     import cStringIO as StringIO
38 except ImportError:
39     import StringIO
40 try:
41     from StructuredText.StructuredText import HTML as StructuredText
42 except ImportError:
43     try: # older version
44         import StructuredText
45     except ImportError:
46         StructuredText = None
47 try:
48     from docutils.core import publish_parts as ReStructuredText
49 except ImportError:
50     ReStructuredText = None
52 # bring in the templating support
53 from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
54 from roundup.cgi.PageTemplates.Expressions import getEngine
55 from roundup.cgi.TAL import TALInterpreter
56 from roundup.cgi import TranslationService, ZTUtils
58 ### i18n services
59 # this global translation service is not thread-safe.
60 # it is left here for backward compatibility
61 # until all Web UI translations are done via client.translator object
62 translationService = TranslationService.get_translation()
63 GlobalTranslationService.setGlobalTranslationService(translationService)
65 ### templating
67 class NoTemplate(Exception):
68     pass
70 class Unauthorised(Exception):
71     def __init__(self, action, klass, translator=None):
72         self.action = action
73         self.klass = klass
74         if translator:
75             self._ = translator.gettext
76         else:
77             self._ = TranslationService.get_translation().gettext
78     def __str__(self):
79         return self._('You are not allowed to %(action)s '
80             'items of class %(class)s') % {
81             'action': self.action, 'class': self.klass}
83 def find_template(dir, name, view):
84     """ Find a template in the nominated dir
85     """
86     # find the source
87     if view:
88         filename = '%s.%s'%(name, view)
89     else:
90         filename = name
92     # try old-style
93     src = os.path.join(dir, filename)
94     if os.path.exists(src):
95         return (src, filename)
97     # try with a .html or .xml extension (new-style)
98     for extension in '.html', '.xml':
99         f = filename + extension
100         src = os.path.join(dir, f)
101         if os.path.exists(src):
102             return (src, f)
104     # no view == no generic template is possible
105     if not view:
106         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
108     # try for a _generic template
109     generic = '_generic.%s'%view
110     src = os.path.join(dir, generic)
111     if os.path.exists(src):
112         return (src, generic)
114     # finally, try _generic.html
115     generic = generic + '.html'
116     src = os.path.join(dir, generic)
117     if os.path.exists(src):
118         return (src, generic)
120     raise NoTemplate('No template file exists for templating "%s" '
121         'with template "%s" (neither "%s" nor "%s")'%(name, view,
122         filename, generic))
124 class Templates:
125     templates = {}
127     def __init__(self, dir):
128         self.dir = dir
130     def precompileTemplates(self):
131         """ Go through a directory and precompile all the templates therein
132         """
133         for filename in os.listdir(self.dir):
134             # skip subdirs
135             if os.path.isdir(filename):
136                 continue
138             # skip files without ".html" or ".xml" extension - .css, .js etc.
139             for extension in '.html', '.xml':
140                 if filename.endswith(extension):
141                     break
142             else:
143                 continue
145             # remove extension
146             filename = filename[:-len(extension)]
148             # load the template
149             if '.' in filename:
150                 name, extension = filename.split('.', 1)
151                 self.get(name, extension)
152             else:
153                 self.get(filename, None)
155     def get(self, name, extension=None):
156         """ Interface to get a template, possibly loading a compiled template.
158             "name" and "extension" indicate the template we're after, which in
159             most cases will be "name.extension". If "extension" is None, then
160             we look for a template just called "name" with no extension.
162             If the file "name.extension" doesn't exist, we look for
163             "_generic.extension" as a fallback.
164         """
165         # default the name to "home"
166         if name is None:
167             name = 'home'
168         elif extension is None and '.' in name:
169             # split name
170             name, extension = name.split('.')
172         # find the source
173         src, filename = find_template(self.dir, name, extension)
175         # has it changed?
176         try:
177             stime = os.stat(src)[os.path.stat.ST_MTIME]
178         except os.error, error:
179             if error.errno != errno.ENOENT:
180                 raise
182         if self.templates.has_key(src) and \
183                 stime <= self.templates[src].mtime:
184             # compiled template is up to date
185             return self.templates[src]
187         # compile the template
188         pt = RoundupPageTemplate()
189         # use pt_edit so we can pass the content_type guess too
190         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
191         pt.pt_edit(open(src).read(), content_type)
192         pt.id = filename
193         pt.mtime = stime
194         # Add it to the cache.  We cannot do this until the template
195         # is fully initialized, as we could otherwise have a race
196         # condition when running with multiple threads:
197         #
198         # 1. Thread A notices the template is not in the cache,
199         #    adds it, but has not yet set "mtime".
200         #
201         # 2. Thread B notices the template is in the cache, checks
202         #    "mtime" (above) and crashes.
203         #
204         # Since Python dictionary access is atomic, as long as we
205         # insert "pt" only after it is fully initialized, we avoid
206         # this race condition.  It's possible that two separate
207         # threads will both do the work of initializing the template,
208         # but the risk of wasted work is offset by avoiding a lock.
209         self.templates[src] = pt
210         return pt
212     def __getitem__(self, name):
213         name, extension = os.path.splitext(name)
214         if extension:
215             extension = extension[1:]
216         try:
217             return self.get(name, extension)
218         except NoTemplate, message:
219             raise KeyError, message
221 def context(client, template=None, classname=None, request=None):
222     """Return the rendering context dictionary
224     The dictionary includes following symbols:
226     *context*
227      this is one of three things:
229      1. None - we're viewing a "home" page
230      2. The current class of item being displayed. This is an HTMLClass
231         instance.
232      3. The current item from the database, if we're viewing a specific
233         item, as an HTMLItem instance.
235     *request*
236       Includes information about the current request, including:
238        - the url
239        - the current index information (``filterspec``, ``filter`` args,
240          ``properties``, etc) parsed out of the form.
241        - methods for easy filterspec link generation
242        - *user*, the current user node as an HTMLItem instance
243        - *form*, the current CGI form information as a FieldStorage
245     *config*
246       The current tracker config.
248     *db*
249       The current database, used to access arbitrary database items.
251     *utils*
252       This is a special class that has its base in the TemplatingUtils
253       class in this file. If the tracker interfaces module defines a
254       TemplatingUtils class then it is mixed in, overriding the methods
255       in the base class.
257     *templates*
258       Access to all the tracker templates by name.
259       Used mainly in *use-macro* commands.
261     *template*
262       Current rendering template.
264     *true*
265       Logical True value.
267     *false*
268       Logical False value.
270     *i18n*
271       Internationalization service, providing string translation
272       methods ``gettext`` and ``ngettext``.
274     """
275     # construct the TemplatingUtils class
276     utils = TemplatingUtils
277     if (hasattr(client.instance, 'interfaces') and
278             hasattr(client.instance.interfaces, 'TemplatingUtils')):
279         class utils(client.instance.interfaces.TemplatingUtils, utils):
280             pass
282     # if template, classname and/or request are not passed explicitely,
283     # compute form client
284     if template is None:
285         template = client.template
286     if classname is None:
287         classname = client.classname
288     if request is None:
289         request = HTMLRequest(client)
291     c = {
292          'context': None,
293          'options': {},
294          'nothing': None,
295          'request': request,
296          'db': HTMLDatabase(client),
297          'config': client.instance.config,
298          'tracker': client.instance,
299          'utils': utils(client),
300          'templates': client.instance.templates,
301          'template': template,
302          'true': 1,
303          'false': 0,
304          'i18n': client.translator
305     }
306     # add in the item if there is one
307     if client.nodeid:
308         c['context'] = HTMLItem(client, classname, client.nodeid,
309             anonymous=1)
310     elif client.db.classes.has_key(classname):
311         c['context'] = HTMLClass(client, classname, anonymous=1)
312     return c
314 class RoundupPageTemplate(PageTemplate.PageTemplate):
315     """A Roundup-specific PageTemplate.
317     Interrogate the client to set up Roundup-specific template variables
318     to be available.  See 'context' function for the list of variables.
320     """
322     # 06-jun-2004 [als] i am not sure if this method is used yet
323     def getContext(self, client, classname, request):
324         return context(client, self, classname, request)
326     def render(self, client, classname, request, **options):
327         """Render this Page Template"""
329         if not self._v_cooked:
330             self._cook()
332         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
334         if self._v_errors:
335             raise PageTemplate.PTRuntimeError, \
336                 'Page Template %s has errors.'%self.id
338         # figure the context
339         c = context(client, self, classname, request)
340         c.update({'options': options})
342         # and go
343         output = StringIO.StringIO()
344         TALInterpreter.TALInterpreter(self._v_program, self.macros,
345             getEngine().getContext(c), output, tal=1, strictinsert=0)()
346         return output.getvalue()
348     def __repr__(self):
349         return '<Roundup PageTemplate %r>'%self.id
351 class HTMLDatabase:
352     """ Return HTMLClasses for valid class fetches
353     """
354     def __init__(self, client):
355         self._client = client
356         self._ = client._
357         self._db = client.db
359         # we want config to be exposed
360         self.config = client.db.config
362     def __getitem__(self, item, desre=re.compile(r'(?P<cl>[a-zA-Z_]+)(?P<id>[-\d]+)')):
363         # check to see if we're actually accessing an item
364         m = desre.match(item)
365         if m:
366             cl = m.group('cl')
367             self._client.db.getclass(cl)
368             return HTMLItem(self._client, cl, m.group('id'))
369         else:
370             self._client.db.getclass(item)
371             return HTMLClass(self._client, item)
373     def __getattr__(self, attr):
374         try:
375             return self[attr]
376         except KeyError:
377             raise AttributeError, attr
379     def classes(self):
380         l = self._client.db.classes.keys()
381         l.sort()
382         m = []
383         for item in l:
384             m.append(HTMLClass(self._client, item))
385         return m
387 num_re = re.compile('^-?\d+$')
389 def lookupIds(db, prop, ids, fail_ok=0, num_re=num_re, do_lookup=True):
390     """ "fail_ok" should be specified if we wish to pass through bad values
391         (most likely form values that we wish to represent back to the user)
392         "do_lookup" is there for preventing lookup by key-value (if we
393         know that the value passed *is* an id)
394     """
395     cl = db.getclass(prop.classname)
396     l = []
397     for entry in ids:
398         if do_lookup:
399             try:
400                 item = cl.lookup(entry)
401             except (TypeError, KeyError):
402                 pass
403             else:
404                 l.append(item)
405                 continue
406         # if fail_ok, ignore lookup error
407         # otherwise entry must be existing object id rather than key value
408         if fail_ok or num_re.match(entry):
409             l.append(entry)
410     return l
412 def lookupKeys(linkcl, key, ids, num_re=num_re):
413     """ Look up the "key" values for "ids" list - though some may already
414     be key values, not ids.
415     """
416     l = []
417     for entry in ids:
418         if num_re.match(entry):
419             label = linkcl.get(entry, key)
420             # fall back to designator if label is None
421             if label is None: label = '%s%s'%(linkcl.classname, entry)
422             l.append(label)
423         else:
424             l.append(entry)
425     return l
427 def _set_input_default_args(dic):
428     # 'text' is the default value anyway --
429     # but for CSS usage it should be present
430     dic.setdefault('type', 'text')
431     # useful e.g for HTML LABELs:
432     if not dic.has_key('id'):
433         try:
434             if dic['text'] in ('radio', 'checkbox'):
435                 dic['id'] = '%(name)s-%(value)s' % dic
436             else:
437                 dic['id'] = dic['name']
438         except KeyError:
439             pass
441 def cgi_escape_attrs(**attrs):
442     return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
443         for k,v in attrs.items()])
445 def input_html4(**attrs):
446     """Generate an 'input' (html4) element with given attributes"""
447     _set_input_default_args(attrs)
448     return '<input %s>'%cgi_escape_attrs(**attrs)
450 def input_xhtml(**attrs):
451     """Generate an 'input' (xhtml) element with given attributes"""
452     _set_input_default_args(attrs)
453     return '<input %s/>'%cgi_escape_attrs(**attrs)
455 class HTMLInputMixin:
456     """ requires a _client property """
457     def __init__(self):
458         html_version = 'html4'
459         if hasattr(self._client.instance.config, 'HTML_VERSION'):
460             html_version = self._client.instance.config.HTML_VERSION
461         if html_version == 'xhtml':
462             self.input = input_xhtml
463         else:
464             self.input = input_html4
465         # self._context is used for translations.
466         # will be initialized by the first call to .gettext()
467         self._context = None
469     def gettext(self, msgid):
470         """Return the localized translation of msgid"""
471         if self._context is None:
472             self._context = context(self._client)
473         return self._client.translator.translate(domain="roundup",
474             msgid=msgid, context=self._context)
476     _ = gettext
478 class HTMLPermissions:
480     def view_check(self):
481         """ Raise the Unauthorised exception if the user's not permitted to
482             view this class.
483         """
484         if not self.is_view_ok():
485             raise Unauthorised("view", self._classname,
486                 translator=self._client.translator)
488     def edit_check(self):
489         """ Raise the Unauthorised exception if the user's not permitted to
490             edit items of this class.
491         """
492         if not self.is_edit_ok():
493             raise Unauthorised("edit", self._classname,
494                 translator=self._client.translator)
496     def retire_check(self):
497         """ Raise the Unauthorised exception if the user's not permitted to
498             retire items of this class.
499         """
500         if not self.is_retire_ok():
501             raise Unauthorised("retire", self._classname,
502                 translator=self._client.translator)
505 class HTMLClass(HTMLInputMixin, HTMLPermissions):
506     """ Accesses through a class (either through *class* or *db.<classname>*)
507     """
508     def __init__(self, client, classname, anonymous=0):
509         self._client = client
510         self._ = client._
511         self._db = client.db
512         self._anonymous = anonymous
514         # we want classname to be exposed, but _classname gives a
515         # consistent API for extending Class/Item
516         self._classname = self.classname = classname
517         self._klass = self._db.getclass(self.classname)
518         self._props = self._klass.getprops()
520         HTMLInputMixin.__init__(self)
522     def is_edit_ok(self):
523         """ Is the user allowed to Create the current class?
524         """
525         perm = self._db.security.hasPermission
526         return perm('Web Access', self._client.userid) and perm('Create',
527             self._client.userid, self._classname)
529     def is_retire_ok(self):
530         """ Is the user allowed to retire items of the current class?
531         """
532         perm = self._db.security.hasPermission
533         return perm('Web Access', self._client.userid) and perm('Retire',
534             self._client.userid, self._classname)
536     def is_view_ok(self):
537         """ Is the user allowed to View the current class?
538         """
539         perm = self._db.security.hasPermission
540         return perm('Web Access', self._client.userid) and perm('View',
541             self._client.userid, self._classname)
543     def is_only_view_ok(self):
544         """ Is the user only allowed to View (ie. not Create) the current class?
545         """
546         return self.is_view_ok() and not self.is_edit_ok()
548     def __repr__(self):
549         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
551     def __getitem__(self, item):
552         """ return an HTMLProperty instance
553         """
555         # we don't exist
556         if item == 'id':
557             return None
559         # get the property
560         try:
561             prop = self._props[item]
562         except KeyError:
563             raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
565         # look up the correct HTMLProperty class
566         form = self._client.form
567         for klass, htmlklass in propclasses:
568             if not isinstance(prop, klass):
569                 continue
570             value = prop.get_default_value()
571             return htmlklass(self._client, self._classname, None, prop, item,
572                 value, self._anonymous)
574         # no good
575         raise KeyError, item
577     def __getattr__(self, attr):
578         """ convenience access """
579         try:
580             return self[attr]
581         except KeyError:
582             raise AttributeError, attr
584     def designator(self):
585         """ Return this class' designator (classname) """
586         return self._classname
588     def getItem(self, itemid, num_re=num_re):
589         """ Get an item of this class by its item id.
590         """
591         # make sure we're looking at an itemid
592         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
593             itemid = self._klass.lookup(itemid)
595         return HTMLItem(self._client, self.classname, itemid)
597     def properties(self, sort=1):
598         """ Return HTMLProperty for all of this class' properties.
599         """
600         l = []
601         for name, prop in self._props.items():
602             for klass, htmlklass in propclasses:
603                 if isinstance(prop, klass):
604                     value = prop.get_default_value()
605                     l.append(htmlklass(self._client, self._classname, '',
606                                        prop, name, value, self._anonymous))
607         if sort:
608             l.sort(lambda a,b:cmp(a._name, b._name))
609         return l
611     def list(self, sort_on=None):
612         """ List all items in this class.
613         """
614         # get the list and sort it nicely
615         l = self._klass.list()
616         sortfunc = make_sort_function(self._db, self._classname, sort_on)
617         l.sort(sortfunc)
619         # check perms
620         check = self._client.db.security.hasPermission
621         userid = self._client.userid
622         if not check('Web Access', userid):
623             return []
625         l = [HTMLItem(self._client, self._classname, id) for id in l
626             if check('View', userid, self._classname, itemid=id)]
628         return l
630     def csv(self):
631         """ Return the items of this class as a chunk of CSV text.
632         """
633         props = self.propnames()
634         s = StringIO.StringIO()
635         writer = csv.writer(s)
636         writer.writerow(props)
637         check = self._client.db.security.hasPermission
638         userid = self._client.userid
639         if not check('Web Access', userid):
640             return ''
641         for nodeid in self._klass.list():
642             l = []
643             for name in props:
644                 # check permission to view this property on this item
645                 if not check('View', userid, itemid=nodeid,
646                         classname=self._klass.classname, property=name):
647                     raise Unauthorised('view', self._klass.classname,
648                         translator=self._client.translator)
649                 value = self._klass.get(nodeid, name)
650                 if value is None:
651                     l.append('')
652                 elif isinstance(value, type([])):
653                     l.append(':'.join(map(str, value)))
654                 else:
655                     l.append(str(self._klass.get(nodeid, name)))
656             writer.writerow(l)
657         return s.getvalue()
659     def propnames(self):
660         """ Return the list of the names of the properties of this class.
661         """
662         idlessprops = self._klass.getprops(protected=0).keys()
663         idlessprops.sort()
664         return ['id'] + idlessprops
666     def filter(self, request=None, filterspec={}, sort=[], group=[]):
667         """ Return a list of items from this class, filtered and sorted
668             by the current requested filterspec/filter/sort/group args
670             "request" takes precedence over the other three arguments.
671         """
672         security = self._db.security
673         userid = self._client.userid
674         if request is not None:
675             # for a request we asume it has already been
676             # security-filtered
677             filterspec = request.filterspec
678             sort = request.sort
679             group = request.group
680         else:
681             cn = self.classname
682             filterspec = security.filterFilterspec(userid, cn, filterspec)
683             sort = security.filterSortspec(userid, cn, sort)
684             group = security.filterSortspec(userid, cn, group)
686         check = security.hasPermission
687         if not check('Web Access', userid):
688             return []
690         l = [HTMLItem(self._client, self.classname, id)
691              for id in self._klass.filter(None, filterspec, sort, group)
692              if check('View', userid, self.classname, itemid=id)]
693         return l
695     def classhelp(self, properties=None, label=''"(list)", width='500',
696             height='400', property='', form='itemSynopsis',
697             pagesize=50, inputtype="checkbox", sort=None, filter=None):
698         """Pop up a javascript window with class help
700         This generates a link to a popup window which displays the
701         properties indicated by "properties" of the class named by
702         "classname". The "properties" should be a comma-separated list
703         (eg. 'id,name,description'). Properties defaults to all the
704         properties of a class (excluding id, creator, created and
705         activity).
707         You may optionally override the label displayed, the width,
708         the height, the number of items per page and the field on which
709         the list is sorted (defaults to username if in the displayed
710         properties).
712         With the "filter" arg it is possible to specify a filter for
713         which items are supposed to be displayed. It has to be of
714         the format "<field>=<values>;<field>=<values>;...".
716         The popup window will be resizable and scrollable.
718         If the "property" arg is given, it's passed through to the
719         javascript help_window function.
721         You can use inputtype="radio" to display a radio box instead
722         of the default checkbox (useful for entering Link-properties)
724         If the "form" arg is given, it's passed through to the
725         javascript help_window function. - it's the name of the form
726         the "property" belongs to.
727         """
728         if properties is None:
729             properties = self._klass.getprops(protected=0).keys()
730             properties.sort()
731             properties = ','.join(properties)
732         if sort is None:
733             if 'username' in properties.split( ',' ):
734                 sort = 'username'
735             else:
736                 sort = self._klass.orderprop()
737         sort = '&amp;@sort=' + sort
738         if property:
739             property = '&amp;property=%s'%property
740         if form:
741             form = '&amp;form=%s'%form
742         if inputtype:
743             type= '&amp;type=%s'%inputtype
744         if filter:
745             filterprops = filter.split(';')
746             filtervalues = []
747             names = []
748             for x in filterprops:
749                 (name, values) = x.split('=')
750                 names.append(name)
751                 filtervalues.append('&amp;%s=%s' % (name, urllib.quote(values)))
752             filter = '&amp;@filter=%s%s' % (','.join(names), ''.join(filtervalues))
753         else:
754            filter = ''
755         help_url = "%s?@startwith=0&amp;@template=help&amp;"\
756                    "properties=%s%s%s%s%s&amp;@pagesize=%s%s" % \
757                    (self.classname, properties, property, form, type,
758                    sort, pagesize, filter)
759         onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
760                   (help_url, width, height)
761         return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
762                (help_url, onclick, self._(label))
764     def submit(self, label=''"Submit New Entry", action="new"):
765         """ Generate a submit button (and action hidden element)
767         Generate nothing if we're not editable.
768         """
769         if not self.is_edit_ok():
770             return ''
772         return self.input(type="hidden", name="@action", value=action) + \
773             '\n' + \
774             self.input(type="submit", name="submit_button", value=self._(label))
776     def history(self):
777         if not self.is_view_ok():
778             return self._('[hidden]')
779         return self._('New node - no history')
781     def renderWith(self, name, **kwargs):
782         """ Render this class with the given template.
783         """
784         # create a new request and override the specified args
785         req = HTMLRequest(self._client)
786         req.classname = self.classname
787         req.update(kwargs)
789         # new template, using the specified classname and request
790         pt = self._client.instance.templates.get(self.classname, name)
792         # use our fabricated request
793         args = {
794             'ok_message': self._client.ok_message,
795             'error_message': self._client.error_message
796         }
797         return pt.render(self._client, self.classname, req, **args)
799 class _HTMLItem(HTMLInputMixin, HTMLPermissions):
800     """ Accesses through an *item*
801     """
802     def __init__(self, client, classname, nodeid, anonymous=0):
803         self._client = client
804         self._db = client.db
805         self._classname = classname
806         self._nodeid = nodeid
807         self._klass = self._db.getclass(classname)
808         self._props = self._klass.getprops()
810         # do we prefix the form items with the item's identification?
811         self._anonymous = anonymous
813         HTMLInputMixin.__init__(self)
815     def is_edit_ok(self):
816         """ Is the user allowed to Edit this item?
817         """
818         perm = self._db.security.hasPermission
819         return perm('Web Access', self._client.userid) and perm('Edit',
820             self._client.userid, self._classname, itemid=self._nodeid)
822     def is_retire_ok(self):
823         """ Is the user allowed to Reture this item?
824         """
825         perm = self._db.security.hasPermission
826         return perm('Web Access', self._client.userid) and perm('Retire',
827             self._client.userid, self._classname, itemid=self._nodeid)
829     def is_view_ok(self):
830         """ Is the user allowed to View this item?
831         """
832         perm = self._db.security.hasPermission
833         if perm('Web Access', self._client.userid) and perm('View',
834                 self._client.userid, self._classname, itemid=self._nodeid):
835             return 1
836         return self.is_edit_ok()
838     def is_only_view_ok(self):
839         """ Is the user only allowed to View (ie. not Edit) this item?
840         """
841         return self.is_view_ok() and not self.is_edit_ok()
843     def __repr__(self):
844         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
845             self._nodeid)
847     def __getitem__(self, item):
848         """ return an HTMLProperty instance
849             this now can handle transitive lookups where item is of the
850             form x.y.z
851         """
852         if item == 'id':
853             return self._nodeid
855         items = item.split('.', 1)
856         has_rest = len(items) > 1
858         # get the property
859         prop = self._props[items[0]]
861         if has_rest and not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)):
862             raise KeyError, item
864         # get the value, handling missing values
865         value = None
866         if int(self._nodeid) > 0:
867             value = self._klass.get(self._nodeid, items[0], None)
868         if value is None:
869             if isinstance(prop, hyperdb.Multilink):
870                 value = []
872         # look up the correct HTMLProperty class
873         htmlprop = None
874         for klass, htmlklass in propclasses:
875             if isinstance(prop, klass):
876                 htmlprop = htmlklass(self._client, self._classname,
877                     self._nodeid, prop, items[0], value, self._anonymous)
878         if htmlprop is not None:
879             if has_rest:
880                 if isinstance(htmlprop, MultilinkHTMLProperty):
881                     return [h[items[1]] for h in htmlprop]
882                 return htmlprop[items[1]]
883             return htmlprop
885         raise KeyError, item
887     def __getattr__(self, attr):
888         """ convenience access to properties """
889         try:
890             return self[attr]
891         except KeyError:
892             raise AttributeError, attr
894     def designator(self):
895         """Return this item's designator (classname + id)."""
896         return '%s%s'%(self._classname, self._nodeid)
898     def is_retired(self):
899         """Is this item retired?"""
900         return self._klass.is_retired(self._nodeid)
902     def submit(self, label=''"Submit Changes", action="edit"):
903         """Generate a submit button.
905         Also sneak in the lastactivity and action hidden elements.
906         """
907         return self.input(type="hidden", name="@lastactivity",
908             value=self.activity.local(0)) + '\n' + \
909             self.input(type="hidden", name="@action", value=action) + '\n' + \
910             self.input(type="submit", name="submit_button", value=self._(label))
912     def journal(self, direction='descending'):
913         """ Return a list of HTMLJournalEntry instances.
914         """
915         # XXX do this
916         return []
918     def history(self, direction='descending', dre=re.compile('^\d+$'),
919             limit=None):
920         if not self.is_view_ok():
921             return self._('[hidden]')
923         # pre-load the history with the current state
924         current = {}
925         for prop_n in self._props.keys():
926             prop = self[prop_n]
927             if not isinstance(prop, HTMLProperty):
928                 continue
929             current[prop_n] = prop.plain(escape=1)
930             # make link if hrefable
931             if (self._props.has_key(prop_n) and
932                     isinstance(self._props[prop_n], hyperdb.Link)):
933                 classname = self._props[prop_n].classname
934                 try:
935                     template = find_template(self._db.config.TEMPLATES,
936                         classname, 'item')
937                     if template[1].startswith('_generic'):
938                         raise NoTemplate, 'not really...'
939                 except NoTemplate:
940                     pass
941                 else:
942                     id = self._klass.get(self._nodeid, prop_n, None)
943                     current[prop_n] = '<a href="%s%s">%s</a>'%(
944                         classname, id, current[prop_n])
946         # get the journal, sort and reverse
947         history = self._klass.history(self._nodeid)
948         history.sort()
949         history.reverse()
951         # restrict the volume
952         if limit:
953             history = history[:limit]
955         timezone = self._db.getUserTimezone()
956         l = []
957         comments = {}
958         for id, evt_date, user, action, args in history:
959             date_s = str(evt_date.local(timezone)).replace("."," ")
960             arg_s = ''
961             if action == 'link' and type(args) == type(()):
962                 if len(args) == 3:
963                     linkcl, linkid, key = args
964                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
965                         linkcl, linkid, key)
966                 else:
967                     arg_s = str(args)
969             elif action == 'unlink' and type(args) == type(()):
970                 if len(args) == 3:
971                     linkcl, linkid, key = args
972                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
973                         linkcl, linkid, key)
974                 else:
975                     arg_s = str(args)
977             elif type(args) == type({}):
978                 cell = []
979                 for k in args.keys():
980                     # try to get the relevant property and treat it
981                     # specially
982                     try:
983                         prop = self._props[k]
984                     except KeyError:
985                         prop = None
986                     if prop is None:
987                         # property no longer exists
988                         comments['no_exist'] = self._(
989                             "<em>The indicated property no longer exists</em>")
990                         cell.append(self._('<em>%s: %s</em>\n')
991                             % (self._(k), str(args[k])))
992                         continue
994                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
995                             isinstance(prop, hyperdb.Link)):
996                         # figure what the link class is
997                         classname = prop.classname
998                         try:
999                             linkcl = self._db.getclass(classname)
1000                         except KeyError:
1001                             labelprop = None
1002                             comments[classname] = self._(
1003                                 "The linked class %(classname)s no longer exists"
1004                             ) % locals()
1005                         labelprop = linkcl.labelprop(1)
1006                         try:
1007                             template = find_template(self._db.config.TEMPLATES,
1008                                 classname, 'item')
1009                             if template[1].startswith('_generic'):
1010                                 raise NoTemplate, 'not really...'
1011                             hrefable = 1
1012                         except NoTemplate:
1013                             hrefable = 0
1015                     if isinstance(prop, hyperdb.Multilink) and args[k]:
1016                         ml = []
1017                         for linkid in args[k]:
1018                             if isinstance(linkid, type(())):
1019                                 sublabel = linkid[0] + ' '
1020                                 linkids = linkid[1]
1021                             else:
1022                                 sublabel = ''
1023                                 linkids = [linkid]
1024                             subml = []
1025                             for linkid in linkids:
1026                                 label = classname + linkid
1027                                 # if we have a label property, try to use it
1028                                 # TODO: test for node existence even when
1029                                 # there's no labelprop!
1030                                 try:
1031                                     if labelprop is not None and \
1032                                             labelprop != 'id':
1033                                         label = linkcl.get(linkid, labelprop)
1034                                         label = cgi.escape(label)
1035                                 except IndexError:
1036                                     comments['no_link'] = self._(
1037                                         "<strike>The linked node"
1038                                         " no longer exists</strike>")
1039                                     subml.append('<strike>%s</strike>'%label)
1040                                 else:
1041                                     if hrefable:
1042                                         subml.append('<a href="%s%s">%s</a>'%(
1043                                             classname, linkid, label))
1044                                     elif label is None:
1045                                         subml.append('%s%s'%(classname,
1046                                             linkid))
1047                                     else:
1048                                         subml.append(label)
1049                             ml.append(sublabel + ', '.join(subml))
1050                         cell.append('%s:\n  %s'%(self._(k), ', '.join(ml)))
1051                     elif isinstance(prop, hyperdb.Link) and args[k]:
1052                         label = classname + args[k]
1053                         # if we have a label property, try to use it
1054                         # TODO: test for node existence even when
1055                         # there's no labelprop!
1056                         if labelprop is not None and labelprop != 'id':
1057                             try:
1058                                 label = cgi.escape(linkcl.get(args[k],
1059                                     labelprop))
1060                             except IndexError:
1061                                 comments['no_link'] = self._(
1062                                     "<strike>The linked node"
1063                                     " no longer exists</strike>")
1064                                 cell.append(' <strike>%s</strike>,\n'%label)
1065                                 # "flag" this is done .... euwww
1066                                 label = None
1067                         if label is not None:
1068                             if hrefable:
1069                                 old = '<a href="%s%s">%s</a>'%(classname,
1070                                     args[k], label)
1071                             else:
1072                                 old = label;
1073                             cell.append('%s: %s' % (self._(k), old))
1074                             if current.has_key(k):
1075                                 cell[-1] += ' -> %s'%current[k]
1076                                 current[k] = old
1078                     elif isinstance(prop, hyperdb.Date) and args[k]:
1079                         if args[k] is None:
1080                             d = ''
1081                         else:
1082                             d = date.Date(args[k],
1083                                 translator=self._client).local(timezone)
1084                         cell.append('%s: %s'%(self._(k), str(d)))
1085                         if current.has_key(k):
1086                             cell[-1] += ' -> %s' % current[k]
1087                             current[k] = str(d)
1089                     elif isinstance(prop, hyperdb.Interval) and args[k]:
1090                         val = str(date.Interval(args[k],
1091                             translator=self._client))
1092                         cell.append('%s: %s'%(self._(k), val))
1093                         if current.has_key(k):
1094                             cell[-1] += ' -> %s'%current[k]
1095                             current[k] = val
1097                     elif isinstance(prop, hyperdb.String) and args[k]:
1098                         val = cgi.escape(args[k])
1099                         cell.append('%s: %s'%(self._(k), val))
1100                         if current.has_key(k):
1101                             cell[-1] += ' -> %s'%current[k]
1102                             current[k] = val
1104                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
1105                         val = args[k] and ''"Yes" or ''"No"
1106                         cell.append('%s: %s'%(self._(k), val))
1107                         if current.has_key(k):
1108                             cell[-1] += ' -> %s'%current[k]
1109                             current[k] = val
1111                     elif isinstance(prop, hyperdb.Password) and args[k] is not None:
1112                         val = args[k].dummystr()
1113                         cell.append('%s: %s'%(self._(k), val))
1114                         if current.has_key(k):
1115                             cell[-1] += ' -> %s'%current[k]
1116                             current[k] = val
1118                     elif not args[k]:
1119                         if current.has_key(k):
1120                             cell.append('%s: %s'%(self._(k), current[k]))
1121                             current[k] = '(no value)'
1122                         else:
1123                             cell.append(self._('%s: (no value)')%self._(k))
1125                     else:
1126                         cell.append('%s: %s'%(self._(k), str(args[k])))
1127                         if current.has_key(k):
1128                             cell[-1] += ' -> %s'%current[k]
1129                             current[k] = str(args[k])
1131                 arg_s = '<br />'.join(cell)
1132             else:
1133                 # unkown event!!
1134                 comments['unknown'] = self._(
1135                     "<strong><em>This event is not handled"
1136                     " by the history display!</em></strong>")
1137                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
1138             date_s = date_s.replace(' ', '&nbsp;')
1139             # if the user's an itemid, figure the username (older journals
1140             # have the username)
1141             if dre.match(user):
1142                 user = self._db.user.get(user, 'username')
1143             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
1144                 date_s, user, self._(action), arg_s))
1145         if comments:
1146             l.append(self._(
1147                 '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
1148         for entry in comments.values():
1149             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
1151         if direction == 'ascending':
1152             l.reverse()
1154         l[0:0] = ['<table class="history">'
1155              '<tr><th colspan="4" class="header">',
1156              self._('History'),
1157              '</th></tr><tr>',
1158              self._('<th>Date</th>'),
1159              self._('<th>User</th>'),
1160              self._('<th>Action</th>'),
1161              self._('<th>Args</th>'),
1162             '</tr>']
1163         l.append('</table>')
1164         return '\n'.join(l)
1166     def renderQueryForm(self):
1167         """ Render this item, which is a query, as a search form.
1168         """
1169         # create a new request and override the specified args
1170         req = HTMLRequest(self._client)
1171         req.classname = self._klass.get(self._nodeid, 'klass')
1172         name = self._klass.get(self._nodeid, 'name')
1173         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
1174             '&@queryname=%s'%urllib.quote(name))
1176         # new template, using the specified classname and request
1177         pt = self._client.instance.templates.get(req.classname, 'search')
1178         # The context for a search page should be the class, not any
1179         # node.
1180         self._client.nodeid = None
1182         # use our fabricated request
1183         return pt.render(self._client, req.classname, req)
1185     def download_url(self):
1186         """ Assume that this item is a FileClass and that it has a name
1187         and content. Construct a URL for the download of the content.
1188         """
1189         name = self._klass.get(self._nodeid, 'name')
1190         url = '%s%s/%s'%(self._classname, self._nodeid, name)
1191         return urllib.quote(url)
1193     def copy_url(self, exclude=("messages", "files")):
1194         """Construct a URL for creating a copy of this item
1196         "exclude" is an optional list of properties that should
1197         not be copied to the new object.  By default, this list
1198         includes "messages" and "files" properties.  Note that
1199         "id" property cannot be copied.
1201         """
1202         exclude = ("id", "activity", "actor", "creation", "creator") \
1203             + tuple(exclude)
1204         query = {
1205             "@template": "item",
1206             "@note": self._("Copy of %(class)s %(id)s") % {
1207                 "class": self._(self._classname), "id": self._nodeid},
1208         }
1209         for name in self._props.keys():
1210             if name not in exclude:
1211                 query[name] = self[name].plain()
1212         return self._classname + "?" + "&".join(
1213             ["%s=%s" % (key, urllib.quote(value))
1214                 for key, value in query.items()])
1216 class _HTMLUser(_HTMLItem):
1217     """Add ability to check for permissions on users.
1218     """
1219     _marker = []
1220     def hasPermission(self, permission, classname=_marker,
1221             property=None, itemid=None):
1222         """Determine if the user has the Permission.
1224         The class being tested defaults to the template's class, but may
1225         be overidden for this test by suppling an alternate classname.
1226         """
1227         if classname is self._marker:
1228             classname = self._client.classname
1229         return self._db.security.hasPermission(permission,
1230             self._nodeid, classname, property, itemid)
1232     def hasRole(self, *rolenames):
1233         """Determine whether the user has any role in rolenames."""
1234         return self._db.user.has_role(self._nodeid, *rolenames)
1236 def HTMLItem(client, classname, nodeid, anonymous=0):
1237     if classname == 'user':
1238         return _HTMLUser(client, classname, nodeid, anonymous)
1239     else:
1240         return _HTMLItem(client, classname, nodeid, anonymous)
1242 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
1243     """ String, Number, Date, Interval HTMLProperty
1245         Has useful attributes:
1247          _name  the name of the property
1248          _value the value of the property if any
1250         A wrapper object which may be stringified for the plain() behaviour.
1251     """
1252     def __init__(self, client, classname, nodeid, prop, name, value,
1253             anonymous=0):
1254         self._client = client
1255         self._db = client.db
1256         self._ = client._
1257         self._classname = classname
1258         self._nodeid = nodeid
1259         self._prop = prop
1260         self._value = value
1261         self._anonymous = anonymous
1262         self._name = name
1263         if not anonymous:
1264             if nodeid:
1265                 self._formname = '%s%s@%s'%(classname, nodeid, name)
1266             else:
1267                 # This case occurs when creating a property for a
1268                 # non-anonymous class.
1269                 self._formname = '%s@%s'%(classname, name)
1270         else:
1271             self._formname = name
1273         # If no value is already present for this property, see if one
1274         # is specified in the current form.
1275         form = self._client.form
1276         if not self._value and form.has_key(self._formname):
1277             if isinstance(prop, hyperdb.Multilink):
1278                 value = lookupIds(self._db, prop,
1279                                   handleListCGIValue(form[self._formname]),
1280                                   fail_ok=1)
1281             elif isinstance(prop, hyperdb.Link):
1282                 value = form.getfirst(self._formname).strip()
1283                 if value:
1284                     value = lookupIds(self._db, prop, [value],
1285                                       fail_ok=1)[0]
1286                 else:
1287                     value = None
1288             else:
1289                 value = form.getfirst(self._formname).strip() or None
1290             self._value = value
1292         HTMLInputMixin.__init__(self)
1294     def __repr__(self):
1295         classname = self.__class__.__name__
1296         return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
1297                                       self._prop, self._value)
1298     def __str__(self):
1299         return self.plain()
1300     def __cmp__(self, other):
1301         if isinstance(other, HTMLProperty):
1302             return cmp(self._value, other._value)
1303         return cmp(self._value, other)
1305     def __nonzero__(self):
1306         return not not self._value
1308     def isset(self):
1309         """Is my _value not None?"""
1310         return self._value is not None
1312     def is_edit_ok(self):
1313         """Should the user be allowed to use an edit form field for this
1314         property. Check "Create" for new items, or "Edit" for existing
1315         ones.
1316         """
1317         perm = self._db.security.hasPermission
1318         userid = self._client.userid
1319         if self._nodeid:
1320             if not perm('Web Access', userid):
1321                 return False
1322             return perm('Edit', userid, self._classname, self._name,
1323                 self._nodeid)
1324         return perm('Create', userid, self._classname, self._name) or \
1325             perm('Register', userid, self._classname, self._name)
1327     def is_view_ok(self):
1328         """ Is the user allowed to View the current class?
1329         """
1330         perm = self._db.security.hasPermission
1331         if perm('Web Access',  self._client.userid) and perm('View',
1332                 self._client.userid, self._classname, self._name, self._nodeid):
1333             return 1
1334         return self.is_edit_ok()
1336 class StringHTMLProperty(HTMLProperty):
1337     hyper_re = re.compile(r'''(
1338         (?P<url>
1339          (
1340           (ht|f)tp(s?)://                   # protocol
1341           ([\w]+(:\w+)?@)?                  # username/password
1342           ([\w\-]+)                         # hostname
1343           ((\.[\w-]+)+)?                    # .domain.etc
1344          |                                  # ... or ...
1345           ([\w]+(:\w+)?@)?                  # username/password
1346           www\.                             # "www."
1347           ([\w\-]+\.)+                      # hostname
1348           [\w]{2,5}                         # TLD
1349          )
1350          (:[\d]{1,5})?                     # port
1351          (/[\w\-$.+!*(),;:@&=?/~\\#%]*)?   # path etc.
1352         )|
1353         (?P<email>[-+=%/\w\.]+@[\w\.\-]+)|
1354         (?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+))
1355     )''', re.X | re.I)
1356     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
1360     def _hyper_repl(self, match):
1361         if match.group('url'):
1362             return self._hyper_repl_url(match, '<a href="%s">%s</a>%s')
1363         elif match.group('email'):
1364             return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
1365         elif len(match.group('id')) < 10:
1366             return self._hyper_repl_item(match,
1367                 '<a href="%(cls)s%(id)s">%(item)s</a>')
1368         else:
1369             # just return the matched text
1370             return match.group(0)
1372     def _hyper_repl_url(self, match, replacement):
1373         u = s = match.group('url')
1374         if not self.protocol_re.search(s):
1375             u = 'http://' + s
1376         end = ''
1377         if '&gt;' in s:
1378             # catch an escaped ">" in the URL
1379             pos = s.find('&gt;')
1380             end = s[pos:]
1381             u = s = s[:pos]
1382         if ')' in s and s.count('(') != s.count(')'):
1383             # don't include extraneous ')' in the link
1384             pos = s.rfind(')')
1385             end = s[pos:] + end
1386             u = s = s[:pos]
1387         return replacement % (u, s, end)
1389     def _hyper_repl_email(self, match, replacement):
1390         s = match.group('email')
1391         return replacement % (s, s)
1393     def _hyper_repl_item(self, match, replacement):
1394         item = match.group('item')
1395         cls = match.group('class').lower()
1396         id = match.group('id')
1397         try:
1398             # make sure cls is a valid tracker classname
1399             cl = self._db.getclass(cls)
1400             if not cl.hasnode(id):
1401                 return item
1402             return replacement % locals()
1403         except KeyError:
1404             return item
1407     def _hyper_repl_rst(self, match):
1408         if match.group('url'):
1409             s = match.group('url')
1410             return '`%s <%s>`_'%(s, s)
1411         elif match.group('email'):
1412             s = match.group('email')
1413             return '`%s <mailto:%s>`_'%(s, s)
1414         elif len(match.group('id')) < 10:
1415             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
1416         else:
1417             # just return the matched text
1418             return match.group(0)
1420     def hyperlinked(self):
1421         """ Render a "hyperlinked" version of the text """
1422         return self.plain(hyperlink=1)
1424     def plain(self, escape=0, hyperlink=0):
1425         """Render a "plain" representation of the property
1427         - "escape" turns on/off HTML quoting
1428         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1429           addresses and designators
1430         """
1431         if not self.is_view_ok():
1432             return self._('[hidden]')
1434         if self._value is None:
1435             return ''
1436         if escape:
1437             s = cgi.escape(str(self._value))
1438         else:
1439             s = str(self._value)
1440         if hyperlink:
1441             # no, we *must* escape this text
1442             if not escape:
1443                 s = cgi.escape(s)
1444             s = self.hyper_re.sub(self._hyper_repl, s)
1445         return s
1447     def wrapped(self, escape=1, hyperlink=1):
1448         """Render a "wrapped" representation of the property.
1450         We wrap long lines at 80 columns on the nearest whitespace. Lines
1451         with no whitespace are not broken to force wrapping.
1453         Note that unlike plain() we default wrapped() to have the escaping
1454         and hyperlinking turned on since that's the most common usage.
1456         - "escape" turns on/off HTML quoting
1457         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1458           addresses and designators
1459         """
1460         if not self.is_view_ok():
1461             return self._('[hidden]')
1463         if self._value is None:
1464             return ''
1465         s = support.wrap(str(self._value), width=80)
1466         if escape:
1467             s = cgi.escape(s)
1468         if hyperlink:
1469             # no, we *must* escape this text
1470             if not escape:
1471                 s = cgi.escape(s)
1472             s = self.hyper_re.sub(self._hyper_repl, s)
1473         return s
1475     def stext(self, escape=0, hyperlink=1):
1476         """ Render the value of the property as StructuredText.
1478             This requires the StructureText module to be installed separately.
1479         """
1480         if not self.is_view_ok():
1481             return self._('[hidden]')
1483         s = self.plain(escape=escape, hyperlink=hyperlink)
1484         if not StructuredText:
1485             return s
1486         return StructuredText(s,level=1,header=0)
1488     def rst(self, hyperlink=1):
1489         """ Render the value of the property as ReStructuredText.
1491             This requires docutils to be installed separately.
1492         """
1493         if not self.is_view_ok():
1494             return self._('[hidden]')
1496         if not ReStructuredText:
1497             return self.plain(escape=0, hyperlink=hyperlink)
1498         s = self.plain(escape=0, hyperlink=0)
1499         if hyperlink:
1500             s = self.hyper_re.sub(self._hyper_repl_rst, s)
1501         return ReStructuredText(s, writer_name="html")["html_body"].encode("utf-8",
1502             "replace")
1504     def field(self, **kwargs):
1505         """ Render the property as a field in HTML.
1507             If not editable, just display the value via plain().
1508         """
1509         if not self.is_edit_ok():
1510             return self.plain(escape=1)
1512         value = self._value
1513         if value is None:
1514             value = ''
1516         kwargs.setdefault("size", 30)
1517         kwargs.update({"name": self._formname, "value": value})
1518         return self.input(**kwargs)
1520     def multiline(self, escape=0, rows=5, cols=40, **kwargs):
1521         """ Render a multiline form edit field for the property.
1523             If not editable, just display the plain() value in a <pre> tag.
1524         """
1525         if not self.is_edit_ok():
1526             return '<pre>%s</pre>'%self.plain()
1528         if self._value is None:
1529             value = ''
1530         else:
1531             value = cgi.escape(str(self._value))
1533             value = '&quot;'.join(value.split('"'))
1534         name = self._formname
1535         passthrough_args = cgi_escape_attrs(**kwargs)
1536         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
1537                 ' rows="%(rows)s" cols="%(cols)s">'
1538                  '%(value)s</textarea>') % locals()
1540     def email(self, escape=1):
1541         """ Render the value of the property as an obscured email address
1542         """
1543         if not self.is_view_ok():
1544             return self._('[hidden]')
1546         if self._value is None:
1547             value = ''
1548         else:
1549             value = str(self._value)
1550         split = value.split('@')
1551         if len(split) == 2:
1552             name, domain = split
1553             domain = ' '.join(domain.split('.')[:-1])
1554             name = name.replace('.', ' ')
1555             value = '%s at %s ...'%(name, domain)
1556         else:
1557             value = value.replace('.', ' ')
1558         if escape:
1559             value = cgi.escape(value)
1560         return value
1562 class PasswordHTMLProperty(HTMLProperty):
1563     def plain(self, escape=0):
1564         """ Render a "plain" representation of the property
1565         """
1566         if not self.is_view_ok():
1567             return self._('[hidden]')
1569         if self._value is None:
1570             return ''
1571         value = self._value.dummystr()
1572         if escape:
1573             value = cgi.escape(value)
1574         return value
1576     def field(self, size=30, **kwargs):
1577         """ Render a form edit field for the property.
1579             If not editable, just display the value via plain().
1580         """
1581         if not self.is_edit_ok():
1582             return self.plain(escape=1)
1584         return self.input(type="password", name=self._formname, size=size,
1585                           **kwargs)
1587     def confirm(self, size=30):
1588         """ Render a second form edit field for the property, used for
1589             confirmation that the user typed the password correctly. Generates
1590             a field with name "@confirm@name".
1592             If not editable, display nothing.
1593         """
1594         if not self.is_edit_ok():
1595             return ''
1597         return self.input(type="password",
1598             name="@confirm@%s"%self._formname,
1599             id="%s-confirm"%self._formname,
1600             size=size)
1602 class NumberHTMLProperty(HTMLProperty):
1603     def plain(self, escape=0):
1604         """ Render a "plain" representation of the property
1605         """
1606         if not self.is_view_ok():
1607             return self._('[hidden]')
1609         if self._value is None:
1610             return ''
1612         return str(self._value)
1614     def field(self, size=30, **kwargs):
1615         """ Render a form edit field for the property.
1617             If not editable, just display the value via plain().
1618         """
1619         if not self.is_edit_ok():
1620             return self.plain(escape=1)
1622         value = self._value
1623         if value is None:
1624             value = ''
1626         return self.input(name=self._formname, value=value, size=size,
1627                           **kwargs)
1629     def __int__(self):
1630         """ Return an int of me
1631         """
1632         return int(self._value)
1634     def __float__(self):
1635         """ Return a float of me
1636         """
1637         return float(self._value)
1640 class BooleanHTMLProperty(HTMLProperty):
1641     def plain(self, escape=0):
1642         """ Render a "plain" representation of the property
1643         """
1644         if not self.is_view_ok():
1645             return self._('[hidden]')
1647         if self._value is None:
1648             return ''
1649         return self._value and self._("Yes") or self._("No")
1651     def field(self, **kwargs):
1652         """ Render a form edit field for the property
1654             If not editable, just display the value via plain().
1655         """
1656         if not self.is_edit_ok():
1657             return self.plain(escape=1)
1659         value = self._value
1660         if isinstance(value, str) or isinstance(value, unicode):
1661             value = value.strip().lower() in ('checked', 'yes', 'true',
1662                 'on', '1')
1664         checked = value and "checked" or ""
1665         if value:
1666             s = self.input(type="radio", name=self._formname, value="yes",
1667                 checked="checked", **kwargs)
1668             s += self._('Yes')
1669             s +=self.input(type="radio", name=self._formname,  value="no",
1670                            **kwargs)
1671             s += self._('No')
1672         else:
1673             s = self.input(type="radio", name=self._formname,  value="yes",
1674                            **kwargs)
1675             s += self._('Yes')
1676             s +=self.input(type="radio", name=self._formname, value="no",
1677                 checked="checked", **kwargs)
1678             s += self._('No')
1679         return s
1681 class DateHTMLProperty(HTMLProperty):
1683     _marker = []
1685     def __init__(self, client, classname, nodeid, prop, name, value,
1686             anonymous=0, offset=None):
1687         HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
1688                 value, anonymous=anonymous)
1689         if self._value and not (isinstance(self._value, str) or
1690                 isinstance(self._value, unicode)):
1691             self._value.setTranslator(self._client.translator)
1692         self._offset = offset
1693         if self._offset is None :
1694             self._offset = self._prop.offset (self._db)
1696     def plain(self, escape=0):
1697         """ Render a "plain" representation of the property
1698         """
1699         if not self.is_view_ok():
1700             return self._('[hidden]')
1702         if self._value is None:
1703             return ''
1704         if self._offset is None:
1705             offset = self._db.getUserTimezone()
1706         else:
1707             offset = self._offset
1708         return str(self._value.local(offset))
1710     def now(self, str_interval=None):
1711         """ Return the current time.
1713             This is useful for defaulting a new value. Returns a
1714             DateHTMLProperty.
1715         """
1716         if not self.is_view_ok():
1717             return self._('[hidden]')
1719         ret = date.Date('.', translator=self._client)
1721         if isinstance(str_interval, basestring):
1722             sign = 1
1723             if str_interval[0] == '-':
1724                 sign = -1
1725                 str_interval = str_interval[1:]
1726             interval = date.Interval(str_interval, translator=self._client)
1727             if sign > 0:
1728                 ret = ret + interval
1729             else:
1730                 ret = ret - interval
1732         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1733             self._prop, self._formname, ret)
1735     def field(self, size=30, default=None, format=_marker, popcal=True,
1736               **kwargs):
1737         """Render a form edit field for the property
1739         If not editable, just display the value via plain().
1741         If "popcal" then include the Javascript calendar editor.
1742         Default=yes.
1744         The format string is a standard python strftime format string.
1745         """
1746         if not self.is_edit_ok():
1747             if format is self._marker:
1748                 return self.plain(escape=1)
1749             else:
1750                 return self.pretty(format)
1752         value = self._value
1754         if value is None:
1755             if default is None:
1756                 raw_value = None
1757             else:
1758                 if isinstance(default, basestring):
1759                     raw_value = date.Date(default, translator=self._client)
1760                 elif isinstance(default, date.Date):
1761                     raw_value = default
1762                 elif isinstance(default, DateHTMLProperty):
1763                     raw_value = default._value
1764                 else:
1765                     raise ValueError, self._('default value for '
1766                         'DateHTMLProperty must be either DateHTMLProperty '
1767                         'or string date representation.')
1768         elif isinstance(value, str) or isinstance(value, unicode):
1769             # most likely erroneous input to be passed back to user
1770             if isinstance(value, unicode): value = value.encode('utf8')
1771             return self.input(name=self._formname, value=value, size=size,
1772                               **kwargs)
1773         else:
1774             raw_value = value
1776         if raw_value is None:
1777             value = ''
1778         elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
1779             if format is self._marker:
1780                 value = raw_value
1781             else:
1782                 value = date.Date(raw_value).pretty(format)
1783         else:
1784             if self._offset is None :
1785                 offset = self._db.getUserTimezone()
1786             else :
1787                 offset = self._offset
1788             value = raw_value.local(offset)
1789             if format is not self._marker:
1790                 value = value.pretty(format)
1792         s = self.input(name=self._formname, value=value, size=size,
1793                        **kwargs)
1794         if popcal:
1795             s += self.popcal()
1796         return s
1798     def reldate(self, pretty=1):
1799         """ Render the interval between the date and now.
1801             If the "pretty" flag is true, then make the display pretty.
1802         """
1803         if not self.is_view_ok():
1804             return self._('[hidden]')
1806         if not self._value:
1807             return ''
1809         # figure the interval
1810         interval = self._value - date.Date('.', translator=self._client)
1811         if pretty:
1812             return interval.pretty()
1813         return str(interval)
1815     def pretty(self, format=_marker):
1816         """ Render the date in a pretty format (eg. month names, spaces).
1818             The format string is a standard python strftime format string.
1819             Note that if the day is zero, and appears at the start of the
1820             string, then it'll be stripped from the output. This is handy
1821             for the situation when a date only specifies a month and a year.
1822         """
1823         if not self.is_view_ok():
1824             return self._('[hidden]')
1826         if self._offset is None:
1827             offset = self._db.getUserTimezone()
1828         else:
1829             offset = self._offset
1831         if not self._value:
1832             return ''
1833         elif format is not self._marker:
1834             return self._value.local(offset).pretty(format)
1835         else:
1836             return self._value.local(offset).pretty()
1838     def local(self, offset):
1839         """ Return the date/time as a local (timezone offset) date/time.
1840         """
1841         if not self.is_view_ok():
1842             return self._('[hidden]')
1844         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1845             self._prop, self._formname, self._value, offset=offset)
1847     def popcal(self, width=300, height=200, label="(cal)",
1848             form="itemSynopsis"):
1849         """Generate a link to a calendar pop-up window.
1851         item: HTMLProperty e.g.: context.deadline
1852         """
1853         if self.isset():
1854             date = "&date=%s"%self._value
1855         else :
1856             date = ""
1857         return ('<a class="classhelp" href="javascript:help_window('
1858             "'%s?@template=calendar&amp;property=%s&amp;form=%s%s', %d, %d)"
1859             '">%s</a>'%(self._classname, self._name, form, date, width,
1860             height, label))
1862 class IntervalHTMLProperty(HTMLProperty):
1863     def __init__(self, client, classname, nodeid, prop, name, value,
1864             anonymous=0):
1865         HTMLProperty.__init__(self, client, classname, nodeid, prop,
1866             name, value, anonymous)
1867         if self._value and not isinstance(self._value, (str, unicode)):
1868             self._value.setTranslator(self._client.translator)
1870     def plain(self, escape=0):
1871         """ Render a "plain" representation of the property
1872         """
1873         if not self.is_view_ok():
1874             return self._('[hidden]')
1876         if self._value is None:
1877             return ''
1878         return str(self._value)
1880     def pretty(self):
1881         """ Render the interval in a pretty format (eg. "yesterday")
1882         """
1883         if not self.is_view_ok():
1884             return self._('[hidden]')
1886         return self._value.pretty()
1888     def field(self, size=30, **kwargs):
1889         """ Render a form edit field for the property
1891             If not editable, just display the value via plain().
1892         """
1893         if not self.is_edit_ok():
1894             return self.plain(escape=1)
1896         value = self._value
1897         if value is None:
1898             value = ''
1900         return self.input(name=self._formname, value=value, size=size,
1901                           **kwargs)
1903 class LinkHTMLProperty(HTMLProperty):
1904     """ Link HTMLProperty
1905         Include the above as well as being able to access the class
1906         information. Stringifying the object itself results in the value
1907         from the item being displayed. Accessing attributes of this object
1908         result in the appropriate entry from the class being queried for the
1909         property accessed (so item/assignedto/name would look up the user
1910         entry identified by the assignedto property on item, and then the
1911         name property of that user)
1912     """
1913     def __init__(self, *args, **kw):
1914         HTMLProperty.__init__(self, *args, **kw)
1915         # if we're representing a form value, then the -1 from the form really
1916         # should be a None
1917         if str(self._value) == '-1':
1918             self._value = None
1920     def __getattr__(self, attr):
1921         """ return a new HTMLItem """
1922         if not self._value:
1923             # handle a special page templates lookup
1924             if attr == '__render_with_namespace__':
1925                 def nothing(*args, **kw):
1926                     return ''
1927                 return nothing
1928             msg = self._('Attempt to look up %(attr)s on a missing value')
1929             return MissingValue(msg%locals())
1930         i = HTMLItem(self._client, self._prop.classname, self._value)
1931         return getattr(i, attr)
1933     def plain(self, escape=0):
1934         """ Render a "plain" representation of the property
1935         """
1936         if not self.is_view_ok():
1937             return self._('[hidden]')
1939         if self._value is None:
1940             return ''
1941         linkcl = self._db.classes[self._prop.classname]
1942         k = linkcl.labelprop(1)
1943         if num_re.match(self._value):
1944             try:
1945                 value = str(linkcl.get(self._value, k))
1946             except IndexError:
1947                 value = self._value
1948         else :
1949             value = self._value
1950         if escape:
1951             value = cgi.escape(value)
1952         return value
1954     def field(self, showid=0, size=None, **kwargs):
1955         """ Render a form edit field for the property
1957             If not editable, just display the value via plain().
1958         """
1959         if not self.is_edit_ok():
1960             return self.plain(escape=1)
1962         # edit field
1963         linkcl = self._db.getclass(self._prop.classname)
1964         if self._value is None:
1965             value = ''
1966         else:
1967             k = linkcl.getkey()
1968             if k and num_re.match(self._value):
1969                 value = linkcl.get(self._value, k)
1970             else:
1971                 value = self._value
1972         return self.input(name=self._formname, value=value, size=size,
1973                           **kwargs)
1975     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
1976              sort_on=None, html_kwargs={}, translate=True, **conditions):
1977         """ Render a form select list for this property
1979             "size" is used to limit the length of the list labels
1980             "height" is used to set the <select> tag's "size" attribute
1981             "showid" includes the item ids in the list labels
1982             "value" specifies which item is pre-selected
1983             "additional" lists properties which should be included in the
1984                 label
1985             "sort_on" indicates the property to sort the list on as
1986                 (direction, property) where direction is '+' or '-'. A
1987                 single string with the direction prepended may be used.
1988                 For example: ('-', 'order'), '+name'.
1989             "html_kwargs" specified additional html args for the
1990             generated html <select>
1991             "translate" indicates if we should do translation of labels
1992             using gettext -- this is often desired (e.g. for status
1993             labels) but sometimes not.
1995             The remaining keyword arguments are used as conditions for
1996             filtering the items in the list - they're passed as the
1997             "filterspec" argument to a Class.filter() call.
1999             If not editable, just display the value via plain().
2000         """
2001         if not self.is_edit_ok():
2002             return self.plain(escape=1)
2004         # Since None indicates the default, we need another way to
2005         # indicate "no selection".  We use -1 for this purpose, as
2006         # that is the value we use when submitting a form without the
2007         # value set.
2008         if value is None:
2009             value = self._value
2010         elif value == '-1':
2011             value = None
2013         linkcl = self._db.getclass(self._prop.classname)
2014         l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
2015                                             **html_kwargs)]
2016         k = linkcl.labelprop(1)
2017         s = ''
2018         if value is None:
2019             s = 'selected="selected" '
2020         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
2022         if sort_on is not None:
2023             if not isinstance(sort_on, tuple):
2024                 if sort_on[0] in '+-':
2025                     sort_on = (sort_on[0], sort_on[1:])
2026                 else:
2027                     sort_on = ('+', sort_on)
2028         else:
2029             sort_on = ('+', linkcl.orderprop())
2031         options = [opt
2032             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
2033             if self._db.security.hasPermission("View", self._client.userid,
2034                 linkcl.classname, itemid=opt)]
2036         # make sure we list the current value if it's retired
2037         if value and value not in options:
2038             options.insert(0, value)
2040         if additional:
2041             additional_fns = []
2042             props = linkcl.getprops()
2043             for propname in additional:
2044                 prop = props[propname]
2045                 if isinstance(prop, hyperdb.Link):
2046                     cl = self._db.getclass(prop.classname)
2047                     labelprop = cl.labelprop()
2048                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2049                                                             propname),
2050                                                  labelprop)
2051                 else:
2052                     fn = lambda optionid: linkcl.get(optionid, propname)
2053             additional_fns.append(fn)
2055         for optionid in options:
2056             # get the option value, and if it's None use an empty string
2057             option = linkcl.get(optionid, k) or ''
2059             # figure if this option is selected
2060             s = ''
2061             if value in [optionid, option]:
2062                 s = 'selected="selected" '
2064             # figure the label
2065             if showid:
2066                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2067             elif not option:
2068                 lab = '%s%s'%(self._prop.classname, optionid)
2069             else:
2070                 lab = option
2072             # truncate if it's too long
2073             if size is not None and len(lab) > size:
2074                 lab = lab[:size-3] + '...'
2075             if additional:
2076                 m = []
2077                 for fn in additional_fns:
2078                     m.append(str(fn(optionid)))
2079                 lab = lab + ' (%s)'%', '.join(m)
2081             # and generate
2082             tr = str
2083             if translate:
2084                 tr = self._
2085             lab = cgi.escape(tr(lab))
2086             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2087         l.append('</select>')
2088         return '\n'.join(l)
2089 #    def checklist(self, ...)
2093 class MultilinkHTMLProperty(HTMLProperty):
2094     """ Multilink HTMLProperty
2096         Also be iterable, returning a wrapper object like the Link case for
2097         each entry in the multilink.
2098     """
2099     def __init__(self, *args, **kwargs):
2100         HTMLProperty.__init__(self, *args, **kwargs)
2101         if self._value:
2102             display_value = lookupIds(self._db, self._prop, self._value,
2103                 fail_ok=1, do_lookup=False)
2104             sortfun = make_sort_function(self._db, self._prop.classname)
2105             # sorting fails if the value contains
2106             # items not yet stored in the database
2107             # ignore these errors to preserve user input
2108             try:
2109                 display_value.sort(sortfun)
2110             except:
2111                 pass
2112             self._value = display_value
2114     def __len__(self):
2115         """ length of the multilink """
2116         return len(self._value)
2118     def __getattr__(self, attr):
2119         """ no extended attribute accesses make sense here """
2120         raise AttributeError, attr
2122     def viewableGenerator(self, values):
2123         """Used to iterate over only the View'able items in a class."""
2124         check = self._db.security.hasPermission
2125         userid = self._client.userid
2126         classname = self._prop.classname
2127         if check('Web Access', userid):
2128             for value in values:
2129                 if check('View', userid, classname, itemid=value):
2130                     yield HTMLItem(self._client, classname, value)
2132     def __iter__(self):
2133         """ iterate and return a new HTMLItem
2134         """
2135         return self.viewableGenerator(self._value)
2137     def reverse(self):
2138         """ return the list in reverse order
2139         """
2140         l = self._value[:]
2141         l.reverse()
2142         return self.viewableGenerator(l)
2144     def sorted(self, property):
2145         """ Return this multilink sorted by the given property """
2146         value = list(self.__iter__())
2147         value.sort(lambda a,b:cmp(a[property], b[property]))
2148         return value
2150     def __contains__(self, value):
2151         """ Support the "in" operator. We have to make sure the passed-in
2152             value is a string first, not a HTMLProperty.
2153         """
2154         return str(value) in self._value
2156     def isset(self):
2157         """Is my _value not []?"""
2158         return self._value != []
2160     def plain(self, escape=0):
2161         """ Render a "plain" representation of the property
2162         """
2163         if not self.is_view_ok():
2164             return self._('[hidden]')
2166         linkcl = self._db.classes[self._prop.classname]
2167         k = linkcl.labelprop(1)
2168         labels = []
2169         for v in self._value:
2170             if num_re.match(v):
2171                 try:
2172                     label = linkcl.get(v, k)
2173                 except IndexError:
2174                     label = None
2175                 # fall back to designator if label is None
2176                 if label is None: label = '%s%s'%(self._prop.classname, k)
2177             else:
2178                 label = v
2179             labels.append(label)
2180         value = ', '.join(labels)
2181         if escape:
2182             value = cgi.escape(value)
2183         return value
2185     def field(self, size=30, showid=0, **kwargs):
2186         """ Render a form edit field for the property
2188             If not editable, just display the value via plain().
2189         """
2190         if not self.is_edit_ok():
2191             return self.plain(escape=1)
2193         linkcl = self._db.getclass(self._prop.classname)
2195         if 'value' not in kwargs:
2196             value = self._value[:]
2197             # map the id to the label property
2198             if not linkcl.getkey():
2199                 showid=1
2200             if not showid:
2201                 k = linkcl.labelprop(1)
2202                 value = lookupKeys(linkcl, k, value)
2203             value = ','.join(value)
2204             kwargs["value"] = value
2206         return self.input(name=self._formname, size=size, **kwargs)
2208     def menu(self, size=None, height=None, showid=0, additional=[],
2209              value=None, sort_on=None, html_kwargs={}, translate=True,
2210              **conditions):
2211         """ Render a form <select> list for this property.
2213             "size" is used to limit the length of the list labels
2214             "height" is used to set the <select> tag's "size" attribute
2215             "showid" includes the item ids in the list labels
2216             "additional" lists properties which should be included in the
2217                 label
2218             "value" specifies which item is pre-selected
2219             "sort_on" indicates the property to sort the list on as
2220                 (direction, property) where direction is '+' or '-'. A
2221                 single string with the direction prepended may be used.
2222                 For example: ('-', 'order'), '+name'.
2224             The remaining keyword arguments are used as conditions for
2225             filtering the items in the list - they're passed as the
2226             "filterspec" argument to a Class.filter() call.
2228             If not editable, just display the value via plain().
2229         """
2230         if not self.is_edit_ok():
2231             return self.plain(escape=1)
2233         if value is None:
2234             value = self._value
2236         linkcl = self._db.getclass(self._prop.classname)
2238         if sort_on is not None:
2239             if not isinstance(sort_on, tuple):
2240                 if sort_on[0] in '+-':
2241                     sort_on = (sort_on[0], sort_on[1:])
2242                 else:
2243                     sort_on = ('+', sort_on)
2244         else:
2245             sort_on = ('+', linkcl.orderprop())
2247         options = [opt
2248             for opt in linkcl.filter(None, conditions, sort_on)
2249             if self._db.security.hasPermission("View", self._client.userid,
2250                 linkcl.classname, itemid=opt)]
2252         # make sure we list the current values if they're retired
2253         for val in value:
2254             if val not in options:
2255                 options.insert(0, val)
2257         if not height:
2258             height = len(options)
2259             if value:
2260                 # The "no selection" option.
2261                 height += 1
2262             height = min(height, 7)
2263         l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2264                                                      size = height,
2265                                                      **html_kwargs)]
2266         k = linkcl.labelprop(1)
2268         if value:
2269             l.append('<option value="%s">- no selection -</option>'
2270                      % ','.join(['-' + v for v in value]))
2272         if additional:
2273             additional_fns = []
2274             props = linkcl.getprops()
2275             for propname in additional:
2276                 prop = props[propname]
2277                 if isinstance(prop, hyperdb.Link):
2278                     cl = self._db.getclass(prop.classname)
2279                     labelprop = cl.labelprop()
2280                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2281                                                             propname),
2282                                                  labelprop)
2283                 else:
2284                     fn = lambda optionid: linkcl.get(optionid, propname)
2285             additional_fns.append(fn)
2287         for optionid in options:
2288             # get the option value, and if it's None use an empty string
2289             option = linkcl.get(optionid, k) or ''
2291             # figure if this option is selected
2292             s = ''
2293             if optionid in value or option in value:
2294                 s = 'selected="selected" '
2296             # figure the label
2297             if showid:
2298                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2299             else:
2300                 lab = option
2301             # truncate if it's too long
2302             if size is not None and len(lab) > size:
2303                 lab = lab[:size-3] + '...'
2304             if additional:
2305                 m = []
2306                 for fn in additional_fns:
2307                     m.append(str(fn(optionid)))
2308                 lab = lab + ' (%s)'%', '.join(m)
2310             # and generate
2311             tr = str
2312             if translate:
2313                 tr = self._
2314             lab = cgi.escape(tr(lab))
2315             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2316                 lab))
2317         l.append('</select>')
2318         return '\n'.join(l)
2321 # set the propclasses for HTMLItem
2322 propclasses = [
2323     (hyperdb.String, StringHTMLProperty),
2324     (hyperdb.Number, NumberHTMLProperty),
2325     (hyperdb.Boolean, BooleanHTMLProperty),
2326     (hyperdb.Date, DateHTMLProperty),
2327     (hyperdb.Interval, IntervalHTMLProperty),
2328     (hyperdb.Password, PasswordHTMLProperty),
2329     (hyperdb.Link, LinkHTMLProperty),
2330     (hyperdb.Multilink, MultilinkHTMLProperty),
2333 def register_propclass(prop, cls):
2334     for index,propclass in enumerate(propclasses):
2335         p, c = propclass
2336         if prop == p:
2337             propclasses[index] = (prop, cls)
2338             break
2339     else:
2340         propclasses.append((prop, cls))
2343 def make_sort_function(db, classname, sort_on=None):
2344     """Make a sort function for a given class.
2346     The list being sorted may contain mixed ids and labels.
2347     """
2348     linkcl = db.getclass(classname)
2349     if sort_on is None:
2350         sort_on = linkcl.orderprop()
2351     def sortfunc(a, b):
2352         if num_re.match(a):
2353             a = linkcl.get(a, sort_on)
2354         if num_re.match(b):
2355             b = linkcl.get(b, sort_on)
2356         return cmp(a, b)
2357     return sortfunc
2359 def handleListCGIValue(value):
2360     """ Value is either a single item or a list of items. Each item has a
2361         .value that we're actually interested in.
2362     """
2363     if isinstance(value, type([])):
2364         return [value.value for value in value]
2365     else:
2366         value = value.value.strip()
2367         if not value:
2368             return []
2369         return [v.strip() for v in value.split(',')]
2371 class HTMLRequest(HTMLInputMixin):
2372     """The *request*, holding the CGI form and environment.
2374     - "form" the CGI form as a cgi.FieldStorage
2375     - "env" the CGI environment variables
2376     - "base" the base URL for this instance
2377     - "user" a HTMLItem instance for this user
2378     - "language" as determined by the browser or config
2379     - "classname" the current classname (possibly None)
2380     - "template" the current template (suffix, also possibly None)
2382     Index args:
2384     - "columns" dictionary of the columns to display in an index page
2385     - "show" a convenience access to columns - request/show/colname will
2386       be true if the columns should be displayed, false otherwise
2387     - "sort" index sort column (direction, column name)
2388     - "group" index grouping property (direction, column name)
2389     - "filter" properties to filter the index on
2390     - "filterspec" values to filter the index on
2391     - "search_text" text to perform a full-text search on for an index
2392     """
2393     def __repr__(self):
2394         return '<HTMLRequest %r>'%self.__dict__
2396     def __init__(self, client):
2397         # _client is needed by HTMLInputMixin
2398         self._client = self.client = client
2400         # easier access vars
2401         self.form = client.form
2402         self.env = client.env
2403         self.base = client.base
2404         self.user = HTMLItem(client, 'user', client.userid)
2405         self.language = client.language
2407         # store the current class name and action
2408         self.classname = client.classname
2409         self.nodeid = client.nodeid
2410         self.template = client.template
2412         # the special char to use for special vars
2413         self.special_char = '@'
2415         HTMLInputMixin.__init__(self)
2417         self._post_init()
2419     def current_url(self):
2420         url = self.base
2421         if self.classname:
2422             url += self.classname
2423             if self.nodeid:
2424                 url += self.nodeid
2425         args = {}
2426         if self.template:
2427             args['@template'] = self.template
2428         return self.indexargs_url(url, args)
2430     def _parse_sort(self, var, name):
2431         """ Parse sort/group options. Append to var
2432         """
2433         fields = []
2434         dirs = []
2435         for special in '@:':
2436             idx = 0
2437             key = '%s%s%d'%(special, name, idx)
2438             while key in self.form:
2439                 self.special_char = special
2440                 fields.append(self.form.getfirst(key))
2441                 dirkey = '%s%sdir%d'%(special, name, idx)
2442                 if dirkey in self.form:
2443                     dirs.append(self.form.getfirst(dirkey))
2444                 else:
2445                     dirs.append(None)
2446                 idx += 1
2447                 key = '%s%s%d'%(special, name, idx)
2448             # backward compatible (and query) URL format
2449             key = special + name
2450             dirkey = key + 'dir'
2451             if key in self.form and not fields:
2452                 fields = handleListCGIValue(self.form[key])
2453                 if dirkey in self.form:
2454                     dirs.append(self.form.getfirst(dirkey))
2455             if fields: # only try other special char if nothing found
2456                 break
2457         for f, d in map(None, fields, dirs):
2458             if f.startswith('-'):
2459                 var.append(('-', f[1:]))
2460             elif d:
2461                 var.append(('-', f))
2462             else:
2463                 var.append(('+', f))
2465     def _post_init(self):
2466         """ Set attributes based on self.form
2467         """
2468         # extract the index display information from the form
2469         self.columns = []
2470         for name in ':columns @columns'.split():
2471             if self.form.has_key(name):
2472                 self.special_char = name[0]
2473                 self.columns = handleListCGIValue(self.form[name])
2474                 break
2475         self.show = support.TruthDict(self.columns)
2476         security = self._client.db.security
2477         userid = self._client.userid
2479         # sorting and grouping
2480         self.sort = []
2481         self.group = []
2482         self._parse_sort(self.sort, 'sort')
2483         self._parse_sort(self.group, 'group')
2484         self.sort = security.filterSortspec(userid, self.classname, self.sort)
2485         self.group = security.filterSortspec(userid, self.classname, self.group)
2487         # filtering
2488         self.filter = []
2489         for name in ':filter @filter'.split():
2490             if self.form.has_key(name):
2491                 self.special_char = name[0]
2492                 self.filter = handleListCGIValue(self.form[name])
2494         self.filterspec = {}
2495         db = self.client.db
2496         if self.classname is not None:
2497             cls = db.getclass (self.classname)
2498             for name in self.filter:
2499                 if not self.form.has_key(name):
2500                     continue
2501                 prop = cls.get_transitive_prop (name)
2502                 fv = self.form[name]
2503                 if (isinstance(prop, hyperdb.Link) or
2504                         isinstance(prop, hyperdb.Multilink)):
2505                     self.filterspec[name] = lookupIds(db, prop,
2506                         handleListCGIValue(fv))
2507                 else:
2508                     if isinstance(fv, type([])):
2509                         self.filterspec[name] = [v.value for v in fv]
2510                     elif name == 'id':
2511                         # special case "id" property
2512                         self.filterspec[name] = handleListCGIValue(fv)
2513                     else:
2514                         self.filterspec[name] = fv.value
2515         self.filterspec = security.filterFilterspec(userid, self.classname,
2516             self.filterspec)
2518         # full-text search argument
2519         self.search_text = None
2520         for name in ':search_text @search_text'.split():
2521             if self.form.has_key(name):
2522                 self.special_char = name[0]
2523                 self.search_text = self.form.getfirst(name)
2525         # pagination - size and start index
2526         # figure batch args
2527         self.pagesize = 50
2528         for name in ':pagesize @pagesize'.split():
2529             if self.form.has_key(name):
2530                 self.special_char = name[0]
2531                 try:
2532                     self.pagesize = int(self.form.getfirst(name))
2533                 except ValueError:
2534                     # not an integer - ignore
2535                     pass
2537         self.startwith = 0
2538         for name in ':startwith @startwith'.split():
2539             if self.form.has_key(name):
2540                 self.special_char = name[0]
2541                 try:
2542                     self.startwith = int(self.form.getfirst(name))
2543                 except ValueError:
2544                     # not an integer - ignore
2545                     pass
2547         # dispname
2548         if self.form.has_key('@dispname'):
2549             self.dispname = self.form.getfirst('@dispname')
2550         else:
2551             self.dispname = None
2553     def updateFromURL(self, url):
2554         """ Parse the URL for query args, and update my attributes using the
2555             values.
2556         """
2557         env = {'QUERY_STRING': url}
2558         self.form = cgi.FieldStorage(environ=env)
2560         self._post_init()
2562     def update(self, kwargs):
2563         """ Update my attributes using the keyword args
2564         """
2565         self.__dict__.update(kwargs)
2566         if kwargs.has_key('columns'):
2567             self.show = support.TruthDict(self.columns)
2569     def description(self):
2570         """ Return a description of the request - handle for the page title.
2571         """
2572         s = [self.client.db.config.TRACKER_NAME]
2573         if self.classname:
2574             if self.client.nodeid:
2575                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2576             else:
2577                 if self.template == 'item':
2578                     s.append('- new %s'%self.classname)
2579                 elif self.template == 'index':
2580                     s.append('- %s index'%self.classname)
2581                 else:
2582                     s.append('- %s %s'%(self.classname, self.template))
2583         else:
2584             s.append('- home')
2585         return ' '.join(s)
2587     def __str__(self):
2588         d = {}
2589         d.update(self.__dict__)
2590         f = ''
2591         for k in self.form.keys():
2592             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2593         d['form'] = f
2594         e = ''
2595         for k,v in self.env.items():
2596             e += '\n     %r=%r'%(k, v)
2597         d['env'] = e
2598         return """
2599 form: %(form)s
2600 base: %(base)r
2601 classname: %(classname)r
2602 template: %(template)r
2603 columns: %(columns)r
2604 sort: %(sort)r
2605 group: %(group)r
2606 filter: %(filter)r
2607 search_text: %(search_text)r
2608 pagesize: %(pagesize)r
2609 startwith: %(startwith)r
2610 env: %(env)s
2611 """%d
2613     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2614             filterspec=1, search_text=1):
2615         """ return the current index args as form elements """
2616         l = []
2617         sc = self.special_char
2618         def add(k, v):
2619             l.append(self.input(type="hidden", name=k, value=v))
2620         if columns and self.columns:
2621             add(sc+'columns', ','.join(self.columns))
2622         if sort:
2623             val = []
2624             for dir, attr in self.sort:
2625                 if dir == '-':
2626                     val.append('-'+attr)
2627                 else:
2628                     val.append(attr)
2629             add(sc+'sort', ','.join (val))
2630         if group:
2631             val = []
2632             for dir, attr in self.group:
2633                 if dir == '-':
2634                     val.append('-'+attr)
2635                 else:
2636                     val.append(attr)
2637             add(sc+'group', ','.join (val))
2638         if filter and self.filter:
2639             add(sc+'filter', ','.join(self.filter))
2640         if self.classname and filterspec:
2641             cls = self.client.db.getclass(self.classname)
2642             for k,v in self.filterspec.items():
2643                 if type(v) == type([]):
2644                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2645                         add(k, ' '.join(v))
2646                     else:
2647                         add(k, ','.join(v))
2648                 else:
2649                     add(k, v)
2650         if search_text and self.search_text:
2651             add(sc+'search_text', self.search_text)
2652         add(sc+'pagesize', self.pagesize)
2653         add(sc+'startwith', self.startwith)
2654         return '\n'.join(l)
2656     def indexargs_url(self, url, args):
2657         """ Embed the current index args in a URL
2658         """
2659         q = urllib.quote
2660         sc = self.special_char
2661         l = ['%s=%s'%(k,v) for k,v in args.items()]
2663         # pull out the special values (prefixed by @ or :)
2664         specials = {}
2665         for key in args.keys():
2666             if key[0] in '@:':
2667                 specials[key[1:]] = args[key]
2669         # ok, now handle the specials we received in the request
2670         if self.columns and not specials.has_key('columns'):
2671             l.append(sc+'columns=%s'%(','.join(self.columns)))
2672         if self.sort and not specials.has_key('sort'):
2673             val = []
2674             for dir, attr in self.sort:
2675                 if dir == '-':
2676                     val.append('-'+attr)
2677                 else:
2678                     val.append(attr)
2679             l.append(sc+'sort=%s'%(','.join(val)))
2680         if self.group and not specials.has_key('group'):
2681             val = []
2682             for dir, attr in self.group:
2683                 if dir == '-':
2684                     val.append('-'+attr)
2685                 else:
2686                     val.append(attr)
2687             l.append(sc+'group=%s'%(','.join(val)))
2688         if self.filter and not specials.has_key('filter'):
2689             l.append(sc+'filter=%s'%(','.join(self.filter)))
2690         if self.search_text and not specials.has_key('search_text'):
2691             l.append(sc+'search_text=%s'%q(self.search_text))
2692         if not specials.has_key('pagesize'):
2693             l.append(sc+'pagesize=%s'%self.pagesize)
2694         if not specials.has_key('startwith'):
2695             l.append(sc+'startwith=%s'%self.startwith)
2697         # finally, the remainder of the filter args in the request
2698         if self.classname and self.filterspec:
2699             cls = self.client.db.getclass(self.classname)
2700             for k,v in self.filterspec.items():
2701                 if not args.has_key(k):
2702                     if type(v) == type([]):
2703                         prop = cls.get_transitive_prop(k)
2704                         if k != 'id' and isinstance(prop, hyperdb.String):
2705                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2706                         else:
2707                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2708                     else:
2709                         l.append('%s=%s'%(k, q(v)))
2710         return '%s?%s'%(url, '&'.join(l))
2711     indexargs_href = indexargs_url
2713     def base_javascript(self):
2714         return """
2715 <script type="text/javascript">
2716 submitted = false;
2717 function submit_once() {
2718     if (submitted) {
2719         alert("Your request is being processed.\\nPlease be patient.");
2720         event.returnValue = 0;    // work-around for IE
2721         return 0;
2722     }
2723     submitted = true;
2724     return 1;
2727 function help_window(helpurl, width, height) {
2728     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2730 </script>
2731 """%self.base
2733     def batch(self, permission='View'):
2734         """ Return a batch object for results from the "current search"
2735         """
2736         check = self._client.db.security.hasPermission
2737         userid = self._client.userid
2738         if not check('Web Access', userid):
2739             return Batch(self.client, [], self.pagesize, self.startwith,
2740                 classname=self.classname)
2742         filterspec = self.filterspec
2743         sort = self.sort
2744         group = self.group
2746         # get the list of ids we're batching over
2747         klass = self.client.db.getclass(self.classname)
2748         if self.search_text:
2749             matches = self.client.db.indexer.search(
2750                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2751                     r'(?u)\b\w{2,25}\b',
2752                     unicode(self.search_text, "utf-8", "replace")
2753                 )], klass)
2754         else:
2755             matches = None
2757         # filter for visibility
2758         l = [id for id in klass.filter(matches, filterspec, sort, group)
2759             if check(permission, userid, self.classname, itemid=id)]
2761         # return the batch object, using IDs only
2762         return Batch(self.client, l, self.pagesize, self.startwith,
2763             classname=self.classname)
2765 # extend the standard ZTUtils Batch object to remove dependency on
2766 # Acquisition and add a couple of useful methods
2767 class Batch(ZTUtils.Batch):
2768     """ Use me to turn a list of items, or item ids of a given class, into a
2769         series of batches.
2771         ========= ========================================================
2772         Parameter  Usage
2773         ========= ========================================================
2774         sequence  a list of HTMLItems or item ids
2775         classname if sequence is a list of ids, this is the class of item
2776         size      how big to make the sequence.
2777         start     where to start (0-indexed) in the sequence.
2778         end       where to end (0-indexed) in the sequence.
2779         orphan    if the next batch would contain less items than this
2780                   value, then it is combined with this batch
2781         overlap   the number of items shared between adjacent batches
2782         ========= ========================================================
2784         Attributes: Note that the "start" attribute, unlike the
2785         argument, is a 1-based index (I know, lame).  "first" is the
2786         0-based index.  "length" is the actual number of elements in
2787         the batch.
2789         "sequence_length" is the length of the original, unbatched, sequence.
2790     """
2791     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2792             overlap=0, classname=None):
2793         self.client = client
2794         self.last_index = self.last_item = None
2795         self.current_item = None
2796         self.classname = classname
2797         self.sequence_length = len(sequence)
2798         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2799             overlap)
2801     # overwrite so we can late-instantiate the HTMLItem instance
2802     def __getitem__(self, index):
2803         if index < 0:
2804             if index + self.end < self.first: raise IndexError, index
2805             return self._sequence[index + self.end]
2807         if index >= self.length:
2808             raise IndexError, index
2810         # move the last_item along - but only if the fetched index changes
2811         # (for some reason, index 0 is fetched twice)
2812         if index != self.last_index:
2813             self.last_item = self.current_item
2814             self.last_index = index
2816         item = self._sequence[index + self.first]
2817         if self.classname:
2818             # map the item ids to instances
2819             item = HTMLItem(self.client, self.classname, item)
2820         self.current_item = item
2821         return item
2823     def propchanged(self, *properties):
2824         """ Detect if one of the properties marked as being a group
2825             property changed in the last iteration fetch
2826         """
2827         # we poke directly at the _value here since MissingValue can screw
2828         # us up and cause Nones to compare strangely
2829         if self.last_item is None:
2830             return 1
2831         for property in properties:
2832             if property == 'id' or isinstance (self.last_item[property], list):
2833                 if (str(self.last_item[property]) !=
2834                     str(self.current_item[property])):
2835                     return 1
2836             else:
2837                 if (self.last_item[property]._value !=
2838                     self.current_item[property]._value):
2839                     return 1
2840         return 0
2842     # override these 'cos we don't have access to acquisition
2843     def previous(self):
2844         if self.start == 1:
2845             return None
2846         return Batch(self.client, self._sequence, self._size,
2847             self.first - self._size + self.overlap, 0, self.orphan,
2848             self.overlap)
2850     def next(self):
2851         try:
2852             self._sequence[self.end]
2853         except IndexError:
2854             return None
2855         return Batch(self.client, self._sequence, self._size,
2856             self.end - self.overlap, 0, self.orphan, self.overlap)
2858 class TemplatingUtils:
2859     """ Utilities for templating
2860     """
2861     def __init__(self, client):
2862         self.client = client
2863     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2864         return Batch(self.client, sequence, size, start, end, orphan,
2865             overlap)
2867     def url_quote(self, url):
2868         """URL-quote the supplied text."""
2869         return urllib.quote(url)
2871     def html_quote(self, html):
2872         """HTML-quote the supplied text."""
2873         return cgi.escape(html)
2875     def __getattr__(self, name):
2876         """Try the tracker's templating_utils."""
2877         if not hasattr(self.client.instance, 'templating_utils'):
2878             # backwards-compatibility
2879             raise AttributeError, name
2880         if not self.client.instance.templating_utils.has_key(name):
2881             raise AttributeError, name
2882         return self.client.instance.templating_utils[name]
2884     def keywords_expressions(self, request):
2885         return render_keywords_expression_editor(request)
2887     def html_calendar(self, request):
2888         """Generate a HTML calendar.
2890         `request`  the roundup.request object
2891                    - @template : name of the template
2892                    - form      : name of the form to store back the date
2893                    - property  : name of the property of the form to store
2894                                  back the date
2895                    - date      : current date
2896                    - display   : when browsing, specifies year and month
2898         html will simply be a table.
2899         """
2900         tz = request.client.db.getUserTimezone()
2901         current_date = date.Date(".").local(tz)
2902         date_str  = request.form.getfirst("date", current_date)
2903         display   = request.form.getfirst("display", date_str)
2904         template  = request.form.getfirst("@template", "calendar")
2905         form      = request.form.getfirst("form")
2906         property  = request.form.getfirst("property")
2907         curr_date = date.Date(date_str) # to highlight
2908         display   = date.Date(display)  # to show
2909         day       = display.day
2911         # for navigation
2912         date_prev_month = display + date.Interval("-1m")
2913         date_next_month = display + date.Interval("+1m")
2914         date_prev_year  = display + date.Interval("-1y")
2915         date_next_year  = display + date.Interval("+1y")
2917         res = []
2919         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2920                     (request.classname, template, property, form, curr_date)
2922         # navigation
2923         # month
2924         res.append('<table class="calendar"><tr><td>')
2925         res.append(' <table width="100%" class="calendar_nav"><tr>')
2926         link = "&display=%s"%date_prev_month
2927         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2928             date_prev_month))
2929         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2930         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2931             date_next_month))
2932         # spacer
2933         res.append('  <td width="100%"></td>')
2934         # year
2935         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2936             date_prev_year))
2937         res.append('  <td>%s</td>'%display.year)
2938         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2939             date_next_year))
2940         res.append(' </tr></table>')
2941         res.append(' </td></tr>')
2943         # the calendar
2944         res.append(' <tr><td><table class="calendar_display">')
2945         res.append('  <tr class="weekdays">')
2946         for day in calendar.weekheader(3).split():
2947             res.append('   <td>%s</td>'%day)
2948         res.append('  </tr>')
2949         for week in calendar.monthcalendar(display.year, display.month):
2950             res.append('  <tr>')
2951             for day in week:
2952                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2953                       "window.close ();"%(display.year, display.month, day)
2954                 if (day == curr_date.day and display.month == curr_date.month
2955                         and display.year == curr_date.year):
2956                     # highlight
2957                     style = "today"
2958                 else :
2959                     style = ""
2960                 if day:
2961                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2962                         style, link, day))
2963                 else :
2964                     res.append('   <td></td>')
2965             res.append('  </tr>')
2966         res.append('</table></td></tr></table>')
2967         return "\n".join(res)
2969 class MissingValue:
2970     def __init__(self, description, **kwargs):
2971         self.__description = description
2972         for key, value in kwargs.items():
2973             self.__dict__[key] = value
2975     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2976     def __getattr__(self, name):
2977         # This allows assignments which assume all intermediate steps are Null
2978         # objects if they don't exist yet.
2979         #
2980         # For example (with just 'client' defined):
2981         #
2982         # client.db.config.TRACKER_WEB = 'BASE/'
2983         self.__dict__[name] = MissingValue(self.__description)
2984         return getattr(self, name)
2986     def __getitem__(self, key): return self
2987     def __nonzero__(self): return 0
2988     def __str__(self): return '[%s]'%self.__description
2989     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2990         self.__description)
2991     def gettext(self, str): return str
2992     _ = gettext
2994 # vim: set et sts=4 sw=4 :