Code

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