Code

b458c6c7e2a932fb8bf3cebe9f6c54b76c1f9a49
[roundup.git] / roundup / cgi / templating.py
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7     import cPickle as pickle
8 except ImportError:
9     import pickle
10 try:
11     import cStringIO as StringIO
12 except ImportError:
13     import StringIO
14 try:
15     import StructuredText
16 except ImportError:
17     StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 #    ''' Interface to get a template, possibly loading a compiled template.
28 #    '''
29 #    # source
30 #    src = os.path.join(dir, name)
31 #
32 #    # see if we can get a compile from the template"c" directory (most
33 #    # likely is "htmlc"
34 #    split = list(os.path.split(dir))
35 #    split[-1] = split[-1] + 'c'
36 #    cdir = os.path.join(*split)
37 #    split.append(name)
38 #    cpl = os.path.join(*split)
39 #
40 #    # ok, now see if the source is newer than the compiled (or if the
41 #    # compiled even exists)
42 #    MTIME = os.path.stat.ST_MTIME
43 #    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 #        # nope, we need to compile
45 #        pt = RoundupPageTemplate()
46 #        pt.write(open(src).read())
47 #        pt.id = name
48 #
49 #        # save off the compiled template
50 #        if not os.path.exists(cdir):
51 #            os.makedirs(cdir)
52 #        f = open(cpl, 'wb')
53 #        pickle.dump(pt, f)
54 #        f.close()
55 #    else:
56 #        # yay, use the compiled template
57 #        f = open(cpl, 'rb')
58 #        pt = pickle.load(f)
59 #    return pt
61 templates = {}
63 class NoTemplate(Exception):
64     pass
66 def getTemplate(dir, name, extension, classname=None, request=None):
67     ''' Interface to get a template, possibly loading a compiled template.
69         "name" and "extension" indicate the template we're after, which in
70         most cases will be "name.extension". If "extension" is None, then
71         we look for a template just called "name" with no extension.
73         If the file "name.extension" doesn't exist, we look for
74         "_generic.extension" as a fallback.
75     '''
76     # default the name to "home"
77     if name is None:
78         name = 'home'
80     # find the source, figure the time it was last modified
81     if extension:
82         filename = '%s.%s'%(name, extension)
83     else:
84         filename = name
85     src = os.path.join(dir, filename)
86     try:
87         stime = os.stat(src)[os.path.stat.ST_MTIME]
88     except os.error, error:
89         if error.errno != errno.ENOENT:
90             raise
91         if not extension:
92             raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
94         # try for a generic template
95         generic = '_generic.%s'%extension
96         src = os.path.join(dir, generic)
97         try:
98             stime = os.stat(src)[os.path.stat.ST_MTIME]
99         except os.error, error:
100             if error.errno != errno.ENOENT:
101                 raise
102             # nicer error
103             raise NoTemplate, 'No template file exists for templating '\
104                 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
105                 extension, filename, generic)
106         filename = generic
108     key = (dir, filename)
109     if templates.has_key(key) and stime < templates[key].mtime:
110         # compiled template is up to date
111         return templates[key]
113     # compile the template
114     templates[key] = pt = RoundupPageTemplate()
115     pt.write(open(src).read())
116     pt.id = filename
117     pt.mtime = time.time()
118     return pt
120 class RoundupPageTemplate(PageTemplate.PageTemplate):
121     ''' A Roundup-specific PageTemplate.
123         Interrogate the client to set up the various template variables to
124         be available:
126         *context*
127          this is one of three things:
128          1. None - we're viewing a "home" page
129          2. The current class of item being displayed. This is an HTMLClass
130             instance.
131          3. The current item from the database, if we're viewing a specific
132             item, as an HTMLItem instance.
133         *request*
134           Includes information about the current request, including:
135            - the url
136            - the current index information (``filterspec``, ``filter`` args,
137              ``properties``, etc) parsed out of the form. 
138            - methods for easy filterspec link generation
139            - *user*, the current user node as an HTMLItem instance
140            - *form*, the current CGI form information as a FieldStorage
141         *instance*
142           The current instance
143         *db*
144           The current database, through which db.config may be reached.
145     '''
146     def getContext(self, client, classname, request):
147         c = {
148              'options': {},
149              'nothing': None,
150              'request': request,
151              'content': client.content,
152              'db': HTMLDatabase(client),
153              'instance': client.instance
154         }
155         # add in the item if there is one
156         if client.nodeid:
157             c['context'] = HTMLItem(client, classname, client.nodeid)
158         else:
159             c['context'] = HTMLClass(client, classname)
160         return c
162     def render(self, client, classname, request, **options):
163         """Render this Page Template"""
165         if not self._v_cooked:
166             self._cook()
168         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
170         if self._v_errors:
171             raise PageTemplate.PTRuntimeError, \
172                 'Page Template %s has errors.' % self.id
174         # figure the context
175         classname = classname or client.classname
176         request = request or HTMLRequest(client)
177         c = self.getContext(client, classname, request)
178         c.update({'options': options})
180         # and go
181         output = StringIO.StringIO()
182         TALInterpreter(self._v_program, self._v_macros,
183             getEngine().getContext(c), output, tal=1, strictinsert=0)()
184         return output.getvalue()
186 class HTMLDatabase:
187     ''' Return HTMLClasses for valid class fetches
188     '''
189     def __init__(self, client):
190         self._client = client
192         # we want config to be exposed
193         self.config = client.db.config
195     def __getattr__(self, attr):
196         try:
197             self._client.db.getclass(attr)
198         except KeyError:
199             raise AttributeError, attr
200         return HTMLClass(self._client, attr)
201     def classes(self):
202         l = self._client.db.classes.keys()
203         l.sort()
204         return [HTMLClass(self._client, cn) for cn in l]
206 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
207     cl = db.getclass(prop.classname)
208     l = []
209     for entry in ids:
210         if num_re.match(entry):
211             l.append(entry)
212         else:
213             l.append(cl.lookup(entry))
214     return l
216 class HTMLClass:
217     ''' Accesses through a class (either through *class* or *db.<classname>*)
218     '''
219     def __init__(self, client, classname):
220         self._client = client
221         self._db = client.db
223         # we want classname to be exposed
224         self.classname = classname
225         if classname is not None:
226             self._klass = self._db.getclass(self.classname)
227             self._props = self._klass.getprops()
229     def __repr__(self):
230         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
232     def __getitem__(self, item):
233         ''' return an HTMLProperty instance
234         '''
235        #print 'HTMLClass.getitem', (self, item)
237         # we don't exist
238         if item == 'id':
239             return None
241         # get the property
242         prop = self._props[item]
244         # look up the correct HTMLProperty class
245         form = self._client.form
246         for klass, htmlklass in propclasses:
247             if not isinstance(prop, klass):
248                 continue
249             if form.has_key(item):
250                 if isinstance(prop, hyperdb.Multilink):
251                     value = lookupIds(self._db, prop,
252                         handleListCGIValue(form[item]))
253                 elif isinstance(prop, hyperdb.Link):
254                     value = form[item].value.strip()
255                     if value:
256                         value = lookupIds(self._db, prop, [value])[0]
257                     else:
258                         value = None
259                 else:
260                     value = form[item].value.strip() or None
261             else:
262                 if isinstance(prop, hyperdb.Multilink):
263                     value = []
264                 else:
265                     value = None
266             print (prop, value)
267             return htmlklass(self._client, '', prop, item, value)
269         # no good
270         raise KeyError, item
272     def __getattr__(self, attr):
273         ''' convenience access '''
274         try:
275             return self[attr]
276         except KeyError:
277             raise AttributeError, attr
279     def properties(self):
280         ''' Return HTMLProperty for all props
281         '''
282         l = []
283         for name, prop in self._props.items():
284             for klass, htmlklass in propclasses:
285                 if isinstance(prop, hyperdb.Multilink):
286                     value = []
287                 else:
288                     value = None
289                 if isinstance(prop, klass):
290                     l.append(htmlklass(self._client, '', prop, name, value))
291         return l
293     def list(self):
294         if self.classname == 'user':
295             klass = HTMLUser
296         else:
297             klass = HTMLItem
298         l = [klass(self._client, self.classname, x) for x in self._klass.list()]
299         return l
301     def csv(self):
302         ''' Return the items of this class as a chunk of CSV text.
303         '''
304         # get the CSV module
305         try:
306             import csv
307         except ImportError:
308             return 'Sorry, you need the csv module to use this function.\n'\
309                 'Get it from: http://www.object-craft.com.au/projects/csv/'
311         props = self.propnames()
312         p = csv.parser()
313         s = StringIO.StringIO()
314         s.write(p.join(props) + '\n')
315         for nodeid in self._klass.list():
316             l = []
317             for name in props:
318                 value = self._klass.get(nodeid, name)
319                 if value is None:
320                     l.append('')
321                 elif isinstance(value, type([])):
322                     l.append(':'.join(map(str, value)))
323                 else:
324                     l.append(str(self._klass.get(nodeid, name)))
325             s.write(p.join(l) + '\n')
326         return s.getvalue()
328     def propnames(self):
329         ''' Return the list of the names of the properties of this class.
330         '''
331         idlessprops = self._klass.getprops(protected=0).keys()
332         idlessprops.sort()
333         return ['id'] + idlessprops
335     def filter(self, request=None):
336         ''' Return a list of items from this class, filtered and sorted
337             by the current requested filterspec/filter/sort/group args
338         '''
339         if request is not None:
340             filterspec = request.filterspec
341             sort = request.sort
342             group = request.group
343         if self.classname == 'user':
344             klass = HTMLUser
345         else:
346             klass = HTMLItem
347         l = [klass(self._client, self.classname, x)
348              for x in self._klass.filter(None, filterspec, sort, group)]
349         return l
351     def classhelp(self, properties, label='?', width='400', height='400'):
352         '''pop up a javascript window with class help
354            This generates a link to a popup window which displays the 
355            properties indicated by "properties" of the class named by
356            "classname". The "properties" should be a comma-separated list
357            (eg. 'id,name,description').
359            You may optionally override the label displayed, the width and
360            height. The popup window will be resizable and scrollable.
361         '''
362         return '<a href="javascript:help_window(\'%s?:template=help&' \
363             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
364             '(%s)</b></a>'%(self.classname, properties, width, height, label)
366     def submit(self, label="Submit New Entry"):
367         ''' Generate a submit button (and action hidden element)
368         '''
369         return '  <input type="hidden" name=":action" value="new">\n'\
370         '  <input type="submit" name="submit" value="%s">'%label
372     def history(self):
373         return 'New node - no history'
375     def renderWith(self, name, **kwargs):
376         ''' Render this class with the given template.
377         '''
378         # create a new request and override the specified args
379         req = HTMLRequest(self._client)
380         req.classname = self.classname
381         req.update(kwargs)
383         # new template, using the specified classname and request
384         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
386         # use our fabricated request
387         return pt.render(self._client, self.classname, req)
389 class HTMLItem:
390     ''' Accesses through an *item*
391     '''
392     def __init__(self, client, classname, nodeid):
393         self._client = client
394         self._db = client.db
395         self._classname = classname
396         self._nodeid = nodeid
397         self._klass = self._db.getclass(classname)
398         self._props = self._klass.getprops()
400     def __repr__(self):
401         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
402             self._nodeid)
404     def __getitem__(self, item):
405         ''' return an HTMLProperty instance
406         '''
407        #print 'HTMLItem.getitem', (self, item)
408         if item == 'id':
409             return self._nodeid
411         # get the property
412         prop = self._props[item]
414         # get the value, handling missing values
415         value = self._klass.get(self._nodeid, item, None)
416         if value is None:
417             if isinstance(self._props[item], hyperdb.Multilink):
418                 value = []
420         # look up the correct HTMLProperty class
421         for klass, htmlklass in propclasses:
422             if isinstance(prop, klass):
423                 return htmlklass(self._client, self._nodeid, prop, item, value)
425         raise KeyErorr, item
427     def __getattr__(self, attr):
428         ''' convenience access to properties '''
429         try:
430             return self[attr]
431         except KeyError:
432             raise AttributeError, attr
433     
434     def submit(self, label="Submit Changes"):
435         ''' Generate a submit button (and action hidden element)
436         '''
437         return '  <input type="hidden" name=":action" value="edit">\n'\
438         '  <input type="submit" name="submit" value="%s">'%label
440     def journal(self, direction='descending'):
441         ''' Return a list of HTMLJournalEntry instances.
442         '''
443         # XXX do this
444         return []
446     def history(self, direction='descending'):
447         l = ['<table class="history">'
448              '<tr><th colspan="4" class="header">',
449              _('History'),
450              '</th></tr><tr>',
451              _('<th>Date</th>'),
452              _('<th>User</th>'),
453              _('<th>Action</th>'),
454              _('<th>Args</th>'),
455             '</tr>']
456         comments = {}
457         history = self._klass.history(self._nodeid)
458         history.sort()
459         if direction == 'descending':
460             history.reverse()
461         for id, evt_date, user, action, args in history:
462             date_s = str(evt_date).replace("."," ")
463             arg_s = ''
464             if action == 'link' and type(args) == type(()):
465                 if len(args) == 3:
466                     linkcl, linkid, key = args
467                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
468                         linkcl, linkid, key)
469                 else:
470                     arg_s = str(args)
472             elif action == 'unlink' and type(args) == type(()):
473                 if len(args) == 3:
474                     linkcl, linkid, key = args
475                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
476                         linkcl, linkid, key)
477                 else:
478                     arg_s = str(args)
480             elif type(args) == type({}):
481                 cell = []
482                 for k in args.keys():
483                     # try to get the relevant property and treat it
484                     # specially
485                     try:
486                         prop = self._props[k]
487                     except KeyError:
488                         prop = None
489                     if prop is not None:
490                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
491                                 isinstance(prop, hyperdb.Link)):
492                             # figure what the link class is
493                             classname = prop.classname
494                             try:
495                                 linkcl = self._db.getclass(classname)
496                             except KeyError:
497                                 labelprop = None
498                                 comments[classname] = _('''The linked class
499                                     %(classname)s no longer exists''')%locals()
500                             labelprop = linkcl.labelprop(1)
501                             hrefable = os.path.exists(
502                                 os.path.join(self._db.config.TEMPLATES,
503                                 classname+'.item'))
505                         if isinstance(prop, hyperdb.Multilink) and \
506                                 len(args[k]) > 0:
507                             ml = []
508                             for linkid in args[k]:
509                                 if isinstance(linkid, type(())):
510                                     sublabel = linkid[0] + ' '
511                                     linkids = linkid[1]
512                                 else:
513                                     sublabel = ''
514                                     linkids = [linkid]
515                                 subml = []
516                                 for linkid in linkids:
517                                     label = classname + linkid
518                                     # if we have a label property, try to use it
519                                     # TODO: test for node existence even when
520                                     # there's no labelprop!
521                                     try:
522                                         if labelprop is not None:
523                                             label = linkcl.get(linkid, labelprop)
524                                     except IndexError:
525                                         comments['no_link'] = _('''<strike>The
526                                             linked node no longer
527                                             exists</strike>''')
528                                         subml.append('<strike>%s</strike>'%label)
529                                     else:
530                                         if hrefable:
531                                             subml.append('<a href="%s%s">%s</a>'%(
532                                                 classname, linkid, label))
533                                 ml.append(sublabel + ', '.join(subml))
534                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
535                         elif isinstance(prop, hyperdb.Link) and args[k]:
536                             label = classname + args[k]
537                             # if we have a label property, try to use it
538                             # TODO: test for node existence even when
539                             # there's no labelprop!
540                             if labelprop is not None:
541                                 try:
542                                     label = linkcl.get(args[k], labelprop)
543                                 except IndexError:
544                                     comments['no_link'] = _('''<strike>The
545                                         linked node no longer
546                                         exists</strike>''')
547                                     cell.append(' <strike>%s</strike>,\n'%label)
548                                     # "flag" this is done .... euwww
549                                     label = None
550                             if label is not None:
551                                 if hrefable:
552                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
553                                         classname, args[k], label))
554                                 else:
555                                     cell.append('%s: %s' % (k,label))
557                         elif isinstance(prop, hyperdb.Date) and args[k]:
558                             d = date.Date(args[k])
559                             cell.append('%s: %s'%(k, str(d)))
561                         elif isinstance(prop, hyperdb.Interval) and args[k]:
562                             d = date.Interval(args[k])
563                             cell.append('%s: %s'%(k, str(d)))
565                         elif isinstance(prop, hyperdb.String) and args[k]:
566                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
568                         elif not args[k]:
569                             cell.append('%s: (no value)\n'%k)
571                         else:
572                             cell.append('%s: %s\n'%(k, str(args[k])))
573                     else:
574                         # property no longer exists
575                         comments['no_exist'] = _('''<em>The indicated property
576                             no longer exists</em>''')
577                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
578                 arg_s = '<br />'.join(cell)
579             else:
580                 # unkown event!!
581                 comments['unknown'] = _('''<strong><em>This event is not
582                     handled by the history display!</em></strong>''')
583                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
584             date_s = date_s.replace(' ', '&nbsp;')
585             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
586                 date_s, user, action, arg_s))
587         if comments:
588             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
589         for entry in comments.values():
590             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
591         l.append('</table>')
592         return '\n'.join(l)
594     def renderQueryForm(self):
595         ''' Render this item, which is a query, as a search form.
596         '''
597         # create a new request and override the specified args
598         req = HTMLRequest(self._client)
599         req.classname = self._klass.get(self._nodeid, 'klass')
600         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
602         # new template, using the specified classname and request
603         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
605         # use our fabricated request
606         return pt.render(self._client, req.classname, req)
608 class HTMLUser(HTMLItem):
609     ''' Accesses through the *user* (a special case of item)
610     '''
611     def __init__(self, client, classname, nodeid):
612         HTMLItem.__init__(self, client, 'user', nodeid)
613         self._default_classname = client.classname
615         # used for security checks
616         self._security = client.db.security
617     _marker = []
618     def hasPermission(self, role, classname=_marker):
619         ''' Determine if the user has the Role.
621             The class being tested defaults to the template's class, but may
622             be overidden for this test by suppling an alternate classname.
623         '''
624         if classname is self._marker:
625             classname = self._default_classname
626         return self._security.hasPermission(role, self._nodeid, classname)
628 class HTMLProperty:
629     ''' String, Number, Date, Interval HTMLProperty
631         Hase useful attributes:
633          _name  the name of the property
634          _value the value of the property if any
636         A wrapper object which may be stringified for the plain() behaviour.
637     '''
638     def __init__(self, client, nodeid, prop, name, value):
639         self._client = client
640         self._db = client.db
641         self._nodeid = nodeid
642         self._prop = prop
643         self._name = name
644         self._value = value
645     def __repr__(self):
646         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
647     def __str__(self):
648         return self.plain()
649     def __cmp__(self, other):
650         if isinstance(other, HTMLProperty):
651             return cmp(self._value, other._value)
652         return cmp(self._value, other)
654 class StringHTMLProperty(HTMLProperty):
655     def plain(self, escape=0):
656         if self._value is None:
657             return ''
658         if escape:
659             return cgi.escape(str(self._value))
660         return str(self._value)
662     def stext(self, escape=0):
663         s = self.plain(escape=escape)
664         if not StructuredText:
665             return s
666         return StructuredText(s,level=1,header=0)
668     def field(self, size = 30):
669         if self._value is None:
670             value = ''
671         else:
672             value = cgi.escape(str(self._value))
673             value = '&quot;'.join(value.split('"'))
674         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
676     def multiline(self, escape=0, rows=5, cols=40):
677         if self._value is None:
678             value = ''
679         else:
680             value = cgi.escape(str(self._value))
681             value = '&quot;'.join(value.split('"'))
682         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
683             self._name, rows, cols, value)
685     def email(self, escape=1):
686         ''' fudge email '''
687         if self._value is None: value = ''
688         else: value = str(self._value)
689         value = value.replace('@', ' at ')
690         value = value.replace('.', ' ')
691         if escape:
692             value = cgi.escape(value)
693         return value
695 class PasswordHTMLProperty(HTMLProperty):
696     def plain(self):
697         if self._value is None:
698             return ''
699         return _('*encrypted*')
701     def field(self, size = 30):
702         return '<input type="password" name="%s" size="%s">'%(self._name, size)
704 class NumberHTMLProperty(HTMLProperty):
705     def plain(self):
706         return str(self._value)
708     def field(self, size = 30):
709         if self._value is None:
710             value = ''
711         else:
712             value = cgi.escape(str(self._value))
713             value = '&quot;'.join(value.split('"'))
714         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
716 class BooleanHTMLProperty(HTMLProperty):
717     def plain(self):
718         if self.value is None:
719             return ''
720         return self._value and "Yes" or "No"
722     def field(self):
723         checked = self._value and "checked" or ""
724         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
725             checked)
726         if checked:
727             checked = ""
728         else:
729             checked = "checked"
730         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
731             checked)
732         return s
734 class DateHTMLProperty(HTMLProperty):
735     def plain(self):
736         if self._value is None:
737             return ''
738         return str(self._value)
740     def field(self, size = 30):
741         if self._value is None:
742             value = ''
743         else:
744             value = cgi.escape(str(self._value))
745             value = '&quot;'.join(value.split('"'))
746         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
748     def reldate(self, pretty=1):
749         if not self._value:
750             return ''
752         # figure the interval
753         interval = date.Date('.') - self._value
754         if pretty:
755             return interval.pretty()
756         return str(interval)
758 class IntervalHTMLProperty(HTMLProperty):
759     def plain(self):
760         if self._value is None:
761             return ''
762         return str(self._value)
764     def pretty(self):
765         return self._value.pretty()
767     def field(self, size = 30):
768         if self._value is None:
769             value = ''
770         else:
771             value = cgi.escape(str(self._value))
772             value = '&quot;'.join(value.split('"'))
773         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
775 class LinkHTMLProperty(HTMLProperty):
776     ''' Link HTMLProperty
777         Include the above as well as being able to access the class
778         information. Stringifying the object itself results in the value
779         from the item being displayed. Accessing attributes of this object
780         result in the appropriate entry from the class being queried for the
781         property accessed (so item/assignedto/name would look up the user
782         entry identified by the assignedto property on item, and then the
783         name property of that user)
784     '''
785     def __getattr__(self, attr):
786         ''' return a new HTMLItem '''
787        #print 'Link.getattr', (self, attr, self._value)
788         if not self._value:
789             raise AttributeError, "Can't access missing value"
790         if self._prop.classname == 'user':
791             klass = HTMLUser
792         else:
793             klass = HTMLItem
794         i = klass(self._client, self._prop.classname, self._value)
795         return getattr(i, attr)
797     def plain(self, escape=0):
798         if self._value is None:
799             return ''
800         linkcl = self._db.classes[self._prop.classname]
801         k = linkcl.labelprop(1)
802         value = str(linkcl.get(self._value, k))
803         if escape:
804             value = cgi.escape(value)
805         return value
807     def field(self):
808         linkcl = self._db.getclass(self._prop.classname)
809         if linkcl.getprops().has_key('order'):  
810             sort_on = 'order'  
811         else:  
812             sort_on = linkcl.labelprop()  
813         options = linkcl.filter(None, {}, [sort_on], []) 
814         # TODO: make this a field display, not a menu one!
815         l = ['<select name="%s">'%property]
816         k = linkcl.labelprop(1)
817         if value is None:
818             s = 'selected '
819         else:
820             s = ''
821         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
822         for optionid in options:
823             option = linkcl.get(optionid, k)
824             s = ''
825             if optionid == value:
826                 s = 'selected '
827             if showid:
828                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
829             else:
830                 lab = option
831             if size is not None and len(lab) > size:
832                 lab = lab[:size-3] + '...'
833             lab = cgi.escape(lab)
834             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
835         l.append('</select>')
836         return '\n'.join(l)
838     def menu(self, size=None, height=None, showid=0, additional=[],
839             **conditions):
840         value = self._value
842         # sort function
843         sortfunc = make_sort_function(self._db, self._prop.classname)
845         # force the value to be a single choice
846         if isinstance(value, type('')):
847             value = value[0]
848         linkcl = self._db.getclass(self._prop.classname)
849         l = ['<select name="%s">'%self._name]
850         k = linkcl.labelprop(1)
851         s = ''
852         if value is None:
853             s = 'selected '
854         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
855         if linkcl.getprops().has_key('order'):  
856             sort_on = ('+', 'order')
857         else:  
858             sort_on = ('+', linkcl.labelprop())
859         options = linkcl.filter(None, conditions, sort_on, (None, None))
860         for optionid in options:
861             option = linkcl.get(optionid, k)
862             s = ''
863             if value in [optionid, option]:
864                 s = 'selected '
865             if showid:
866                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
867             else:
868                 lab = option
869             if size is not None and len(lab) > size:
870                 lab = lab[:size-3] + '...'
871             if additional:
872                 m = []
873                 for propname in additional:
874                     m.append(linkcl.get(optionid, propname))
875                 lab = lab + ' (%s)'%', '.join(map(str, m))
876             lab = cgi.escape(lab)
877             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
878         l.append('</select>')
879         return '\n'.join(l)
880 #    def checklist(self, ...)
882 class MultilinkHTMLProperty(HTMLProperty):
883     ''' Multilink HTMLProperty
885         Also be iterable, returning a wrapper object like the Link case for
886         each entry in the multilink.
887     '''
888     def __len__(self):
889         ''' length of the multilink '''
890         return len(self._value)
892     def __getattr__(self, attr):
893         ''' no extended attribute accesses make sense here '''
894         raise AttributeError, attr
896     def __getitem__(self, num):
897         ''' iterate and return a new HTMLItem
898         '''
899        #print 'Multi.getitem', (self, num)
900         value = self._value[num]
901         if self._prop.classname == 'user':
902             klass = HTMLUser
903         else:
904             klass = HTMLItem
905         return klass(self._client, self._prop.classname, value)
907     def __contains__(self, value):
908         ''' Support the "in" operator
909         '''
910         return value in self._value
912     def reverse(self):
913         ''' return the list in reverse order
914         '''
915         l = self._value[:]
916         l.reverse()
917         if self._prop.classname == 'user':
918             klass = HTMLUser
919         else:
920             klass = HTMLItem
921         return [klass(self._client, self._prop.classname, value) for value in l]
923     def plain(self, escape=0):
924         linkcl = self._db.classes[self._prop.classname]
925         k = linkcl.labelprop(1)
926         labels = []
927         for v in self._value:
928             labels.append(linkcl.get(v, k))
929         value = ', '.join(labels)
930         if escape:
931             value = cgi.escape(value)
932         return value
934     def field(self, size=30, showid=0):
935         sortfunc = make_sort_function(self._db, self._prop.classname)
936         linkcl = self._db.getclass(self._prop.classname)
937         value = self._value[:]
938         if value:
939             value.sort(sortfunc)
940         # map the id to the label property
941         if not showid:
942             k = linkcl.labelprop(1)
943             value = [linkcl.get(v, k) for v in value]
944         value = cgi.escape(','.join(value))
945         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
947     def menu(self, size=None, height=None, showid=0, additional=[],
948             **conditions):
949         value = self._value
951         # sort function
952         sortfunc = make_sort_function(self._db, self._prop.classname)
954         linkcl = self._db.getclass(self._prop.classname)
955         if linkcl.getprops().has_key('order'):  
956             sort_on = ('+', 'order')
957         else:  
958             sort_on = ('+', linkcl.labelprop())
959         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
960         height = height or min(len(options), 7)
961         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
962         k = linkcl.labelprop(1)
963         for optionid in options:
964             option = linkcl.get(optionid, k)
965             s = ''
966             if optionid in value or option in value:
967                 s = 'selected '
968             if showid:
969                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
970             else:
971                 lab = option
972             if size is not None and len(lab) > size:
973                 lab = lab[:size-3] + '...'
974             if additional:
975                 m = []
976                 for propname in additional:
977                     m.append(linkcl.get(optionid, propname))
978                 lab = lab + ' (%s)'%', '.join(m)
979             lab = cgi.escape(lab)
980             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
981                 lab))
982         l.append('</select>')
983         return '\n'.join(l)
985 # set the propclasses for HTMLItem
986 propclasses = (
987     (hyperdb.String, StringHTMLProperty),
988     (hyperdb.Number, NumberHTMLProperty),
989     (hyperdb.Boolean, BooleanHTMLProperty),
990     (hyperdb.Date, DateHTMLProperty),
991     (hyperdb.Interval, IntervalHTMLProperty),
992     (hyperdb.Password, PasswordHTMLProperty),
993     (hyperdb.Link, LinkHTMLProperty),
994     (hyperdb.Multilink, MultilinkHTMLProperty),
997 def make_sort_function(db, classname):
998     '''Make a sort function for a given class
999     '''
1000     linkcl = db.getclass(classname)
1001     if linkcl.getprops().has_key('order'):
1002         sort_on = 'order'
1003     else:
1004         sort_on = linkcl.labelprop()
1005     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1006         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1007     return sortfunc
1009 def handleListCGIValue(value):
1010     ''' Value is either a single item or a list of items. Each item has a
1011         .value that we're actually interested in.
1012     '''
1013     if isinstance(value, type([])):
1014         return [value.value for value in value]
1015     else:
1016         value = value.value.strip()
1017         if not value:
1018             return []
1019         return value.split(',')
1021 class ShowDict:
1022     ''' A convenience access to the :columns index parameters
1023     '''
1024     def __init__(self, columns):
1025         self.columns = {}
1026         for col in columns:
1027             self.columns[col] = 1
1028     def __getitem__(self, name):
1029         return self.columns.has_key(name)
1031 class HTMLRequest:
1032     ''' The *request*, holding the CGI form and environment.
1034         "form" the CGI form as a cgi.FieldStorage
1035         "env" the CGI environment variables
1036         "url" the current URL path for this request
1037         "base" the base URL for this instance
1038         "user" a HTMLUser instance for this user
1039         "classname" the current classname (possibly None)
1040         "template" the current template (suffix, also possibly None)
1042         Index args:
1043         "columns" dictionary of the columns to display in an index page
1044         "show" a convenience access to columns - request/show/colname will
1045                be true if the columns should be displayed, false otherwise
1046         "sort" index sort column (direction, column name)
1047         "group" index grouping property (direction, column name)
1048         "filter" properties to filter the index on
1049         "filterspec" values to filter the index on
1050         "search_text" text to perform a full-text search on for an index
1052     '''
1053     def __init__(self, client):
1054         self.client = client
1056         # easier access vars
1057         self.form = client.form
1058         self.env = client.env
1059         self.base = client.base
1060         self.url = client.url
1061         self.user = HTMLUser(client, 'user', client.userid)
1063         # store the current class name and action
1064         self.classname = client.classname
1065         self.template = client.template
1067         self._post_init()
1069     def _post_init(self):
1070         ''' Set attributes based on self.form
1071         '''
1072         # extract the index display information from the form
1073         self.columns = []
1074         if self.form.has_key(':columns'):
1075             self.columns = handleListCGIValue(self.form[':columns'])
1076         self.show = ShowDict(self.columns)
1078         # sorting
1079         self.sort = (None, None)
1080         if self.form.has_key(':sort'):
1081             sort = self.form[':sort'].value
1082             if sort.startswith('-'):
1083                 self.sort = ('-', sort[1:])
1084             else:
1085                 self.sort = ('+', sort)
1086         if self.form.has_key(':sortdir'):
1087             self.sort = ('-', self.sort[1])
1089         # grouping
1090         self.group = (None, None)
1091         if self.form.has_key(':group'):
1092             group = self.form[':group'].value
1093             if group.startswith('-'):
1094                 self.group = ('-', group[1:])
1095             else:
1096                 self.group = ('+', group)
1097         if self.form.has_key(':groupdir'):
1098             self.group = ('-', self.group[1])
1100         # filtering
1101         self.filter = []
1102         if self.form.has_key(':filter'):
1103             self.filter = handleListCGIValue(self.form[':filter'])
1104         self.filterspec = {}
1105         if self.classname is not None:
1106             props = self.client.db.getclass(self.classname).getprops()
1107             for name in self.filter:
1108                 if self.form.has_key(name):
1109                     prop = props[name]
1110                     fv = self.form[name]
1111                     if (isinstance(prop, hyperdb.Link) or
1112                             isinstance(prop, hyperdb.Multilink)):
1113                         self.filterspec[name] = handleListCGIValue(fv)
1114                     else:
1115                         self.filterspec[name] = fv.value
1117         # full-text search argument
1118         self.search_text = None
1119         if self.form.has_key(':search_text'):
1120             self.search_text = self.form[':search_text'].value
1122         # pagination - size and start index
1123         # figure batch args
1124         if self.form.has_key(':pagesize'):
1125             self.pagesize = int(self.form[':pagesize'].value)
1126         else:
1127             self.pagesize = 50
1128         if self.form.has_key(':startwith'):
1129             self.startwith = int(self.form[':startwith'].value)
1130         else:
1131             self.startwith = 0
1133     def updateFromURL(self, url):
1134         ''' Parse the URL for query args, and update my attributes using the
1135             values.
1136         ''' 
1137         self.form = {}
1138         for name, value in cgi.parse_qsl(url):
1139             if self.form.has_key(name):
1140                 if isinstance(self.form[name], type([])):
1141                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1142                 else:
1143                     self.form[name] = [self.form[name],
1144                         cgi.MiniFieldStorage(name, value)]
1145             else:
1146                 self.form[name] = cgi.MiniFieldStorage(name, value)
1147         self._post_init()
1149     def update(self, kwargs):
1150         ''' Update my attributes using the keyword args
1151         '''
1152         self.__dict__.update(kwargs)
1153         if kwargs.has_key('columns'):
1154             self.show = ShowDict(self.columns)
1156     def description(self):
1157         ''' Return a description of the request - handle for the page title.
1158         '''
1159         s = [self.client.db.config.TRACKER_NAME]
1160         if self.classname:
1161             if self.client.nodeid:
1162                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1163             else:
1164                 s.append('- index of '+self.classname)
1165         else:
1166             s.append('- home')
1167         return ' '.join(s)
1169     def __str__(self):
1170         d = {}
1171         d.update(self.__dict__)
1172         f = ''
1173         for k in self.form.keys():
1174             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1175         d['form'] = f
1176         e = ''
1177         for k,v in self.env.items():
1178             e += '\n     %r=%r'%(k, v)
1179         d['env'] = e
1180         return '''
1181 form: %(form)s
1182 url: %(url)r
1183 base: %(base)r
1184 classname: %(classname)r
1185 template: %(template)r
1186 columns: %(columns)r
1187 sort: %(sort)r
1188 group: %(group)r
1189 filter: %(filter)r
1190 search_text: %(search_text)r
1191 pagesize: %(pagesize)r
1192 startwith: %(startwith)r
1193 env: %(env)s
1194 '''%d
1196     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1197             filterspec=1):
1198         ''' return the current index args as form elements '''
1199         l = []
1200         s = '<input type="hidden" name="%s" value="%s">'
1201         if columns and self.columns:
1202             l.append(s%(':columns', ','.join(self.columns)))
1203         if sort and self.sort[1] is not None:
1204             if self.sort[0] == '-':
1205                 val = '-'+self.sort[1]
1206             else:
1207                 val = self.sort[1]
1208             l.append(s%(':sort', val))
1209         if group and self.group[1] is not None:
1210             if self.group[0] == '-':
1211                 val = '-'+self.group[1]
1212             else:
1213                 val = self.group[1]
1214             l.append(s%(':group', val))
1215         if filter and self.filter:
1216             l.append(s%(':filter', ','.join(self.filter)))
1217         if filterspec:
1218             for k,v in self.filterspec.items():
1219                 l.append(s%(k, ','.join(v)))
1220         if self.search_text:
1221             l.append(s%(':search_text', self.search_text))
1222         l.append(s%(':pagesize', self.pagesize))
1223         l.append(s%(':startwith', self.startwith))
1224         return '\n'.join(l)
1226     def indexargs_href(self, url, args):
1227         ''' embed the current index args in a URL '''
1228         l = ['%s=%s'%(k,v) for k,v in args.items()]
1229         if self.columns and not args.has_key(':columns'):
1230             l.append(':columns=%s'%(','.join(self.columns)))
1231         if self.sort[1] is not None and not args.has_key(':sort'):
1232             if self.sort[0] == '-':
1233                 val = '-'+self.sort[1]
1234             else:
1235                 val = self.sort[1]
1236             l.append(':sort=%s'%val)
1237         if self.group[1] is not None and not args.has_key(':group'):
1238             if self.group[0] == '-':
1239                 val = '-'+self.group[1]
1240             else:
1241                 val = self.group[1]
1242             l.append(':group=%s'%val)
1243         if self.filter and not args.has_key(':columns'):
1244             l.append(':filter=%s'%(','.join(self.filter)))
1245         for k,v in self.filterspec.items():
1246             if not args.has_key(k):
1247                 l.append('%s=%s'%(k, ','.join(v)))
1248         if self.search_text and not args.has_key(':search_text'):
1249             l.append(':search_text=%s'%self.search_text)
1250         if not args.has_key(':pagesize'):
1251             l.append(':pagesize=%s'%self.pagesize)
1252         if not args.has_key(':startwith'):
1253             l.append(':startwith=%s'%self.startwith)
1254         return '%s?%s'%(url, '&'.join(l))
1256     def base_javascript(self):
1257         return '''
1258 <script language="javascript">
1259 submitted = false;
1260 function submit_once() {
1261     if (submitted) {
1262         alert("Your request is being processed.\\nPlease be patient.");
1263         return 0;
1264     }
1265     submitted = true;
1266     return 1;
1269 function help_window(helpurl, width, height) {
1270     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1272 </script>
1273 '''%self.base
1275     def batch(self):
1276         ''' Return a batch object for results from the "current search"
1277         '''
1278         filterspec = self.filterspec
1279         sort = self.sort
1280         group = self.group
1282         # get the list of ids we're batching over
1283         klass = self.client.db.getclass(self.classname)
1284         if self.search_text:
1285             matches = self.client.db.indexer.search(
1286                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1287         else:
1288             matches = None
1289         l = klass.filter(matches, filterspec, sort, group)
1291         # return the batch object
1292         return Batch(self.client, self.classname, l, self.pagesize,
1293             self.startwith)
1296 # extend the standard ZTUtils Batch object to remove dependency on
1297 # Acquisition and add a couple of useful methods
1298 class Batch(ZTUtils.Batch):
1299     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1300         self.client = client
1301         self.classname = classname
1302         self.last_index = self.last_item = None
1303         self.current_item = None
1304         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1306     # overwrite so we can late-instantiate the HTMLItem instance
1307     def __getitem__(self, index):
1308         if index < 0:
1309             if index + self.end < self.first: raise IndexError, index
1310             return self._sequence[index + self.end]
1311         
1312         if index >= self.length: raise IndexError, index
1314         # move the last_item along - but only if the fetched index changes
1315         # (for some reason, index 0 is fetched twice)
1316         if index != self.last_index:
1317             self.last_item = self.current_item
1318             self.last_index = index
1320         # wrap the return in an HTMLItem
1321         if self.classname == 'user':
1322             klass = HTMLUser
1323         else:
1324             klass = HTMLItem
1325         self.current_item = klass(self.client, self.classname,
1326             self._sequence[index+self.first])
1327         return self.current_item
1329     def propchanged(self, property):
1330         ''' Detect if the property marked as being the group property
1331             changed in the last iteration fetch
1332         '''
1333         if (self.last_item is None or
1334                 self.last_item[property] != self.current_item[property]):
1335             return 1
1336         return 0
1338     # override these 'cos we don't have access to acquisition
1339     def previous(self):
1340         if self.start == 1:
1341             return None
1342         return Batch(self.client, self.classname, self._sequence, self._size,
1343             self.first - self._size + self.overlap, 0, self.orphan,
1344             self.overlap)
1346     def next(self):
1347         try:
1348             self._sequence[self.end]
1349         except IndexError:
1350             return None
1351         return Batch(self.client, self.classname, self._sequence, self._size,
1352             self.end - self.overlap, 0, self.orphan, self.overlap)
1354     def length(self):
1355         self.sequence_length = l = len(self._sequence)
1356         return l