Code

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