Code

more doc, bugfix in Batch
[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         value = value.replace('@', ' at ')
710         value = value.replace('.', ' ')
711         if escape:
712             value = cgi.escape(value)
713         return value
715 class PasswordHTMLProperty(HTMLProperty):
716     def plain(self):
717         ''' Render a "plain" representation of the property
718         '''
719         if self._value is None:
720             return ''
721         return _('*encrypted*')
723     def field(self, size = 30):
724         ''' Render a form edit field for the property
725         '''
726         return '<input type="password" name="%s" size="%s">'%(self._name, size)
728 class NumberHTMLProperty(HTMLProperty):
729     def plain(self):
730         ''' Render a "plain" representation of the property
731         '''
732         return str(self._value)
734     def field(self, size = 30):
735         ''' Render a form edit field for the property
736         '''
737         if self._value is None:
738             value = ''
739         else:
740             value = cgi.escape(str(self._value))
741             value = '&quot;'.join(value.split('"'))
742         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
744 class BooleanHTMLProperty(HTMLProperty):
745     def plain(self):
746         ''' Render a "plain" representation of the property
747         '''
748         if self.value is None:
749             return ''
750         return self._value and "Yes" or "No"
752     def field(self):
753         ''' Render a form edit field for the property
754         '''
755         checked = self._value and "checked" or ""
756         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
757             checked)
758         if checked:
759             checked = ""
760         else:
761             checked = "checked"
762         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
763             checked)
764         return s
766 class DateHTMLProperty(HTMLProperty):
767     def plain(self):
768         ''' Render a "plain" representation of the property
769         '''
770         if self._value is None:
771             return ''
772         return str(self._value)
774     def field(self, size = 30):
775         ''' Render a form edit field for the property
776         '''
777         if self._value is None:
778             value = ''
779         else:
780             value = cgi.escape(str(self._value))
781             value = '&quot;'.join(value.split('"'))
782         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
784     def reldate(self, pretty=1):
785         ''' Render the interval between the date and now.
787             If the "pretty" flag is true, then make the display pretty.
788         '''
789         if not self._value:
790             return ''
792         # figure the interval
793         interval = date.Date('.') - self._value
794         if pretty:
795             return interval.pretty()
796         return str(interval)
798 class IntervalHTMLProperty(HTMLProperty):
799     def plain(self):
800         ''' Render a "plain" representation of the property
801         '''
802         if self._value is None:
803             return ''
804         return str(self._value)
806     def pretty(self):
807         ''' Render the interval in a pretty format (eg. "yesterday")
808         '''
809         return self._value.pretty()
811     def field(self, size = 30):
812         ''' Render a form edit field for the property
813         '''
814         if self._value is None:
815             value = ''
816         else:
817             value = cgi.escape(str(self._value))
818             value = '&quot;'.join(value.split('"'))
819         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
821 class LinkHTMLProperty(HTMLProperty):
822     ''' Link HTMLProperty
823         Include the above as well as being able to access the class
824         information. Stringifying the object itself results in the value
825         from the item being displayed. Accessing attributes of this object
826         result in the appropriate entry from the class being queried for the
827         property accessed (so item/assignedto/name would look up the user
828         entry identified by the assignedto property on item, and then the
829         name property of that user)
830     '''
831     def __getattr__(self, attr):
832         ''' return a new HTMLItem '''
833        #print 'Link.getattr', (self, attr, self._value)
834         if not self._value:
835             raise AttributeError, "Can't access missing value"
836         if self._prop.classname == 'user':
837             klass = HTMLUser
838         else:
839             klass = HTMLItem
840         i = klass(self._client, self._prop.classname, self._value)
841         return getattr(i, attr)
843     def plain(self, escape=0):
844         ''' Render a "plain" representation of the property
845         '''
846         if self._value is None:
847             return ''
848         linkcl = self._db.classes[self._prop.classname]
849         k = linkcl.labelprop(1)
850         value = str(linkcl.get(self._value, k))
851         if escape:
852             value = cgi.escape(value)
853         return value
855     def field(self):
856         ''' Render a form edit field for the property
857         '''
858         linkcl = self._db.getclass(self._prop.classname)
859         if linkcl.getprops().has_key('order'):  
860             sort_on = 'order'  
861         else:  
862             sort_on = linkcl.labelprop()  
863         options = linkcl.filter(None, {}, [sort_on], []) 
864         # TODO: make this a field display, not a menu one!
865         l = ['<select name="%s">'%property]
866         k = linkcl.labelprop(1)
867         if value is None:
868             s = 'selected '
869         else:
870             s = ''
871         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
872         for optionid in options:
873             option = linkcl.get(optionid, k)
874             s = ''
875             if optionid == value:
876                 s = 'selected '
877             if showid:
878                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
879             else:
880                 lab = option
881             if size is not None and len(lab) > size:
882                 lab = lab[:size-3] + '...'
883             lab = cgi.escape(lab)
884             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
885         l.append('</select>')
886         return '\n'.join(l)
888     def menu(self, size=None, height=None, showid=0, additional=[],
889             **conditions):
890         ''' Render a form select list for this property
891         '''
892         value = self._value
894         # sort function
895         sortfunc = make_sort_function(self._db, self._prop.classname)
897         # force the value to be a single choice
898         if isinstance(value, type('')):
899             value = value[0]
900         linkcl = self._db.getclass(self._prop.classname)
901         l = ['<select name="%s">'%self._name]
902         k = linkcl.labelprop(1)
903         s = ''
904         if value is None:
905             s = 'selected '
906         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
907         if linkcl.getprops().has_key('order'):  
908             sort_on = ('+', 'order')
909         else:  
910             sort_on = ('+', linkcl.labelprop())
911         options = linkcl.filter(None, conditions, sort_on, (None, None))
912         for optionid in options:
913             option = linkcl.get(optionid, k)
914             s = ''
915             if value in [optionid, option]:
916                 s = 'selected '
917             if showid:
918                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
919             else:
920                 lab = option
921             if size is not None and len(lab) > size:
922                 lab = lab[:size-3] + '...'
923             if additional:
924                 m = []
925                 for propname in additional:
926                     m.append(linkcl.get(optionid, propname))
927                 lab = lab + ' (%s)'%', '.join(map(str, m))
928             lab = cgi.escape(lab)
929             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
930         l.append('</select>')
931         return '\n'.join(l)
932 #    def checklist(self, ...)
934 class MultilinkHTMLProperty(HTMLProperty):
935     ''' Multilink HTMLProperty
937         Also be iterable, returning a wrapper object like the Link case for
938         each entry in the multilink.
939     '''
940     def __len__(self):
941         ''' length of the multilink '''
942         return len(self._value)
944     def __getattr__(self, attr):
945         ''' no extended attribute accesses make sense here '''
946         raise AttributeError, attr
948     def __getitem__(self, num):
949         ''' iterate and return a new HTMLItem
950         '''
951        #print 'Multi.getitem', (self, num)
952         value = self._value[num]
953         if self._prop.classname == 'user':
954             klass = HTMLUser
955         else:
956             klass = HTMLItem
957         return klass(self._client, self._prop.classname, value)
959     def __contains__(self, value):
960         ''' Support the "in" operator
961         '''
962         return value in self._value
964     def reverse(self):
965         ''' return the list in reverse order
966         '''
967         l = self._value[:]
968         l.reverse()
969         if self._prop.classname == 'user':
970             klass = HTMLUser
971         else:
972             klass = HTMLItem
973         return [klass(self._client, self._prop.classname, value) for value in l]
975     def plain(self, escape=0):
976         ''' Render a "plain" representation of the property
977         '''
978         linkcl = self._db.classes[self._prop.classname]
979         k = linkcl.labelprop(1)
980         labels = []
981         for v in self._value:
982             labels.append(linkcl.get(v, k))
983         value = ', '.join(labels)
984         if escape:
985             value = cgi.escape(value)
986         return value
988     def field(self, size=30, showid=0):
989         ''' Render a form edit field for the property
990         '''
991         sortfunc = make_sort_function(self._db, self._prop.classname)
992         linkcl = self._db.getclass(self._prop.classname)
993         value = self._value[:]
994         if value:
995             value.sort(sortfunc)
996         # map the id to the label property
997         if not showid:
998             k = linkcl.labelprop(1)
999             value = [linkcl.get(v, k) for v in value]
1000         value = cgi.escape(','.join(value))
1001         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1003     def menu(self, size=None, height=None, showid=0, additional=[],
1004             **conditions):
1005         ''' Render a form select list for this property
1006         '''
1007         value = self._value
1009         # sort function
1010         sortfunc = make_sort_function(self._db, self._prop.classname)
1012         linkcl = self._db.getclass(self._prop.classname)
1013         if linkcl.getprops().has_key('order'):  
1014             sort_on = ('+', 'order')
1015         else:  
1016             sort_on = ('+', linkcl.labelprop())
1017         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1018         height = height or min(len(options), 7)
1019         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1020         k = linkcl.labelprop(1)
1021         for optionid in options:
1022             option = linkcl.get(optionid, k)
1023             s = ''
1024             if optionid in value or option in value:
1025                 s = 'selected '
1026             if showid:
1027                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1028             else:
1029                 lab = option
1030             if size is not None and len(lab) > size:
1031                 lab = lab[:size-3] + '...'
1032             if additional:
1033                 m = []
1034                 for propname in additional:
1035                     m.append(linkcl.get(optionid, propname))
1036                 lab = lab + ' (%s)'%', '.join(m)
1037             lab = cgi.escape(lab)
1038             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1039                 lab))
1040         l.append('</select>')
1041         return '\n'.join(l)
1043 # set the propclasses for HTMLItem
1044 propclasses = (
1045     (hyperdb.String, StringHTMLProperty),
1046     (hyperdb.Number, NumberHTMLProperty),
1047     (hyperdb.Boolean, BooleanHTMLProperty),
1048     (hyperdb.Date, DateHTMLProperty),
1049     (hyperdb.Interval, IntervalHTMLProperty),
1050     (hyperdb.Password, PasswordHTMLProperty),
1051     (hyperdb.Link, LinkHTMLProperty),
1052     (hyperdb.Multilink, MultilinkHTMLProperty),
1055 def make_sort_function(db, classname):
1056     '''Make a sort function for a given class
1057     '''
1058     linkcl = db.getclass(classname)
1059     if linkcl.getprops().has_key('order'):
1060         sort_on = 'order'
1061     else:
1062         sort_on = linkcl.labelprop()
1063     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1064         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1065     return sortfunc
1067 def handleListCGIValue(value):
1068     ''' Value is either a single item or a list of items. Each item has a
1069         .value that we're actually interested in.
1070     '''
1071     if isinstance(value, type([])):
1072         return [value.value for value in value]
1073     else:
1074         value = value.value.strip()
1075         if not value:
1076             return []
1077         return value.split(',')
1079 class ShowDict:
1080     ''' A convenience access to the :columns index parameters
1081     '''
1082     def __init__(self, columns):
1083         self.columns = {}
1084         for col in columns:
1085             self.columns[col] = 1
1086     def __getitem__(self, name):
1087         return self.columns.has_key(name)
1089 class HTMLRequest:
1090     ''' The *request*, holding the CGI form and environment.
1092         "form" the CGI form as a cgi.FieldStorage
1093         "env" the CGI environment variables
1094         "url" the current URL path for this request
1095         "base" the base URL for this instance
1096         "user" a HTMLUser instance for this user
1097         "classname" the current classname (possibly None)
1098         "template" the current template (suffix, also possibly None)
1100         Index args:
1101         "columns" dictionary of the columns to display in an index page
1102         "show" a convenience access to columns - request/show/colname will
1103                be true if the columns should be displayed, false otherwise
1104         "sort" index sort column (direction, column name)
1105         "group" index grouping property (direction, column name)
1106         "filter" properties to filter the index on
1107         "filterspec" values to filter the index on
1108         "search_text" text to perform a full-text search on for an index
1110     '''
1111     def __init__(self, client):
1112         self.client = client
1114         # easier access vars
1115         self.form = client.form
1116         self.env = client.env
1117         self.base = client.base
1118         self.url = client.url
1119         self.user = HTMLUser(client, 'user', client.userid)
1121         # store the current class name and action
1122         self.classname = client.classname
1123         self.template = client.template
1125         self._post_init()
1127     def _post_init(self):
1128         ''' Set attributes based on self.form
1129         '''
1130         # extract the index display information from the form
1131         self.columns = []
1132         if self.form.has_key(':columns'):
1133             self.columns = handleListCGIValue(self.form[':columns'])
1134         self.show = ShowDict(self.columns)
1136         # sorting
1137         self.sort = (None, None)
1138         if self.form.has_key(':sort'):
1139             sort = self.form[':sort'].value
1140             if sort.startswith('-'):
1141                 self.sort = ('-', sort[1:])
1142             else:
1143                 self.sort = ('+', sort)
1144         if self.form.has_key(':sortdir'):
1145             self.sort = ('-', self.sort[1])
1147         # grouping
1148         self.group = (None, None)
1149         if self.form.has_key(':group'):
1150             group = self.form[':group'].value
1151             if group.startswith('-'):
1152                 self.group = ('-', group[1:])
1153             else:
1154                 self.group = ('+', group)
1155         if self.form.has_key(':groupdir'):
1156             self.group = ('-', self.group[1])
1158         # filtering
1159         self.filter = []
1160         if self.form.has_key(':filter'):
1161             self.filter = handleListCGIValue(self.form[':filter'])
1162         self.filterspec = {}
1163         if self.classname is not None:
1164             props = self.client.db.getclass(self.classname).getprops()
1165             for name in self.filter:
1166                 if self.form.has_key(name):
1167                     prop = props[name]
1168                     fv = self.form[name]
1169                     if (isinstance(prop, hyperdb.Link) or
1170                             isinstance(prop, hyperdb.Multilink)):
1171                         self.filterspec[name] = handleListCGIValue(fv)
1172                     else:
1173                         self.filterspec[name] = fv.value
1175         # full-text search argument
1176         self.search_text = None
1177         if self.form.has_key(':search_text'):
1178             self.search_text = self.form[':search_text'].value
1180         # pagination - size and start index
1181         # figure batch args
1182         if self.form.has_key(':pagesize'):
1183             self.pagesize = int(self.form[':pagesize'].value)
1184         else:
1185             self.pagesize = 50
1186         if self.form.has_key(':startwith'):
1187             self.startwith = int(self.form[':startwith'].value)
1188         else:
1189             self.startwith = 0
1191     def updateFromURL(self, url):
1192         ''' Parse the URL for query args, and update my attributes using the
1193             values.
1194         ''' 
1195         self.form = {}
1196         for name, value in cgi.parse_qsl(url):
1197             if self.form.has_key(name):
1198                 if isinstance(self.form[name], type([])):
1199                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1200                 else:
1201                     self.form[name] = [self.form[name],
1202                         cgi.MiniFieldStorage(name, value)]
1203             else:
1204                 self.form[name] = cgi.MiniFieldStorage(name, value)
1205         self._post_init()
1207     def update(self, kwargs):
1208         ''' Update my attributes using the keyword args
1209         '''
1210         self.__dict__.update(kwargs)
1211         if kwargs.has_key('columns'):
1212             self.show = ShowDict(self.columns)
1214     def description(self):
1215         ''' Return a description of the request - handle for the page title.
1216         '''
1217         s = [self.client.db.config.TRACKER_NAME]
1218         if self.classname:
1219             if self.client.nodeid:
1220                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1221             else:
1222                 if self.template == 'item':
1223                     s.append('- new %s'%self.classname)
1224                 elif self.template == 'index':
1225                     s.append('- %s index'%self.classname)
1226                 else:
1227                     s.append('- %s %s'%(self.classname, self.template))
1228         else:
1229             s.append('- home')
1230         return ' '.join(s)
1232     def __str__(self):
1233         d = {}
1234         d.update(self.__dict__)
1235         f = ''
1236         for k in self.form.keys():
1237             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1238         d['form'] = f
1239         e = ''
1240         for k,v in self.env.items():
1241             e += '\n     %r=%r'%(k, v)
1242         d['env'] = e
1243         return '''
1244 form: %(form)s
1245 url: %(url)r
1246 base: %(base)r
1247 classname: %(classname)r
1248 template: %(template)r
1249 columns: %(columns)r
1250 sort: %(sort)r
1251 group: %(group)r
1252 filter: %(filter)r
1253 search_text: %(search_text)r
1254 pagesize: %(pagesize)r
1255 startwith: %(startwith)r
1256 env: %(env)s
1257 '''%d
1259     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1260             filterspec=1):
1261         ''' return the current index args as form elements '''
1262         l = []
1263         s = '<input type="hidden" name="%s" value="%s">'
1264         if columns and self.columns:
1265             l.append(s%(':columns', ','.join(self.columns)))
1266         if sort and self.sort[1] is not None:
1267             if self.sort[0] == '-':
1268                 val = '-'+self.sort[1]
1269             else:
1270                 val = self.sort[1]
1271             l.append(s%(':sort', val))
1272         if group and self.group[1] is not None:
1273             if self.group[0] == '-':
1274                 val = '-'+self.group[1]
1275             else:
1276                 val = self.group[1]
1277             l.append(s%(':group', val))
1278         if filter and self.filter:
1279             l.append(s%(':filter', ','.join(self.filter)))
1280         if filterspec:
1281             for k,v in self.filterspec.items():
1282                 l.append(s%(k, ','.join(v)))
1283         if self.search_text:
1284             l.append(s%(':search_text', self.search_text))
1285         l.append(s%(':pagesize', self.pagesize))
1286         l.append(s%(':startwith', self.startwith))
1287         return '\n'.join(l)
1289     def indexargs_url(self, url, args):
1290         ''' embed the current index args in a URL '''
1291         l = ['%s=%s'%(k,v) for k,v in args.items()]
1292         if self.columns and not args.has_key(':columns'):
1293             l.append(':columns=%s'%(','.join(self.columns)))
1294         if self.sort[1] is not None and not args.has_key(':sort'):
1295             if self.sort[0] == '-':
1296                 val = '-'+self.sort[1]
1297             else:
1298                 val = self.sort[1]
1299             l.append(':sort=%s'%val)
1300         if self.group[1] is not None and not args.has_key(':group'):
1301             if self.group[0] == '-':
1302                 val = '-'+self.group[1]
1303             else:
1304                 val = self.group[1]
1305             l.append(':group=%s'%val)
1306         if self.filter and not args.has_key(':columns'):
1307             l.append(':filter=%s'%(','.join(self.filter)))
1308         for k,v in self.filterspec.items():
1309             if not args.has_key(k):
1310                 l.append('%s=%s'%(k, ','.join(v)))
1311         if self.search_text and not args.has_key(':search_text'):
1312             l.append(':search_text=%s'%self.search_text)
1313         if not args.has_key(':pagesize'):
1314             l.append(':pagesize=%s'%self.pagesize)
1315         if not args.has_key(':startwith'):
1316             l.append(':startwith=%s'%self.startwith)
1317         return '%s?%s'%(url, '&'.join(l))
1318     indexargs_href = indexargs_url
1320     def base_javascript(self):
1321         return '''
1322 <script language="javascript">
1323 submitted = false;
1324 function submit_once() {
1325     if (submitted) {
1326         alert("Your request is being processed.\\nPlease be patient.");
1327         return 0;
1328     }
1329     submitted = true;
1330     return 1;
1333 function help_window(helpurl, width, height) {
1334     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1336 </script>
1337 '''%self.base
1339     def batch(self):
1340         ''' Return a batch object for results from the "current search"
1341         '''
1342         filterspec = self.filterspec
1343         sort = self.sort
1344         group = self.group
1346         # get the list of ids we're batching over
1347         klass = self.client.db.getclass(self.classname)
1348         if self.search_text:
1349             matches = self.client.db.indexer.search(
1350                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1351         else:
1352             matches = None
1353         l = klass.filter(matches, filterspec, sort, group)
1355         # map the item ids to instances
1356         if self.classname == 'user':
1357             klass = HTMLUser
1358         else:
1359             klass = HTMLItem
1360         l = [klass(self.client, self.classname, item) for item in l]
1362         # return the batch object
1363         return Batch(self.client, l, self.pagesize, self.startwith)
1365 # extend the standard ZTUtils Batch object to remove dependency on
1366 # Acquisition and add a couple of useful methods
1367 class Batch(ZTUtils.Batch):
1368     ''' Use me to turn a list of items, or item ids of a given class, into a
1369         series of batches.
1371         ========= ========================================================
1372         Parameter  Usage
1373         ========= ========================================================
1374         sequence  a list of HTMLItems
1375         size      how big to make the sequence.
1376         start     where to start (0-indexed) in the sequence.
1377         end       where to end (0-indexed) in the sequence.
1378         orphan    if the next batch would contain less items than this
1379                   value, then it is combined with this batch
1380         overlap   the number of items shared between adjacent batches
1381         ========= ========================================================
1383         Attributes: Note that the "start" attribute, unlike the
1384         argument, is a 1-based index (I know, lame).  "first" is the
1385         0-based index.  "length" is the actual number of elements in
1386         the batch.
1388         "sequence_length" is the length of the original, unbatched, sequence.
1389     '''
1390     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1391             overlap=0):
1392         self.client = client
1393         self.last_index = self.last_item = None
1394         self.current_item = None
1395         self.sequence_length = len(sequence)
1396         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1397             overlap)
1399     # overwrite so we can late-instantiate the HTMLItem instance
1400     def __getitem__(self, index):
1401         if index < 0:
1402             if index + self.end < self.first: raise IndexError, index
1403             return self._sequence[index + self.end]
1404         
1405         if index >= self.length:
1406             raise IndexError, index
1408         # move the last_item along - but only if the fetched index changes
1409         # (for some reason, index 0 is fetched twice)
1410         if index != self.last_index:
1411             self.last_item = self.current_item
1412             self.last_index = index
1414         self.current_item = self._sequence[index + self.first]
1415         return self.current_item
1417     def propchanged(self, property):
1418         ''' Detect if the property marked as being the group property
1419             changed in the last iteration fetch
1420         '''
1421         if (self.last_item is None or
1422                 self.last_item[property] != self.current_item[property]):
1423             return 1
1424         return 0
1426     # override these 'cos we don't have access to acquisition
1427     def previous(self):
1428         if self.start == 1:
1429             return None
1430         return Batch(self.client, self._sequence, self._size,
1431             self.first - self._size + self.overlap, 0, self.orphan,
1432             self.overlap)
1434     def next(self):
1435         try:
1436             self._sequence[self.end]
1437         except IndexError:
1438             return None
1439         return Batch(self.client, self._sequence, self._size,
1440             self.end - self.overlap, 0, self.orphan, self.overlap)
1442 class TemplatingUtils:
1443     ''' Utilities for templating
1444     '''
1445     def __init__(self, client):
1446         self.client = client
1447     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1448         return Batch(self.client, sequence, size, start, end, orphan,
1449             overlap)