Code

query "editing" now working, minus filling the form in with the query params
[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]
205         
206 class HTMLClass:
207     ''' Accesses through a class (either through *class* or *db.<classname>*)
208     '''
209     def __init__(self, client, classname):
210         self._client = client
211         self._db = client.db
213         # we want classname to be exposed
214         self.classname = classname
215         if classname is not None:
216             self._klass = self._db.getclass(self.classname)
217             self._props = self._klass.getprops()
219     def __repr__(self):
220         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
222     def __getitem__(self, item):
223         ''' return an HTMLProperty instance
224         '''
225         #print 'getitem', (self, item)
227         # we don't exist
228         if item == 'id':
229             return None
230         if item == 'creator':
231             # but we will be created by this user...
232             return HTMLUser(self._client, 'user', self._client.userid)
234         # get the property
235         prop = self._props[item]
237         # look up the correct HTMLProperty class
238         for klass, htmlklass in propclasses:
239             if isinstance(prop, hyperdb.Multilink):
240                 value = []
241             else:
242                 value = None
243             if isinstance(prop, klass):
244                 return htmlklass(self._client, '', prop, item, value)
246         # no good
247         raise KeyError, item
249     def __getattr__(self, attr):
250         ''' convenience access '''
251         try:
252             return self[attr]
253         except KeyError:
254             raise AttributeError, attr
256     def properties(self):
257         ''' Return HTMLProperty for all props
258         '''
259         l = []
260         for name, prop in self._props.items():
261             for klass, htmlklass in propclasses:
262                 if isinstance(prop, hyperdb.Multilink):
263                     value = []
264                 else:
265                     value = None
266                 if isinstance(prop, klass):
267                     l.append(htmlklass(self._client, '', prop, name, value))
268         return l
270     def list(self):
271         if self.classname == 'user':
272             klass = HTMLUser
273         else:
274             klass = HTMLItem
275         l = [klass(self._client, self.classname, x) for x in self._klass.list()]
276         return l
278     def csv(self):
279         ''' Return the items of this class as a chunk of CSV text.
280         '''
281         # get the CSV module
282         try:
283             import csv
284         except ImportError:
285             return 'Sorry, you need the csv module to use this function.\n'\
286                 'Get it from: http://www.object-craft.com.au/projects/csv/'
288         props = self.propnames()
289         p = csv.parser()
290         s = StringIO.StringIO()
291         s.write(p.join(props) + '\n')
292         for nodeid in self._klass.list():
293             l = []
294             for name in props:
295                 value = self._klass.get(nodeid, name)
296                 if value is None:
297                     l.append('')
298                 elif isinstance(value, type([])):
299                     l.append(':'.join(map(str, value)))
300                 else:
301                     l.append(str(self._klass.get(nodeid, name)))
302             s.write(p.join(l) + '\n')
303         return s.getvalue()
305     def propnames(self):
306         ''' Return the list of the names of the properties of this class.
307         '''
308         idlessprops = self._klass.getprops(protected=0).keys()
309         idlessprops.sort()
310         return ['id'] + idlessprops
312     def filter(self, request=None):
313         ''' Return a list of items from this class, filtered and sorted
314             by the current requested filterspec/filter/sort/group args
315         '''
316         if request is not None:
317             filterspec = request.filterspec
318             sort = request.sort
319             group = request.group
320         if self.classname == 'user':
321             klass = HTMLUser
322         else:
323             klass = HTMLItem
324         l = [klass(self._client, self.classname, x)
325              for x in self._klass.filter(None, filterspec, sort, group)]
326         return l
328     def classhelp(self, properties, label='?', width='400', height='400'):
329         '''pop up a javascript window with class help
331            This generates a link to a popup window which displays the 
332            properties indicated by "properties" of the class named by
333            "classname". The "properties" should be a comma-separated list
334            (eg. 'id,name,description').
336            You may optionally override the label displayed, the width and
337            height. The popup window will be resizable and scrollable.
338         '''
339         return '<a href="javascript:help_window(\'%s?:template=help&' \
340             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
341             '(%s)</b></a>'%(self.classname, properties, width, height, label)
343     def submit(self, label="Submit New Entry"):
344         ''' Generate a submit button (and action hidden element)
345         '''
346         return '  <input type="hidden" name=":action" value="new">\n'\
347         '  <input type="submit" name="submit" value="%s">'%label
349     def history(self):
350         return 'New node - no history'
352     def renderWith(self, name, **kwargs):
353         ''' Render this class with the given template.
354         '''
355         # create a new request and override the specified args
356         req = HTMLRequest(self._client)
357         req.classname = self.classname
358         req.update(kwargs)
360         # new template, using the specified classname and request
361         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
363         # use our fabricated request
364         return pt.render(self._client, self.classname, req)
366 class HTMLItem:
367     ''' Accesses through an *item*
368     '''
369     def __init__(self, client, classname, nodeid):
370         self._client = client
371         self._db = client.db
372         self._classname = classname
373         self._nodeid = nodeid
374         self._klass = self._db.getclass(classname)
375         self._props = self._klass.getprops()
377     def __repr__(self):
378         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
379             self._nodeid)
381     def __getitem__(self, item):
382         ''' return an HTMLProperty instance
383         '''
384         #print 'getitem', (self, item)
385         if item == 'id':
386             return self._nodeid
388         # get the property
389         prop = self._props[item]
391         # get the value, handling missing values
392         value = self._klass.get(self._nodeid, item, None)
393         if value is None:
394             if isinstance(self._props[item], hyperdb.Multilink):
395                 value = []
397         # look up the correct HTMLProperty class
398         for klass, htmlklass in propclasses:
399             if isinstance(prop, klass):
400                 return htmlklass(self._client, self._nodeid, prop, item, value)
402         raise KeyErorr, item
404     def __getattr__(self, attr):
405         ''' convenience access to properties '''
406         try:
407             return self[attr]
408         except KeyError:
409             raise AttributeError, attr
410     
411     def submit(self, label="Submit Changes"):
412         ''' Generate a submit button (and action hidden element)
413         '''
414         return '  <input type="hidden" name=":action" value="edit">\n'\
415         '  <input type="submit" name="submit" value="%s">'%label
417     # XXX this probably should just return the history items, not the HTML
418     def history(self, direction='descending'):
419         l = ['<table class="history">'
420              '<tr><th colspan="4" class="header">',
421              _('History'),
422              '</th></tr><tr>',
423              _('<th>Date</th>'),
424              _('<th>User</th>'),
425              _('<th>Action</th>'),
426              _('<th>Args</th>'),
427             '</tr>']
428         comments = {}
429         history = self._klass.history(self._nodeid)
430         history.sort()
431         if direction == 'descending':
432             history.reverse()
433         for id, evt_date, user, action, args in history:
434             date_s = str(evt_date).replace("."," ")
435             arg_s = ''
436             if action == 'link' and type(args) == type(()):
437                 if len(args) == 3:
438                     linkcl, linkid, key = args
439                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
440                         linkcl, linkid, key)
441                 else:
442                     arg_s = str(args)
444             elif action == 'unlink' and type(args) == type(()):
445                 if len(args) == 3:
446                     linkcl, linkid, key = args
447                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
448                         linkcl, linkid, key)
449                 else:
450                     arg_s = str(args)
452             elif type(args) == type({}):
453                 cell = []
454                 for k in args.keys():
455                     # try to get the relevant property and treat it
456                     # specially
457                     try:
458                         prop = self._props[k]
459                     except KeyError:
460                         prop = None
461                     if prop is not None:
462                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
463                                 isinstance(prop, hyperdb.Link)):
464                             # figure what the link class is
465                             classname = prop.classname
466                             try:
467                                 linkcl = self._db.getclass(classname)
468                             except KeyError:
469                                 labelprop = None
470                                 comments[classname] = _('''The linked class
471                                     %(classname)s no longer exists''')%locals()
472                             labelprop = linkcl.labelprop(1)
473                             hrefable = os.path.exists(
474                                 os.path.join(self._db.config.TEMPLATES,
475                                 classname+'.item'))
477                         if isinstance(prop, hyperdb.Multilink) and \
478                                 len(args[k]) > 0:
479                             ml = []
480                             for linkid in args[k]:
481                                 if isinstance(linkid, type(())):
482                                     sublabel = linkid[0] + ' '
483                                     linkids = linkid[1]
484                                 else:
485                                     sublabel = ''
486                                     linkids = [linkid]
487                                 subml = []
488                                 for linkid in linkids:
489                                     label = classname + linkid
490                                     # if we have a label property, try to use it
491                                     # TODO: test for node existence even when
492                                     # there's no labelprop!
493                                     try:
494                                         if labelprop is not None:
495                                             label = linkcl.get(linkid, labelprop)
496                                     except IndexError:
497                                         comments['no_link'] = _('''<strike>The
498                                             linked node no longer
499                                             exists</strike>''')
500                                         subml.append('<strike>%s</strike>'%label)
501                                     else:
502                                         if hrefable:
503                                             subml.append('<a href="%s%s">%s</a>'%(
504                                                 classname, linkid, label))
505                                 ml.append(sublabel + ', '.join(subml))
506                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
507                         elif isinstance(prop, hyperdb.Link) and args[k]:
508                             label = classname + args[k]
509                             # if we have a label property, try to use it
510                             # TODO: test for node existence even when
511                             # there's no labelprop!
512                             if labelprop is not None:
513                                 try:
514                                     label = linkcl.get(args[k], labelprop)
515                                 except IndexError:
516                                     comments['no_link'] = _('''<strike>The
517                                         linked node no longer
518                                         exists</strike>''')
519                                     cell.append(' <strike>%s</strike>,\n'%label)
520                                     # "flag" this is done .... euwww
521                                     label = None
522                             if label is not None:
523                                 if hrefable:
524                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
525                                         classname, args[k], label))
526                                 else:
527                                     cell.append('%s: %s' % (k,label))
529                         elif isinstance(prop, hyperdb.Date) and args[k]:
530                             d = date.Date(args[k])
531                             cell.append('%s: %s'%(k, str(d)))
533                         elif isinstance(prop, hyperdb.Interval) and args[k]:
534                             d = date.Interval(args[k])
535                             cell.append('%s: %s'%(k, str(d)))
537                         elif isinstance(prop, hyperdb.String) and args[k]:
538                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
540                         elif not args[k]:
541                             cell.append('%s: (no value)\n'%k)
543                         else:
544                             cell.append('%s: %s\n'%(k, str(args[k])))
545                     else:
546                         # property no longer exists
547                         comments['no_exist'] = _('''<em>The indicated property
548                             no longer exists</em>''')
549                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
550                 arg_s = '<br />'.join(cell)
551             else:
552                 # unkown event!!
553                 comments['unknown'] = _('''<strong><em>This event is not
554                     handled by the history display!</em></strong>''')
555                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
556             date_s = date_s.replace(' ', '&nbsp;')
557             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
558                 date_s, user, action, arg_s))
559         if comments:
560             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
561         for entry in comments.values():
562             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
563         l.append('</table>')
564         return '\n'.join(l)
566     def renderQueryForm(self):
567         ''' Render this item, which is a query, as a search form.
568         '''
569         # create a new request and override the specified args
570         req = HTMLRequest(self._client)
571         req.classname = self._klass.get(self._nodeid, 'klass')
572         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
574         # new template, using the specified classname and request
575         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
577         # use our fabricated request
578         return pt.render(self._client, req.classname, req)
580 class HTMLUser(HTMLItem):
581     ''' Accesses through the *user* (a special case of item)
582     '''
583     def __init__(self, client, classname, nodeid):
584         HTMLItem.__init__(self, client, 'user', nodeid)
585         self._default_classname = client.classname
587         # used for security checks
588         self._security = client.db.security
589     _marker = []
590     def hasPermission(self, role, classname=_marker):
591         ''' Determine if the user has the Role.
593             The class being tested defaults to the template's class, but may
594             be overidden for this test by suppling an alternate classname.
595         '''
596         if classname is self._marker:
597             classname = self._default_classname
598         return self._security.hasPermission(role, self._nodeid, classname)
600 class HTMLProperty:
601     ''' String, Number, Date, Interval HTMLProperty
603         Hase useful attributes:
605          _name  the name of the property
606          _value the value of the property if any
608         A wrapper object which may be stringified for the plain() behaviour.
609     '''
610     def __init__(self, client, nodeid, prop, name, value):
611         self._client = client
612         self._db = client.db
613         self._nodeid = nodeid
614         self._prop = prop
615         self._name = name
616         self._value = value
617     def __repr__(self):
618         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
619     def __str__(self):
620         return self.plain()
621     def __cmp__(self, other):
622         if isinstance(other, HTMLProperty):
623             return cmp(self._value, other._value)
624         return cmp(self._value, other)
626 class StringHTMLProperty(HTMLProperty):
627     def plain(self, escape=0):
628         if self._value is None:
629             return ''
630         if escape:
631             return cgi.escape(str(self._value))
632         return str(self._value)
634     def stext(self, escape=0):
635         s = self.plain(escape=escape)
636         if not StructuredText:
637             return s
638         return StructuredText(s,level=1,header=0)
640     def field(self, size = 30):
641         if self._value is None:
642             value = ''
643         else:
644             value = cgi.escape(str(self._value))
645             value = '&quot;'.join(value.split('"'))
646         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
648     def multiline(self, escape=0, rows=5, cols=40):
649         if self._value is None:
650             value = ''
651         else:
652             value = cgi.escape(str(self._value))
653             value = '&quot;'.join(value.split('"'))
654         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
655             self._name, rows, cols, value)
657     def email(self, escape=1):
658         ''' fudge email '''
659         if self._value is None: value = ''
660         else: value = str(self._value)
661         value = value.replace('@', ' at ')
662         value = value.replace('.', ' ')
663         if escape:
664             value = cgi.escape(value)
665         return value
667 class PasswordHTMLProperty(HTMLProperty):
668     def plain(self):
669         if self._value is None:
670             return ''
671         return _('*encrypted*')
673     def field(self, size = 30):
674         return '<input type="password" name="%s" size="%s">'%(self._name, size)
676 class NumberHTMLProperty(HTMLProperty):
677     def plain(self):
678         return str(self._value)
680     def field(self, size = 30):
681         if self._value is None:
682             value = ''
683         else:
684             value = cgi.escape(str(self._value))
685             value = '&quot;'.join(value.split('"'))
686         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
688 class BooleanHTMLProperty(HTMLProperty):
689     def plain(self):
690         if self.value is None:
691             return ''
692         return self._value and "Yes" or "No"
694     def field(self):
695         checked = self._value and "checked" or ""
696         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
697             checked)
698         if checked:
699             checked = ""
700         else:
701             checked = "checked"
702         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
703             checked)
704         return s
706 class DateHTMLProperty(HTMLProperty):
707     def plain(self):
708         if self._value is None:
709             return ''
710         return str(self._value)
712     def field(self, size = 30):
713         if self._value is None:
714             value = ''
715         else:
716             value = cgi.escape(str(self._value))
717             value = '&quot;'.join(value.split('"'))
718         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
720     def reldate(self, pretty=1):
721         if not self._value:
722             return ''
724         # figure the interval
725         interval = date.Date('.') - self._value
726         if pretty:
727             return interval.pretty()
728         return str(interval)
730 class IntervalHTMLProperty(HTMLProperty):
731     def plain(self):
732         if self._value is None:
733             return ''
734         return str(self._value)
736     def pretty(self):
737         return self._value.pretty()
739     def field(self, size = 30):
740         if self._value is None:
741             value = ''
742         else:
743             value = cgi.escape(str(self._value))
744             value = '&quot;'.join(value.split('"'))
745         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
747 class LinkHTMLProperty(HTMLProperty):
748     ''' Link HTMLProperty
749         Include the above as well as being able to access the class
750         information. Stringifying the object itself results in the value
751         from the item being displayed. Accessing attributes of this object
752         result in the appropriate entry from the class being queried for the
753         property accessed (so item/assignedto/name would look up the user
754         entry identified by the assignedto property on item, and then the
755         name property of that user)
756     '''
757     def __getattr__(self, attr):
758         ''' return a new HTMLItem '''
759         #print 'getattr', (self, attr, self._value)
760         if not self._value:
761             raise AttributeError, "Can't access missing value"
762         if self._prop.classname == 'user':
763             klass = HTMLItem
764         else:
765             klass = HTMLUser
766         i = klass(self._client, self._prop.classname, self._value)
767         return getattr(i, attr)
769     def plain(self, escape=0):
770         if self._value is None:
771             return _('[unselected]')
772         linkcl = self._db.classes[self._prop.classname]
773         k = linkcl.labelprop(1)
774         value = str(linkcl.get(self._value, k))
775         if escape:
776             value = cgi.escape(value)
777         return value
779     def field(self):
780         linkcl = self._db.getclass(self._prop.classname)
781         if linkcl.getprops().has_key('order'):  
782             sort_on = 'order'  
783         else:  
784             sort_on = linkcl.labelprop()  
785         options = linkcl.filter(None, {}, [sort_on], []) 
786         # TODO: make this a field display, not a menu one!
787         l = ['<select name="%s">'%property]
788         k = linkcl.labelprop(1)
789         if value is None:
790             s = 'selected '
791         else:
792             s = ''
793         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
794         for optionid in options:
795             option = linkcl.get(optionid, k)
796             s = ''
797             if optionid == value:
798                 s = 'selected '
799             if showid:
800                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
801             else:
802                 lab = option
803             if size is not None and len(lab) > size:
804                 lab = lab[:size-3] + '...'
805             lab = cgi.escape(lab)
806             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
807         l.append('</select>')
808         return '\n'.join(l)
810     def download(self, showid=0):
811         linkname = self._prop.classname
812         linkcl = self._db.getclass(linkname)
813         k = linkcl.labelprop(1)
814         linkvalue = cgi.escape(str(linkcl.get(self._value, k)))
815         if showid:
816             label = value
817             title = ' title="%s"'%linkvalue
818             # note ... this should be urllib.quote(linkcl.get(value, k))
819         else:
820             label = linkvalue
821             title = ''
822         return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self._value,
823             linkvalue, title, label)
825     def menu(self, size=None, height=None, showid=0, additional=[],
826             **conditions):
827         value = self._value
829         # sort function
830         sortfunc = make_sort_function(self._db, self._prop.classname)
832         # force the value to be a single choice
833         if isinstance(value, type('')):
834             value = value[0]
835         linkcl = self._db.getclass(self._prop.classname)
836         l = ['<select name="%s">'%self._name]
837         k = linkcl.labelprop(1)
838         s = ''
839         if value is None:
840             s = 'selected '
841         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
842         if linkcl.getprops().has_key('order'):  
843             sort_on = ('+', 'order')
844         else:  
845             sort_on = ('+', linkcl.labelprop())
846         options = linkcl.filter(None, conditions, sort_on, (None, None))
847         for optionid in options:
848             option = linkcl.get(optionid, k)
849             s = ''
850             if value in [optionid, option]:
851                 s = 'selected '
852             if showid:
853                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
854             else:
855                 lab = option
856             if size is not None and len(lab) > size:
857                 lab = lab[:size-3] + '...'
858             if additional:
859                 m = []
860                 for propname in additional:
861                     m.append(linkcl.get(optionid, propname))
862                 lab = lab + ' (%s)'%', '.join(map(str, m))
863             lab = cgi.escape(lab)
864             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
865         l.append('</select>')
866         return '\n'.join(l)
868 #    def checklist(self, ...)
870 class MultilinkHTMLProperty(HTMLProperty):
871     ''' Multilink HTMLProperty
873         Also be iterable, returning a wrapper object like the Link case for
874         each entry in the multilink.
875     '''
876     def __len__(self):
877         ''' length of the multilink '''
878         return len(self._value)
880     def __getattr__(self, attr):
881         ''' no extended attribute accesses make sense here '''
882         raise AttributeError, attr
884     def __getitem__(self, num):
885         ''' iterate and return a new HTMLItem
886         '''
887         #print 'getitem', (self, num)
888         value = self._value[num]
889         if self._prop.classname == 'user':
890             klass = HTMLUser
891         else:
892             klass = HTMLItem
893         return klass(self._client, self._prop.classname, value)
895     def reverse(self):
896         ''' return the list in reverse order
897         '''
898         l = self._value[:]
899         l.reverse()
900         if self._prop.classname == 'user':
901             klass = HTMLUser
902         else:
903             klass = HTMLItem
904         return [klass(self._client, self._prop.classname, value) for value in l]
906     def plain(self, escape=0):
907         linkcl = self._db.classes[self._prop.classname]
908         k = linkcl.labelprop(1)
909         labels = []
910         for v in self._value:
911             labels.append(linkcl.get(v, k))
912         value = ', '.join(labels)
913         if escape:
914             value = cgi.escape(value)
915         return value
917     def field(self, size=30, showid=0):
918         sortfunc = make_sort_function(self._db, self._prop.classname)
919         linkcl = self._db.getclass(self._prop.classname)
920         value = self._value[:]
921         if value:
922             value.sort(sortfunc)
923         # map the id to the label property
924         if not showid:
925             k = linkcl.labelprop(1)
926             value = [linkcl.get(v, k) for v in value]
927         value = cgi.escape(','.join(value))
928         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
930     def menu(self, size=None, height=None, showid=0, additional=[],
931             **conditions):
932         value = self._value
934         # sort function
935         sortfunc = make_sort_function(self._db, self._prop.classname)
937         linkcl = self._db.getclass(self._prop.classname)
938         if linkcl.getprops().has_key('order'):  
939             sort_on = ('+', 'order')
940         else:  
941             sort_on = ('+', linkcl.labelprop())
942         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
943         height = height or min(len(options), 7)
944         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
945         k = linkcl.labelprop(1)
946         for optionid in options:
947             option = linkcl.get(optionid, k)
948             s = ''
949             if optionid in value or option in value:
950                 s = 'selected '
951             if showid:
952                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
953             else:
954                 lab = option
955             if size is not None and len(lab) > size:
956                 lab = lab[:size-3] + '...'
957             if additional:
958                 m = []
959                 for propname in additional:
960                     m.append(linkcl.get(optionid, propname))
961                 lab = lab + ' (%s)'%', '.join(m)
962             lab = cgi.escape(lab)
963             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
964                 lab))
965         l.append('</select>')
966         return '\n'.join(l)
968 # set the propclasses for HTMLItem
969 propclasses = (
970     (hyperdb.String, StringHTMLProperty),
971     (hyperdb.Number, NumberHTMLProperty),
972     (hyperdb.Boolean, BooleanHTMLProperty),
973     (hyperdb.Date, DateHTMLProperty),
974     (hyperdb.Interval, IntervalHTMLProperty),
975     (hyperdb.Password, PasswordHTMLProperty),
976     (hyperdb.Link, LinkHTMLProperty),
977     (hyperdb.Multilink, MultilinkHTMLProperty),
980 def make_sort_function(db, classname):
981     '''Make a sort function for a given class
982     '''
983     linkcl = db.getclass(classname)
984     if linkcl.getprops().has_key('order'):
985         sort_on = 'order'
986     else:
987         sort_on = linkcl.labelprop()
988     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
989         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
990     return sortfunc
992 def handleListCGIValue(value):
993     ''' Value is either a single item or a list of items. Each item has a
994         .value that we're actually interested in.
995     '''
996     if isinstance(value, type([])):
997         return [value.value for value in value]
998     else:
999         return value.value.split(',')
1001 class ShowDict:
1002     ''' A convenience access to the :columns index parameters
1003     '''
1004     def __init__(self, columns):
1005         self.columns = {}
1006         for col in columns:
1007             self.columns[col] = 1
1008     def __getitem__(self, name):
1009         return self.columns.has_key(name)
1011 class HTMLRequest:
1012     ''' The *request*, holding the CGI form and environment.
1014         "form" the CGI form as a cgi.FieldStorage
1015         "env" the CGI environment variables
1016         "url" the current URL path for this request
1017         "base" the base URL for this instance
1018         "user" a HTMLUser instance for this user
1019         "classname" the current classname (possibly None)
1020         "template" the current template (suffix, also possibly None)
1022         Index args:
1023         "columns" dictionary of the columns to display in an index page
1024         "show" a convenience access to columns - request/show/colname will
1025                be true if the columns should be displayed, false otherwise
1026         "sort" index sort column (direction, column name)
1027         "group" index grouping property (direction, column name)
1028         "filter" properties to filter the index on
1029         "filterspec" values to filter the index on
1030         "search_text" text to perform a full-text search on for an index
1032     '''
1033     def __init__(self, client):
1034         self.client = client
1036         # easier access vars
1037         self.form = client.form
1038         self.env = client.env
1039         self.base = client.base
1040         self.url = client.url
1041         self.user = HTMLUser(client, 'user', client.userid)
1043         # store the current class name and action
1044         self.classname = client.classname
1045         self.template = client.template
1047         self._post_init()
1049     def _post_init(self):
1050         ''' Set attributes based on self.form
1051         '''
1052         # extract the index display information from the form
1053         self.columns = []
1054         if self.form.has_key(':columns'):
1055             self.columns = handleListCGIValue(self.form[':columns'])
1056         self.show = ShowDict(self.columns)
1058         # sorting
1059         self.sort = (None, None)
1060         if self.form.has_key(':sort'):
1061             sort = self.form[':sort'].value
1062             if sort.startswith('-'):
1063                 self.sort = ('-', sort[1:])
1064             else:
1065                 self.sort = ('+', sort)
1066         if self.form.has_key(':sortdir'):
1067             self.sort = ('-', self.sort[1])
1069         # grouping
1070         self.group = (None, None)
1071         if self.form.has_key(':group'):
1072             group = self.form[':group'].value
1073             if group.startswith('-'):
1074                 self.group = ('-', group[1:])
1075             else:
1076                 self.group = ('+', group)
1077         if self.form.has_key(':groupdir'):
1078             self.group = ('-', self.group[1])
1080         # filtering
1081         self.filter = []
1082         if self.form.has_key(':filter'):
1083             self.filter = handleListCGIValue(self.form[':filter'])
1084         self.filterspec = {}
1085         if self.classname is not None:
1086             props = self.client.db.getclass(self.classname).getprops()
1087             for name in self.filter:
1088                 if self.form.has_key(name):
1089                     prop = props[name]
1090                     fv = self.form[name]
1091                     if (isinstance(prop, hyperdb.Link) or
1092                             isinstance(prop, hyperdb.Multilink)):
1093                         self.filterspec[name] = handleListCGIValue(fv)
1094                     else:
1095                         self.filterspec[name] = fv.value
1097         # full-text search argument
1098         self.search_text = None
1099         if self.form.has_key(':search_text'):
1100             self.search_text = self.form[':search_text'].value
1102         # pagination - size and start index
1103         # figure batch args
1104         if self.form.has_key(':pagesize'):
1105             self.pagesize = int(self.form[':pagesize'].value)
1106         else:
1107             self.pagesize = 50
1108         if self.form.has_key(':startwith'):
1109             self.startwith = int(self.form[':startwith'].value)
1110         else:
1111             self.startwith = 0
1113     def updateFromURL(self, url):
1114         ''' Parse the URL for query args, and update my attributes using the
1115             values.
1116         ''' 
1117         self.form = {}
1118         for name, value in cgi.parse_qsl(url):
1119             if self.form.has_key(name):
1120                 if isinstance(self.form[name], type([])):
1121                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1122                 else:
1123                     self.form[name] = [self.form[name],
1124                         cgi.MiniFieldStorage(name, value)]
1125             else:
1126                 self.form[name] = cgi.MiniFieldStorage(name, value)
1127         self._post_init()
1129     def update(self, kwargs):
1130         ''' Update my attributes using the keyword args
1131         '''
1132         self.__dict__.update(kwargs)
1133         if kwargs.has_key('columns'):
1134             self.show = ShowDict(self.columns)
1136     def description(self):
1137         ''' Return a description of the request - handle for the page title.
1138         '''
1139         s = [self.client.db.config.TRACKER_NAME]
1140         if self.classname:
1141             if self.client.nodeid:
1142                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1143             else:
1144                 s.append('- index of '+self.classname)
1145         else:
1146             s.append('- home')
1147         return ' '.join(s)
1149     def __str__(self):
1150         d = {}
1151         d.update(self.__dict__)
1152         f = ''
1153         for k in self.form.keys():
1154             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1155         d['form'] = f
1156         e = ''
1157         for k,v in self.env.items():
1158             e += '\n     %r=%r'%(k, v)
1159         d['env'] = e
1160         return '''
1161 form: %(form)s
1162 url: %(url)r
1163 base: %(base)r
1164 classname: %(classname)r
1165 template: %(template)r
1166 columns: %(columns)r
1167 sort: %(sort)r
1168 group: %(group)r
1169 filter: %(filter)r
1170 search_text: %(search_text)r
1171 pagesize: %(pagesize)r
1172 startwith: %(startwith)r
1173 env: %(env)s
1174 '''%d
1176     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1177             filterspec=1):
1178         ''' return the current index args as form elements '''
1179         l = []
1180         s = '<input type="hidden" name="%s" value="%s">'
1181         if columns and self.columns:
1182             l.append(s%(':columns', ','.join(self.columns)))
1183         if sort and self.sort[1] is not None:
1184             if self.sort[0] == '-':
1185                 val = '-'+self.sort[1]
1186             else:
1187                 val = self.sort[1]
1188             l.append(s%(':sort', val))
1189         if group and self.group[1] is not None:
1190             if self.group[0] == '-':
1191                 val = '-'+self.group[1]
1192             else:
1193                 val = self.group[1]
1194             l.append(s%(':group', val))
1195         if filter and self.filter:
1196             l.append(s%(':filter', ','.join(self.filter)))
1197         if filterspec:
1198             for k,v in self.filterspec.items():
1199                 l.append(s%(k, ','.join(v)))
1200         if self.search_text:
1201             l.append(s%(':search_text', self.search_text))
1202         l.append(s%(':pagesize', self.pagesize))
1203         l.append(s%(':startwith', self.startwith))
1204         return '\n'.join(l)
1206     def indexargs_href(self, url, args):
1207         ''' embed the current index args in a URL '''
1208         l = ['%s=%s'%(k,v) for k,v in args.items()]
1209         if self.columns and not args.has_key(':columns'):
1210             l.append(':columns=%s'%(','.join(self.columns)))
1211         if self.sort[1] is not None and not args.has_key(':sort'):
1212             if self.sort[0] == '-':
1213                 val = '-'+self.sort[1]
1214             else:
1215                 val = self.sort[1]
1216             l.append(':sort=%s'%val)
1217         if self.group[1] is not None and not args.has_key(':group'):
1218             if self.group[0] == '-':
1219                 val = '-'+self.group[1]
1220             else:
1221                 val = self.group[1]
1222             l.append(':group=%s'%val)
1223         if self.filter and not args.has_key(':columns'):
1224             l.append(':filter=%s'%(','.join(self.filter)))
1225         for k,v in self.filterspec.items():
1226             if not args.has_key(k):
1227                 l.append('%s=%s'%(k, ','.join(v)))
1228         if self.search_text and not args.has_key(':search_text'):
1229             l.append(':search_text=%s'%self.search_text)
1230         if not args.has_key(':pagesize'):
1231             l.append(':pagesize=%s'%self.pagesize)
1232         if not args.has_key(':startwith'):
1233             l.append(':startwith=%s'%self.startwith)
1234         return '%s?%s'%(url, '&'.join(l))
1236     def base_javascript(self):
1237         return '''
1238 <script language="javascript">
1239 submitted = false;
1240 function submit_once() {
1241     if (submitted) {
1242         alert("Your request is being processed.\\nPlease be patient.");
1243         return 0;
1244     }
1245     submitted = true;
1246     return 1;
1249 function help_window(helpurl, width, height) {
1250     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1252 </script>
1253 '''%self.base
1255     def batch(self):
1256         ''' Return a batch object for results from the "current search"
1257         '''
1258         filterspec = self.filterspec
1259         sort = self.sort
1260         group = self.group
1262         # get the list of ids we're batching over
1263         klass = self.client.db.getclass(self.classname)
1264         if self.search_text:
1265             matches = self.client.db.indexer.search(
1266                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1267         else:
1268             matches = None
1269         l = klass.filter(matches, filterspec, sort, group)
1271         # return the batch object
1272         return Batch(self.client, self.classname, l, self.pagesize,
1273             self.startwith)
1276 # extend the standard ZTUtils Batch object to remove dependency on
1277 # Acquisition and add a couple of useful methods
1278 class Batch(ZTUtils.Batch):
1279     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1280         self.client = client
1281         self.classname = classname
1282         self.last_index = self.last_item = None
1283         self.current_item = None
1284         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1286     # overwrite so we can late-instantiate the HTMLItem instance
1287     def __getitem__(self, index):
1288         if index < 0:
1289             if index + self.end < self.first: raise IndexError, index
1290             return self._sequence[index + self.end]
1291         
1292         if index >= self.length: raise IndexError, index
1294         # move the last_item along - but only if the fetched index changes
1295         # (for some reason, index 0 is fetched twice)
1296         if index != self.last_index:
1297             self.last_item = self.current_item
1298             self.last_index = index
1300         # wrap the return in an HTMLItem
1301         if self.classname == 'user':
1302             klass = HTMLUser
1303         else:
1304             klass = HTMLItem
1305         self.current_item = klass(self.client, self.classname,
1306             self._sequence[index+self.first])
1307         return self.current_item
1309     def propchanged(self, property):
1310         ''' Detect if the property marked as being the group property
1311             changed in the last iteration fetch
1312         '''
1313         if (self.last_item is None or
1314                 self.last_item[property] != self.current_item[property]):
1315             return 1
1316         return 0
1318     # override these 'cos we don't have access to acquisition
1319     def previous(self):
1320         if self.start == 1:
1321             return None
1322         return Batch(self.client, self.classname, self._sequence, self._size,
1323             self.first - self._size + self.overlap, 0, self.orphan,
1324             self.overlap)
1326     def next(self):
1327         try:
1328             self._sequence[self.end]
1329         except IndexError:
1330             return None
1331         return Batch(self.client, self.classname, self._sequence, self._size,
1332             self.end - self.overlap, 0, self.orphan, self.overlap)
1334     def length(self):
1335         self.sequence_length = l = len(self._sequence)
1336         return l