Code

Cleaned up CHANGES/TODO
[roundup.git] / roundup / cgi / templating.py
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7     import cPickle as pickle
8 except ImportError:
9     import pickle
10 try:
11     import cStringIO as StringIO
12 except ImportError:
13     import StringIO
14 try:
15     import StructuredText
16 except ImportError:
17     StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 #    ''' Interface to get a template, possibly loading a compiled template.
28 #    '''
29 #    # source
30 #    src = os.path.join(dir, name)
31 #
32 #    # see if we can get a compile from the template"c" directory (most
33 #    # likely is "htmlc"
34 #    split = list(os.path.split(dir))
35 #    split[-1] = split[-1] + 'c'
36 #    cdir = os.path.join(*split)
37 #    split.append(name)
38 #    cpl = os.path.join(*split)
39 #
40 #    # ok, now see if the source is newer than the compiled (or if the
41 #    # compiled even exists)
42 #    MTIME = os.path.stat.ST_MTIME
43 #    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 #        # nope, we need to compile
45 #        pt = RoundupPageTemplate()
46 #        pt.write(open(src).read())
47 #        pt.id = name
48 #
49 #        # save off the compiled template
50 #        if not os.path.exists(cdir):
51 #            os.makedirs(cdir)
52 #        f = open(cpl, 'wb')
53 #        pickle.dump(pt, f)
54 #        f.close()
55 #    else:
56 #        # yay, use the compiled template
57 #        f = open(cpl, 'rb')
58 #        pt = pickle.load(f)
59 #    return pt
61 templates = {}
63 class NoTemplate(Exception):
64     pass
66 def getTemplate(dir, name, extension, classname=None, request=None):
67     ''' Interface to get a template, possibly loading a compiled template.
69         "name" and "extension" indicate the template we're after, which in
70         most cases will be "name.extension". If "extension" is None, then
71         we look for a template just called "name" with no extension.
73         If the file "name.extension" doesn't exist, we look for
74         "_generic.extension" as a fallback.
75     '''
76     # default the name to "home"
77     if name is None:
78         name = 'home'
80     # find the source, figure the time it was last modified
81     if extension:
82         filename = '%s.%s'%(name, extension)
83     else:
84         filename = name
85     src = os.path.join(dir, filename)
86     try:
87         stime = os.stat(src)[os.path.stat.ST_MTIME]
88     except os.error, error:
89         if error.errno != errno.ENOENT:
90             raise
91         if not extension:
92             raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
94         # try for a generic template
95         generic = '_generic.%s'%extension
96         src = os.path.join(dir, generic)
97         try:
98             stime = os.stat(src)[os.path.stat.ST_MTIME]
99         except os.error, error:
100             if error.errno != errno.ENOENT:
101                 raise
102             # nicer error
103             raise NoTemplate, 'No template file exists for templating '\
104                 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
105                 extension, filename, generic)
106         filename = generic
108     key = (dir, filename)
109     if templates.has_key(key) and stime < templates[key].mtime:
110         # compiled template is up to date
111         return templates[key]
113     # compile the template
114     templates[key] = pt = RoundupPageTemplate()
115     pt.write(open(src).read())
116     pt.id = filename
117     pt.mtime = time.time()
118     return pt
120 class RoundupPageTemplate(PageTemplate.PageTemplate):
121     ''' A Roundup-specific PageTemplate.
123         Interrogate the client to set up the various template variables to
124         be available:
126         *context*
127          this is one of three things:
128          1. None - we're viewing a "home" page
129          2. The current class of item being displayed. This is an HTMLClass
130             instance.
131          3. The current item from the database, if we're viewing a specific
132             item, as an HTMLItem instance.
133         *request*
134           Includes information about the current request, including:
135            - the url
136            - the current index information (``filterspec``, ``filter`` args,
137              ``properties``, etc) parsed out of the form. 
138            - methods for easy filterspec link generation
139            - *user*, the current user node as an HTMLItem instance
140            - *form*, the current CGI form information as a FieldStorage
141         *instance*
142           The current instance
143         *db*
144           The current database, through which db.config may be reached.
145     '''
146     def getContext(self, client, classname, request):
147         c = {
148              'options': {},
149              'nothing': None,
150              'request': request,
151              'content': client.content,
152              'db': HTMLDatabase(client),
153              'instance': client.instance,
154              'utils': TemplatingUtils(client),
155         }
156         # add in the item if there is one
157         if client.nodeid:
158             c['context'] = HTMLItem(client, classname, client.nodeid)
159         else:
160             c['context'] = HTMLClass(client, classname)
161         return c
163     def render(self, client, classname, request, **options):
164         """Render this Page Template"""
166         if not self._v_cooked:
167             self._cook()
169         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
171         if self._v_errors:
172             raise PageTemplate.PTRuntimeError, \
173                 'Page Template %s has errors.' % self.id
175         # figure the context
176         classname = classname or client.classname
177         request = request or HTMLRequest(client)
178         c = self.getContext(client, classname, request)
179         c.update({'options': options})
181         # and go
182         output = StringIO.StringIO()
183         TALInterpreter(self._v_program, self._v_macros,
184             getEngine().getContext(c), output, tal=1, strictinsert=0)()
185         return output.getvalue()
187 class HTMLDatabase:
188     ''' Return HTMLClasses for valid class fetches
189     '''
190     def __init__(self, client):
191         self._client = client
193         # we want config to be exposed
194         self.config = client.db.config
196     def __getattr__(self, attr):
197         try:
198             self._client.db.getclass(attr)
199         except KeyError:
200             raise AttributeError, attr
201         return HTMLClass(self._client, attr)
202     def classes(self):
203         l = self._client.db.classes.keys()
204         l.sort()
205         return [HTMLClass(self._client, cn) for cn in l]
207 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
208     cl = db.getclass(prop.classname)
209     l = []
210     for entry in ids:
211         if num_re.match(entry):
212             l.append(entry)
213         else:
214             l.append(cl.lookup(entry))
215     return l
217 class HTMLClass:
218     ''' Accesses through a class (either through *class* or *db.<classname>*)
219     '''
220     def __init__(self, client, classname):
221         self._client = client
222         self._db = client.db
224         # we want classname to be exposed
225         self.classname = classname
226         if classname is not None:
227             self._klass = self._db.getclass(self.classname)
228             self._props = self._klass.getprops()
230     def __repr__(self):
231         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
233     def __getitem__(self, item):
234         ''' return an HTMLProperty instance
235         '''
236        #print 'HTMLClass.getitem', (self, item)
238         # we don't exist
239         if item == 'id':
240             return None
242         # get the property
243         prop = self._props[item]
245         # look up the correct HTMLProperty class
246         form = self._client.form
247         for klass, htmlklass in propclasses:
248             if not isinstance(prop, klass):
249                 continue
250             if form.has_key(item):
251                 if isinstance(prop, hyperdb.Multilink):
252                     value = lookupIds(self._db, prop,
253                         handleListCGIValue(form[item]))
254                 elif isinstance(prop, hyperdb.Link):
255                     value = form[item].value.strip()
256                     if value:
257                         value = lookupIds(self._db, prop, [value])[0]
258                     else:
259                         value = None
260                 else:
261                     value = form[item].value.strip() or None
262             else:
263                 if isinstance(prop, hyperdb.Multilink):
264                     value = []
265                 else:
266                     value = None
267             return htmlklass(self._client, '', prop, item, value)
269         # no good
270         raise KeyError, item
272     def __getattr__(self, attr):
273         ''' convenience access '''
274         try:
275             return self[attr]
276         except KeyError:
277             raise AttributeError, attr
279     def properties(self):
280         ''' Return HTMLProperty for all of this class' properties.
281         '''
282         l = []
283         for name, prop in self._props.items():
284             for klass, htmlklass in propclasses:
285                 if isinstance(prop, hyperdb.Multilink):
286                     value = []
287                 else:
288                     value = None
289                 if isinstance(prop, klass):
290                     l.append(htmlklass(self._client, '', prop, name, value))
291         return l
293     def list(self):
294         ''' List all items in this class.
295         '''
296         if self.classname == 'user':
297             klass = HTMLUser
298         else:
299             klass = HTMLItem
300         l = [klass(self._client, self.classname, x) for x in self._klass.list()]
301         return l
303     def csv(self):
304         ''' Return the items of this class as a chunk of CSV text.
305         '''
306         # get the CSV module
307         try:
308             import csv
309         except ImportError:
310             return 'Sorry, you need the csv module to use this function.\n'\
311                 'Get it from: http://www.object-craft.com.au/projects/csv/'
313         props = self.propnames()
314         p = csv.parser()
315         s = StringIO.StringIO()
316         s.write(p.join(props) + '\n')
317         for nodeid in self._klass.list():
318             l = []
319             for name in props:
320                 value = self._klass.get(nodeid, name)
321                 if value is None:
322                     l.append('')
323                 elif isinstance(value, type([])):
324                     l.append(':'.join(map(str, value)))
325                 else:
326                     l.append(str(self._klass.get(nodeid, name)))
327             s.write(p.join(l) + '\n')
328         return s.getvalue()
330     def propnames(self):
331         ''' Return the list of the names of the properties of this class.
332         '''
333         idlessprops = self._klass.getprops(protected=0).keys()
334         idlessprops.sort()
335         return ['id'] + idlessprops
337     def filter(self, request=None):
338         ''' Return a list of items from this class, filtered and sorted
339             by the current requested filterspec/filter/sort/group args
340         '''
341         if request is not None:
342             filterspec = request.filterspec
343             sort = request.sort
344             group = request.group
345         if self.classname == 'user':
346             klass = HTMLUser
347         else:
348             klass = HTMLItem
349         l = [klass(self._client, self.classname, x)
350              for x in self._klass.filter(None, filterspec, sort, group)]
351         return l
353     def classhelp(self, properties=None, label='list', width='500',
354             height='400'):
355         ''' Pop up a javascript window with class help
357             This generates a link to a popup window which displays the 
358             properties indicated by "properties" of the class named by
359             "classname". The "properties" should be a comma-separated list
360             (eg. 'id,name,description'). Properties defaults to all the
361             properties of a class (excluding id, creator, created and
362             activity).
364             You may optionally override the label displayed, the width and
365             height. The popup window will be resizable and scrollable.
366         '''
367         if properties is None:
368             properties = self._klass.getprops(protected=0).keys()
369             properties.sort()
370             properties = ','.join(properties)
371         return '<a href="javascript:help_window(\'%s?:template=help&' \
372             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
373             '(%s)</b></a>'%(self.classname, properties, width, height, label)
375     def submit(self, label="Submit New Entry"):
376         ''' Generate a submit button (and action hidden element)
377         '''
378         return '  <input type="hidden" name=":action" value="new">\n'\
379         '  <input type="submit" name="submit" value="%s">'%label
381     def history(self):
382         return 'New node - no history'
384     def renderWith(self, name, **kwargs):
385         ''' Render this class with the given template.
386         '''
387         # create a new request and override the specified args
388         req = HTMLRequest(self._client)
389         req.classname = self.classname
390         req.update(kwargs)
392         # new template, using the specified classname and request
393         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
395         # use our fabricated request
396         return pt.render(self._client, self.classname, req)
398 class HTMLItem:
399     ''' Accesses through an *item*
400     '''
401     def __init__(self, client, classname, nodeid):
402         self._client = client
403         self._db = client.db
404         self._classname = classname
405         self._nodeid = nodeid
406         self._klass = self._db.getclass(classname)
407         self._props = self._klass.getprops()
409     def __repr__(self):
410         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
411             self._nodeid)
413     def __getitem__(self, item):
414         ''' return an HTMLProperty instance
415         '''
416        #print 'HTMLItem.getitem', (self, item)
417         if item == 'id':
418             return self._nodeid
420         # get the property
421         prop = self._props[item]
423         # get the value, handling missing values
424         value = self._klass.get(self._nodeid, item, None)
425         if value is None:
426             if isinstance(self._props[item], hyperdb.Multilink):
427                 value = []
429         # look up the correct HTMLProperty class
430         for klass, htmlklass in propclasses:
431             if isinstance(prop, klass):
432                 return htmlklass(self._client, self._nodeid, prop, item, value)
434         raise KeyErorr, item
436     def __getattr__(self, attr):
437         ''' convenience access to properties '''
438         try:
439             return self[attr]
440         except KeyError:
441             raise AttributeError, attr
442     
443     def submit(self, label="Submit Changes"):
444         ''' Generate a submit button (and action hidden element)
445         '''
446         return '  <input type="hidden" name=":action" value="edit">\n'\
447         '  <input type="submit" name="submit" value="%s">'%label
449     def journal(self, direction='descending'):
450         ''' Return a list of HTMLJournalEntry instances.
451         '''
452         # XXX do this
453         return []
455     def history(self, direction='descending'):
456         l = ['<table class="history">'
457              '<tr><th colspan="4" class="header">',
458              _('History'),
459              '</th></tr><tr>',
460              _('<th>Date</th>'),
461              _('<th>User</th>'),
462              _('<th>Action</th>'),
463              _('<th>Args</th>'),
464             '</tr>']
465         comments = {}
466         history = self._klass.history(self._nodeid)
467         history.sort()
468         if direction == 'descending':
469             history.reverse()
470         for id, evt_date, user, action, args in history:
471             date_s = str(evt_date).replace("."," ")
472             arg_s = ''
473             if action == 'link' and type(args) == type(()):
474                 if len(args) == 3:
475                     linkcl, linkid, key = args
476                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
477                         linkcl, linkid, key)
478                 else:
479                     arg_s = str(args)
481             elif action == 'unlink' and type(args) == type(()):
482                 if len(args) == 3:
483                     linkcl, linkid, key = args
484                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
485                         linkcl, linkid, key)
486                 else:
487                     arg_s = str(args)
489             elif type(args) == type({}):
490                 cell = []
491                 for k in args.keys():
492                     # try to get the relevant property and treat it
493                     # specially
494                     try:
495                         prop = self._props[k]
496                     except KeyError:
497                         prop = None
498                     if prop is not None:
499                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
500                                 isinstance(prop, hyperdb.Link)):
501                             # figure what the link class is
502                             classname = prop.classname
503                             try:
504                                 linkcl = self._db.getclass(classname)
505                             except KeyError:
506                                 labelprop = None
507                                 comments[classname] = _('''The linked class
508                                     %(classname)s no longer exists''')%locals()
509                             labelprop = linkcl.labelprop(1)
510                             hrefable = os.path.exists(
511                                 os.path.join(self._db.config.TEMPLATES,
512                                 classname+'.item'))
514                         if isinstance(prop, hyperdb.Multilink) and \
515                                 len(args[k]) > 0:
516                             ml = []
517                             for linkid in args[k]:
518                                 if isinstance(linkid, type(())):
519                                     sublabel = linkid[0] + ' '
520                                     linkids = linkid[1]
521                                 else:
522                                     sublabel = ''
523                                     linkids = [linkid]
524                                 subml = []
525                                 for linkid in linkids:
526                                     label = classname + linkid
527                                     # if we have a label property, try to use it
528                                     # TODO: test for node existence even when
529                                     # there's no labelprop!
530                                     try:
531                                         if labelprop is not None:
532                                             label = linkcl.get(linkid, labelprop)
533                                     except IndexError:
534                                         comments['no_link'] = _('''<strike>The
535                                             linked node no longer
536                                             exists</strike>''')
537                                         subml.append('<strike>%s</strike>'%label)
538                                     else:
539                                         if hrefable:
540                                             subml.append('<a href="%s%s">%s</a>'%(
541                                                 classname, linkid, label))
542                                 ml.append(sublabel + ', '.join(subml))
543                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
544                         elif isinstance(prop, hyperdb.Link) and args[k]:
545                             label = classname + args[k]
546                             # if we have a label property, try to use it
547                             # TODO: test for node existence even when
548                             # there's no labelprop!
549                             if labelprop is not None:
550                                 try:
551                                     label = linkcl.get(args[k], labelprop)
552                                 except IndexError:
553                                     comments['no_link'] = _('''<strike>The
554                                         linked node no longer
555                                         exists</strike>''')
556                                     cell.append(' <strike>%s</strike>,\n'%label)
557                                     # "flag" this is done .... euwww
558                                     label = None
559                             if label is not None:
560                                 if hrefable:
561                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
562                                         classname, args[k], label))
563                                 else:
564                                     cell.append('%s: %s' % (k,label))
566                         elif isinstance(prop, hyperdb.Date) and args[k]:
567                             d = date.Date(args[k])
568                             cell.append('%s: %s'%(k, str(d)))
570                         elif isinstance(prop, hyperdb.Interval) and args[k]:
571                             d = date.Interval(args[k])
572                             cell.append('%s: %s'%(k, str(d)))
574                         elif isinstance(prop, hyperdb.String) and args[k]:
575                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
577                         elif not args[k]:
578                             cell.append('%s: (no value)\n'%k)
580                         else:
581                             cell.append('%s: %s\n'%(k, str(args[k])))
582                     else:
583                         # property no longer exists
584                         comments['no_exist'] = _('''<em>The indicated property
585                             no longer exists</em>''')
586                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
587                 arg_s = '<br />'.join(cell)
588             else:
589                 # unkown event!!
590                 comments['unknown'] = _('''<strong><em>This event is not
591                     handled by the history display!</em></strong>''')
592                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
593             date_s = date_s.replace(' ', '&nbsp;')
594             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
595                 date_s, user, action, arg_s))
596         if comments:
597             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
598         for entry in comments.values():
599             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
600         l.append('</table>')
601         return '\n'.join(l)
603     def renderQueryForm(self):
604         ''' Render this item, which is a query, as a search form.
605         '''
606         # create a new request and override the specified args
607         req = HTMLRequest(self._client)
608         req.classname = self._klass.get(self._nodeid, 'klass')
609         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
611         # new template, using the specified classname and request
612         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
614         # use our fabricated request
615         return pt.render(self._client, req.classname, req)
617 class HTMLUser(HTMLItem):
618     ''' Accesses through the *user* (a special case of item)
619     '''
620     def __init__(self, client, classname, nodeid):
621         HTMLItem.__init__(self, client, 'user', nodeid)
622         self._default_classname = client.classname
624         # used for security checks
625         self._security = client.db.security
626     _marker = []
627     def hasPermission(self, role, classname=_marker):
628         ''' Determine if the user has the Role.
630             The class being tested defaults to the template's class, but may
631             be overidden for this test by suppling an alternate classname.
632         '''
633         if classname is self._marker:
634             classname = self._default_classname
635         return self._security.hasPermission(role, self._nodeid, classname)
637 class HTMLProperty:
638     ''' String, Number, Date, Interval HTMLProperty
640         Has useful attributes:
642          _name  the name of the property
643          _value the value of the property if any
645         A wrapper object which may be stringified for the plain() behaviour.
646     '''
647     def __init__(self, client, nodeid, prop, name, value):
648         self._client = client
649         self._db = client.db
650         self._nodeid = nodeid
651         self._prop = prop
652         self._name = name
653         self._value = value
654     def __repr__(self):
655         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
656     def __str__(self):
657         return self.plain()
658     def __cmp__(self, other):
659         if isinstance(other, HTMLProperty):
660             return cmp(self._value, other._value)
661         return cmp(self._value, other)
663 class StringHTMLProperty(HTMLProperty):
664     def plain(self, escape=0):
665         ''' Render a "plain" representation of the property
666         '''
667         if self._value is None:
668             return ''
669         if escape:
670             return cgi.escape(str(self._value))
671         return str(self._value)
673     def stext(self, escape=0):
674         ''' Render the value of the property as StructuredText.
676             This requires the StructureText module to be installed separately.
677         '''
678         s = self.plain(escape=escape)
679         if not StructuredText:
680             return s
681         return StructuredText(s,level=1,header=0)
683     def field(self, size = 30):
684         ''' Render a form edit field for the property
685         '''
686         if self._value is None:
687             value = ''
688         else:
689             value = cgi.escape(str(self._value))
690             value = '&quot;'.join(value.split('"'))
691         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
693     def multiline(self, escape=0, rows=5, cols=40):
694         ''' Render a multiline form edit field for the property
695         '''
696         if self._value is None:
697             value = ''
698         else:
699             value = cgi.escape(str(self._value))
700             value = '&quot;'.join(value.split('"'))
701         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
702             self._name, rows, cols, value)
704     def email(self, escape=1):
705         ''' Render the value of the property as an obscured email address
706         '''
707         if self._value is None: value = ''
708         else: value = str(self._value)
709         if value.find('@') != -1:
710             name, domain = value.split('@')
711             domain = ' '.join(domain.split('.')[:-1])
712             name = name.replace('.', ' ')
713             value = '%s at %s ...'%(name, domain)
714         else:
715             value = value.replace('.', ' ')
716         if escape:
717             value = cgi.escape(value)
718         return value
720 class PasswordHTMLProperty(HTMLProperty):
721     def plain(self):
722         ''' Render a "plain" representation of the property
723         '''
724         if self._value is None:
725             return ''
726         return _('*encrypted*')
728     def field(self, size = 30):
729         ''' Render a form edit field for the property
730         '''
731         return '<input type="password" name="%s" size="%s">'%(self._name, size)
733 class NumberHTMLProperty(HTMLProperty):
734     def plain(self):
735         ''' Render a "plain" representation of the property
736         '''
737         return str(self._value)
739     def field(self, size = 30):
740         ''' Render a form edit field for the property
741         '''
742         if self._value is None:
743             value = ''
744         else:
745             value = cgi.escape(str(self._value))
746             value = '&quot;'.join(value.split('"'))
747         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
749 class BooleanHTMLProperty(HTMLProperty):
750     def plain(self):
751         ''' Render a "plain" representation of the property
752         '''
753         if self.value is None:
754             return ''
755         return self._value and "Yes" or "No"
757     def field(self):
758         ''' Render a form edit field for the property
759         '''
760         checked = self._value and "checked" or ""
761         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
762             checked)
763         if checked:
764             checked = ""
765         else:
766             checked = "checked"
767         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
768             checked)
769         return s
771 class DateHTMLProperty(HTMLProperty):
772     def plain(self):
773         ''' Render a "plain" representation of the property
774         '''
775         if self._value is None:
776             return ''
777         return str(self._value)
779     def field(self, size = 30):
780         ''' Render a form edit field for the property
781         '''
782         if self._value is None:
783             value = ''
784         else:
785             value = cgi.escape(str(self._value))
786             value = '&quot;'.join(value.split('"'))
787         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
789     def reldate(self, pretty=1):
790         ''' Render the interval between the date and now.
792             If the "pretty" flag is true, then make the display pretty.
793         '''
794         if not self._value:
795             return ''
797         # figure the interval
798         interval = date.Date('.') - self._value
799         if pretty:
800             return interval.pretty()
801         return str(interval)
803 class IntervalHTMLProperty(HTMLProperty):
804     def plain(self):
805         ''' Render a "plain" representation of the property
806         '''
807         if self._value is None:
808             return ''
809         return str(self._value)
811     def pretty(self):
812         ''' Render the interval in a pretty format (eg. "yesterday")
813         '''
814         return self._value.pretty()
816     def field(self, size = 30):
817         ''' Render a form edit field for the property
818         '''
819         if self._value is None:
820             value = ''
821         else:
822             value = cgi.escape(str(self._value))
823             value = '&quot;'.join(value.split('"'))
824         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
826 class LinkHTMLProperty(HTMLProperty):
827     ''' Link HTMLProperty
828         Include the above as well as being able to access the class
829         information. Stringifying the object itself results in the value
830         from the item being displayed. Accessing attributes of this object
831         result in the appropriate entry from the class being queried for the
832         property accessed (so item/assignedto/name would look up the user
833         entry identified by the assignedto property on item, and then the
834         name property of that user)
835     '''
836     def __getattr__(self, attr):
837         ''' return a new HTMLItem '''
838        #print 'Link.getattr', (self, attr, self._value)
839         if not self._value:
840             raise AttributeError, "Can't access missing value"
841         if self._prop.classname == 'user':
842             klass = HTMLUser
843         else:
844             klass = HTMLItem
845         i = klass(self._client, self._prop.classname, self._value)
846         return getattr(i, attr)
848     def plain(self, escape=0):
849         ''' Render a "plain" representation of the property
850         '''
851         if self._value is None:
852             return ''
853         linkcl = self._db.classes[self._prop.classname]
854         k = linkcl.labelprop(1)
855         value = str(linkcl.get(self._value, k))
856         if escape:
857             value = cgi.escape(value)
858         return value
860     def field(self):
861         ''' Render a form edit field for the property
862         '''
863         linkcl = self._db.getclass(self._prop.classname)
864         if linkcl.getprops().has_key('order'):  
865             sort_on = 'order'  
866         else:  
867             sort_on = linkcl.labelprop()  
868         options = linkcl.filter(None, {}, [sort_on], []) 
869         # TODO: make this a field display, not a menu one!
870         l = ['<select name="%s">'%property]
871         k = linkcl.labelprop(1)
872         if value is None:
873             s = 'selected '
874         else:
875             s = ''
876         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
877         for optionid in options:
878             option = linkcl.get(optionid, k)
879             s = ''
880             if optionid == value:
881                 s = 'selected '
882             if showid:
883                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
884             else:
885                 lab = option
886             if size is not None and len(lab) > size:
887                 lab = lab[:size-3] + '...'
888             lab = cgi.escape(lab)
889             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
890         l.append('</select>')
891         return '\n'.join(l)
893     def menu(self, size=None, height=None, showid=0, additional=[],
894             **conditions):
895         ''' Render a form select list for this property
896         '''
897         value = self._value
899         # sort function
900         sortfunc = make_sort_function(self._db, self._prop.classname)
902         # force the value to be a single choice
903         if isinstance(value, type('')):
904             value = value[0]
905         linkcl = self._db.getclass(self._prop.classname)
906         l = ['<select name="%s">'%self._name]
907         k = linkcl.labelprop(1)
908         s = ''
909         if value is None:
910             s = 'selected '
911         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
912         if linkcl.getprops().has_key('order'):  
913             sort_on = ('+', 'order')
914         else:  
915             sort_on = ('+', linkcl.labelprop())
916         options = linkcl.filter(None, conditions, sort_on, (None, None))
917         for optionid in options:
918             option = linkcl.get(optionid, k)
919             s = ''
920             if value in [optionid, option]:
921                 s = 'selected '
922             if showid:
923                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
924             else:
925                 lab = option
926             if size is not None and len(lab) > size:
927                 lab = lab[:size-3] + '...'
928             if additional:
929                 m = []
930                 for propname in additional:
931                     m.append(linkcl.get(optionid, propname))
932                 lab = lab + ' (%s)'%', '.join(map(str, m))
933             lab = cgi.escape(lab)
934             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
935         l.append('</select>')
936         return '\n'.join(l)
937 #    def checklist(self, ...)
939 class MultilinkHTMLProperty(HTMLProperty):
940     ''' Multilink HTMLProperty
942         Also be iterable, returning a wrapper object like the Link case for
943         each entry in the multilink.
944     '''
945     def __len__(self):
946         ''' length of the multilink '''
947         return len(self._value)
949     def __getattr__(self, attr):
950         ''' no extended attribute accesses make sense here '''
951         raise AttributeError, attr
953     def __getitem__(self, num):
954         ''' iterate and return a new HTMLItem
955         '''
956        #print 'Multi.getitem', (self, num)
957         value = self._value[num]
958         if self._prop.classname == 'user':
959             klass = HTMLUser
960         else:
961             klass = HTMLItem
962         return klass(self._client, self._prop.classname, value)
964     def __contains__(self, value):
965         ''' Support the "in" operator
966         '''
967         return value in self._value
969     def reverse(self):
970         ''' return the list in reverse order
971         '''
972         l = self._value[:]
973         l.reverse()
974         if self._prop.classname == 'user':
975             klass = HTMLUser
976         else:
977             klass = HTMLItem
978         return [klass(self._client, self._prop.classname, value) for value in l]
980     def plain(self, escape=0):
981         ''' Render a "plain" representation of the property
982         '''
983         linkcl = self._db.classes[self._prop.classname]
984         k = linkcl.labelprop(1)
985         labels = []
986         for v in self._value:
987             labels.append(linkcl.get(v, k))
988         value = ', '.join(labels)
989         if escape:
990             value = cgi.escape(value)
991         return value
993     def field(self, size=30, showid=0):
994         ''' Render a form edit field for the property
995         '''
996         sortfunc = make_sort_function(self._db, self._prop.classname)
997         linkcl = self._db.getclass(self._prop.classname)
998         value = self._value[:]
999         if value:
1000             value.sort(sortfunc)
1001         # map the id to the label property
1002         if not showid:
1003             k = linkcl.labelprop(1)
1004             value = [linkcl.get(v, k) for v in value]
1005         value = cgi.escape(','.join(value))
1006         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1008     def menu(self, size=None, height=None, showid=0, additional=[],
1009             **conditions):
1010         ''' Render a form select list for this property
1011         '''
1012         value = self._value
1014         # sort function
1015         sortfunc = make_sort_function(self._db, self._prop.classname)
1017         linkcl = self._db.getclass(self._prop.classname)
1018         if linkcl.getprops().has_key('order'):  
1019             sort_on = ('+', 'order')
1020         else:  
1021             sort_on = ('+', linkcl.labelprop())
1022         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1023         height = height or min(len(options), 7)
1024         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1025         k = linkcl.labelprop(1)
1026         for optionid in options:
1027             option = linkcl.get(optionid, k)
1028             s = ''
1029             if optionid in value or option in value:
1030                 s = 'selected '
1031             if showid:
1032                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1033             else:
1034                 lab = option
1035             if size is not None and len(lab) > size:
1036                 lab = lab[:size-3] + '...'
1037             if additional:
1038                 m = []
1039                 for propname in additional:
1040                     m.append(linkcl.get(optionid, propname))
1041                 lab = lab + ' (%s)'%', '.join(m)
1042             lab = cgi.escape(lab)
1043             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1044                 lab))
1045         l.append('</select>')
1046         return '\n'.join(l)
1048 # set the propclasses for HTMLItem
1049 propclasses = (
1050     (hyperdb.String, StringHTMLProperty),
1051     (hyperdb.Number, NumberHTMLProperty),
1052     (hyperdb.Boolean, BooleanHTMLProperty),
1053     (hyperdb.Date, DateHTMLProperty),
1054     (hyperdb.Interval, IntervalHTMLProperty),
1055     (hyperdb.Password, PasswordHTMLProperty),
1056     (hyperdb.Link, LinkHTMLProperty),
1057     (hyperdb.Multilink, MultilinkHTMLProperty),
1060 def make_sort_function(db, classname):
1061     '''Make a sort function for a given class
1062     '''
1063     linkcl = db.getclass(classname)
1064     if linkcl.getprops().has_key('order'):
1065         sort_on = 'order'
1066     else:
1067         sort_on = linkcl.labelprop()
1068     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1069         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1070     return sortfunc
1072 def handleListCGIValue(value):
1073     ''' Value is either a single item or a list of items. Each item has a
1074         .value that we're actually interested in.
1075     '''
1076     if isinstance(value, type([])):
1077         return [value.value for value in value]
1078     else:
1079         value = value.value.strip()
1080         if not value:
1081             return []
1082         return value.split(',')
1084 class ShowDict:
1085     ''' A convenience access to the :columns index parameters
1086     '''
1087     def __init__(self, columns):
1088         self.columns = {}
1089         for col in columns:
1090             self.columns[col] = 1
1091     def __getitem__(self, name):
1092         return self.columns.has_key(name)
1094 class HTMLRequest:
1095     ''' The *request*, holding the CGI form and environment.
1097         "form" the CGI form as a cgi.FieldStorage
1098         "env" the CGI environment variables
1099         "url" the current URL path for this request
1100         "base" the base URL for this instance
1101         "user" a HTMLUser instance for this user
1102         "classname" the current classname (possibly None)
1103         "template" the current template (suffix, also possibly None)
1105         Index args:
1106         "columns" dictionary of the columns to display in an index page
1107         "show" a convenience access to columns - request/show/colname will
1108                be true if the columns should be displayed, false otherwise
1109         "sort" index sort column (direction, column name)
1110         "group" index grouping property (direction, column name)
1111         "filter" properties to filter the index on
1112         "filterspec" values to filter the index on
1113         "search_text" text to perform a full-text search on for an index
1115     '''
1116     def __init__(self, client):
1117         self.client = client
1119         # easier access vars
1120         self.form = client.form
1121         self.env = client.env
1122         self.base = client.base
1123         self.url = client.url
1124         self.user = HTMLUser(client, 'user', client.userid)
1126         # store the current class name and action
1127         self.classname = client.classname
1128         self.template = client.template
1130         self._post_init()
1132     def _post_init(self):
1133         ''' Set attributes based on self.form
1134         '''
1135         # extract the index display information from the form
1136         self.columns = []
1137         if self.form.has_key(':columns'):
1138             self.columns = handleListCGIValue(self.form[':columns'])
1139         self.show = ShowDict(self.columns)
1141         # sorting
1142         self.sort = (None, None)
1143         if self.form.has_key(':sort'):
1144             sort = self.form[':sort'].value
1145             if sort.startswith('-'):
1146                 self.sort = ('-', sort[1:])
1147             else:
1148                 self.sort = ('+', sort)
1149         if self.form.has_key(':sortdir'):
1150             self.sort = ('-', self.sort[1])
1152         # grouping
1153         self.group = (None, None)
1154         if self.form.has_key(':group'):
1155             group = self.form[':group'].value
1156             if group.startswith('-'):
1157                 self.group = ('-', group[1:])
1158             else:
1159                 self.group = ('+', group)
1160         if self.form.has_key(':groupdir'):
1161             self.group = ('-', self.group[1])
1163         # filtering
1164         self.filter = []
1165         if self.form.has_key(':filter'):
1166             self.filter = handleListCGIValue(self.form[':filter'])
1167         self.filterspec = {}
1168         if self.classname is not None:
1169             props = self.client.db.getclass(self.classname).getprops()
1170             for name in self.filter:
1171                 if self.form.has_key(name):
1172                     prop = props[name]
1173                     fv = self.form[name]
1174                     if (isinstance(prop, hyperdb.Link) or
1175                             isinstance(prop, hyperdb.Multilink)):
1176                         self.filterspec[name] = handleListCGIValue(fv)
1177                     else:
1178                         self.filterspec[name] = fv.value
1180         # full-text search argument
1181         self.search_text = None
1182         if self.form.has_key(':search_text'):
1183             self.search_text = self.form[':search_text'].value
1185         # pagination - size and start index
1186         # figure batch args
1187         if self.form.has_key(':pagesize'):
1188             self.pagesize = int(self.form[':pagesize'].value)
1189         else:
1190             self.pagesize = 50
1191         if self.form.has_key(':startwith'):
1192             self.startwith = int(self.form[':startwith'].value)
1193         else:
1194             self.startwith = 0
1196     def updateFromURL(self, url):
1197         ''' Parse the URL for query args, and update my attributes using the
1198             values.
1199         ''' 
1200         self.form = {}
1201         for name, value in cgi.parse_qsl(url):
1202             if self.form.has_key(name):
1203                 if isinstance(self.form[name], type([])):
1204                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1205                 else:
1206                     self.form[name] = [self.form[name],
1207                         cgi.MiniFieldStorage(name, value)]
1208             else:
1209                 self.form[name] = cgi.MiniFieldStorage(name, value)
1210         self._post_init()
1212     def update(self, kwargs):
1213         ''' Update my attributes using the keyword args
1214         '''
1215         self.__dict__.update(kwargs)
1216         if kwargs.has_key('columns'):
1217             self.show = ShowDict(self.columns)
1219     def description(self):
1220         ''' Return a description of the request - handle for the page title.
1221         '''
1222         s = [self.client.db.config.TRACKER_NAME]
1223         if self.classname:
1224             if self.client.nodeid:
1225                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1226             else:
1227                 if self.template == 'item':
1228                     s.append('- new %s'%self.classname)
1229                 elif self.template == 'index':
1230                     s.append('- %s index'%self.classname)
1231                 else:
1232                     s.append('- %s %s'%(self.classname, self.template))
1233         else:
1234             s.append('- home')
1235         return ' '.join(s)
1237     def __str__(self):
1238         d = {}
1239         d.update(self.__dict__)
1240         f = ''
1241         for k in self.form.keys():
1242             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1243         d['form'] = f
1244         e = ''
1245         for k,v in self.env.items():
1246             e += '\n     %r=%r'%(k, v)
1247         d['env'] = e
1248         return '''
1249 form: %(form)s
1250 url: %(url)r
1251 base: %(base)r
1252 classname: %(classname)r
1253 template: %(template)r
1254 columns: %(columns)r
1255 sort: %(sort)r
1256 group: %(group)r
1257 filter: %(filter)r
1258 search_text: %(search_text)r
1259 pagesize: %(pagesize)r
1260 startwith: %(startwith)r
1261 env: %(env)s
1262 '''%d
1264     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1265             filterspec=1):
1266         ''' return the current index args as form elements '''
1267         l = []
1268         s = '<input type="hidden" name="%s" value="%s">'
1269         if columns and self.columns:
1270             l.append(s%(':columns', ','.join(self.columns)))
1271         if sort and self.sort[1] is not None:
1272             if self.sort[0] == '-':
1273                 val = '-'+self.sort[1]
1274             else:
1275                 val = self.sort[1]
1276             l.append(s%(':sort', val))
1277         if group and self.group[1] is not None:
1278             if self.group[0] == '-':
1279                 val = '-'+self.group[1]
1280             else:
1281                 val = self.group[1]
1282             l.append(s%(':group', val))
1283         if filter and self.filter:
1284             l.append(s%(':filter', ','.join(self.filter)))
1285         if filterspec:
1286             for k,v in self.filterspec.items():
1287                 l.append(s%(k, ','.join(v)))
1288         if self.search_text:
1289             l.append(s%(':search_text', self.search_text))
1290         l.append(s%(':pagesize', self.pagesize))
1291         l.append(s%(':startwith', self.startwith))
1292         return '\n'.join(l)
1294     def indexargs_url(self, url, args):
1295         ''' embed the current index args in a URL '''
1296         l = ['%s=%s'%(k,v) for k,v in args.items()]
1297         if self.columns and not args.has_key(':columns'):
1298             l.append(':columns=%s'%(','.join(self.columns)))
1299         if self.sort[1] is not None and not args.has_key(':sort'):
1300             if self.sort[0] == '-':
1301                 val = '-'+self.sort[1]
1302             else:
1303                 val = self.sort[1]
1304             l.append(':sort=%s'%val)
1305         if self.group[1] is not None and not args.has_key(':group'):
1306             if self.group[0] == '-':
1307                 val = '-'+self.group[1]
1308             else:
1309                 val = self.group[1]
1310             l.append(':group=%s'%val)
1311         if self.filter and not args.has_key(':columns'):
1312             l.append(':filter=%s'%(','.join(self.filter)))
1313         for k,v in self.filterspec.items():
1314             if not args.has_key(k):
1315                 l.append('%s=%s'%(k, ','.join(v)))
1316         if self.search_text and not args.has_key(':search_text'):
1317             l.append(':search_text=%s'%self.search_text)
1318         if not args.has_key(':pagesize'):
1319             l.append(':pagesize=%s'%self.pagesize)
1320         if not args.has_key(':startwith'):
1321             l.append(':startwith=%s'%self.startwith)
1322         return '%s?%s'%(url, '&'.join(l))
1323     indexargs_href = indexargs_url
1325     def base_javascript(self):
1326         return '''
1327 <script language="javascript">
1328 submitted = false;
1329 function submit_once() {
1330     if (submitted) {
1331         alert("Your request is being processed.\\nPlease be patient.");
1332         return 0;
1333     }
1334     submitted = true;
1335     return 1;
1338 function help_window(helpurl, width, height) {
1339     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1341 </script>
1342 '''%self.base
1344     def batch(self):
1345         ''' Return a batch object for results from the "current search"
1346         '''
1347         filterspec = self.filterspec
1348         sort = self.sort
1349         group = self.group
1351         # get the list of ids we're batching over
1352         klass = self.client.db.getclass(self.classname)
1353         if self.search_text:
1354             matches = self.client.db.indexer.search(
1355                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1356         else:
1357             matches = None
1358         l = klass.filter(matches, filterspec, sort, group)
1360         # map the item ids to instances
1361         if self.classname == 'user':
1362             klass = HTMLUser
1363         else:
1364             klass = HTMLItem
1365         l = [klass(self.client, self.classname, item) for item in l]
1367         # return the batch object
1368         return Batch(self.client, l, self.pagesize, self.startwith)
1370 # extend the standard ZTUtils Batch object to remove dependency on
1371 # Acquisition and add a couple of useful methods
1372 class Batch(ZTUtils.Batch):
1373     ''' Use me to turn a list of items, or item ids of a given class, into a
1374         series of batches.
1376         ========= ========================================================
1377         Parameter  Usage
1378         ========= ========================================================
1379         sequence  a list of HTMLItems
1380         size      how big to make the sequence.
1381         start     where to start (0-indexed) in the sequence.
1382         end       where to end (0-indexed) in the sequence.
1383         orphan    if the next batch would contain less items than this
1384                   value, then it is combined with this batch
1385         overlap   the number of items shared between adjacent batches
1386         ========= ========================================================
1388         Attributes: Note that the "start" attribute, unlike the
1389         argument, is a 1-based index (I know, lame).  "first" is the
1390         0-based index.  "length" is the actual number of elements in
1391         the batch.
1393         "sequence_length" is the length of the original, unbatched, sequence.
1394     '''
1395     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1396             overlap=0):
1397         self.client = client
1398         self.last_index = self.last_item = None
1399         self.current_item = None
1400         self.sequence_length = len(sequence)
1401         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1402             overlap)
1404     # overwrite so we can late-instantiate the HTMLItem instance
1405     def __getitem__(self, index):
1406         if index < 0:
1407             if index + self.end < self.first: raise IndexError, index
1408             return self._sequence[index + self.end]
1409         
1410         if index >= self.length:
1411             raise IndexError, index
1413         # move the last_item along - but only if the fetched index changes
1414         # (for some reason, index 0 is fetched twice)
1415         if index != self.last_index:
1416             self.last_item = self.current_item
1417             self.last_index = index
1419         self.current_item = self._sequence[index + self.first]
1420         return self.current_item
1422     def propchanged(self, property):
1423         ''' Detect if the property marked as being the group property
1424             changed in the last iteration fetch
1425         '''
1426         if (self.last_item is None or
1427                 self.last_item[property] != self.current_item[property]):
1428             return 1
1429         return 0
1431     # override these 'cos we don't have access to acquisition
1432     def previous(self):
1433         if self.start == 1:
1434             return None
1435         return Batch(self.client, self._sequence, self._size,
1436             self.first - self._size + self.overlap, 0, self.orphan,
1437             self.overlap)
1439     def next(self):
1440         try:
1441             self._sequence[self.end]
1442         except IndexError:
1443             return None
1444         return Batch(self.client, self._sequence, self._size,
1445             self.end - self.overlap, 0, self.orphan, self.overlap)
1447 class TemplatingUtils:
1448     ''' Utilities for templating
1449     '''
1450     def __init__(self, client):
1451         self.client = client
1452     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1453         return Batch(self.client, sequence, size, start, end, orphan,
1454             overlap)