Code

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