Code

Fixed history and its nihilism
[roundup.git] / roundup / cgi / templating.py
1 import sys, cgi, urllib, os
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 __getattr__(self, attr):
140         ''' return an HTMLItem instance'''
141         #print 'getattr', (self, attr)
142         if attr == 'creator':
143             return HTMLUser(self.client)
145         if not self.props.has_key(attr):
146             raise AttributeError, attr
147         prop = self.props[attr]
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, attr, value)
158         # no good
159         raise AttributeError, attr
161     def properties(self):
162         ''' Return HTMLProperty for all props
163         '''
164         l = []
165         for name, prop in self.props.items():
166             for klass, htmlklass in propclasses:
167                 if isinstance(prop, hyperdb.Multilink):
168                     value = []
169                 else:
170                     value = None
171                 if isinstance(prop, klass):
172                     l.append(htmlklass(self.db, '', prop, name, value))
173         return l
175     def list(self):
176         l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
177         return l
179     def filter(self, request=None):
180         ''' Return a list of items from this class, filtered and sorted
181             by the current requested filterspec/filter/sort/group args
182         '''
183         if request is not None:
184             filterspec = request.filterspec
185             sort = request.sort
186             group = request.group
187         l = [HTMLItem(self.db, self.classname, x)
188              for x in self.klass.filter(None, filterspec, sort, group)]
189         return l
191     def classhelp(self, properties, label='?', width='400', height='400'):
192         '''pop up a javascript window with class help
194            This generates a link to a popup window which displays the 
195            properties indicated by "properties" of the class named by
196            "classname". The "properties" should be a comma-separated list
197            (eg. 'id,name,description').
199            You may optionally override the label displayed, the width and
200            height. The popup window will be resizable and scrollable.
201         '''
202         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
203             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
204             properties, width, height, label)
206     def submit(self, label="Submit New Entry"):
207         ''' Generate a submit button (and action hidden element)
208         '''
209         return '  <input type="hidden" name=":action" value="new">\n'\
210         '  <input type="submit" name="submit" value="%s">'%label
212     def history(self):
213         return 'New node - no history'
215     def renderWith(self, name, **kwargs):
216         ''' Render this class with the given template.
217         '''
218         # create a new request and override the specified args
219         req = HTMLRequest(self.client)
220         req.classname = self.classname
221         req.__dict__.update(kwargs)
223         # new template, using the specified classname and request
224         pt = RoundupPageTemplate(self.client, self.classname, req)
226         # use the specified template
227         name = self.classname + '.' + name
228         pt.write(open('/tmp/test/html/%s'%name).read())
229         pt.id = name
231         # XXX handle PT rendering errors here nicely
232         try:
233             return pt.render()
234         except PageTemplate.PTRuntimeError, message:
235             return '<strong>%s</strong><ol>%s</ol>'%(message,
236                 cgi.escape('<li>'.join(pt._v_errors)))
238 class HTMLItem:
239     ''' Accesses through an *item*
240     '''
241     def __init__(self, db, classname, nodeid):
242         self.db = db
243         self.classname = classname
244         self.nodeid = nodeid
245         self.klass = self.db.getclass(classname)
246         self.props = self.klass.getprops()
248     def __repr__(self):
249         return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
251     def __getattr__(self, attr):
252         ''' return an HTMLItem instance'''
253         #print 'getattr', (self, attr)
254         if attr == 'id':
255             return self.nodeid
257         if not self.props.has_key(attr):
258             raise AttributeError, attr
259         prop = self.props[attr]
261         # get the value, handling missing values
262         value = self.klass.get(self.nodeid, attr, None)
263         if value is None:
264             if isinstance(self.props[attr], hyperdb.Multilink):
265                 value = []
267         # look up the correct HTMLProperty class
268         for klass, htmlklass in propclasses:
269             if isinstance(prop, klass):
270                 return htmlklass(self.db, self.nodeid, prop, attr, value)
272         # no good
273         raise AttributeError, attr
274     
275     def submit(self, label="Submit Changes"):
276         ''' Generate a submit button (and action hidden element)
277         '''
278         return '  <input type="hidden" name=":action" value="edit">\n'\
279         '  <input type="submit" name="submit" value="%s">'%label
281     # XXX this probably should just return the history items, not the HTML
282     def history(self, direction='descending'):
283         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
284             '<tr class="list-header">',
285             _('<th align=left><span class="list-item">Date</span></th>'),
286             _('<th align=left><span class="list-item">User</span></th>'),
287             _('<th align=left><span class="list-item">Action</span></th>'),
288             _('<th align=left><span class="list-item">Args</span></th>'),
289             '</tr>']
290         comments = {}
291         history = self.klass.history(self.nodeid)
292         history.sort()
293         if direction == 'descending':
294             history.reverse()
295         for id, evt_date, user, action, args in history:
296             date_s = str(evt_date).replace("."," ")
297             arg_s = ''
298             if action == 'link' and type(args) == type(()):
299                 if len(args) == 3:
300                     linkcl, linkid, key = args
301                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
302                         linkcl, linkid, key)
303                 else:
304                     arg_s = str(args)
306             elif action == 'unlink' and type(args) == type(()):
307                 if len(args) == 3:
308                     linkcl, linkid, key = args
309                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
310                         linkcl, linkid, key)
311                 else:
312                     arg_s = str(args)
314             elif type(args) == type({}):
315                 cell = []
316                 for k in args.keys():
317                     # try to get the relevant property and treat it
318                     # specially
319                     try:
320                         prop = self.props[k]
321                     except KeyError:
322                         prop = None
323                     if prop is not None:
324                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
325                                 isinstance(prop, hyperdb.Link)):
326                             # figure what the link class is
327                             classname = prop.classname
328                             try:
329                                 linkcl = self.db.getclass(classname)
330                             except KeyError:
331                                 labelprop = None
332                                 comments[classname] = _('''The linked class
333                                     %(classname)s no longer exists''')%locals()
334                             labelprop = linkcl.labelprop(1)
335                             hrefable = os.path.exists(
336                                 os.path.join(self.db.config.TEMPLATES,
337                                 classname+'.item'))
339                         if isinstance(prop, hyperdb.Multilink) and \
340                                 len(args[k]) > 0:
341                             ml = []
342                             for linkid in args[k]:
343                                 if isinstance(linkid, type(())):
344                                     sublabel = linkid[0] + ' '
345                                     linkids = linkid[1]
346                                 else:
347                                     sublabel = ''
348                                     linkids = [linkid]
349                                 subml = []
350                                 for linkid in linkids:
351                                     label = classname + linkid
352                                     # if we have a label property, try to use it
353                                     # TODO: test for node existence even when
354                                     # there's no labelprop!
355                                     try:
356                                         if labelprop is not None:
357                                             label = linkcl.get(linkid, labelprop)
358                                     except IndexError:
359                                         comments['no_link'] = _('''<strike>The
360                                             linked node no longer
361                                             exists</strike>''')
362                                         subml.append('<strike>%s</strike>'%label)
363                                     else:
364                                         if hrefable:
365                                             subml.append('<a href="%s%s">%s</a>'%(
366                                                 classname, linkid, label))
367                                 ml.append(sublabel + ', '.join(subml))
368                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
369                         elif isinstance(prop, hyperdb.Link) and args[k]:
370                             label = classname + args[k]
371                             # if we have a label property, try to use it
372                             # TODO: test for node existence even when
373                             # there's no labelprop!
374                             if labelprop is not None:
375                                 try:
376                                     label = linkcl.get(args[k], labelprop)
377                                 except IndexError:
378                                     comments['no_link'] = _('''<strike>The
379                                         linked node no longer
380                                         exists</strike>''')
381                                     cell.append(' <strike>%s</strike>,\n'%label)
382                                     # "flag" this is done .... euwww
383                                     label = None
384                             if label is not None:
385                                 if hrefable:
386                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
387                                         classname, args[k], label))
388                                 else:
389                                     cell.append('%s: %s' % (k,label))
391                         elif isinstance(prop, hyperdb.Date) and args[k]:
392                             d = date.Date(args[k])
393                             cell.append('%s: %s'%(k, str(d)))
395                         elif isinstance(prop, hyperdb.Interval) and args[k]:
396                             d = date.Interval(args[k])
397                             cell.append('%s: %s'%(k, str(d)))
399                         elif isinstance(prop, hyperdb.String) and args[k]:
400                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
402                         elif not args[k]:
403                             cell.append('%s: (no value)\n'%k)
405                         else:
406                             cell.append('%s: %s\n'%(k, str(args[k])))
407                     else:
408                         # property no longer exists
409                         comments['no_exist'] = _('''<em>The indicated property
410                             no longer exists</em>''')
411                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
412                 arg_s = '<br />'.join(cell)
413             else:
414                 # unkown event!!
415                 comments['unknown'] = _('''<strong><em>This event is not
416                     handled by the history display!</em></strong>''')
417                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
418             date_s = date_s.replace(' ', '&nbsp;')
419             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
420                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
421                 user, action, arg_s))
422         if comments:
423             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
424         for entry in comments.values():
425             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
426         l.append('</table>')
427         return '\n'.join(l)
429     def remove(self):
430         # XXX do what?
431         return ''
433 class HTMLUser(HTMLItem):
434     ''' Accesses through the *user* (a special case of item)
435     '''
436     def __init__(self, client):
437         HTMLItem.__init__(self, client.db, 'user', client.userid)
438         self.default_classname = client.classname
439         self.userid = client.userid
441         # used for security checks
442         self.security = client.db.security
443     _marker = []
444     def hasPermission(self, role, classname=_marker):
445         ''' Determine if the user has the Role.
447             The class being tested defaults to the template's class, but may
448             be overidden for this test by suppling an alternate classname.
449         '''
450         if classname is self._marker:
451             classname = self.default_classname
452         return self.security.hasPermission(role, self.userid, classname)
454 class HTMLProperty:
455     ''' String, Number, Date, Interval HTMLProperty
457         A wrapper object which may be stringified for the plain() behaviour.
458     '''
459     def __init__(self, db, nodeid, prop, name, value):
460         self.db = db
461         self.nodeid = nodeid
462         self.prop = prop
463         self.name = name
464         self.value = value
465     def __repr__(self):
466         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
467     def __str__(self):
468         return self.plain()
469     def __cmp__(self, other):
470         if isinstance(other, HTMLProperty):
471             return cmp(self.value, other.value)
472         return cmp(self.value, other)
474 class StringHTMLProperty(HTMLProperty):
475     def plain(self, escape=0):
476         if self.value is None:
477             return ''
478         if escape:
479             return cgi.escape(str(self.value))
480         return str(self.value)
482     def stext(self, escape=0):
483         s = self.plain(escape=escape)
484         if not StructuredText:
485             return s
486         return StructuredText(s,level=1,header=0)
488     def field(self, size = 30):
489         if self.value is None:
490             value = ''
491         else:
492             value = cgi.escape(str(self.value))
493             value = '&quot;'.join(value.split('"'))
494         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
496     def multiline(self, escape=0, rows=5, cols=40):
497         if self.value is None:
498             value = ''
499         else:
500             value = cgi.escape(str(self.value))
501             value = '&quot;'.join(value.split('"'))
502         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
503             self.name, rows, cols, value)
505     def email(self, escape=1):
506         ''' fudge email '''
507         if self.value is None: value = ''
508         else: value = str(self.value)
509         value = value.replace('@', ' at ')
510         value = value.replace('.', ' ')
511         if escape:
512             value = cgi.escape(value)
513         return value
515 class PasswordHTMLProperty(HTMLProperty):
516     def plain(self):
517         if self.value is None:
518             return ''
519         return _('*encrypted*')
521     def field(self, size = 30):
522         return '<input type="password" name="%s" size="%s">'%(self.name, size)
524 class NumberHTMLProperty(HTMLProperty):
525     def plain(self):
526         return str(self.value)
528     def field(self, size = 30):
529         if self.value is None:
530             value = ''
531         else:
532             value = cgi.escape(str(self.value))
533             value = '&quot;'.join(value.split('"'))
534         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
536 class BooleanHTMLProperty(HTMLProperty):
537     def plain(self):
538         if self.value is None:
539             return ''
540         return self.value and "Yes" or "No"
542     def field(self):
543         checked = self.value and "checked" or ""
544         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
545             checked)
546         if checked:
547             checked = ""
548         else:
549             checked = "checked"
550         s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
551             checked)
552         return s
554 class DateHTMLProperty(HTMLProperty):
555     def plain(self):
556         if self.value is None:
557             return ''
558         return str(self.value)
560     def field(self, size = 30):
561         if self.value is None:
562             value = ''
563         else:
564             value = cgi.escape(str(self.value))
565             value = '&quot;'.join(value.split('"'))
566         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
568     def reldate(self, pretty=1):
569         if not self.value:
570             return ''
572         # figure the interval
573         interval = date.Date('.') - self.value
574         if pretty:
575             return interval.pretty()
576         return str(interval)
578 class IntervalHTMLProperty(HTMLProperty):
579     def plain(self):
580         if self.value is None:
581             return ''
582         return str(self.value)
584     def pretty(self):
585         return self.value.pretty()
587     def field(self, size = 30):
588         if self.value is None:
589             value = ''
590         else:
591             value = cgi.escape(str(self.value))
592             value = '&quot;'.join(value.split('"'))
593         return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
595 class LinkHTMLProperty(HTMLProperty):
596     ''' Link HTMLProperty
597         Include the above as well as being able to access the class
598         information. Stringifying the object itself results in the value
599         from the item being displayed. Accessing attributes of this object
600         result in the appropriate entry from the class being queried for the
601         property accessed (so item/assignedto/name would look up the user
602         entry identified by the assignedto property on item, and then the
603         name property of that user)
604     '''
605     def __getattr__(self, attr):
606         ''' return a new HTMLItem '''
607         #print 'getattr', (self, attr, self.value)
608         if not self.value:
609             raise AttributeError, "Can't access missing value"
610         i = HTMLItem(self.db, self.prop.classname, self.value)
611         return getattr(i, attr)
613     def plain(self, escape=0):
614         if self.value is None:
615             return _('[unselected]')
616         linkcl = self.db.classes[self.klass.classname]
617         k = linkcl.labelprop(1)
618         value = str(linkcl.get(self.value, k))
619         if escape:
620             value = cgi.escape(value)
621         return value
623     # XXX most of the stuff from here down is of dubious utility - it's easy
624     # enough to do in the template by hand (and in some cases, it's shorter
625     # and clearer...
627     def field(self):
628         linkcl = self.db.getclass(self.prop.classname)
629         if linkcl.getprops().has_key('order'):  
630             sort_on = 'order'  
631         else:  
632             sort_on = linkcl.labelprop()  
633         options = linkcl.filter(None, {}, [sort_on], []) 
634         # TODO: make this a field display, not a menu one!
635         l = ['<select name="%s">'%property]
636         k = linkcl.labelprop(1)
637         if value is None:
638             s = 'selected '
639         else:
640             s = ''
641         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
642         for optionid in options:
643             option = linkcl.get(optionid, k)
644             s = ''
645             if optionid == value:
646                 s = 'selected '
647             if showid:
648                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
649             else:
650                 lab = option
651             if size is not None and len(lab) > size:
652                 lab = lab[:size-3] + '...'
653             lab = cgi.escape(lab)
654             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
655         l.append('</select>')
656         return '\n'.join(l)
658     def download(self, showid=0):
659         linkname = self.prop.classname
660         linkcl = self.db.getclass(linkname)
661         k = linkcl.labelprop(1)
662         linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
663         if showid:
664             label = value
665             title = ' title="%s"'%linkvalue
666             # note ... this should be urllib.quote(linkcl.get(value, k))
667         else:
668             label = linkvalue
669             title = ''
670         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
671             linkvalue, title, label)
673     def menu(self, size=None, height=None, showid=0, additional=[],
674             **conditions):
675         value = self.value
677         # sort function
678         sortfunc = make_sort_function(self.db, self.prop.classname)
680         # force the value to be a single choice
681         if isinstance(value, type('')):
682             value = value[0]
683         linkcl = self.db.getclass(self.prop.classname)
684         l = ['<select name="%s">'%self.name]
685         k = linkcl.labelprop(1)
686         s = ''
687         if value is None:
688             s = 'selected '
689         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
690         if linkcl.getprops().has_key('order'):  
691             sort_on = 'order'  
692         else:  
693             sort_on = linkcl.labelprop() 
694         options = linkcl.filter(None, conditions, [sort_on], []) 
695         for optionid in options:
696             option = linkcl.get(optionid, k)
697             s = ''
698             if value in [optionid, option]:
699                 s = 'selected '
700             if showid:
701                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
702             else:
703                 lab = option
704             if size is not None and len(lab) > size:
705                 lab = lab[:size-3] + '...'
706             if additional:
707                 m = []
708                 for propname in additional:
709                     m.append(linkcl.get(optionid, propname))
710                 lab = lab + ' (%s)'%', '.join(map(str, m))
711             lab = cgi.escape(lab)
712             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
713         l.append('</select>')
714         return '\n'.join(l)
716 #    def checklist(self, ...)
718 class MultilinkHTMLProperty(HTMLProperty):
719     ''' Multilink HTMLProperty
721         Also be iterable, returning a wrapper object like the Link case for
722         each entry in the multilink.
723     '''
724     def __len__(self):
725         ''' length of the multilink '''
726         return len(self.value)
728     def __getattr__(self, attr):
729         ''' no extended attribute accesses make sense here '''
730         raise AttributeError, attr
732     def __getitem__(self, num):
733         ''' iterate and return a new HTMLItem '''
734         #print 'getitem', (self, num)
735         value = self.value[num]
736         return HTMLItem(self.db, self.prop.classname, value)
738     def plain(self, escape=0):
739         linkcl = self.db.classes[self.prop.classname]
740         k = linkcl.labelprop(1)
741         labels = []
742         for v in self.value:
743             labels.append(linkcl.get(v, k))
744         value = ', '.join(labels)
745         if escape:
746             value = cgi.escape(value)
747         return value
749     # XXX most of the stuff from here down is of dubious utility - it's easy
750     # enough to do in the template by hand (and in some cases, it's shorter
751     # and clearer...
753     def field(self, size=30, showid=0):
754         sortfunc = make_sort_function(self.db, self.prop.classname)
755         linkcl = self.db.getclass(self.prop.classname)
756         value = self.value[:]
757         if value:
758             value.sort(sortfunc)
759         # map the id to the label property
760         if not showid:
761             k = linkcl.labelprop(1)
762             value = [linkcl.get(v, k) for v in value]
763         value = cgi.escape(','.join(value))
764         return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
766     def menu(self, size=None, height=None, showid=0, additional=[],
767             **conditions):
768         value = self.value
770         # sort function
771         sortfunc = make_sort_function(self.db, self.prop.classname)
773         linkcl = self.db.getclass(self.prop.classname)
774         if linkcl.getprops().has_key('order'):  
775             sort_on = 'order'  
776         else:  
777             sort_on = linkcl.labelprop()
778         options = linkcl.filter(None, conditions, [sort_on], []) 
779         height = height or min(len(options), 7)
780         l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
781         k = linkcl.labelprop(1)
782         for optionid in options:
783             option = linkcl.get(optionid, k)
784             s = ''
785             if optionid in value or option in value:
786                 s = 'selected '
787             if showid:
788                 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
789             else:
790                 lab = option
791             if size is not None and len(lab) > size:
792                 lab = lab[:size-3] + '...'
793             if additional:
794                 m = []
795                 for propname in additional:
796                     m.append(linkcl.get(optionid, propname))
797                 lab = lab + ' (%s)'%', '.join(m)
798             lab = cgi.escape(lab)
799             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
800                 lab))
801         l.append('</select>')
802         return '\n'.join(l)
804 # set the propclasses for HTMLItem
805 propclasses = (
806     (hyperdb.String, StringHTMLProperty),
807     (hyperdb.Number, NumberHTMLProperty),
808     (hyperdb.Boolean, BooleanHTMLProperty),
809     (hyperdb.Date, DateHTMLProperty),
810     (hyperdb.Interval, IntervalHTMLProperty),
811     (hyperdb.Password, PasswordHTMLProperty),
812     (hyperdb.Link, LinkHTMLProperty),
813     (hyperdb.Multilink, MultilinkHTMLProperty),
816 def make_sort_function(db, classname):
817     '''Make a sort function for a given class
818     '''
819     linkcl = db.getclass(classname)
820     if linkcl.getprops().has_key('order'):
821         sort_on = 'order'
822     else:
823         sort_on = linkcl.labelprop()
824     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
825         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
826     return sortfunc
828 def handleListCGIValue(value):
829     ''' Value is either a single item or a list of items. Each item has a
830         .value that we're actually interested in.
831     '''
832     if isinstance(value, type([])):
833         return [value.value for value in value]
834     else:
835         return value.value.split(',')
837 # XXX This is starting to look a lot (in data terms) like the client object
838 # itself!
839 class HTMLRequest:
840     ''' The *request*, holding the CGI form and environment.
842     '''
843     def __init__(self, client):
844         self.client = client
846         # easier access vars
847         self.form = client.form
848         self.env = client.env
849         self.base = client.base
850         self.user = HTMLUser(client)
852         # store the current class name and action
853         self.classname = client.classname
854         self.template_type = client.template_type
856         # extract the index display information from the form
857         self.columns = {}
858         if self.form.has_key(':columns'):
859             for entry in handleListCGIValue(self.form[':columns']):
860                 self.columns[entry] = 1
861         self.sort = []
862         if self.form.has_key(':sort'):
863             self.sort = handleListCGIValue(self.form[':sort'])
864         self.group = []
865         if self.form.has_key(':group'):
866             self.group = handleListCGIValue(self.form[':group'])
867         self.filter = []
868         if self.form.has_key(':filter'):
869             self.filter = handleListCGIValue(self.form[':filter'])
870         self.filterspec = {}
871         for name in self.filter:
872             if self.form.has_key(name):
873                 self.filterspec[name]=handleListCGIValue(self.form[name])
875     def __str__(self):
876         d = {}
877         d.update(self.__dict__)
878         f = ''
879         for k in self.form.keys():
880             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
881         d['form'] = f
882         e = ''
883         for k,v in self.env.items():
884             e += '\n     %r=%r'%(k, v)
885         d['env'] = e
886         return '''
887 form: %(form)s
888 base: %(base)r
889 classname: %(classname)r
890 template_type: %(template_type)r
891 columns: %(columns)r
892 sort: %(sort)r
893 group: %(group)r
894 filter: %(filter)r
895 filterspec: %(filterspec)r
896 env: %(env)s
897 '''%d
899     def indexargs_form(self):
900         ''' return the current index args as form elements '''
901         l = []
902         s = '<input type="hidden" name="%s" value="%s">'
903         if self.columns:
904             l.append(s%(':columns', ','.join(self.columns.keys())))
905         if self.sort:
906             l.append(s%(':sort', ','.join(self.sort)))
907         if self.group:
908             l.append(s%(':group', ','.join(self.group)))
909         if self.filter:
910             l.append(s%(':filter', ','.join(self.filter)))
911         for k,v in self.filterspec.items():
912             l.append(s%(k, ','.join(v)))
913         return '\n'.join(l)
915     def indexargs_href(self, url, args):
916         l = ['%s=%s'%(k,v) for k,v in args.items()]
917         if self.columns:
918             l.append(':columns=%s'%(','.join(self.columns.keys())))
919         if self.sort:
920             l.append(':sort=%s'%(','.join(self.sort)))
921         if self.group:
922             l.append(':group=%s'%(','.join(self.group)))
923         if self.filter:
924             l.append(':filter=%s'%(','.join(self.filter)))
925         for k,v in self.filterspec.items():
926             l.append('%s=%s'%(k, ','.join(v)))
927         return '%s?%s'%(url, '&'.join(l))
929     def base_javascript(self):
930         return '''
931 <script language="javascript">
932 submitted = false;
933 function submit_once() {
934     if (submitted) {
935         alert("Your request is being processed.\\nPlease be patient.");
936         return 0;
937     }
938     submitted = true;
939     return 1;
942 function help_window(helpurl, width, height) {
943     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
945 </script>
946 '''%self.base
948     def batch(self):
949         ''' Return a batch object for results from the "current search"
950         '''
951         filterspec = self.filterspec
952         sort = self.sort
953         group = self.group
955         # get the list of ids we're batching over
956         klass = self.client.db.getclass(self.classname)
957         l = klass.filter(None, filterspec, sort, group)
959         # figure batch args
960         if self.form.has_key(':pagesize'):
961             size = int(self.form[':pagesize'].value)
962         else:
963             size = 50
964         if self.form.has_key(':startwith'):
965             start = int(self.form[':startwith'].value)
966         else:
967             start = 0
969         # return the batch object
970         return Batch(self.client, self.classname, l, size, start)
972 class Batch(ZTUtils.Batch):
973     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
974         self.client = client
975         self.classname = classname
976         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
978     # overwrite so we can late-instantiate the HTMLItem instance
979     def __getitem__(self, index):
980         if index < 0:
981             if index + self.end < self.first: raise IndexError, index
982             return self._sequence[index + self.end]
983         
984         if index >= self.length: raise IndexError, index
986         # wrap the return in an HTMLItem
987         return HTMLItem(self.client.db, self.classname,
988             self._sequence[index+self.first])
990     # override these 'cos we don't have access to acquisition
991     def previous(self):
992         print self.start
993         if self.start == 1:
994             return None
995         return Batch(self.client, self.classname, self._sequence, self._size,
996             self.first - self._size + self.overlap, 0, self.orphan,
997             self.overlap)
999     def next(self):
1000         try:
1001             self._sequence[self.end]
1002         except IndexError:
1003             return None
1004         return Batch(self.client, self.classname, self._sequence, self._size,
1005             self.end - self.overlap, 0, self.orphan, self.overlap)
1007     def length(self):
1008         self.sequence_length = l = len(self._sequence)
1009         return l