Code

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