Code

much nicer error messages when there's a templating error
[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 def getTemplate(dir, name, extension, classname=None, request=None):
64     ''' Interface to get a template, possibly loading a compiled template.
66         "name" and "extension" indicate the template we're after, which in
67         most cases will be "name.extension". If "extension" is None, then
68         we look for a template just called "name" with no extension.
70         If the file "name.extension" doesn't exist, we look for
71         "_generic.extension" as a fallback.
72     '''
73     # default the name to "home"
74     if name is None:
75         name = 'home'
77     # find the source, figure the time it was last modified
78     if extension:
79         filename = '%s.%s'%(name, extension)
80     else:
81         filename = name
82     src = os.path.join(dir, filename)
83     try:
84         stime = os.stat(src)[os.path.stat.ST_MTIME]
85     except os.error, error:
86         if error.errno != errno.ENOENT or not extension:
87             raise
88         # try for a generic template
89         filename = '_generic.%s'%extension
90         src = os.path.join(dir, filename)
91         stime = os.stat(src)[os.path.stat.ST_MTIME]
93     key = (dir, filename)
94     if templates.has_key(key) and stime < templates[key].mtime:
95         # compiled template is up to date
96         return templates[key]
98     # compile the template
99     templates[key] = pt = RoundupPageTemplate()
100     pt.write(open(src).read())
101     pt.id = filename
102     pt.mtime = time.time()
103     return pt
105 class RoundupPageTemplate(PageTemplate.PageTemplate):
106     ''' A Roundup-specific PageTemplate.
108         Interrogate the client to set up the various template variables to
109         be available:
111         *class*
112           The current class of node being displayed as an HTMLClass
113           instance.
114         *item*
115           The current node from the database, if we're viewing a specific
116           node, as an HTMLItem instance. If it doesn't exist, then we're
117           on a new item page.
118         (*classname*)
119           this is one of two things:
121           1. the *item* is also available under its classname, so a *user*
122              node would also be available under the name *user*. This is
123              also an HTMLItem instance.
124           2. if there's no *item* then the current class is available
125              through this name, thus "user/name" and "user/name/menu" will
126              still work - the latter will pull information from the form
127              if it can.
128         *form*
129           The current CGI form information as a mapping of form argument
130           name to value
131         *request*
132           Includes information about the current request, including:
133            - the url
134            - the current index information (``filterspec``, ``filter`` args,
135              ``properties``, etc) parsed out of the form. 
136            - methods for easy filterspec link generation
137            - *user*, the current user node as an HTMLItem instance
138         *instance*
139           The current instance
140         *db*
141           The current database, through which db.config may be reached.
143         Maybe also:
145         *modules*
146           python modules made available (XXX: not sure what's actually in
147           there tho)
148     '''
149     def getContext(self, client, classname, request):
150         c = {
151              'klass': HTMLClass(client, classname),
152              'options': {},
153              'nothing': None,
154              'request': request,
155              'content': client.content,
156              'db': HTMLDatabase(client),
157              'instance': client.instance
158         }
159         # add in the item if there is one
160         if client.nodeid:
161             c['item'] = HTMLItem(client.db, classname, client.nodeid)
162             c[classname] = c['item']
163         else:
164             c[classname] = c['klass']
165         return c
167     def render(self, client, classname, request, **options):
168         """Render this Page Template"""
170         if not self._v_cooked:
171             self._cook()
173         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
175         if self._v_errors:
176             raise PTRuntimeError, 'Page Template %s has errors.' % self.id
178         # figure the context
179         classname = classname or client.classname
180         request = request or HTMLRequest(client)
181         c = self.getContext(client, classname, request)
182         c.update({'options': options})
184         # and go
185         output = StringIO.StringIO()
186         TALInterpreter(self._v_program, self._v_macros,
187             getEngine().getContext(c), output, tal=1, strictinsert=0)()
188         return output.getvalue()
190 class HTMLDatabase:
191     ''' Return HTMLClasses for valid class fetches
192     '''
193     def __init__(self, client):
194         self.client = client
195         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]
206         
207 class HTMLClass:
208     ''' Accesses through a class (either through *class* or *db.<classname>*)
209     '''
210     def __init__(self, client, classname):
211         self.client = client
212         self.db = client.db
213         self.classname = classname
214         if classname is not None:
215             self.klass = self.db.getclass(self.classname)
216             self.props = self.klass.getprops()
218     def __repr__(self):
219         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
221     def __getitem__(self, item):
222         ''' return an HTMLItem instance'''
223         #print 'getitem', (self, attr)
224         if item == 'creator':
225             return HTMLUser(self.client)
227         if not self.props.has_key(item):
228             raise KeyError, item
229         prop = self.props[item]
231         # look up the correct HTMLProperty class
232         for klass, htmlklass in propclasses:
233             if isinstance(prop, hyperdb.Multilink):
234                 value = []
235             else:
236                 value = None
237             if isinstance(prop, klass):
238                 return htmlklass(self.db, '', prop, item, value)
240         # no good
241         raise KeyError, item
243     def __getattr__(self, attr):
244         ''' convenience access '''
245         try:
246             return self[attr]
247         except KeyError:
248             raise AttributeError, attr
250     def properties(self):
251         ''' Return HTMLProperty for all props
252         '''
253         l = []
254         for name, prop in self.props.items():
255             for klass, htmlklass in propclasses:
256                 if isinstance(prop, hyperdb.Multilink):
257                     value = []
258                 else:
259                     value = None
260                 if isinstance(prop, klass):
261                     l.append(htmlklass(self.db, '', prop, name, value))
262         return l
264     def list(self):
265         l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
266         return l
268     def csv(self):
269         ''' Return the items of this class as a chunk of CSV text.
270         '''
271         # get the CSV module
272         try:
273             import csv
274         except ImportError:
275             return 'Sorry, you need the csv module to use this function.\n'\
276                 'Get it from: http://www.object-craft.com.au/projects/csv/'
278         props = self.propnames()
279         p = csv.parser()
280         s = StringIO.StringIO()
281         s.write(p.join(props) + '\n')
282         for nodeid in self.klass.list():
283             l = []
284             for name in props:
285                 value = self.klass.get(nodeid, name)
286                 if value is None:
287                     l.append('')
288                 elif isinstance(value, type([])):
289                     l.append(':'.join(map(str, value)))
290                 else:
291                     l.append(str(self.klass.get(nodeid, name)))
292             s.write(p.join(l) + '\n')
293         return s.getvalue()
295     def propnames(self):
296         ''' Return the list of the names of the properties of this class.
297         '''
298         idlessprops = self.klass.getprops(protected=0).keys()
299         idlessprops.sort()
300         return ['id'] + idlessprops
302     def filter(self, request=None):
303         ''' Return a list of items from this class, filtered and sorted
304             by the current requested filterspec/filter/sort/group args
305         '''
306         if request is not None:
307             filterspec = request.filterspec
308             sort = request.sort
309             group = request.group
310         l = [HTMLItem(self.db, self.classname, x)
311              for x in self.klass.filter(None, filterspec, sort, group)]
312         return l
314     def classhelp(self, properties, label='?', width='400', height='400'):
315         '''pop up a javascript window with class help
317            This generates a link to a popup window which displays the 
318            properties indicated by "properties" of the class named by
319            "classname". The "properties" should be a comma-separated list
320            (eg. 'id,name,description').
322            You may optionally override the label displayed, the width and
323            height. The popup window will be resizable and scrollable.
324         '''
325         return '<a href="javascript:help_window(\'%s?:template=help&' \
326             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
327             '(%s)</b></a>'%(self.classname, properties, width, height, label)
329     def submit(self, label="Submit New Entry"):
330         ''' Generate a submit button (and action hidden element)
331         '''
332         return '  <input type="hidden" name=":action" value="new">\n'\
333         '  <input type="submit" name="submit" value="%s">'%label
335     def history(self):
336         return 'New node - no history'
338     def renderWith(self, name, **kwargs):
339         ''' Render this class with the given template.
340         '''
341         # create a new request and override the specified args
342         req = HTMLRequest(self.client)
343         req.classname = self.classname
344         req.update(kwargs)
346         # new template, using the specified classname and request
347         pt = getTemplate(self.db.config.TEMPLATES, self.classname, name)
349         # XXX handle PT rendering errors here nicely
350         try:
351             # use our fabricated request
352             return pt.render(self.client, self.classname, req)
353         except PageTemplate.PTRuntimeError, message:
354             return '<strong>%s</strong><ol>%s</ol>'%(message,
355                 cgi.escape('<li>'.join(pt._v_errors)))
357 class HTMLItem:
358     ''' Accesses through an *item*
359     '''
360     def __init__(self, db, classname, nodeid):
361         self.db = db
362         self.classname = classname
363         self.nodeid = nodeid
364         self.klass = self.db.getclass(classname)
365         self.props = self.klass.getprops()
367     def __repr__(self):
368         return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
370     def __getitem__(self, item):
371         ''' return an HTMLItem instance'''
372         if item == 'id':
373             return self.nodeid
374         if not self.props.has_key(item):
375             raise KeyError, item
376         prop = self.props[item]
378         # get the value, handling missing values
379         value = self.klass.get(self.nodeid, item, None)
380         if value is None:
381             if isinstance(self.props[item], hyperdb.Multilink):
382                 value = []
384         # look up the correct HTMLProperty class
385         for klass, htmlklass in propclasses:
386             if isinstance(prop, klass):
387                 return htmlklass(self.db, self.nodeid, prop, item, value)
389         raise KeyErorr, item
391     def __getattr__(self, attr):
392         ''' convenience access to properties '''
393         try:
394             return self[attr]
395         except KeyError:
396             raise AttributeError, attr
397     
398     def submit(self, label="Submit Changes"):
399         ''' Generate a submit button (and action hidden element)
400         '''
401         return '  <input type="hidden" name=":action" value="edit">\n'\
402         '  <input type="submit" name="submit" value="%s">'%label
404     # XXX this probably should just return the history items, not the HTML
405     def history(self, direction='descending'):
406         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
407             '<tr class="list-header">',
408             _('<th align=left><span class="list-item">Date</span></th>'),
409             _('<th align=left><span class="list-item">User</span></th>'),
410             _('<th align=left><span class="list-item">Action</span></th>'),
411             _('<th align=left><span class="list-item">Args</span></th>'),
412             '</tr>']
413         comments = {}
414         history = self.klass.history(self.nodeid)
415         history.sort()
416         if direction == 'descending':
417             history.reverse()
418         for id, evt_date, user, action, args in history:
419             date_s = str(evt_date).replace("."," ")
420             arg_s = ''
421             if action == 'link' and type(args) == type(()):
422                 if len(args) == 3:
423                     linkcl, linkid, key = args
424                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
425                         linkcl, linkid, key)
426                 else:
427                     arg_s = str(args)
429             elif action == 'unlink' and type(args) == type(()):
430                 if len(args) == 3:
431                     linkcl, linkid, key = args
432                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
433                         linkcl, linkid, key)
434                 else:
435                     arg_s = str(args)
437             elif type(args) == type({}):
438                 cell = []
439                 for k in args.keys():
440                     # try to get the relevant property and treat it
441                     # specially
442                     try:
443                         prop = self.props[k]
444                     except KeyError:
445                         prop = None
446                     if prop is not None:
447                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
448                                 isinstance(prop, hyperdb.Link)):
449                             # figure what the link class is
450                             classname = prop.classname
451                             try:
452                                 linkcl = self.db.getclass(classname)
453                             except KeyError:
454                                 labelprop = None
455                                 comments[classname] = _('''The linked class
456                                     %(classname)s no longer exists''')%locals()
457                             labelprop = linkcl.labelprop(1)
458                             hrefable = os.path.exists(
459                                 os.path.join(self.db.config.TEMPLATES,
460                                 classname+'.item'))
462                         if isinstance(prop, hyperdb.Multilink) and \
463                                 len(args[k]) > 0:
464                             ml = []
465                             for linkid in args[k]:
466                                 if isinstance(linkid, type(())):
467                                     sublabel = linkid[0] + ' '
468                                     linkids = linkid[1]
469                                 else:
470                                     sublabel = ''
471                                     linkids = [linkid]
472                                 subml = []
473                                 for linkid in linkids:
474                                     label = classname + linkid
475                                     # if we have a label property, try to use it
476                                     # TODO: test for node existence even when
477                                     # there's no labelprop!
478                                     try:
479                                         if labelprop is not None:
480                                             label = linkcl.get(linkid, labelprop)
481                                     except IndexError:
482                                         comments['no_link'] = _('''<strike>The
483                                             linked node no longer
484                                             exists</strike>''')
485                                         subml.append('<strike>%s</strike>'%label)
486                                     else:
487                                         if hrefable:
488                                             subml.append('<a href="%s%s">%s</a>'%(
489                                                 classname, linkid, label))
490                                 ml.append(sublabel + ', '.join(subml))
491                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
492                         elif isinstance(prop, hyperdb.Link) and args[k]:
493                             label = classname + args[k]
494                             # if we have a label property, try to use it
495                             # TODO: test for node existence even when
496                             # there's no labelprop!
497                             if labelprop is not None:
498                                 try:
499                                     label = linkcl.get(args[k], labelprop)
500                                 except IndexError:
501                                     comments['no_link'] = _('''<strike>The
502                                         linked node no longer
503                                         exists</strike>''')
504                                     cell.append(' <strike>%s</strike>,\n'%label)
505                                     # "flag" this is done .... euwww
506                                     label = None
507                             if label is not None:
508                                 if hrefable:
509                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
510                                         classname, args[k], label))
511                                 else:
512                                     cell.append('%s: %s' % (k,label))
514                         elif isinstance(prop, hyperdb.Date) and args[k]:
515                             d = date.Date(args[k])
516                             cell.append('%s: %s'%(k, str(d)))
518                         elif isinstance(prop, hyperdb.Interval) and args[k]:
519                             d = date.Interval(args[k])
520                             cell.append('%s: %s'%(k, str(d)))
522                         elif isinstance(prop, hyperdb.String) and args[k]:
523                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
525                         elif not args[k]:
526                             cell.append('%s: (no value)\n'%k)
528                         else:
529                             cell.append('%s: %s\n'%(k, str(args[k])))
530                     else:
531                         # property no longer exists
532                         comments['no_exist'] = _('''<em>The indicated property
533                             no longer exists</em>''')
534                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
535                 arg_s = '<br />'.join(cell)
536             else:
537                 # unkown event!!
538                 comments['unknown'] = _('''<strong><em>This event is not
539                     handled by the history display!</em></strong>''')
540                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
541             date_s = date_s.replace(' ', '&nbsp;')
542             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
543                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
544                 user, action, arg_s))
545         if comments:
546             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
547         for entry in comments.values():
548             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
549         l.append('</table>')
550         return '\n'.join(l)
552     def remove(self):
553         # XXX do what?
554         return ''
556 class HTMLUser(HTMLItem):
557     ''' Accesses through the *user* (a special case of item)
558     '''
559     def __init__(self, client):
560         HTMLItem.__init__(self, client.db, 'user', client.userid)
561         self.default_classname = client.classname
562         self.userid = client.userid
564         # used for security checks
565         self.security = client.db.security
566     _marker = []
567     def hasPermission(self, role, classname=_marker):
568         ''' Determine if the user has the Role.
570             The class being tested defaults to the template's class, but may
571             be overidden for this test by suppling an alternate classname.
572         '''
573         if classname is self._marker:
574             classname = self.default_classname
575         return self.security.hasPermission(role, self.userid, classname)
577 class HTMLProperty:
578     ''' String, Number, Date, Interval HTMLProperty
580         A wrapper object which may be stringified for the plain() behaviour.
581     '''
582     def __init__(self, db, nodeid, prop, name, value):
583         self.db = db
584         self.nodeid = nodeid
585         self.prop = prop
586         self.name = name
587         self.value = value
588     def __repr__(self):
589         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
590     def __str__(self):
591         return self.plain()
592     def __cmp__(self, other):
593         if isinstance(other, HTMLProperty):
594             return cmp(self.value, other.value)
595         return cmp(self.value, other)
597 class StringHTMLProperty(HTMLProperty):
598     def plain(self, escape=0):
599         if self.value is None:
600             return ''
601         if escape:
602             return cgi.escape(str(self.value))
603         return str(self.value)
605     def stext(self, escape=0):
606         s = self.plain(escape=escape)
607         if not StructuredText:
608             return s
609         return StructuredText(s,level=1,header=0)
611     def field(self, size = 30):
612         if self.value is None:
613             value = ''
614         else:
615             value = cgi.escape(str(self.value))
616             value = '&quot;'.join(value.split('"'))
617         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
619     def multiline(self, escape=0, rows=5, cols=40):
620         if self.value is None:
621             value = ''
622         else:
623             value = cgi.escape(str(self.value))
624             value = '&quot;'.join(value.split('"'))
625         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
626             self.name, rows, cols, value)
628     def email(self, escape=1):
629         ''' fudge email '''
630         if self.value is None: value = ''
631         else: value = str(self.value)
632         value = value.replace('@', ' at ')
633         value = value.replace('.', ' ')
634         if escape:
635             value = cgi.escape(value)
636         return value
638 class PasswordHTMLProperty(HTMLProperty):
639     def plain(self):
640         if self.value is None:
641             return ''
642         return _('*encrypted*')
644     def field(self, size = 30):
645         return '<input type="password" name="%s" size="%s">'%(self.name, size)
647 class NumberHTMLProperty(HTMLProperty):
648     def plain(self):
649         return str(self.value)
651     def field(self, size = 30):
652         if self.value is None:
653             value = ''
654         else:
655             value = cgi.escape(str(self.value))
656             value = '&quot;'.join(value.split('"'))
657         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
659 class BooleanHTMLProperty(HTMLProperty):
660     def plain(self):
661         if self.value is None:
662             return ''
663         return self.value and "Yes" or "No"
665     def field(self):
666         checked = self.value and "checked" or ""
667         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
668             checked)
669         if checked:
670             checked = ""
671         else:
672             checked = "checked"
673         s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
674             checked)
675         return s
677 class DateHTMLProperty(HTMLProperty):
678     def plain(self):
679         if self.value is None:
680             return ''
681         return str(self.value)
683     def field(self, size = 30):
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 '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
691     def reldate(self, pretty=1):
692         if not self.value:
693             return ''
695         # figure the interval
696         interval = date.Date('.') - self.value
697         if pretty:
698             return interval.pretty()
699         return str(interval)
701 class IntervalHTMLProperty(HTMLProperty):
702     def plain(self):
703         if self.value is None:
704             return ''
705         return str(self.value)
707     def pretty(self):
708         return self.value.pretty()
710     def field(self, size = 30):
711         if self.value is None:
712             value = ''
713         else:
714             value = cgi.escape(str(self.value))
715             value = '&quot;'.join(value.split('"'))
716         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
718 class LinkHTMLProperty(HTMLProperty):
719     ''' Link HTMLProperty
720         Include the above as well as being able to access the class
721         information. Stringifying the object itself results in the value
722         from the item being displayed. Accessing attributes of this object
723         result in the appropriate entry from the class being queried for the
724         property accessed (so item/assignedto/name would look up the user
725         entry identified by the assignedto property on item, and then the
726         name property of that user)
727     '''
728     def __getattr__(self, attr):
729         ''' return a new HTMLItem '''
730         #print 'getattr', (self, attr, self.value)
731         if not self.value:
732             raise AttributeError, "Can't access missing value"
733         i = HTMLItem(self.db, self.prop.classname, self.value)
734         return getattr(i, attr)
736     def plain(self, escape=0):
737         if self.value is None:
738             return _('[unselected]')
739         linkcl = self.db.classes[self.prop.classname]
740         k = linkcl.labelprop(1)
741         value = str(linkcl.get(self.value, k))
742         if escape:
743             value = cgi.escape(value)
744         return value
746     # XXX most of the stuff from here down is of dubious utility - it's easy
747     # enough to do in the template by hand (and in some cases, it's shorter
748     # and clearer...
750     def field(self):
751         linkcl = self.db.getclass(self.prop.classname)
752         if linkcl.getprops().has_key('order'):  
753             sort_on = 'order'  
754         else:  
755             sort_on = linkcl.labelprop()  
756         options = linkcl.filter(None, {}, [sort_on], []) 
757         # TODO: make this a field display, not a menu one!
758         l = ['<select name="%s">'%property]
759         k = linkcl.labelprop(1)
760         if value is None:
761             s = 'selected '
762         else:
763             s = ''
764         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
765         for optionid in options:
766             option = linkcl.get(optionid, k)
767             s = ''
768             if optionid == value:
769                 s = 'selected '
770             if showid:
771                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
772             else:
773                 lab = option
774             if size is not None and len(lab) > size:
775                 lab = lab[:size-3] + '...'
776             lab = cgi.escape(lab)
777             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
778         l.append('</select>')
779         return '\n'.join(l)
781     def download(self, showid=0):
782         linkname = self.prop.classname
783         linkcl = self.db.getclass(linkname)
784         k = linkcl.labelprop(1)
785         linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
786         if showid:
787             label = value
788             title = ' title="%s"'%linkvalue
789             # note ... this should be urllib.quote(linkcl.get(value, k))
790         else:
791             label = linkvalue
792             title = ''
793         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
794             linkvalue, title, label)
796     def menu(self, size=None, height=None, showid=0, additional=[],
797             **conditions):
798         value = self.value
800         # sort function
801         sortfunc = make_sort_function(self.db, self.prop.classname)
803         # force the value to be a single choice
804         if isinstance(value, type('')):
805             value = value[0]
806         linkcl = self.db.getclass(self.prop.classname)
807         l = ['<select name="%s">'%self.name]
808         k = linkcl.labelprop(1)
809         s = ''
810         if value is None:
811             s = 'selected '
812         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
813         if linkcl.getprops().has_key('order'):  
814             sort_on = ('+', 'order')
815         else:  
816             sort_on = ('+', linkcl.labelprop())
817         options = linkcl.filter(None, conditions, sort_on, (None, None))
818         for optionid in options:
819             option = linkcl.get(optionid, k)
820             s = ''
821             if value in [optionid, option]:
822                 s = 'selected '
823             if showid:
824                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
825             else:
826                 lab = option
827             if size is not None and len(lab) > size:
828                 lab = lab[:size-3] + '...'
829             if additional:
830                 m = []
831                 for propname in additional:
832                     m.append(linkcl.get(optionid, propname))
833                 lab = lab + ' (%s)'%', '.join(map(str, m))
834             lab = cgi.escape(lab)
835             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
836         l.append('</select>')
837         return '\n'.join(l)
839 #    def checklist(self, ...)
841 class MultilinkHTMLProperty(HTMLProperty):
842     ''' Multilink HTMLProperty
844         Also be iterable, returning a wrapper object like the Link case for
845         each entry in the multilink.
846     '''
847     def __len__(self):
848         ''' length of the multilink '''
849         return len(self.value)
851     def __getattr__(self, attr):
852         ''' no extended attribute accesses make sense here '''
853         raise AttributeError, attr
855     def __getitem__(self, num):
856         ''' iterate and return a new HTMLItem '''
857         #print 'getitem', (self, num)
858         value = self.value[num]
859         return HTMLItem(self.db, self.prop.classname, value)
861     def reverse(self):
862         ''' return the list in reverse order '''
863         l = self.value[:]
864         l.reverse()
865         return [HTMLItem(self.db, self.prop.classname, value) for value in l]
867     def plain(self, escape=0):
868         linkcl = self.db.classes[self.prop.classname]
869         k = linkcl.labelprop(1)
870         labels = []
871         for v in self.value:
872             labels.append(linkcl.get(v, k))
873         value = ', '.join(labels)
874         if escape:
875             value = cgi.escape(value)
876         return value
878     # XXX most of the stuff from here down is of dubious utility - it's easy
879     # enough to do in the template by hand (and in some cases, it's shorter
880     # and clearer...
882     def field(self, size=30, showid=0):
883         sortfunc = make_sort_function(self.db, self.prop.classname)
884         linkcl = self.db.getclass(self.prop.classname)
885         value = self.value[:]
886         if value:
887             value.sort(sortfunc)
888         # map the id to the label property
889         if not showid:
890             k = linkcl.labelprop(1)
891             value = [linkcl.get(v, k) for v in value]
892         value = cgi.escape(','.join(value))
893         return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
895     def menu(self, size=None, height=None, showid=0, additional=[],
896             **conditions):
897         value = self.value
899         # sort function
900         sortfunc = make_sort_function(self.db, self.prop.classname)
902         linkcl = self.db.getclass(self.prop.classname)
903         if linkcl.getprops().has_key('order'):  
904             sort_on = ('+', 'order')
905         else:  
906             sort_on = ('+', linkcl.labelprop())
907         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
908         height = height or min(len(options), 7)
909         l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
910         k = linkcl.labelprop(1)
911         for optionid in options:
912             option = linkcl.get(optionid, k)
913             s = ''
914             if optionid in value or option in value:
915                 s = 'selected '
916             if showid:
917                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
918             else:
919                 lab = option
920             if size is not None and len(lab) > size:
921                 lab = lab[:size-3] + '...'
922             if additional:
923                 m = []
924                 for propname in additional:
925                     m.append(linkcl.get(optionid, propname))
926                 lab = lab + ' (%s)'%', '.join(m)
927             lab = cgi.escape(lab)
928             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
929                 lab))
930         l.append('</select>')
931         return '\n'.join(l)
933 # set the propclasses for HTMLItem
934 propclasses = (
935     (hyperdb.String, StringHTMLProperty),
936     (hyperdb.Number, NumberHTMLProperty),
937     (hyperdb.Boolean, BooleanHTMLProperty),
938     (hyperdb.Date, DateHTMLProperty),
939     (hyperdb.Interval, IntervalHTMLProperty),
940     (hyperdb.Password, PasswordHTMLProperty),
941     (hyperdb.Link, LinkHTMLProperty),
942     (hyperdb.Multilink, MultilinkHTMLProperty),
945 def make_sort_function(db, classname):
946     '''Make a sort function for a given class
947     '''
948     linkcl = db.getclass(classname)
949     if linkcl.getprops().has_key('order'):
950         sort_on = 'order'
951     else:
952         sort_on = linkcl.labelprop()
953     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
954         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
955     return sortfunc
957 def handleListCGIValue(value):
958     ''' Value is either a single item or a list of items. Each item has a
959         .value that we're actually interested in.
960     '''
961     if isinstance(value, type([])):
962         return [value.value for value in value]
963     else:
964         return value.value.split(',')
966 class ShowDict:
967     ''' A convenience access to the :columns index parameters
968     '''
969     def __init__(self, columns):
970         self.columns = {}
971         for col in columns:
972             self.columns[col] = 1
973     def __getitem__(self, name):
974         return self.columns.has_key(name)
976 class HTMLRequest:
977     ''' The *request*, holding the CGI form and environment.
979         "form" the CGI form as a cgi.FieldStorage
980         "env" the CGI environment variables
981         "url" the current URL path for this request
982         "base" the base URL for this instance
983         "user" a HTMLUser instance for this user
984         "classname" the current classname (possibly None)
985         "template" the current template (suffix, also possibly None)
987         Index args:
988         "columns" dictionary of the columns to display in an index page
989         "show" a convenience access to columns - request/show/colname will
990                be true if the columns should be displayed, false otherwise
991         "sort" index sort column (direction, column name)
992         "group" index grouping property (direction, column name)
993         "filter" properties to filter the index on
994         "filterspec" values to filter the index on
995         "search_text" text to perform a full-text search on for an index
997     '''
998     def __init__(self, client):
999         self.client = client
1001         # easier access vars
1002         self.form = client.form
1003         self.env = client.env
1004         self.base = client.base
1005         self.url = client.url
1006         self.user = HTMLUser(client)
1008         # store the current class name and action
1009         self.classname = client.classname
1010         self.template = client.template
1012         # extract the index display information from the form
1013         self.columns = []
1014         if self.form.has_key(':columns'):
1015             self.columns = handleListCGIValue(self.form[':columns'])
1016         self.show = ShowDict(self.columns)
1018         # sorting
1019         self.sort = (None, None)
1020         if self.form.has_key(':sort'):
1021             sort = self.form[':sort'].value
1022             if sort.startswith('-'):
1023                 self.sort = ('-', sort[1:])
1024             else:
1025                 self.sort = ('+', sort)
1026         if self.form.has_key(':sortdir'):
1027             self.sort = ('-', self.sort[1])
1029         # grouping
1030         self.group = (None, None)
1031         if self.form.has_key(':group'):
1032             group = self.form[':group'].value
1033             if group.startswith('-'):
1034                 self.group = ('-', group[1:])
1035             else:
1036                 self.group = ('+', group)
1037         if self.form.has_key(':groupdir'):
1038             self.group = ('-', self.group[1])
1040         # filtering
1041         self.filter = []
1042         if self.form.has_key(':filter'):
1043             self.filter = handleListCGIValue(self.form[':filter'])
1044         self.filterspec = {}
1045         if self.classname is not None:
1046             props = self.client.db.getclass(self.classname).getprops()
1047             for name in self.filter:
1048                 if self.form.has_key(name):
1049                     prop = props[name]
1050                     fv = self.form[name]
1051                     if (isinstance(prop, hyperdb.Link) or
1052                             isinstance(prop, hyperdb.Multilink)):
1053                         self.filterspec[name] = handleListCGIValue(fv)
1054                     else:
1055                         self.filterspec[name] = fv.value
1057         # full-text search argument
1058         self.search_text = None
1059         if self.form.has_key(':search_text'):
1060             self.search_text = self.form[':search_text'].value
1062         # pagination - size and start index
1063         # figure batch args
1064         if self.form.has_key(':pagesize'):
1065             self.pagesize = int(self.form[':pagesize'].value)
1066         else:
1067             self.pagesize = 50
1068         if self.form.has_key(':startwith'):
1069             self.startwith = int(self.form[':startwith'].value)
1070         else:
1071             self.startwith = 0
1073     def update(self, kwargs):
1074         self.__dict__.update(kwargs)
1075         if kwargs.has_key('columns'):
1076             self.show = ShowDict(self.columns)
1078     def description(self):
1079         ''' Return a description of the request - handle for the page title.
1080         '''
1081         s = [self.client.db.config.INSTANCE_NAME]
1082         if self.classname:
1083             if self.client.nodeid:
1084                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1085             else:
1086                 s.append('- index of '+self.classname)
1087         else:
1088             s.append('- home')
1089         return ' '.join(s)
1091     def __str__(self):
1092         d = {}
1093         d.update(self.__dict__)
1094         f = ''
1095         for k in self.form.keys():
1096             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1097         d['form'] = f
1098         e = ''
1099         for k,v in self.env.items():
1100             e += '\n     %r=%r'%(k, v)
1101         d['env'] = e
1102         return '''
1103 form: %(form)s
1104 url: %(url)r
1105 base: %(base)r
1106 classname: %(classname)r
1107 template: %(template)r
1108 columns: %(columns)r
1109 sort: %(sort)r
1110 group: %(group)r
1111 filter: %(filter)r
1112 search_text: %(search_text)r
1113 pagesize: %(pagesize)r
1114 startwith: %(startwith)r
1115 env: %(env)s
1116 '''%d
1118     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1119             filterspec=1):
1120         ''' return the current index args as form elements '''
1121         l = []
1122         s = '<input type="hidden" name="%s" value="%s">'
1123         if columns and self.columns:
1124             l.append(s%(':columns', ','.join(self.columns)))
1125         if sort and self.sort[1] is not None:
1126             if self.sort[0] == '-':
1127                 val = '-'+self.sort[1]
1128             else:
1129                 val = self.sort[1]
1130             l.append(s%(':sort', val))
1131         if group and self.group[1] is not None:
1132             if self.group[0] == '-':
1133                 val = '-'+self.group[1]
1134             else:
1135                 val = self.group[1]
1136             l.append(s%(':group', val))
1137         if filter and self.filter:
1138             l.append(s%(':filter', ','.join(self.filter)))
1139         if filterspec:
1140             for k,v in self.filterspec.items():
1141                 l.append(s%(k, ','.join(v)))
1142         if self.search_text:
1143             l.append(s%(':search_text', self.search_text))
1144         l.append(s%(':pagesize', self.pagesize))
1145         l.append(s%(':startwith', self.startwith))
1146         return '\n'.join(l)
1148     def indexargs_href(self, url, args):
1149         ''' embed the current index args in a URL '''
1150         l = ['%s=%s'%(k,v) for k,v in args.items()]
1151         if self.columns and not args.has_key(':columns'):
1152             l.append(':columns=%s'%(','.join(self.columns)))
1153         if self.sort[1] is not None and not args.has_key(':sort'):
1154             if self.sort[0] == '-':
1155                 val = '-'+self.sort[1]
1156             else:
1157                 val = self.sort[1]
1158             l.append(':sort=%s'%val)
1159         if self.group[1] is not None and not args.has_key(':group'):
1160             if self.group[0] == '-':
1161                 val = '-'+self.group[1]
1162             else:
1163                 val = self.group[1]
1164             l.append(':group=%s'%val)
1165         if self.filter and not args.has_key(':columns'):
1166             l.append(':filter=%s'%(','.join(self.filter)))
1167         for k,v in self.filterspec.items():
1168             if not args.has_key(k):
1169                 l.append('%s=%s'%(k, ','.join(v)))
1170         if self.search_text and not args.has_key(':search_text'):
1171             l.append(':search_text=%s'%self.search_text)
1172         if not args.has_key(':pagesize'):
1173             l.append(':pagesize=%s'%self.pagesize)
1174         if not args.has_key(':startwith'):
1175             l.append(':startwith=%s'%self.startwith)
1176         return '%s?%s'%(url, '&'.join(l))
1178     def base_javascript(self):
1179         return '''
1180 <script language="javascript">
1181 submitted = false;
1182 function submit_once() {
1183     if (submitted) {
1184         alert("Your request is being processed.\\nPlease be patient.");
1185         return 0;
1186     }
1187     submitted = true;
1188     return 1;
1191 function help_window(helpurl, width, height) {
1192     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1194 </script>
1195 '''%self.base
1197     def batch(self):
1198         ''' Return a batch object for results from the "current search"
1199         '''
1200         filterspec = self.filterspec
1201         sort = self.sort
1202         group = self.group
1204         # get the list of ids we're batching over
1205         klass = self.client.db.getclass(self.classname)
1206         if self.search_text:
1207             matches = self.client.db.indexer.search(
1208                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1209         else:
1210             matches = None
1211         l = klass.filter(matches, filterspec, sort, group)
1213         # return the batch object
1214         return Batch(self.client, self.classname, l, self.pagesize,
1215             self.startwith)
1218 # extend the standard ZTUtils Batch object to remove dependency on
1219 # Acquisition and add a couple of useful methods
1220 class Batch(ZTUtils.Batch):
1221     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1222         self.client = client
1223         self.classname = classname
1224         self.last_index = self.last_item = None
1225         self.current_item = None
1226         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1228     # overwrite so we can late-instantiate the HTMLItem instance
1229     def __getitem__(self, index):
1230         if index < 0:
1231             if index + self.end < self.first: raise IndexError, index
1232             return self._sequence[index + self.end]
1233         
1234         if index >= self.length: raise IndexError, index
1236         # move the last_item along - but only if the fetched index changes
1237         # (for some reason, index 0 is fetched twice)
1238         if index != self.last_index:
1239             self.last_item = self.current_item
1240             self.last_index = index
1242         # wrap the return in an HTMLItem
1243         self.current_item = HTMLItem(self.client.db, self.classname,
1244             self._sequence[index+self.first])
1245         return self.current_item
1247     def propchanged(self, property):
1248         ''' Detect if the property marked as being the group property
1249             changed in the last iteration fetch
1250         '''
1251         if (self.last_item is None or
1252                 self.last_item[property] != self.current_item[property]):
1253             return 1
1254         return 0
1256     # override these 'cos we don't have access to acquisition
1257     def previous(self):
1258         if self.start == 1:
1259             return None
1260         return Batch(self.client, self.classname, self._sequence, self._size,
1261             self.first - self._size + self.overlap, 0, self.orphan,
1262             self.overlap)
1264     def next(self):
1265         try:
1266             self._sequence[self.end]
1267         except IndexError:
1268             return None
1269         return Batch(self.client, self.classname, self._sequence, self._size,
1270             self.end - self.overlap, 0, self.orphan, self.overlap)
1272     def length(self):
1273         self.sequence_length = l = len(self._sequence)
1274         return l