Code

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