Code

More cleaning up of configuration, and the "instance" -> "tracker"
[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         }
155         # add in the item if there is one
156         if client.nodeid:
157             c['context'] = HTMLItem(client, classname, client.nodeid)
158         else:
159             c['context'] = HTMLClass(client, classname)
160         return c
162     def render(self, client, classname, request, **options):
163         """Render this Page Template"""
165         if not self._v_cooked:
166             self._cook()
168         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
170         if self._v_errors:
171             raise PageTemplate.PTRuntimeError, \
172                 'Page Template %s has errors.' % self.id
174         # figure the context
175         classname = classname or client.classname
176         request = request or HTMLRequest(client)
177         c = self.getContext(client, classname, request)
178         c.update({'options': options})
180         # and go
181         output = StringIO.StringIO()
182         TALInterpreter(self._v_program, self._v_macros,
183             getEngine().getContext(c), output, tal=1, strictinsert=0)()
184         return output.getvalue()
186 class HTMLDatabase:
187     ''' Return HTMLClasses for valid class fetches
188     '''
189     def __init__(self, client):
190         self._client = client
192         # we want config to be exposed
193         self.config = client.db.config
195     def __getattr__(self, attr):
196         try:
197             self._client.db.getclass(attr)
198         except KeyError:
199             raise AttributeError, attr
200         return HTMLClass(self._client, attr)
201     def classes(self):
202         l = self._client.db.classes.keys()
203         l.sort()
204         return [HTMLClass(self._client, cn) for cn in l]
205         
206 class HTMLClass:
207     ''' Accesses through a class (either through *class* or *db.<classname>*)
208     '''
209     def __init__(self, client, classname):
210         self._client = client
211         self._db = client.db
213         # we want classname to be exposed
214         self.classname = classname
215         if classname is not None:
216             self._klass = self._db.getclass(self.classname)
217             self._props = self._klass.getprops()
219     def __repr__(self):
220         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
222     def __getitem__(self, item):
223         ''' return an HTMLProperty instance
224         '''
225         #print 'getitem', (self, item)
227         # we don't exist
228         if item == 'id':
229             return None
230         if item == 'creator':
231             # but we will be created by this user...
232             return HTMLUser(self._client, 'user', self._client.userid)
234         # get the property
235         prop = self._props[item]
237         # look up the correct HTMLProperty class
238         for klass, htmlklass in propclasses:
239             if isinstance(prop, hyperdb.Multilink):
240                 value = []
241             else:
242                 value = None
243             if isinstance(prop, klass):
244                 return htmlklass(self._client, '', prop, item, value)
246         # no good
247         raise KeyError, item
249     def __getattr__(self, attr):
250         ''' convenience access '''
251         try:
252             return self[attr]
253         except KeyError:
254             raise AttributeError, attr
256     def properties(self):
257         ''' Return HTMLProperty for all props
258         '''
259         l = []
260         for name, prop in self._props.items():
261             for klass, htmlklass in propclasses:
262                 if isinstance(prop, hyperdb.Multilink):
263                     value = []
264                 else:
265                     value = None
266                 if isinstance(prop, klass):
267                     l.append(htmlklass(self._client, '', prop, name, value))
268         return l
270     def list(self):
271         if self.classname == 'user':
272             klass = HTMLUser
273         else:
274             klass = HTMLItem
275         l = [klass(self._client, self.classname, x) for x in self._klass.list()]
276         return l
278     def csv(self):
279         ''' Return the items of this class as a chunk of CSV text.
280         '''
281         # get the CSV module
282         try:
283             import csv
284         except ImportError:
285             return 'Sorry, you need the csv module to use this function.\n'\
286                 'Get it from: http://www.object-craft.com.au/projects/csv/'
288         props = self.propnames()
289         p = csv.parser()
290         s = StringIO.StringIO()
291         s.write(p.join(props) + '\n')
292         for nodeid in self._klass.list():
293             l = []
294             for name in props:
295                 value = self._klass.get(nodeid, name)
296                 if value is None:
297                     l.append('')
298                 elif isinstance(value, type([])):
299                     l.append(':'.join(map(str, value)))
300                 else:
301                     l.append(str(self._klass.get(nodeid, name)))
302             s.write(p.join(l) + '\n')
303         return s.getvalue()
305     def propnames(self):
306         ''' Return the list of the names of the properties of this class.
307         '''
308         idlessprops = self._klass.getprops(protected=0).keys()
309         idlessprops.sort()
310         return ['id'] + idlessprops
312     def filter(self, request=None):
313         ''' Return a list of items from this class, filtered and sorted
314             by the current requested filterspec/filter/sort/group args
315         '''
316         if request is not None:
317             filterspec = request.filterspec
318             sort = request.sort
319             group = request.group
320         if self.classname == 'user':
321             klass = HTMLUser
322         else:
323             klass = HTMLItem
324         l = [klass(self._client, self.classname, x)
325              for x in self._klass.filter(None, filterspec, sort, group)]
326         return l
328     def classhelp(self, properties, label='?', width='400', height='400'):
329         '''pop up a javascript window with class help
331            This generates a link to a popup window which displays the 
332            properties indicated by "properties" of the class named by
333            "classname". The "properties" should be a comma-separated list
334            (eg. 'id,name,description').
336            You may optionally override the label displayed, the width and
337            height. The popup window will be resizable and scrollable.
338         '''
339         return '<a href="javascript:help_window(\'%s?:template=help&' \
340             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
341             '(%s)</b></a>'%(self.classname, properties, width, height, label)
343     def submit(self, label="Submit New Entry"):
344         ''' Generate a submit button (and action hidden element)
345         '''
346         return '  <input type="hidden" name=":action" value="new">\n'\
347         '  <input type="submit" name="submit" value="%s">'%label
349     def history(self):
350         return 'New node - no history'
352     def renderWith(self, name, **kwargs):
353         ''' Render this class with the given template.
354         '''
355         # create a new request and override the specified args
356         req = HTMLRequest(self._client)
357         req.classname = self.classname
358         req.update(kwargs)
360         # new template, using the specified classname and request
361         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
363         # XXX handle PT rendering errors here nicely
364         try:
365             # use our fabricated request
366             return pt.render(self._client, self.classname, req)
367         except PageTemplate.PTRuntimeError, message:
368             return '<strong>%s</strong><ol>%s</ol>'%(message,
369                 cgi.escape('<li>'.join(pt._v_errors)))
371 class HTMLItem:
372     ''' Accesses through an *item*
373     '''
374     def __init__(self, client, classname, nodeid):
375         self._client = client
376         self._db = client.db
377         self._classname = classname
378         self._nodeid = nodeid
379         self._klass = self._db.getclass(classname)
380         self._props = self._klass.getprops()
382     def __repr__(self):
383         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
384             self._nodeid)
386     def __getitem__(self, item):
387         ''' return an HTMLProperty instance
388         '''
389         #print 'getitem', (self, item)
390         if item == 'id':
391             return self._nodeid
393         # get the property
394         prop = self._props[item]
396         # get the value, handling missing values
397         value = self._klass.get(self._nodeid, item, None)
398         if value is None:
399             if isinstance(self._props[item], hyperdb.Multilink):
400                 value = []
402         # look up the correct HTMLProperty class
403         for klass, htmlklass in propclasses:
404             if isinstance(prop, klass):
405                 return htmlklass(self._client, self._nodeid, prop, item, value)
407         raise KeyErorr, item
409     def __getattr__(self, attr):
410         ''' convenience access to properties '''
411         try:
412             return self[attr]
413         except KeyError:
414             raise AttributeError, attr
415     
416     def submit(self, label="Submit Changes"):
417         ''' Generate a submit button (and action hidden element)
418         '''
419         return '  <input type="hidden" name=":action" value="edit">\n'\
420         '  <input type="submit" name="submit" value="%s">'%label
422     # XXX this probably should just return the history items, not the HTML
423     def history(self, direction='descending'):
424         l = ['<table class="history">'
425              '<tr><th colspan="4" class="header">',
426              _('History'),
427              '</th></tr><tr>',
428              _('<th>Date</th>'),
429              _('<th>User</th>'),
430              _('<th>Action</th>'),
431              _('<th>Args</th>'),
432             '</tr>']
433         comments = {}
434         history = self._klass.history(self._nodeid)
435         history.sort()
436         if direction == 'descending':
437             history.reverse()
438         for id, evt_date, user, action, args in history:
439             date_s = str(evt_date).replace("."," ")
440             arg_s = ''
441             if action == 'link' and type(args) == type(()):
442                 if len(args) == 3:
443                     linkcl, linkid, key = args
444                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
445                         linkcl, linkid, key)
446                 else:
447                     arg_s = str(args)
449             elif action == 'unlink' and type(args) == type(()):
450                 if len(args) == 3:
451                     linkcl, linkid, key = args
452                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
453                         linkcl, linkid, key)
454                 else:
455                     arg_s = str(args)
457             elif type(args) == type({}):
458                 cell = []
459                 for k in args.keys():
460                     # try to get the relevant property and treat it
461                     # specially
462                     try:
463                         prop = self._props[k]
464                     except KeyError:
465                         prop = None
466                     if prop is not None:
467                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
468                                 isinstance(prop, hyperdb.Link)):
469                             # figure what the link class is
470                             classname = prop.classname
471                             try:
472                                 linkcl = self._db.getclass(classname)
473                             except KeyError:
474                                 labelprop = None
475                                 comments[classname] = _('''The linked class
476                                     %(classname)s no longer exists''')%locals()
477                             labelprop = linkcl.labelprop(1)
478                             hrefable = os.path.exists(
479                                 os.path.join(self._db.config.TEMPLATES,
480                                 classname+'.item'))
482                         if isinstance(prop, hyperdb.Multilink) and \
483                                 len(args[k]) > 0:
484                             ml = []
485                             for linkid in args[k]:
486                                 if isinstance(linkid, type(())):
487                                     sublabel = linkid[0] + ' '
488                                     linkids = linkid[1]
489                                 else:
490                                     sublabel = ''
491                                     linkids = [linkid]
492                                 subml = []
493                                 for linkid in linkids:
494                                     label = classname + linkid
495                                     # if we have a label property, try to use it
496                                     # TODO: test for node existence even when
497                                     # there's no labelprop!
498                                     try:
499                                         if labelprop is not None:
500                                             label = linkcl.get(linkid, labelprop)
501                                     except IndexError:
502                                         comments['no_link'] = _('''<strike>The
503                                             linked node no longer
504                                             exists</strike>''')
505                                         subml.append('<strike>%s</strike>'%label)
506                                     else:
507                                         if hrefable:
508                                             subml.append('<a href="%s%s">%s</a>'%(
509                                                 classname, linkid, label))
510                                 ml.append(sublabel + ', '.join(subml))
511                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
512                         elif isinstance(prop, hyperdb.Link) and args[k]:
513                             label = classname + args[k]
514                             # if we have a label property, try to use it
515                             # TODO: test for node existence even when
516                             # there's no labelprop!
517                             if labelprop is not None:
518                                 try:
519                                     label = linkcl.get(args[k], labelprop)
520                                 except IndexError:
521                                     comments['no_link'] = _('''<strike>The
522                                         linked node no longer
523                                         exists</strike>''')
524                                     cell.append(' <strike>%s</strike>,\n'%label)
525                                     # "flag" this is done .... euwww
526                                     label = None
527                             if label is not None:
528                                 if hrefable:
529                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
530                                         classname, args[k], label))
531                                 else:
532                                     cell.append('%s: %s' % (k,label))
534                         elif isinstance(prop, hyperdb.Date) and args[k]:
535                             d = date.Date(args[k])
536                             cell.append('%s: %s'%(k, str(d)))
538                         elif isinstance(prop, hyperdb.Interval) and args[k]:
539                             d = date.Interval(args[k])
540                             cell.append('%s: %s'%(k, str(d)))
542                         elif isinstance(prop, hyperdb.String) and args[k]:
543                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
545                         elif not args[k]:
546                             cell.append('%s: (no value)\n'%k)
548                         else:
549                             cell.append('%s: %s\n'%(k, str(args[k])))
550                     else:
551                         # property no longer exists
552                         comments['no_exist'] = _('''<em>The indicated property
553                             no longer exists</em>''')
554                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
555                 arg_s = '<br />'.join(cell)
556             else:
557                 # unkown event!!
558                 comments['unknown'] = _('''<strong><em>This event is not
559                     handled by the history display!</em></strong>''')
560                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
561             date_s = date_s.replace(' ', '&nbsp;')
562             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
563                 date_s, user, action, arg_s))
564         if comments:
565             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
566         for entry in comments.values():
567             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
568         l.append('</table>')
569         return '\n'.join(l)
571 class HTMLUser(HTMLItem):
572     ''' Accesses through the *user* (a special case of item)
573     '''
574     def __init__(self, client, classname, nodeid):
575         HTMLItem.__init__(self, client, 'user', nodeid)
576         self._default_classname = client.classname
578         # used for security checks
579         self._security = client.db.security
580     _marker = []
581     def hasPermission(self, role, classname=_marker):
582         ''' Determine if the user has the Role.
584             The class being tested defaults to the template's class, but may
585             be overidden for this test by suppling an alternate classname.
586         '''
587         if classname is self._marker:
588             classname = self._default_classname
589         return self._security.hasPermission(role, self._nodeid, classname)
591 class HTMLProperty:
592     ''' String, Number, Date, Interval HTMLProperty
594         Hase useful attributes:
596          _name  the name of the property
597          _value the value of the property if any
599         A wrapper object which may be stringified for the plain() behaviour.
600     '''
601     def __init__(self, client, nodeid, prop, name, value):
602         self._client = client
603         self._db = client.db
604         self._nodeid = nodeid
605         self._prop = prop
606         self._name = name
607         self._value = value
608     def __repr__(self):
609         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
610     def __str__(self):
611         return self.plain()
612     def __cmp__(self, other):
613         if isinstance(other, HTMLProperty):
614             return cmp(self._value, other._value)
615         return cmp(self._value, other)
617 class StringHTMLProperty(HTMLProperty):
618     def plain(self, escape=0):
619         if self._value is None:
620             return ''
621         if escape:
622             return cgi.escape(str(self._value))
623         return str(self._value)
625     def stext(self, escape=0):
626         s = self.plain(escape=escape)
627         if not StructuredText:
628             return s
629         return StructuredText(s,level=1,header=0)
631     def field(self, size = 30):
632         if self._value is None:
633             value = ''
634         else:
635             value = cgi.escape(str(self._value))
636             value = '&quot;'.join(value.split('"'))
637         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
639     def multiline(self, escape=0, rows=5, cols=40):
640         if self._value is None:
641             value = ''
642         else:
643             value = cgi.escape(str(self._value))
644             value = '&quot;'.join(value.split('"'))
645         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
646             self._name, rows, cols, value)
648     def email(self, escape=1):
649         ''' fudge email '''
650         if self._value is None: value = ''
651         else: value = str(self._value)
652         value = value.replace('@', ' at ')
653         value = value.replace('.', ' ')
654         if escape:
655             value = cgi.escape(value)
656         return value
658 class PasswordHTMLProperty(HTMLProperty):
659     def plain(self):
660         if self._value is None:
661             return ''
662         return _('*encrypted*')
664     def field(self, size = 30):
665         return '<input type="password" name="%s" size="%s">'%(self._name, size)
667 class NumberHTMLProperty(HTMLProperty):
668     def plain(self):
669         return str(self._value)
671     def field(self, size = 30):
672         if self._value is None:
673             value = ''
674         else:
675             value = cgi.escape(str(self._value))
676             value = '&quot;'.join(value.split('"'))
677         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
679 class BooleanHTMLProperty(HTMLProperty):
680     def plain(self):
681         if self.value is None:
682             return ''
683         return self._value and "Yes" or "No"
685     def field(self):
686         checked = self._value and "checked" or ""
687         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
688             checked)
689         if checked:
690             checked = ""
691         else:
692             checked = "checked"
693         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
694             checked)
695         return s
697 class DateHTMLProperty(HTMLProperty):
698     def plain(self):
699         if self._value is None:
700             return ''
701         return str(self._value)
703     def field(self, size = 30):
704         if self._value is None:
705             value = ''
706         else:
707             value = cgi.escape(str(self._value))
708             value = '&quot;'.join(value.split('"'))
709         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
711     def reldate(self, pretty=1):
712         if not self._value:
713             return ''
715         # figure the interval
716         interval = date.Date('.') - self._value
717         if pretty:
718             return interval.pretty()
719         return str(interval)
721 class IntervalHTMLProperty(HTMLProperty):
722     def plain(self):
723         if self._value is None:
724             return ''
725         return str(self._value)
727     def pretty(self):
728         return self._value.pretty()
730     def field(self, size = 30):
731         if self._value is None:
732             value = ''
733         else:
734             value = cgi.escape(str(self._value))
735             value = '&quot;'.join(value.split('"'))
736         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
738 class LinkHTMLProperty(HTMLProperty):
739     ''' Link HTMLProperty
740         Include the above as well as being able to access the class
741         information. Stringifying the object itself results in the value
742         from the item being displayed. Accessing attributes of this object
743         result in the appropriate entry from the class being queried for the
744         property accessed (so item/assignedto/name would look up the user
745         entry identified by the assignedto property on item, and then the
746         name property of that user)
747     '''
748     def __getattr__(self, attr):
749         ''' return a new HTMLItem '''
750         #print 'getattr', (self, attr, self._value)
751         if not self._value:
752             raise AttributeError, "Can't access missing value"
753         if self._prop.classname == 'user':
754             klass = HTMLItem
755         else:
756             klass = HTMLUser
757         i = klass(self._client, self._prop.classname, self._value)
758         return getattr(i, attr)
760     def plain(self, escape=0):
761         if self._value is None:
762             return _('[unselected]')
763         linkcl = self._db.classes[self._prop.classname]
764         k = linkcl.labelprop(1)
765         value = str(linkcl.get(self._value, k))
766         if escape:
767             value = cgi.escape(value)
768         return value
770     def field(self):
771         linkcl = self._db.getclass(self._prop.classname)
772         if linkcl.getprops().has_key('order'):  
773             sort_on = 'order'  
774         else:  
775             sort_on = linkcl.labelprop()  
776         options = linkcl.filter(None, {}, [sort_on], []) 
777         # TODO: make this a field display, not a menu one!
778         l = ['<select name="%s">'%property]
779         k = linkcl.labelprop(1)
780         if value is None:
781             s = 'selected '
782         else:
783             s = ''
784         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
785         for optionid in options:
786             option = linkcl.get(optionid, k)
787             s = ''
788             if optionid == value:
789                 s = 'selected '
790             if showid:
791                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
792             else:
793                 lab = option
794             if size is not None and len(lab) > size:
795                 lab = lab[:size-3] + '...'
796             lab = cgi.escape(lab)
797             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
798         l.append('</select>')
799         return '\n'.join(l)
801     def download(self, showid=0):
802         linkname = self._prop.classname
803         linkcl = self._db.getclass(linkname)
804         k = linkcl.labelprop(1)
805         linkvalue = cgi.escape(str(linkcl.get(self._value, k)))
806         if showid:
807             label = value
808             title = ' title="%s"'%linkvalue
809             # note ... this should be urllib.quote(linkcl.get(value, k))
810         else:
811             label = linkvalue
812             title = ''
813         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self._value,
814             linkvalue, title, label)
816     def menu(self, size=None, height=None, showid=0, additional=[],
817             **conditions):
818         value = self._value
820         # sort function
821         sortfunc = make_sort_function(self._db, self._prop.classname)
823         # force the value to be a single choice
824         if isinstance(value, type('')):
825             value = value[0]
826         linkcl = self._db.getclass(self._prop.classname)
827         l = ['<select name="%s">'%self._name]
828         k = linkcl.labelprop(1)
829         s = ''
830         if value is None:
831             s = 'selected '
832         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
833         if linkcl.getprops().has_key('order'):  
834             sort_on = ('+', 'order')
835         else:  
836             sort_on = ('+', linkcl.labelprop())
837         options = linkcl.filter(None, conditions, sort_on, (None, None))
838         for optionid in options:
839             option = linkcl.get(optionid, k)
840             s = ''
841             if value in [optionid, option]:
842                 s = 'selected '
843             if showid:
844                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
845             else:
846                 lab = option
847             if size is not None and len(lab) > size:
848                 lab = lab[:size-3] + '...'
849             if additional:
850                 m = []
851                 for propname in additional:
852                     m.append(linkcl.get(optionid, propname))
853                 lab = lab + ' (%s)'%', '.join(map(str, m))
854             lab = cgi.escape(lab)
855             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
856         l.append('</select>')
857         return '\n'.join(l)
859 #    def checklist(self, ...)
861 class MultilinkHTMLProperty(HTMLProperty):
862     ''' Multilink HTMLProperty
864         Also be iterable, returning a wrapper object like the Link case for
865         each entry in the multilink.
866     '''
867     def __len__(self):
868         ''' length of the multilink '''
869         return len(self._value)
871     def __getattr__(self, attr):
872         ''' no extended attribute accesses make sense here '''
873         raise AttributeError, attr
875     def __getitem__(self, num):
876         ''' iterate and return a new HTMLItem
877         '''
878         #print 'getitem', (self, num)
879         value = self._value[num]
880         if self._prop.classname == 'user':
881             klass = HTMLUser
882         else:
883             klass = HTMLItem
884         return klass(self._client, self._prop.classname, value)
886     def reverse(self):
887         ''' return the list in reverse order
888         '''
889         l = self._value[:]
890         l.reverse()
891         if self._prop.classname == 'user':
892             klass = HTMLUser
893         else:
894             klass = HTMLItem
895         return [klass(self._client, self._prop.classname, value) for value in l]
897     def plain(self, escape=0):
898         linkcl = self._db.classes[self._prop.classname]
899         k = linkcl.labelprop(1)
900         labels = []
901         for v in self._value:
902             labels.append(linkcl.get(v, k))
903         value = ', '.join(labels)
904         if escape:
905             value = cgi.escape(value)
906         return value
908     def field(self, size=30, showid=0):
909         sortfunc = make_sort_function(self._db, self._prop.classname)
910         linkcl = self._db.getclass(self._prop.classname)
911         value = self._value[:]
912         if value:
913             value.sort(sortfunc)
914         # map the id to the label property
915         if not showid:
916             k = linkcl.labelprop(1)
917             value = [linkcl.get(v, k) for v in value]
918         value = cgi.escape(','.join(value))
919         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
921     def menu(self, size=None, height=None, showid=0, additional=[],
922             **conditions):
923         value = self._value
925         # sort function
926         sortfunc = make_sort_function(self._db, self._prop.classname)
928         linkcl = self._db.getclass(self._prop.classname)
929         if linkcl.getprops().has_key('order'):  
930             sort_on = ('+', 'order')
931         else:  
932             sort_on = ('+', linkcl.labelprop())
933         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
934         height = height or min(len(options), 7)
935         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
936         k = linkcl.labelprop(1)
937         for optionid in options:
938             option = linkcl.get(optionid, k)
939             s = ''
940             if optionid in value or option in value:
941                 s = 'selected '
942             if showid:
943                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
944             else:
945                 lab = option
946             if size is not None and len(lab) > size:
947                 lab = lab[:size-3] + '...'
948             if additional:
949                 m = []
950                 for propname in additional:
951                     m.append(linkcl.get(optionid, propname))
952                 lab = lab + ' (%s)'%', '.join(m)
953             lab = cgi.escape(lab)
954             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
955                 lab))
956         l.append('</select>')
957         return '\n'.join(l)
959 # set the propclasses for HTMLItem
960 propclasses = (
961     (hyperdb.String, StringHTMLProperty),
962     (hyperdb.Number, NumberHTMLProperty),
963     (hyperdb.Boolean, BooleanHTMLProperty),
964     (hyperdb.Date, DateHTMLProperty),
965     (hyperdb.Interval, IntervalHTMLProperty),
966     (hyperdb.Password, PasswordHTMLProperty),
967     (hyperdb.Link, LinkHTMLProperty),
968     (hyperdb.Multilink, MultilinkHTMLProperty),
971 def make_sort_function(db, classname):
972     '''Make a sort function for a given class
973     '''
974     linkcl = db.getclass(classname)
975     if linkcl.getprops().has_key('order'):
976         sort_on = 'order'
977     else:
978         sort_on = linkcl.labelprop()
979     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
980         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
981     return sortfunc
983 def handleListCGIValue(value):
984     ''' Value is either a single item or a list of items. Each item has a
985         .value that we're actually interested in.
986     '''
987     if isinstance(value, type([])):
988         return [value.value for value in value]
989     else:
990         return value.value.split(',')
992 class ShowDict:
993     ''' A convenience access to the :columns index parameters
994     '''
995     def __init__(self, columns):
996         self.columns = {}
997         for col in columns:
998             self.columns[col] = 1
999     def __getitem__(self, name):
1000         return self.columns.has_key(name)
1002 class HTMLRequest:
1003     ''' The *request*, holding the CGI form and environment.
1005         "form" the CGI form as a cgi.FieldStorage
1006         "env" the CGI environment variables
1007         "url" the current URL path for this request
1008         "base" the base URL for this instance
1009         "user" a HTMLUser instance for this user
1010         "classname" the current classname (possibly None)
1011         "template" the current template (suffix, also possibly None)
1013         Index args:
1014         "columns" dictionary of the columns to display in an index page
1015         "show" a convenience access to columns - request/show/colname will
1016                be true if the columns should be displayed, false otherwise
1017         "sort" index sort column (direction, column name)
1018         "group" index grouping property (direction, column name)
1019         "filter" properties to filter the index on
1020         "filterspec" values to filter the index on
1021         "search_text" text to perform a full-text search on for an index
1023     '''
1024     def __init__(self, client):
1025         self.client = client
1027         # easier access vars
1028         self.form = client.form
1029         self.env = client.env
1030         self.base = client.base
1031         self.url = client.url
1032         self.user = HTMLUser(client, 'user', client.userid)
1034         # store the current class name and action
1035         self.classname = client.classname
1036         self.template = client.template
1038         # extract the index display information from the form
1039         self.columns = []
1040         if self.form.has_key(':columns'):
1041             self.columns = handleListCGIValue(self.form[':columns'])
1042         self.show = ShowDict(self.columns)
1044         # sorting
1045         self.sort = (None, None)
1046         if self.form.has_key(':sort'):
1047             sort = self.form[':sort'].value
1048             if sort.startswith('-'):
1049                 self.sort = ('-', sort[1:])
1050             else:
1051                 self.sort = ('+', sort)
1052         if self.form.has_key(':sortdir'):
1053             self.sort = ('-', self.sort[1])
1055         # grouping
1056         self.group = (None, None)
1057         if self.form.has_key(':group'):
1058             group = self.form[':group'].value
1059             if group.startswith('-'):
1060                 self.group = ('-', group[1:])
1061             else:
1062                 self.group = ('+', group)
1063         if self.form.has_key(':groupdir'):
1064             self.group = ('-', self.group[1])
1066         # filtering
1067         self.filter = []
1068         if self.form.has_key(':filter'):
1069             self.filter = handleListCGIValue(self.form[':filter'])
1070         self.filterspec = {}
1071         if self.classname is not None:
1072             props = self.client.db.getclass(self.classname).getprops()
1073             for name in self.filter:
1074                 if self.form.has_key(name):
1075                     prop = props[name]
1076                     fv = self.form[name]
1077                     if (isinstance(prop, hyperdb.Link) or
1078                             isinstance(prop, hyperdb.Multilink)):
1079                         self.filterspec[name] = handleListCGIValue(fv)
1080                     else:
1081                         self.filterspec[name] = fv.value
1083         # full-text search argument
1084         self.search_text = None
1085         if self.form.has_key(':search_text'):
1086             self.search_text = self.form[':search_text'].value
1088         # pagination - size and start index
1089         # figure batch args
1090         if self.form.has_key(':pagesize'):
1091             self.pagesize = int(self.form[':pagesize'].value)
1092         else:
1093             self.pagesize = 50
1094         if self.form.has_key(':startwith'):
1095             self.startwith = int(self.form[':startwith'].value)
1096         else:
1097             self.startwith = 0
1099     def update(self, kwargs):
1100         self.__dict__.update(kwargs)
1101         if kwargs.has_key('columns'):
1102             self.show = ShowDict(self.columns)
1104     def description(self):
1105         ''' Return a description of the request - handle for the page title.
1106         '''
1107         s = [self.client.db.config.TRACKER_NAME]
1108         if self.classname:
1109             if self.client.nodeid:
1110                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1111             else:
1112                 s.append('- index of '+self.classname)
1113         else:
1114             s.append('- home')
1115         return ' '.join(s)
1117     def __str__(self):
1118         d = {}
1119         d.update(self.__dict__)
1120         f = ''
1121         for k in self.form.keys():
1122             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1123         d['form'] = f
1124         e = ''
1125         for k,v in self.env.items():
1126             e += '\n     %r=%r'%(k, v)
1127         d['env'] = e
1128         return '''
1129 form: %(form)s
1130 url: %(url)r
1131 base: %(base)r
1132 classname: %(classname)r
1133 template: %(template)r
1134 columns: %(columns)r
1135 sort: %(sort)r
1136 group: %(group)r
1137 filter: %(filter)r
1138 search_text: %(search_text)r
1139 pagesize: %(pagesize)r
1140 startwith: %(startwith)r
1141 env: %(env)s
1142 '''%d
1144     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1145             filterspec=1):
1146         ''' return the current index args as form elements '''
1147         l = []
1148         s = '<input type="hidden" name="%s" value="%s">'
1149         if columns and self.columns:
1150             l.append(s%(':columns', ','.join(self.columns)))
1151         if sort and self.sort[1] is not None:
1152             if self.sort[0] == '-':
1153                 val = '-'+self.sort[1]
1154             else:
1155                 val = self.sort[1]
1156             l.append(s%(':sort', val))
1157         if group and self.group[1] is not None:
1158             if self.group[0] == '-':
1159                 val = '-'+self.group[1]
1160             else:
1161                 val = self.group[1]
1162             l.append(s%(':group', val))
1163         if filter and self.filter:
1164             l.append(s%(':filter', ','.join(self.filter)))
1165         if filterspec:
1166             for k,v in self.filterspec.items():
1167                 l.append(s%(k, ','.join(v)))
1168         if self.search_text:
1169             l.append(s%(':search_text', self.search_text))
1170         l.append(s%(':pagesize', self.pagesize))
1171         l.append(s%(':startwith', self.startwith))
1172         return '\n'.join(l)
1174     def indexargs_href(self, url, args):
1175         ''' embed the current index args in a URL '''
1176         l = ['%s=%s'%(k,v) for k,v in args.items()]
1177         if self.columns and not args.has_key(':columns'):
1178             l.append(':columns=%s'%(','.join(self.columns)))
1179         if self.sort[1] is not None and not args.has_key(':sort'):
1180             if self.sort[0] == '-':
1181                 val = '-'+self.sort[1]
1182             else:
1183                 val = self.sort[1]
1184             l.append(':sort=%s'%val)
1185         if self.group[1] is not None and not args.has_key(':group'):
1186             if self.group[0] == '-':
1187                 val = '-'+self.group[1]
1188             else:
1189                 val = self.group[1]
1190             l.append(':group=%s'%val)
1191         if self.filter and not args.has_key(':columns'):
1192             l.append(':filter=%s'%(','.join(self.filter)))
1193         for k,v in self.filterspec.items():
1194             if not args.has_key(k):
1195                 l.append('%s=%s'%(k, ','.join(v)))
1196         if self.search_text and not args.has_key(':search_text'):
1197             l.append(':search_text=%s'%self.search_text)
1198         if not args.has_key(':pagesize'):
1199             l.append(':pagesize=%s'%self.pagesize)
1200         if not args.has_key(':startwith'):
1201             l.append(':startwith=%s'%self.startwith)
1202         return '%s?%s'%(url, '&'.join(l))
1204     def base_javascript(self):
1205         return '''
1206 <script language="javascript">
1207 submitted = false;
1208 function submit_once() {
1209     if (submitted) {
1210         alert("Your request is being processed.\\nPlease be patient.");
1211         return 0;
1212     }
1213     submitted = true;
1214     return 1;
1217 function help_window(helpurl, width, height) {
1218     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1220 </script>
1221 '''%self.base
1223     def batch(self):
1224         ''' Return a batch object for results from the "current search"
1225         '''
1226         filterspec = self.filterspec
1227         sort = self.sort
1228         group = self.group
1230         # get the list of ids we're batching over
1231         klass = self.client.db.getclass(self.classname)
1232         if self.search_text:
1233             matches = self.client.db.indexer.search(
1234                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1235         else:
1236             matches = None
1237         l = klass.filter(matches, filterspec, sort, group)
1239         # return the batch object
1240         return Batch(self.client, self.classname, l, self.pagesize,
1241             self.startwith)
1244 # extend the standard ZTUtils Batch object to remove dependency on
1245 # Acquisition and add a couple of useful methods
1246 class Batch(ZTUtils.Batch):
1247     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1248         self.client = client
1249         self.classname = classname
1250         self.last_index = self.last_item = None
1251         self.current_item = None
1252         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1254     # overwrite so we can late-instantiate the HTMLItem instance
1255     def __getitem__(self, index):
1256         if index < 0:
1257             if index + self.end < self.first: raise IndexError, index
1258             return self._sequence[index + self.end]
1259         
1260         if index >= self.length: raise IndexError, index
1262         # move the last_item along - but only if the fetched index changes
1263         # (for some reason, index 0 is fetched twice)
1264         if index != self.last_index:
1265             self.last_item = self.current_item
1266             self.last_index = index
1268         # wrap the return in an HTMLItem
1269         if self.classname == 'user':
1270             klass = HTMLUser
1271         else:
1272             klass = HTMLItem
1273         self.current_item = klass(self.client, self.classname,
1274             self._sequence[index+self.first])
1275         return self.current_item
1277     def propchanged(self, property):
1278         ''' Detect if the property marked as being the group property
1279             changed in the last iteration fetch
1280         '''
1281         if (self.last_item is None or
1282                 self.last_item[property] != self.current_item[property]):
1283             return 1
1284         return 0
1286     # override these 'cos we don't have access to acquisition
1287     def previous(self):
1288         if self.start == 1:
1289             return None
1290         return Batch(self.client, self.classname, self._sequence, self._size,
1291             self.first - self._size + self.overlap, 0, self.orphan,
1292             self.overlap)
1294     def next(self):
1295         try:
1296             self._sequence[self.end]
1297         except IndexError:
1298             return None
1299         return Batch(self.client, self.classname, self._sequence, self._size,
1300             self.end - self.overlap, 0, self.orphan, self.overlap)
1302     def length(self):
1303         self.sequence_length = l = len(self._sequence)
1304         return l