Code

b0eaf022bc34120b2084c9bc433d5d39b7dd5e1a
[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 = {}, **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'.
1990             The remaining keyword arguments are used as conditions for
1991             filtering the items in the list - they're passed as the
1992             "filterspec" argument to a Class.filter() call.
1994             If not editable, just display the value via plain().
1995         """
1996         if not self.is_edit_ok():
1997             return self.plain(escape=1)
1999         # Since None indicates the default, we need another way to
2000         # indicate "no selection".  We use -1 for this purpose, as
2001         # that is the value we use when submitting a form without the
2002         # value set.
2003         if value is None:
2004             value = self._value
2005         elif value == '-1':
2006             value = None
2008         linkcl = self._db.getclass(self._prop.classname)
2009         l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
2010                                             **html_kwargs)]
2011         k = linkcl.labelprop(1)
2012         s = ''
2013         if value is None:
2014             s = 'selected="selected" '
2015         l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
2017         if sort_on is not None:
2018             if not isinstance(sort_on, tuple):
2019                 if sort_on[0] in '+-':
2020                     sort_on = (sort_on[0], sort_on[1:])
2021                 else:
2022                     sort_on = ('+', sort_on)
2023         else:
2024             sort_on = ('+', linkcl.orderprop())
2026         options = [opt
2027             for opt in linkcl.filter(None, conditions, sort_on, (None, None))
2028             if self._db.security.hasPermission("View", self._client.userid,
2029                 linkcl.classname, itemid=opt)]
2031         # make sure we list the current value if it's retired
2032         if value and value not in options:
2033             options.insert(0, value)
2035         if additional:
2036             additional_fns = []
2037             props = linkcl.getprops()
2038             for propname in additional:
2039                 prop = props[propname]
2040                 if isinstance(prop, hyperdb.Link):
2041                     cl = self._db.getclass(prop.classname)
2042                     labelprop = cl.labelprop()
2043                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2044                                                             propname),
2045                                                  labelprop)
2046                 else:
2047                     fn = lambda optionid: linkcl.get(optionid, propname)
2048             additional_fns.append(fn)
2050         for optionid in options:
2051             # get the option value, and if it's None use an empty string
2052             option = linkcl.get(optionid, k) or ''
2054             # figure if this option is selected
2055             s = ''
2056             if value in [optionid, option]:
2057                 s = 'selected="selected" '
2059             # figure the label
2060             if showid:
2061                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2062             elif not option:
2063                 lab = '%s%s'%(self._prop.classname, optionid)
2064             else:
2065                 lab = option
2067             # truncate if it's too long
2068             if size is not None and len(lab) > size:
2069                 lab = lab[:size-3] + '...'
2070             if additional:
2071                 m = []
2072                 for fn in additional_fns:
2073                     m.append(str(fn(optionid)))
2074                 lab = lab + ' (%s)'%', '.join(m)
2076             # and generate
2077             lab = cgi.escape(self._(lab))
2078             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
2079         l.append('</select>')
2080         return '\n'.join(l)
2081 #    def checklist(self, ...)
2085 class MultilinkHTMLProperty(HTMLProperty):
2086     """ Multilink HTMLProperty
2088         Also be iterable, returning a wrapper object like the Link case for
2089         each entry in the multilink.
2090     """
2091     def __init__(self, *args, **kwargs):
2092         HTMLProperty.__init__(self, *args, **kwargs)
2093         if self._value:
2094             display_value = lookupIds(self._db, self._prop, self._value,
2095                 fail_ok=1, do_lookup=False)
2096             sortfun = make_sort_function(self._db, self._prop.classname)
2097             # sorting fails if the value contains
2098             # items not yet stored in the database
2099             # ignore these errors to preserve user input
2100             try:
2101                 display_value.sort(sortfun)
2102             except:
2103                 pass
2104             self._value = display_value
2106     def __len__(self):
2107         """ length of the multilink """
2108         return len(self._value)
2110     def __getattr__(self, attr):
2111         """ no extended attribute accesses make sense here """
2112         raise AttributeError, attr
2114     def viewableGenerator(self, values):
2115         """Used to iterate over only the View'able items in a class."""
2116         check = self._db.security.hasPermission
2117         userid = self._client.userid
2118         classname = self._prop.classname
2119         if check('Web Access', userid):
2120             for value in values:
2121                 if check('View', userid, classname, itemid=value):
2122                     yield HTMLItem(self._client, classname, value)
2124     def __iter__(self):
2125         """ iterate and return a new HTMLItem
2126         """
2127         return self.viewableGenerator(self._value)
2129     def reverse(self):
2130         """ return the list in reverse order
2131         """
2132         l = self._value[:]
2133         l.reverse()
2134         return self.viewableGenerator(l)
2136     def sorted(self, property):
2137         """ Return this multilink sorted by the given property """
2138         value = list(self.__iter__())
2139         value.sort(lambda a,b:cmp(a[property], b[property]))
2140         return value
2142     def __contains__(self, value):
2143         """ Support the "in" operator. We have to make sure the passed-in
2144             value is a string first, not a HTMLProperty.
2145         """
2146         return str(value) in self._value
2148     def isset(self):
2149         """Is my _value not []?"""
2150         return self._value != []
2152     def plain(self, escape=0):
2153         """ Render a "plain" representation of the property
2154         """
2155         if not self.is_view_ok():
2156             return self._('[hidden]')
2158         linkcl = self._db.classes[self._prop.classname]
2159         k = linkcl.labelprop(1)
2160         labels = []
2161         for v in self._value:
2162             if num_re.match(v):
2163                 try:
2164                     label = linkcl.get(v, k)
2165                 except IndexError:
2166                     label = None
2167                 # fall back to designator if label is None
2168                 if label is None: label = '%s%s'%(self._prop.classname, k)
2169             else:
2170                 label = v
2171             labels.append(label)
2172         value = ', '.join(labels)
2173         if escape:
2174             value = cgi.escape(value)
2175         return value
2177     def field(self, size=30, showid=0, **kwargs):
2178         """ Render a form edit field for the property
2180             If not editable, just display the value via plain().
2181         """
2182         if not self.is_edit_ok():
2183             return self.plain(escape=1)
2185         linkcl = self._db.getclass(self._prop.classname)
2187         if 'value' not in kwargs:
2188             value = self._value[:]
2189             # map the id to the label property
2190             if not linkcl.getkey():
2191                 showid=1
2192             if not showid:
2193                 k = linkcl.labelprop(1)
2194                 value = lookupKeys(linkcl, k, value)
2195             value = ','.join(value)
2196             kwargs["value"] = value
2198         return self.input(name=self._formname, size=size, **kwargs)
2200     def menu(self, size=None, height=None, showid=0, additional=[],
2201              value=None, sort_on=None, html_kwargs = {}, **conditions):
2202         """ Render a form <select> list for this property.
2204             "size" is used to limit the length of the list labels
2205             "height" is used to set the <select> tag's "size" attribute
2206             "showid" includes the item ids in the list labels
2207             "additional" lists properties which should be included in the
2208                 label
2209             "value" specifies which item is pre-selected
2210             "sort_on" indicates the property to sort the list on as
2211                 (direction, property) where direction is '+' or '-'. A
2212                 single string with the direction prepended may be used.
2213                 For example: ('-', 'order'), '+name'.
2215             The remaining keyword arguments are used as conditions for
2216             filtering the items in the list - they're passed as the
2217             "filterspec" argument to a Class.filter() call.
2219             If not editable, just display the value via plain().
2220         """
2221         if not self.is_edit_ok():
2222             return self.plain(escape=1)
2224         if value is None:
2225             value = self._value
2227         linkcl = self._db.getclass(self._prop.classname)
2229         if sort_on is not None:
2230             if not isinstance(sort_on, tuple):
2231                 if sort_on[0] in '+-':
2232                     sort_on = (sort_on[0], sort_on[1:])
2233                 else:
2234                     sort_on = ('+', sort_on)
2235         else:
2236             sort_on = ('+', linkcl.orderprop())
2238         options = [opt
2239             for opt in linkcl.filter(None, conditions, sort_on)
2240             if self._db.security.hasPermission("View", self._client.userid,
2241                 linkcl.classname, itemid=opt)]
2243         # make sure we list the current values if they're retired
2244         for val in value:
2245             if val not in options:
2246                 options.insert(0, val)
2248         if not height:
2249             height = len(options)
2250             if value:
2251                 # The "no selection" option.
2252                 height += 1
2253             height = min(height, 7)
2254         l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
2255                                                      size = height,
2256                                                      **html_kwargs)]
2257         k = linkcl.labelprop(1)
2259         if value:
2260             l.append('<option value="%s">- no selection -</option>'
2261                      % ','.join(['-' + v for v in value]))
2263         if additional:
2264             additional_fns = []
2265             props = linkcl.getprops()
2266             for propname in additional:
2267                 prop = props[propname]
2268                 if isinstance(prop, hyperdb.Link):
2269                     cl = self._db.getclass(prop.classname)
2270                     labelprop = cl.labelprop()
2271                     fn = lambda optionid: cl.get(linkcl.get(optionid,
2272                                                             propname),
2273                                                  labelprop)
2274                 else:
2275                     fn = lambda optionid: linkcl.get(optionid, propname)
2276             additional_fns.append(fn)
2278         for optionid in options:
2279             # get the option value, and if it's None use an empty string
2280             option = linkcl.get(optionid, k) or ''
2282             # figure if this option is selected
2283             s = ''
2284             if optionid in value or option in value:
2285                 s = 'selected="selected" '
2287             # figure the label
2288             if showid:
2289                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
2290             else:
2291                 lab = option
2292             # truncate if it's too long
2293             if size is not None and len(lab) > size:
2294                 lab = lab[:size-3] + '...'
2295             if additional:
2296                 m = []
2297                 for fn in additional_fns:
2298                     m.append(str(fn(optionid)))
2299                 lab = lab + ' (%s)'%', '.join(m)
2301             # and generate
2302             lab = cgi.escape(self._(lab))
2303             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
2304                 lab))
2305         l.append('</select>')
2306         return '\n'.join(l)
2309 # set the propclasses for HTMLItem
2310 propclasses = [
2311     (hyperdb.String, StringHTMLProperty),
2312     (hyperdb.Number, NumberHTMLProperty),
2313     (hyperdb.Boolean, BooleanHTMLProperty),
2314     (hyperdb.Date, DateHTMLProperty),
2315     (hyperdb.Interval, IntervalHTMLProperty),
2316     (hyperdb.Password, PasswordHTMLProperty),
2317     (hyperdb.Link, LinkHTMLProperty),
2318     (hyperdb.Multilink, MultilinkHTMLProperty),
2321 def register_propclass(prop, cls):
2322     for index,propclass in enumerate(propclasses):
2323         p, c = propclass
2324         if prop == p:
2325             propclasses[index] = (prop, cls)
2326             break
2327     else:
2328         propclasses.append((prop, cls))
2331 def make_sort_function(db, classname, sort_on=None):
2332     """Make a sort function for a given class.
2334     The list being sorted may contain mixed ids and labels.
2335     """
2336     linkcl = db.getclass(classname)
2337     if sort_on is None:
2338         sort_on = linkcl.orderprop()
2339     def sortfunc(a, b):
2340         if num_re.match(a):
2341             a = linkcl.get(a, sort_on)
2342         if num_re.match(b):
2343             b = linkcl.get(b, sort_on)
2344         return cmp(a, b)
2345     return sortfunc
2347 def handleListCGIValue(value):
2348     """ Value is either a single item or a list of items. Each item has a
2349         .value that we're actually interested in.
2350     """
2351     if isinstance(value, type([])):
2352         return [value.value for value in value]
2353     else:
2354         value = value.value.strip()
2355         if not value:
2356             return []
2357         return [v.strip() for v in value.split(',')]
2359 class HTMLRequest(HTMLInputMixin):
2360     """The *request*, holding the CGI form and environment.
2362     - "form" the CGI form as a cgi.FieldStorage
2363     - "env" the CGI environment variables
2364     - "base" the base URL for this instance
2365     - "user" a HTMLItem instance for this user
2366     - "language" as determined by the browser or config
2367     - "classname" the current classname (possibly None)
2368     - "template" the current template (suffix, also possibly None)
2370     Index args:
2372     - "columns" dictionary of the columns to display in an index page
2373     - "show" a convenience access to columns - request/show/colname will
2374       be true if the columns should be displayed, false otherwise
2375     - "sort" index sort column (direction, column name)
2376     - "group" index grouping property (direction, column name)
2377     - "filter" properties to filter the index on
2378     - "filterspec" values to filter the index on
2379     - "search_text" text to perform a full-text search on for an index
2380     """
2381     def __repr__(self):
2382         return '<HTMLRequest %r>'%self.__dict__
2384     def __init__(self, client):
2385         # _client is needed by HTMLInputMixin
2386         self._client = self.client = client
2388         # easier access vars
2389         self.form = client.form
2390         self.env = client.env
2391         self.base = client.base
2392         self.user = HTMLItem(client, 'user', client.userid)
2393         self.language = client.language
2395         # store the current class name and action
2396         self.classname = client.classname
2397         self.nodeid = client.nodeid
2398         self.template = client.template
2400         # the special char to use for special vars
2401         self.special_char = '@'
2403         HTMLInputMixin.__init__(self)
2405         self._post_init()
2407     def current_url(self):
2408         url = self.base
2409         if self.classname:
2410             url += self.classname
2411             if self.nodeid:
2412                 url += self.nodeid
2413         args = {}
2414         if self.template:
2415             args['@template'] = self.template
2416         return self.indexargs_url(url, args)
2418     def _parse_sort(self, var, name):
2419         """ Parse sort/group options. Append to var
2420         """
2421         fields = []
2422         dirs = []
2423         for special in '@:':
2424             idx = 0
2425             key = '%s%s%d'%(special, name, idx)
2426             while key in self.form:
2427                 self.special_char = special
2428                 fields.append(self.form.getfirst(key))
2429                 dirkey = '%s%sdir%d'%(special, name, idx)
2430                 if dirkey in self.form:
2431                     dirs.append(self.form.getfirst(dirkey))
2432                 else:
2433                     dirs.append(None)
2434                 idx += 1
2435                 key = '%s%s%d'%(special, name, idx)
2436             # backward compatible (and query) URL format
2437             key = special + name
2438             dirkey = key + 'dir'
2439             if key in self.form and not fields:
2440                 fields = handleListCGIValue(self.form[key])
2441                 if dirkey in self.form:
2442                     dirs.append(self.form.getfirst(dirkey))
2443             if fields: # only try other special char if nothing found
2444                 break
2445         for f, d in map(None, fields, dirs):
2446             if f.startswith('-'):
2447                 var.append(('-', f[1:]))
2448             elif d:
2449                 var.append(('-', f))
2450             else:
2451                 var.append(('+', f))
2453     def _post_init(self):
2454         """ Set attributes based on self.form
2455         """
2456         # extract the index display information from the form
2457         self.columns = []
2458         for name in ':columns @columns'.split():
2459             if self.form.has_key(name):
2460                 self.special_char = name[0]
2461                 self.columns = handleListCGIValue(self.form[name])
2462                 break
2463         self.show = support.TruthDict(self.columns)
2464         security = self._client.db.security
2465         userid = self._client.userid
2467         # sorting and grouping
2468         self.sort = []
2469         self.group = []
2470         self._parse_sort(self.sort, 'sort')
2471         self._parse_sort(self.group, 'group')
2472         self.sort = security.filterSortspec(userid, self.classname, self.sort)
2473         self.group = security.filterSortspec(userid, self.classname, self.group)
2475         # filtering
2476         self.filter = []
2477         for name in ':filter @filter'.split():
2478             if self.form.has_key(name):
2479                 self.special_char = name[0]
2480                 self.filter = handleListCGIValue(self.form[name])
2482         self.filterspec = {}
2483         db = self.client.db
2484         if self.classname is not None:
2485             cls = db.getclass (self.classname)
2486             for name in self.filter:
2487                 if not self.form.has_key(name):
2488                     continue
2489                 prop = cls.get_transitive_prop (name)
2490                 fv = self.form[name]
2491                 if (isinstance(prop, hyperdb.Link) or
2492                         isinstance(prop, hyperdb.Multilink)):
2493                     self.filterspec[name] = lookupIds(db, prop,
2494                         handleListCGIValue(fv))
2495                 else:
2496                     if isinstance(fv, type([])):
2497                         self.filterspec[name] = [v.value for v in fv]
2498                     elif name == 'id':
2499                         # special case "id" property
2500                         self.filterspec[name] = handleListCGIValue(fv)
2501                     else:
2502                         self.filterspec[name] = fv.value
2503         self.filterspec = security.filterFilterspec(userid, self.classname,
2504             self.filterspec)
2506         # full-text search argument
2507         self.search_text = None
2508         for name in ':search_text @search_text'.split():
2509             if self.form.has_key(name):
2510                 self.special_char = name[0]
2511                 self.search_text = self.form.getfirst(name)
2513         # pagination - size and start index
2514         # figure batch args
2515         self.pagesize = 50
2516         for name in ':pagesize @pagesize'.split():
2517             if self.form.has_key(name):
2518                 self.special_char = name[0]
2519                 try:
2520                     self.pagesize = int(self.form.getfirst(name))
2521                 except ValueError:
2522                     # not an integer - ignore
2523                     pass
2525         self.startwith = 0
2526         for name in ':startwith @startwith'.split():
2527             if self.form.has_key(name):
2528                 self.special_char = name[0]
2529                 try:
2530                     self.startwith = int(self.form.getfirst(name))
2531                 except ValueError:
2532                     # not an integer - ignore
2533                     pass
2535         # dispname
2536         if self.form.has_key('@dispname'):
2537             self.dispname = self.form.getfirst('@dispname')
2538         else:
2539             self.dispname = None
2541     def updateFromURL(self, url):
2542         """ Parse the URL for query args, and update my attributes using the
2543             values.
2544         """
2545         env = {'QUERY_STRING': url}
2546         self.form = cgi.FieldStorage(environ=env)
2548         self._post_init()
2550     def update(self, kwargs):
2551         """ Update my attributes using the keyword args
2552         """
2553         self.__dict__.update(kwargs)
2554         if kwargs.has_key('columns'):
2555             self.show = support.TruthDict(self.columns)
2557     def description(self):
2558         """ Return a description of the request - handle for the page title.
2559         """
2560         s = [self.client.db.config.TRACKER_NAME]
2561         if self.classname:
2562             if self.client.nodeid:
2563                 s.append('- %s%s'%(self.classname, self.client.nodeid))
2564             else:
2565                 if self.template == 'item':
2566                     s.append('- new %s'%self.classname)
2567                 elif self.template == 'index':
2568                     s.append('- %s index'%self.classname)
2569                 else:
2570                     s.append('- %s %s'%(self.classname, self.template))
2571         else:
2572             s.append('- home')
2573         return ' '.join(s)
2575     def __str__(self):
2576         d = {}
2577         d.update(self.__dict__)
2578         f = ''
2579         for k in self.form.keys():
2580             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
2581         d['form'] = f
2582         e = ''
2583         for k,v in self.env.items():
2584             e += '\n     %r=%r'%(k, v)
2585         d['env'] = e
2586         return """
2587 form: %(form)s
2588 base: %(base)r
2589 classname: %(classname)r
2590 template: %(template)r
2591 columns: %(columns)r
2592 sort: %(sort)r
2593 group: %(group)r
2594 filter: %(filter)r
2595 search_text: %(search_text)r
2596 pagesize: %(pagesize)r
2597 startwith: %(startwith)r
2598 env: %(env)s
2599 """%d
2601     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
2602             filterspec=1, search_text=1):
2603         """ return the current index args as form elements """
2604         l = []
2605         sc = self.special_char
2606         def add(k, v):
2607             l.append(self.input(type="hidden", name=k, value=v))
2608         if columns and self.columns:
2609             add(sc+'columns', ','.join(self.columns))
2610         if sort:
2611             val = []
2612             for dir, attr in self.sort:
2613                 if dir == '-':
2614                     val.append('-'+attr)
2615                 else:
2616                     val.append(attr)
2617             add(sc+'sort', ','.join (val))
2618         if group:
2619             val = []
2620             for dir, attr in self.group:
2621                 if dir == '-':
2622                     val.append('-'+attr)
2623                 else:
2624                     val.append(attr)
2625             add(sc+'group', ','.join (val))
2626         if filter and self.filter:
2627             add(sc+'filter', ','.join(self.filter))
2628         if self.classname and filterspec:
2629             cls = self.client.db.getclass(self.classname)
2630             for k,v in self.filterspec.items():
2631                 if type(v) == type([]):
2632                     if isinstance(cls.get_transitive_prop(k), hyperdb.String):
2633                         add(k, ' '.join(v))
2634                     else:
2635                         add(k, ','.join(v))
2636                 else:
2637                     add(k, v)
2638         if search_text and self.search_text:
2639             add(sc+'search_text', self.search_text)
2640         add(sc+'pagesize', self.pagesize)
2641         add(sc+'startwith', self.startwith)
2642         return '\n'.join(l)
2644     def indexargs_url(self, url, args):
2645         """ Embed the current index args in a URL
2646         """
2647         q = urllib.quote
2648         sc = self.special_char
2649         l = ['%s=%s'%(k,v) for k,v in args.items()]
2651         # pull out the special values (prefixed by @ or :)
2652         specials = {}
2653         for key in args.keys():
2654             if key[0] in '@:':
2655                 specials[key[1:]] = args[key]
2657         # ok, now handle the specials we received in the request
2658         if self.columns and not specials.has_key('columns'):
2659             l.append(sc+'columns=%s'%(','.join(self.columns)))
2660         if self.sort and not specials.has_key('sort'):
2661             val = []
2662             for dir, attr in self.sort:
2663                 if dir == '-':
2664                     val.append('-'+attr)
2665                 else:
2666                     val.append(attr)
2667             l.append(sc+'sort=%s'%(','.join(val)))
2668         if self.group and not specials.has_key('group'):
2669             val = []
2670             for dir, attr in self.group:
2671                 if dir == '-':
2672                     val.append('-'+attr)
2673                 else:
2674                     val.append(attr)
2675             l.append(sc+'group=%s'%(','.join(val)))
2676         if self.filter and not specials.has_key('filter'):
2677             l.append(sc+'filter=%s'%(','.join(self.filter)))
2678         if self.search_text and not specials.has_key('search_text'):
2679             l.append(sc+'search_text=%s'%q(self.search_text))
2680         if not specials.has_key('pagesize'):
2681             l.append(sc+'pagesize=%s'%self.pagesize)
2682         if not specials.has_key('startwith'):
2683             l.append(sc+'startwith=%s'%self.startwith)
2685         # finally, the remainder of the filter args in the request
2686         if self.classname and self.filterspec:
2687             cls = self.client.db.getclass(self.classname)
2688             for k,v in self.filterspec.items():
2689                 if not args.has_key(k):
2690                     if type(v) == type([]):
2691                         prop = cls.get_transitive_prop(k)
2692                         if k != 'id' and isinstance(prop, hyperdb.String):
2693                             l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
2694                         else:
2695                             l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
2696                     else:
2697                         l.append('%s=%s'%(k, q(v)))
2698         return '%s?%s'%(url, '&'.join(l))
2699     indexargs_href = indexargs_url
2701     def base_javascript(self):
2702         return """
2703 <script type="text/javascript">
2704 submitted = false;
2705 function submit_once() {
2706     if (submitted) {
2707         alert("Your request is being processed.\\nPlease be patient.");
2708         event.returnValue = 0;    // work-around for IE
2709         return 0;
2710     }
2711     submitted = true;
2712     return 1;
2715 function help_window(helpurl, width, height) {
2716     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
2718 </script>
2719 """%self.base
2721     def batch(self, permission='View'):
2722         """ Return a batch object for results from the "current search"
2723         """
2724         check = self._client.db.security.hasPermission
2725         userid = self._client.userid
2726         if not check('Web Access', userid):
2727             return Batch(self.client, [], self.pagesize, self.startwith,
2728                 classname=self.classname)
2730         filterspec = self.filterspec
2731         sort = self.sort
2732         group = self.group
2734         # get the list of ids we're batching over
2735         klass = self.client.db.getclass(self.classname)
2736         if self.search_text:
2737             matches = self.client.db.indexer.search(
2738                 [w.upper().encode("utf-8", "replace") for w in re.findall(
2739                     r'(?u)\b\w{2,25}\b',
2740                     unicode(self.search_text, "utf-8", "replace")
2741                 )], klass)
2742         else:
2743             matches = None
2745         # filter for visibility
2746         l = [id for id in klass.filter(matches, filterspec, sort, group)
2747             if check(permission, userid, self.classname, itemid=id)]
2749         # return the batch object, using IDs only
2750         return Batch(self.client, l, self.pagesize, self.startwith,
2751             classname=self.classname)
2753 # extend the standard ZTUtils Batch object to remove dependency on
2754 # Acquisition and add a couple of useful methods
2755 class Batch(ZTUtils.Batch):
2756     """ Use me to turn a list of items, or item ids of a given class, into a
2757         series of batches.
2759         ========= ========================================================
2760         Parameter  Usage
2761         ========= ========================================================
2762         sequence  a list of HTMLItems or item ids
2763         classname if sequence is a list of ids, this is the class of item
2764         size      how big to make the sequence.
2765         start     where to start (0-indexed) in the sequence.
2766         end       where to end (0-indexed) in the sequence.
2767         orphan    if the next batch would contain less items than this
2768                   value, then it is combined with this batch
2769         overlap   the number of items shared between adjacent batches
2770         ========= ========================================================
2772         Attributes: Note that the "start" attribute, unlike the
2773         argument, is a 1-based index (I know, lame).  "first" is the
2774         0-based index.  "length" is the actual number of elements in
2775         the batch.
2777         "sequence_length" is the length of the original, unbatched, sequence.
2778     """
2779     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2780             overlap=0, classname=None):
2781         self.client = client
2782         self.last_index = self.last_item = None
2783         self.current_item = None
2784         self.classname = classname
2785         self.sequence_length = len(sequence)
2786         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2787             overlap)
2789     # overwrite so we can late-instantiate the HTMLItem instance
2790     def __getitem__(self, index):
2791         if index < 0:
2792             if index + self.end < self.first: raise IndexError, index
2793             return self._sequence[index + self.end]
2795         if index >= self.length:
2796             raise IndexError, index
2798         # move the last_item along - but only if the fetched index changes
2799         # (for some reason, index 0 is fetched twice)
2800         if index != self.last_index:
2801             self.last_item = self.current_item
2802             self.last_index = index
2804         item = self._sequence[index + self.first]
2805         if self.classname:
2806             # map the item ids to instances
2807             item = HTMLItem(self.client, self.classname, item)
2808         self.current_item = item
2809         return item
2811     def propchanged(self, *properties):
2812         """ Detect if one of the properties marked as being a group
2813             property changed in the last iteration fetch
2814         """
2815         # we poke directly at the _value here since MissingValue can screw
2816         # us up and cause Nones to compare strangely
2817         if self.last_item is None:
2818             return 1
2819         for property in properties:
2820             if property == 'id' or isinstance (self.last_item[property], list):
2821                 if (str(self.last_item[property]) !=
2822                     str(self.current_item[property])):
2823                     return 1
2824             else:
2825                 if (self.last_item[property]._value !=
2826                     self.current_item[property]._value):
2827                     return 1
2828         return 0
2830     # override these 'cos we don't have access to acquisition
2831     def previous(self):
2832         if self.start == 1:
2833             return None
2834         return Batch(self.client, self._sequence, self._size,
2835             self.first - self._size + self.overlap, 0, self.orphan,
2836             self.overlap)
2838     def next(self):
2839         try:
2840             self._sequence[self.end]
2841         except IndexError:
2842             return None
2843         return Batch(self.client, self._sequence, self._size,
2844             self.end - self.overlap, 0, self.orphan, self.overlap)
2846 class TemplatingUtils:
2847     """ Utilities for templating
2848     """
2849     def __init__(self, client):
2850         self.client = client
2851     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2852         return Batch(self.client, sequence, size, start, end, orphan,
2853             overlap)
2855     def url_quote(self, url):
2856         """URL-quote the supplied text."""
2857         return urllib.quote(url)
2859     def html_quote(self, html):
2860         """HTML-quote the supplied text."""
2861         return cgi.escape(html)
2863     def __getattr__(self, name):
2864         """Try the tracker's templating_utils."""
2865         if not hasattr(self.client.instance, 'templating_utils'):
2866             # backwards-compatibility
2867             raise AttributeError, name
2868         if not self.client.instance.templating_utils.has_key(name):
2869             raise AttributeError, name
2870         return self.client.instance.templating_utils[name]
2872     def keywords_expressions(self, request):
2873         return render_keywords_expression_editor(request)
2875     def html_calendar(self, request):
2876         """Generate a HTML calendar.
2878         `request`  the roundup.request object
2879                    - @template : name of the template
2880                    - form      : name of the form to store back the date
2881                    - property  : name of the property of the form to store
2882                                  back the date
2883                    - date      : current date
2884                    - display   : when browsing, specifies year and month
2886         html will simply be a table.
2887         """
2888         tz = request.client.db.getUserTimezone()
2889         current_date = date.Date(".").local(tz)
2890         date_str  = request.form.getfirst("date", current_date)
2891         display   = request.form.getfirst("display", date_str)
2892         template  = request.form.getfirst("@template", "calendar")
2893         form      = request.form.getfirst("form")
2894         property  = request.form.getfirst("property")
2895         curr_date = date.Date(date_str) # to highlight
2896         display   = date.Date(display)  # to show
2897         day       = display.day
2899         # for navigation
2900         date_prev_month = display + date.Interval("-1m")
2901         date_next_month = display + date.Interval("+1m")
2902         date_prev_year  = display + date.Interval("-1y")
2903         date_next_year  = display + date.Interval("+1y")
2905         res = []
2907         base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
2908                     (request.classname, template, property, form, curr_date)
2910         # navigation
2911         # month
2912         res.append('<table class="calendar"><tr><td>')
2913         res.append(' <table width="100%" class="calendar_nav"><tr>')
2914         link = "&display=%s"%date_prev_month
2915         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2916             date_prev_month))
2917         res.append('  <td>%s</td>'%calendar.month_name[display.month])
2918         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2919             date_next_month))
2920         # spacer
2921         res.append('  <td width="100%"></td>')
2922         # year
2923         res.append('  <td><a href="%s&display=%s">&lt;</a></td>'%(base_link,
2924             date_prev_year))
2925         res.append('  <td>%s</td>'%display.year)
2926         res.append('  <td><a href="%s&display=%s">&gt;</a></td>'%(base_link,
2927             date_next_year))
2928         res.append(' </tr></table>')
2929         res.append(' </td></tr>')
2931         # the calendar
2932         res.append(' <tr><td><table class="calendar_display">')
2933         res.append('  <tr class="weekdays">')
2934         for day in calendar.weekheader(3).split():
2935             res.append('   <td>%s</td>'%day)
2936         res.append('  </tr>')
2937         for week in calendar.monthcalendar(display.year, display.month):
2938             res.append('  <tr>')
2939             for day in week:
2940                 link = "javascript:form[field].value = '%d-%02d-%02d'; " \
2941                       "window.close ();"%(display.year, display.month, day)
2942                 if (day == curr_date.day and display.month == curr_date.month
2943                         and display.year == curr_date.year):
2944                     # highlight
2945                     style = "today"
2946                 else :
2947                     style = ""
2948                 if day:
2949                     res.append('   <td class="%s"><a href="%s">%s</a></td>'%(
2950                         style, link, day))
2951                 else :
2952                     res.append('   <td></td>')
2953             res.append('  </tr>')
2954         res.append('</table></td></tr></table>')
2955         return "\n".join(res)
2957 class MissingValue:
2958     def __init__(self, description, **kwargs):
2959         self.__description = description
2960         for key, value in kwargs.items():
2961             self.__dict__[key] = value
2963     def __call__(self, *args, **kwargs): return MissingValue(self.__description)
2964     def __getattr__(self, name):
2965         # This allows assignments which assume all intermediate steps are Null
2966         # objects if they don't exist yet.
2967         #
2968         # For example (with just 'client' defined):
2969         #
2970         # client.db.config.TRACKER_WEB = 'BASE/'
2971         self.__dict__[name] = MissingValue(self.__description)
2972         return getattr(self, name)
2974     def __getitem__(self, key): return self
2975     def __nonzero__(self): return 0
2976     def __str__(self): return '[%s]'%self.__description
2977     def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
2978         self.__description)
2979     def gettext(self, str): return str
2980     _ = gettext
2982 # vim: set et sts=4 sw=4 :