Code

54a592b6341fa30cc21d1eeffc55e4c801edaa48
[roundup.git] / roundup / cgi / templating.py
1 import sys, cgi, urllib, os, re
3 from roundup import hyperdb, date
4 from roundup.i18n import _
7 try:
8     import StructuredText
9 except ImportError:
10     StructuredText = None
12 # Make sure these modules are loaded
13 # I need these to run PageTemplates outside of Zope :(
14 # If we're running in a Zope environment, these modules will be loaded
15 # already...
16 if not sys.modules.has_key('zLOG'):
17     import zLOG
18     sys.modules['zLOG'] = zLOG
19 if not sys.modules.has_key('MultiMapping'):
20     import MultiMapping
21     sys.modules['MultiMapping'] = MultiMapping
22 if not sys.modules.has_key('ComputedAttribute'):
23     import ComputedAttribute
24     sys.modules['ComputedAttribute'] = ComputedAttribute
25 if not sys.modules.has_key('ExtensionClass'):
26     import ExtensionClass
27     sys.modules['ExtensionClass'] = ExtensionClass
28 if not sys.modules.has_key('Acquisition'):
29     import Acquisition
30     sys.modules['Acquisition'] = Acquisition
32 # now it's safe to import PageTemplates and ZTUtils
33 from PageTemplates import PageTemplate
34 import ZTUtils
36 class RoundupPageTemplate(PageTemplate.PageTemplate):
37     ''' A Roundup-specific PageTemplate.
39         Interrogate the client to set up the various template variables to
40         be available:
42         *class*
43           The current class of node being displayed as an HTMLClass
44           instance.
45         *item*
46           The current node from the database, if we're viewing a specific
47           node, as an HTMLItem instance. If it doesn't exist, then we're
48           on a new item page.
49         (*classname*)
50           this is one of two things:
52           1. the *item* is also available under its classname, so a *user*
53              node would also be available under the name *user*. This is
54              also an HTMLItem instance.
55           2. if there's no *item* then the current class is available
56              through this name, thus "user/name" and "user/name/menu" will
57              still work - the latter will pull information from the form
58              if it can.
59         *form*
60           The current CGI form information as a mapping of form argument
61           name to value
62         *request*
63           Includes information about the current request, including:
64            - the url
65            - the current index information (``filterspec``, ``filter`` args,
66              ``properties``, etc) parsed out of the form. 
67            - methods for easy filterspec link generation
68            - *user*, the current user node as an HTMLItem instance
69         *instance*
70           The current instance
71         *db*
72           The current database, through which db.config may be reached.
74         Maybe also:
76         *modules*
77           python modules made available (XXX: not sure what's actually in
78           there tho)
79     '''
80     def __init__(self, client, classname=None, request=None):
81         ''' Extract the vars from the client and install in the context.
82         '''
83         self.client = client
84         self.classname = classname or self.client.classname
85         self.request = request or HTMLRequest(self.client)
87     def pt_getContext(self):
88         c = {
89              'klass': HTMLClass(self.client, self.classname),
90              'options': {},
91              'nothing': None,
92              'request': self.request,
93              'content': self.client.content,
94              'db': HTMLDatabase(self.client),
95              'instance': self.client.instance
96         }
97         # add in the item if there is one
98         if self.client.nodeid:
99             c['item'] = HTMLItem(self.client.db, self.classname,
100                 self.client.nodeid)
101             c[self.classname] = c['item']
102         else:
103             c[self.classname] = c['klass']
104         return c
105    
106     def render(self, *args, **kwargs):
107         if not kwargs.has_key('args'):
108             kwargs['args'] = args
109         return self.pt_render(extra_context={'options': kwargs})
111 class HTMLDatabase:
112     ''' Return HTMLClasses for valid class fetches
113     '''
114     def __init__(self, client):
115         self.client = client
116         self.config = client.db.config
117     def __getattr__(self, attr):
118         self.client.db.getclass(attr)
119         return HTMLClass(self.client, attr)
120     def classes(self):
121         l = self.client.db.classes.keys()
122         l.sort()
123         return [HTMLClass(self.client, cn) for cn in l]
124         
125 class HTMLClass:
126     ''' Accesses through a class (either through *class* or *db.<classname>*)
127     '''
128     def __init__(self, client, classname):
129         self.client = client
130         self.db = client.db
131         self.classname = classname
132         if classname is not None:
133             self.klass = self.db.getclass(self.classname)
134             self.props = self.klass.getprops()
136     def __repr__(self):
137         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
139     def __getitem__(self, item):
140         ''' return an HTMLItem instance'''
141         #print 'getitem', (self, attr)
142         if item == 'creator':
143             return HTMLUser(self.client)
145         if not self.props.has_key(item):
146             raise KeyError, item
147         prop = self.props[item]
149         # look up the correct HTMLProperty class
150         for klass, htmlklass in propclasses:
151             if isinstance(prop, hyperdb.Multilink):
152                 value = []
153             else:
154                 value = None
155             if isinstance(prop, klass):
156                 return htmlklass(self.db, '', prop, item, value)
158         # no good
159         raise KeyError, item
161     def __getattr__(self, attr):
162         ''' convenience access '''
163         try:
164             return self[attr]
165         except KeyError:
166             raise AttributeError, attr
168     def properties(self):
169         ''' Return HTMLProperty for all props
170         '''
171         l = []
172         for name, prop in self.props.items():
173             for klass, htmlklass in propclasses:
174                 if isinstance(prop, hyperdb.Multilink):
175                     value = []
176                 else:
177                     value = None
178                 if isinstance(prop, klass):
179                     l.append(htmlklass(self.db, '', prop, name, value))
180         return l
182     def list(self):
183         l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
184         return l
186     def filter(self, request=None):
187         ''' Return a list of items from this class, filtered and sorted
188             by the current requested filterspec/filter/sort/group args
189         '''
190         if request is not None:
191             filterspec = request.filterspec
192             sort = request.sort
193             group = request.group
194         l = [HTMLItem(self.db, self.classname, x)
195              for x in self.klass.filter(None, filterspec, sort, group)]
196         return l
198     def classhelp(self, properties, label='?', width='400', height='400'):
199         '''pop up a javascript window with class help
201            This generates a link to a popup window which displays the 
202            properties indicated by "properties" of the class named by
203            "classname". The "properties" should be a comma-separated list
204            (eg. 'id,name,description').
206            You may optionally override the label displayed, the width and
207            height. The popup window will be resizable and scrollable.
208         '''
209         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
210             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
211             properties, width, height, label)
213     def submit(self, label="Submit New Entry"):
214         ''' Generate a submit button (and action hidden element)
215         '''
216         return '  <input type="hidden" name=":action" value="new">\n'\
217         '  <input type="submit" name="submit" value="%s">'%label
219     def history(self):
220         return 'New node - no history'
222     def renderWith(self, name, **kwargs):
223         ''' Render this class with the given template.
224         '''
225         # create a new request and override the specified args
226         req = HTMLRequest(self.client)
227         req.classname = self.classname
228         req.__dict__.update(kwargs)
230         # new template, using the specified classname and request
231         pt = RoundupPageTemplate(self.client, self.classname, req)
233         # use the specified template
234         name = self.classname + '.' + name
235         pt.write(open('/tmp/test/html/%s'%name).read())
236         pt.id = name
238         # XXX handle PT rendering errors here nicely
239         try:
240             return pt.render()
241         except PageTemplate.PTRuntimeError, message:
242             return '<strong>%s</strong><ol>%s</ol>'%(message,
243                 cgi.escape('<li>'.join(pt._v_errors)))
245 class HTMLItem:
246     ''' Accesses through an *item*
247     '''
248     def __init__(self, db, classname, nodeid):
249         self.db = db
250         self.classname = classname
251         self.nodeid = nodeid
252         self.klass = self.db.getclass(classname)
253         self.props = self.klass.getprops()
255     def __repr__(self):
256         return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
258     def __getitem__(self, item):
259         ''' return an HTMLItem instance'''
260         if item == 'id':
261             return self.nodeid
262         if not self.props.has_key(item):
263             raise KeyError, item
264         prop = self.props[item]
266         # get the value, handling missing values
267         value = self.klass.get(self.nodeid, item, None)
268         if value is None:
269             if isinstance(self.props[item], hyperdb.Multilink):
270                 value = []
272         # look up the correct HTMLProperty class
273         for klass, htmlklass in propclasses:
274             if isinstance(prop, klass):
275                 return htmlklass(self.db, self.nodeid, prop, item, value)
277         raise KeyErorr, item
279     def __getattr__(self, attr):
280         ''' convenience access to properties '''
281         try:
282             return self[attr]
283         except KeyError:
284             raise AttributeError, attr
285     
286     def submit(self, label="Submit Changes"):
287         ''' Generate a submit button (and action hidden element)
288         '''
289         return '  <input type="hidden" name=":action" value="edit">\n'\
290         '  <input type="submit" name="submit" value="%s">'%label
292     # XXX this probably should just return the history items, not the HTML
293     def history(self, direction='descending'):
294         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
295             '<tr class="list-header">',
296             _('<th align=left><span class="list-item">Date</span></th>'),
297             _('<th align=left><span class="list-item">User</span></th>'),
298             _('<th align=left><span class="list-item">Action</span></th>'),
299             _('<th align=left><span class="list-item">Args</span></th>'),
300             '</tr>']
301         comments = {}
302         history = self.klass.history(self.nodeid)
303         history.sort()
304         if direction == 'descending':
305             history.reverse()
306         for id, evt_date, user, action, args in history:
307             date_s = str(evt_date).replace("."," ")
308             arg_s = ''
309             if action == 'link' and type(args) == type(()):
310                 if len(args) == 3:
311                     linkcl, linkid, key = args
312                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
313                         linkcl, linkid, key)
314                 else:
315                     arg_s = str(args)
317             elif action == 'unlink' and type(args) == type(()):
318                 if len(args) == 3:
319                     linkcl, linkid, key = args
320                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
321                         linkcl, linkid, key)
322                 else:
323                     arg_s = str(args)
325             elif type(args) == type({}):
326                 cell = []
327                 for k in args.keys():
328                     # try to get the relevant property and treat it
329                     # specially
330                     try:
331                         prop = self.props[k]
332                     except KeyError:
333                         prop = None
334                     if prop is not None:
335                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
336                                 isinstance(prop, hyperdb.Link)):
337                             # figure what the link class is
338                             classname = prop.classname
339                             try:
340                                 linkcl = self.db.getclass(classname)
341                             except KeyError:
342                                 labelprop = None
343                                 comments[classname] = _('''The linked class
344                                     %(classname)s no longer exists''')%locals()
345                             labelprop = linkcl.labelprop(1)
346                             hrefable = os.path.exists(
347                                 os.path.join(self.db.config.TEMPLATES,
348                                 classname+'.item'))
350                         if isinstance(prop, hyperdb.Multilink) and \
351                                 len(args[k]) > 0:
352                             ml = []
353                             for linkid in args[k]:
354                                 if isinstance(linkid, type(())):
355                                     sublabel = linkid[0] + ' '
356                                     linkids = linkid[1]
357                                 else:
358                                     sublabel = ''
359                                     linkids = [linkid]
360                                 subml = []
361                                 for linkid in linkids:
362                                     label = classname + linkid
363                                     # if we have a label property, try to use it
364                                     # TODO: test for node existence even when
365                                     # there's no labelprop!
366                                     try:
367                                         if labelprop is not None:
368                                             label = linkcl.get(linkid, labelprop)
369                                     except IndexError:
370                                         comments['no_link'] = _('''<strike>The
371                                             linked node no longer
372                                             exists</strike>''')
373                                         subml.append('<strike>%s</strike>'%label)
374                                     else:
375                                         if hrefable:
376                                             subml.append('<a href="%s%s">%s</a>'%(
377                                                 classname, linkid, label))
378                                 ml.append(sublabel + ', '.join(subml))
379                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
380                         elif isinstance(prop, hyperdb.Link) and args[k]:
381                             label = classname + args[k]
382                             # if we have a label property, try to use it
383                             # TODO: test for node existence even when
384                             # there's no labelprop!
385                             if labelprop is not None:
386                                 try:
387                                     label = linkcl.get(args[k], labelprop)
388                                 except IndexError:
389                                     comments['no_link'] = _('''<strike>The
390                                         linked node no longer
391                                         exists</strike>''')
392                                     cell.append(' <strike>%s</strike>,\n'%label)
393                                     # "flag" this is done .... euwww
394                                     label = None
395                             if label is not None:
396                                 if hrefable:
397                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
398                                         classname, args[k], label))
399                                 else:
400                                     cell.append('%s: %s' % (k,label))
402                         elif isinstance(prop, hyperdb.Date) and args[k]:
403                             d = date.Date(args[k])
404                             cell.append('%s: %s'%(k, str(d)))
406                         elif isinstance(prop, hyperdb.Interval) and args[k]:
407                             d = date.Interval(args[k])
408                             cell.append('%s: %s'%(k, str(d)))
410                         elif isinstance(prop, hyperdb.String) and args[k]:
411                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
413                         elif not args[k]:
414                             cell.append('%s: (no value)\n'%k)
416                         else:
417                             cell.append('%s: %s\n'%(k, str(args[k])))
418                     else:
419                         # property no longer exists
420                         comments['no_exist'] = _('''<em>The indicated property
421                             no longer exists</em>''')
422                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
423                 arg_s = '<br />'.join(cell)
424             else:
425                 # unkown event!!
426                 comments['unknown'] = _('''<strong><em>This event is not
427                     handled by the history display!</em></strong>''')
428                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
429             date_s = date_s.replace(' ', '&nbsp;')
430             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
431                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
432                 user, action, arg_s))
433         if comments:
434             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
435         for entry in comments.values():
436             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
437         l.append('</table>')
438         return '\n'.join(l)
440     def remove(self):
441         # XXX do what?
442         return ''
444 class HTMLUser(HTMLItem):
445     ''' Accesses through the *user* (a special case of item)
446     '''
447     def __init__(self, client):
448         HTMLItem.__init__(self, client.db, 'user', client.userid)
449         self.default_classname = client.classname
450         self.userid = client.userid
452         # used for security checks
453         self.security = client.db.security
454     _marker = []
455     def hasPermission(self, role, classname=_marker):
456         ''' Determine if the user has the Role.
458             The class being tested defaults to the template's class, but may
459             be overidden for this test by suppling an alternate classname.
460         '''
461         if classname is self._marker:
462             classname = self.default_classname
463         return self.security.hasPermission(role, self.userid, classname)
465 class HTMLProperty:
466     ''' String, Number, Date, Interval HTMLProperty
468         A wrapper object which may be stringified for the plain() behaviour.
469     '''
470     def __init__(self, db, nodeid, prop, name, value):
471         self.db = db
472         self.nodeid = nodeid
473         self.prop = prop
474         self.name = name
475         self.value = value
476     def __repr__(self):
477         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
478     def __str__(self):
479         return self.plain()
480     def __cmp__(self, other):
481         if isinstance(other, HTMLProperty):
482             return cmp(self.value, other.value)
483         return cmp(self.value, other)
485 class StringHTMLProperty(HTMLProperty):
486     def plain(self, escape=0):
487         if self.value is None:
488             return ''
489         if escape:
490             return cgi.escape(str(self.value))
491         return str(self.value)
493     def stext(self, escape=0):
494         s = self.plain(escape=escape)
495         if not StructuredText:
496             return s
497         return StructuredText(s,level=1,header=0)
499     def field(self, size = 30):
500         if self.value is None:
501             value = ''
502         else:
503             value = cgi.escape(str(self.value))
504             value = '&quot;'.join(value.split('"'))
505         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
507     def multiline(self, escape=0, rows=5, cols=40):
508         if self.value is None:
509             value = ''
510         else:
511             value = cgi.escape(str(self.value))
512             value = '&quot;'.join(value.split('"'))
513         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
514             self.name, rows, cols, value)
516     def email(self, escape=1):
517         ''' fudge email '''
518         if self.value is None: value = ''
519         else: value = str(self.value)
520         value = value.replace('@', ' at ')
521         value = value.replace('.', ' ')
522         if escape:
523             value = cgi.escape(value)
524         return value
526 class PasswordHTMLProperty(HTMLProperty):
527     def plain(self):
528         if self.value is None:
529             return ''
530         return _('*encrypted*')
532     def field(self, size = 30):
533         return '<input type="password" name="%s" size="%s">'%(self.name, size)
535 class NumberHTMLProperty(HTMLProperty):
536     def plain(self):
537         return str(self.value)
539     def field(self, size = 30):
540         if self.value is None:
541             value = ''
542         else:
543             value = cgi.escape(str(self.value))
544             value = '&quot;'.join(value.split('"'))
545         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
547 class BooleanHTMLProperty(HTMLProperty):
548     def plain(self):
549         if self.value is None:
550             return ''
551         return self.value and "Yes" or "No"
553     def field(self):
554         checked = self.value and "checked" or ""
555         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
556             checked)
557         if checked:
558             checked = ""
559         else:
560             checked = "checked"
561         s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
562             checked)
563         return s
565 class DateHTMLProperty(HTMLProperty):
566     def plain(self):
567         if self.value is None:
568             return ''
569         return str(self.value)
571     def field(self, size = 30):
572         if self.value is None:
573             value = ''
574         else:
575             value = cgi.escape(str(self.value))
576             value = '&quot;'.join(value.split('"'))
577         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
579     def reldate(self, pretty=1):
580         if not self.value:
581             return ''
583         # figure the interval
584         interval = date.Date('.') - self.value
585         if pretty:
586             return interval.pretty()
587         return str(interval)
589 class IntervalHTMLProperty(HTMLProperty):
590     def plain(self):
591         if self.value is None:
592             return ''
593         return str(self.value)
595     def pretty(self):
596         return self.value.pretty()
598     def field(self, size = 30):
599         if self.value is None:
600             value = ''
601         else:
602             value = cgi.escape(str(self.value))
603             value = '&quot;'.join(value.split('"'))
604         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
606 class LinkHTMLProperty(HTMLProperty):
607     ''' Link HTMLProperty
608         Include the above as well as being able to access the class
609         information. Stringifying the object itself results in the value
610         from the item being displayed. Accessing attributes of this object
611         result in the appropriate entry from the class being queried for the
612         property accessed (so item/assignedto/name would look up the user
613         entry identified by the assignedto property on item, and then the
614         name property of that user)
615     '''
616     def __getattr__(self, attr):
617         ''' return a new HTMLItem '''
618         #print 'getattr', (self, attr, self.value)
619         if not self.value:
620             raise AttributeError, "Can't access missing value"
621         i = HTMLItem(self.db, self.prop.classname, self.value)
622         return getattr(i, attr)
624     def plain(self, escape=0):
625         if self.value is None:
626             return _('[unselected]')
627         linkcl = self.db.classes[self.prop.classname]
628         k = linkcl.labelprop(1)
629         value = str(linkcl.get(self.value, k))
630         if escape:
631             value = cgi.escape(value)
632         return value
634     # XXX most of the stuff from here down is of dubious utility - it's easy
635     # enough to do in the template by hand (and in some cases, it's shorter
636     # and clearer...
638     def field(self):
639         linkcl = self.db.getclass(self.prop.classname)
640         if linkcl.getprops().has_key('order'):  
641             sort_on = 'order'  
642         else:  
643             sort_on = linkcl.labelprop()  
644         options = linkcl.filter(None, {}, [sort_on], []) 
645         # TODO: make this a field display, not a menu one!
646         l = ['<select name="%s">'%property]
647         k = linkcl.labelprop(1)
648         if value is None:
649             s = 'selected '
650         else:
651             s = ''
652         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
653         for optionid in options:
654             option = linkcl.get(optionid, k)
655             s = ''
656             if optionid == value:
657                 s = 'selected '
658             if showid:
659                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
660             else:
661                 lab = option
662             if size is not None and len(lab) > size:
663                 lab = lab[:size-3] + '...'
664             lab = cgi.escape(lab)
665             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
666         l.append('</select>')
667         return '\n'.join(l)
669     def download(self, showid=0):
670         linkname = self.prop.classname
671         linkcl = self.db.getclass(linkname)
672         k = linkcl.labelprop(1)
673         linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
674         if showid:
675             label = value
676             title = ' title="%s"'%linkvalue
677             # note ... this should be urllib.quote(linkcl.get(value, k))
678         else:
679             label = linkvalue
680             title = ''
681         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
682             linkvalue, title, label)
684     def menu(self, size=None, height=None, showid=0, additional=[],
685             **conditions):
686         value = self.value
688         # sort function
689         sortfunc = make_sort_function(self.db, self.prop.classname)
691         # force the value to be a single choice
692         if isinstance(value, type('')):
693             value = value[0]
694         linkcl = self.db.getclass(self.prop.classname)
695         l = ['<select name="%s">'%self.name]
696         k = linkcl.labelprop(1)
697         s = ''
698         if value is None:
699             s = 'selected '
700         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
701         if linkcl.getprops().has_key('order'):  
702             sort_on = ('+', 'order')
703         else:  
704             sort_on = ('+', linkcl.labelprop())
705         options = linkcl.filter(None, conditions, sort_on, (None, None))
706         for optionid in options:
707             option = linkcl.get(optionid, k)
708             s = ''
709             if value in [optionid, option]:
710                 s = 'selected '
711             if showid:
712                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
713             else:
714                 lab = option
715             if size is not None and len(lab) > size:
716                 lab = lab[:size-3] + '...'
717             if additional:
718                 m = []
719                 for propname in additional:
720                     m.append(linkcl.get(optionid, propname))
721                 lab = lab + ' (%s)'%', '.join(map(str, m))
722             lab = cgi.escape(lab)
723             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
724         l.append('</select>')
725         return '\n'.join(l)
727 #    def checklist(self, ...)
729 class MultilinkHTMLProperty(HTMLProperty):
730     ''' Multilink HTMLProperty
732         Also be iterable, returning a wrapper object like the Link case for
733         each entry in the multilink.
734     '''
735     def __len__(self):
736         ''' length of the multilink '''
737         return len(self.value)
739     def __getattr__(self, attr):
740         ''' no extended attribute accesses make sense here '''
741         raise AttributeError, attr
743     def __getitem__(self, num):
744         ''' iterate and return a new HTMLItem '''
745         #print 'getitem', (self, num)
746         value = self.value[num]
747         return HTMLItem(self.db, self.prop.classname, value)
749     def reverse(self):
750         ''' return the list in reverse order '''
751         l = self.value[:]
752         l.reverse()
753         return [HTMLItem(self.db, self.prop.classname, value) for value in l]
755     def plain(self, escape=0):
756         linkcl = self.db.classes[self.prop.classname]
757         k = linkcl.labelprop(1)
758         labels = []
759         for v in self.value:
760             labels.append(linkcl.get(v, k))
761         value = ', '.join(labels)
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, size=30, showid=0):
771         sortfunc = make_sort_function(self.db, self.prop.classname)
772         linkcl = self.db.getclass(self.prop.classname)
773         value = self.value[:]
774         if value:
775             value.sort(sortfunc)
776         # map the id to the label property
777         if not showid:
778             k = linkcl.labelprop(1)
779             value = [linkcl.get(v, k) for v in value]
780         value = cgi.escape(','.join(value))
781         return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
783     def menu(self, size=None, height=None, showid=0, additional=[],
784             **conditions):
785         value = self.value
787         # sort function
788         sortfunc = make_sort_function(self.db, self.prop.classname)
790         linkcl = self.db.getclass(self.prop.classname)
791         if linkcl.getprops().has_key('order'):  
792             sort_on = ('+', 'order')
793         else:  
794             sort_on = ('+', linkcl.labelprop())
795         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
796         height = height or min(len(options), 7)
797         l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
798         k = linkcl.labelprop(1)
799         for optionid in options:
800             option = linkcl.get(optionid, k)
801             s = ''
802             if optionid in value or option in value:
803                 s = 'selected '
804             if showid:
805                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
806             else:
807                 lab = option
808             if size is not None and len(lab) > size:
809                 lab = lab[:size-3] + '...'
810             if additional:
811                 m = []
812                 for propname in additional:
813                     m.append(linkcl.get(optionid, propname))
814                 lab = lab + ' (%s)'%', '.join(m)
815             lab = cgi.escape(lab)
816             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
817                 lab))
818         l.append('</select>')
819         return '\n'.join(l)
821 # set the propclasses for HTMLItem
822 propclasses = (
823     (hyperdb.String, StringHTMLProperty),
824     (hyperdb.Number, NumberHTMLProperty),
825     (hyperdb.Boolean, BooleanHTMLProperty),
826     (hyperdb.Date, DateHTMLProperty),
827     (hyperdb.Interval, IntervalHTMLProperty),
828     (hyperdb.Password, PasswordHTMLProperty),
829     (hyperdb.Link, LinkHTMLProperty),
830     (hyperdb.Multilink, MultilinkHTMLProperty),
833 def make_sort_function(db, classname):
834     '''Make a sort function for a given class
835     '''
836     linkcl = db.getclass(classname)
837     if linkcl.getprops().has_key('order'):
838         sort_on = 'order'
839     else:
840         sort_on = linkcl.labelprop()
841     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
842         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
843     return sortfunc
845 def handleListCGIValue(value):
846     ''' Value is either a single item or a list of items. Each item has a
847         .value that we're actually interested in.
848     '''
849     if isinstance(value, type([])):
850         return [value.value for value in value]
851     else:
852         return value.value.split(',')
854 class HTMLRequest:
855     ''' The *request*, holding the CGI form and environment.
857         "form" the CGI form as a cgi.FieldStorage
858         "env" the CGI environment variables
859         "url" the current URL path for this request
860         "base" the base URL for this instance
861         "user" a HTMLUser instance for this user
862         "classname" the current classname (possibly None)
863         "template_type" the current template type (suffix, also possibly None)
865         Index args:
866         "columns" dictionary of the columns to display in an index page
867         "sort" index sort column (direction, column name)
868         "group" index grouping property (direction, column name)
869         "filter" properties to filter the index on
870         "filterspec" values to filter the index on
871         "search_text" text to perform a full-text search on for an index
873     '''
874     def __init__(self, client):
875         self.client = client
877         # easier access vars
878         self.form = client.form
879         self.env = client.env
880         self.base = client.base
881         self.url = client.url
882         self.user = HTMLUser(client)
884         # store the current class name and action
885         self.classname = client.classname
886         self.template_type = client.template_type
888         # extract the index display information from the form
889         self.columns = {}
890         if self.form.has_key(':columns'):
891             for entry in handleListCGIValue(self.form[':columns']):
892                 self.columns[entry] = 1
894         # sorting
895         self.sort = (None, None)
896         if self.form.has_key(':sort'):
897             sort = self.form[':sort'].value
898             if sort.startswith('-'):
899                 self.sort = ('-', sort[1:])
900             else:
901                 self.sort = ('+', sort)
902         if self.form.has_key(':sortdir'):
903             self.sort = ('-', self.sort[1])
905         # grouping
906         self.group = (None, None)
907         if self.form.has_key(':group'):
908             group = self.form[':group'].value
909             if group.startswith('-'):
910                 self.group = ('-', group[1:])
911             else:
912                 self.group = ('+', group)
913         if self.form.has_key(':groupdir'):
914             self.group = ('-', self.group[1])
916         # filtering
917         self.filter = []
918         if self.form.has_key(':filter'):
919             self.filter = handleListCGIValue(self.form[':filter'])
920         self.filterspec = {}
921         if self.classname is not None:
922             props = self.client.db.getclass(self.classname).getprops()
923             for name in self.filter:
924                 if self.form.has_key(name):
925                     prop = props[name]
926                     fv = self.form[name]
927                     if (isinstance(prop, hyperdb.Link) or
928                             isinstance(prop, hyperdb.Multilink)):
929                         self.filterspec[name] = handleListCGIValue(fv)
930                     else:
931                         self.filterspec[name] = fv.value
933         # full-text search argument
934         self.search_text = None
935         if self.form.has_key(':search_text'):
936             self.search_text = self.form[':search_text'].value
938     def __str__(self):
939         d = {}
940         d.update(self.__dict__)
941         f = ''
942         for k in self.form.keys():
943             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
944         d['form'] = f
945         e = ''
946         for k,v in self.env.items():
947             e += '\n     %r=%r'%(k, v)
948         d['env'] = e
949         return '''
950 form: %(form)s
951 url: %(url)r
952 base: %(base)r
953 classname: %(classname)r
954 template_type: %(template_type)r
955 columns: %(columns)r
956 sort: %(sort)r
957 group: %(group)r
958 filter: %(filter)r
959 filterspec: %(filterspec)r
960 env: %(env)s
961 '''%d
963     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
964             filterspec=1):
965         ''' return the current index args as form elements '''
966         l = []
967         s = '<input type="hidden" name="%s" value="%s">'
968         if columns and self.columns:
969             l.append(s%(':columns', ','.join(self.columns.keys())))
970         if sort and self.sort is not None:
971             if self.sort[0] == '-':
972                 val = '-'+self.sort[1]
973             else:
974                 val = self.sort[1]
975             l.append(s%(':sort', val))
976         if group and self.group is not None:
977             if self.group[0] == '-':
978                 val = '-'+self.group[1]
979             else:
980                 val = self.group[1]
981             l.append(s%(':group', val))
982         if filter and self.filter:
983             l.append(s%(':filter', ','.join(self.filter)))
984         if filterspec:
985             for k,v in self.filterspec.items():
986                 l.append(s%(k, ','.join(v)))
987         return '\n'.join(l)
989     def indexargs_href(self, url, args):
990         ''' embed the current index args in a URL '''
991         l = ['%s=%s'%(k,v) for k,v in args.items()]
992         if self.columns:
993             l.append(':columns=%s'%(','.join(self.columns.keys())))
994         if self.sort is not None:
995             if self.sort[0] == '-':
996                 val = '-'+self.sort[1]
997             else:
998                 val = self.sort[1]
999             l.append(':sort=%s'%val)
1000         if self.group is not None:
1001             if self.group[0] == '-':
1002                 val = '-'+self.group[1]
1003             else:
1004                 val = self.group[1]
1005             l.append(':group=%s'%val)
1006         if self.filter:
1007             l.append(':filter=%s'%(','.join(self.filter)))
1008         for k,v in self.filterspec.items():
1009             l.append('%s=%s'%(k, ','.join(v)))
1010         return '%s?%s'%(url, '&'.join(l))
1012     def base_javascript(self):
1013         return '''
1014 <script language="javascript">
1015 submitted = false;
1016 function submit_once() {
1017     if (submitted) {
1018         alert("Your request is being processed.\\nPlease be patient.");
1019         return 0;
1020     }
1021     submitted = true;
1022     return 1;
1025 function help_window(helpurl, width, height) {
1026     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1028 </script>
1029 '''%self.base
1031     def batch(self):
1032         ''' Return a batch object for results from the "current search"
1033         '''
1034         filterspec = self.filterspec
1035         sort = self.sort
1036         group = self.group
1038         # get the list of ids we're batching over
1039         klass = self.client.db.getclass(self.classname)
1040         if self.search_text:
1041             matches = self.client.db.indexer.search(
1042                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1043         else:
1044             matches = None
1045         l = klass.filter(matches, filterspec, sort, group)
1047         # figure batch args
1048         if self.form.has_key(':pagesize'):
1049             size = int(self.form[':pagesize'].value)
1050         else:
1051             size = 50
1052         if self.form.has_key(':startwith'):
1053             start = int(self.form[':startwith'].value)
1054         else:
1055             start = 0
1057         # return the batch object
1058         return Batch(self.client, self.classname, l, size, start)
1061 # extend the standard ZTUtils Batch object to remove dependency on
1062 # Acquisition and add a couple of useful methods
1063 class Batch(ZTUtils.Batch):
1064     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1065         self.client = client
1066         self.classname = classname
1067         self.last_index = self.last_item = None
1068         self.current_item = None
1069         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1071     # overwrite so we can late-instantiate the HTMLItem instance
1072     def __getitem__(self, index):
1073         if index < 0:
1074             if index + self.end < self.first: raise IndexError, index
1075             return self._sequence[index + self.end]
1076         
1077         if index >= self.length: raise IndexError, index
1079         # move the last_item along - but only if the fetched index changes
1080         # (for some reason, index 0 is fetched twice)
1081         if index != self.last_index:
1082             self.last_item = self.current_item
1083             self.last_index = index
1085         # wrap the return in an HTMLItem
1086         self.current_item = HTMLItem(self.client.db, self.classname,
1087             self._sequence[index+self.first])
1088         return self.current_item
1090     def propchanged(self, property):
1091         ''' Detect if the property marked as being the group property
1092             changed in the last iteration fetch
1093         '''
1094         if (self.last_item is None or
1095                 self.last_item[property] != self.current_item[property]):
1096             return 1
1097         return 0
1099     # override these 'cos we don't have access to acquisition
1100     def previous(self):
1101         if self.start == 1:
1102             return None
1103         return Batch(self.client, self.classname, self._sequence, self._size,
1104             self.first - self._size + self.overlap, 0, self.orphan,
1105             self.overlap)
1107     def next(self):
1108         try:
1109             self._sequence[self.end]
1110         except IndexError:
1111             return None
1112         return Batch(self.client, self.classname, self._sequence, self._size,
1113             self.end - self.overlap, 0, self.orphan, self.overlap)
1115     def length(self):
1116         self.sequence_length = l = len(self._sequence)
1117         return l