Code

f1b78b448790ed8da5cf57b7dc7e219f38a89e1b
[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=None, label='list', width='500',
352             height='400'):
353         ''' Pop up a javascript window with class help
355             This generates a link to a popup window which displays the 
356             properties indicated by "properties" of the class named by
357             "classname". The "properties" should be a comma-separated list
358             (eg. 'id,name,description'). Properties defaults to all the
359             properties of a class (excluding id, creator, created and
360             activity).
362             You may optionally override the label displayed, the width and
363             height. The popup window will be resizable and scrollable.
364         '''
365         if properties is None:
366             properties = self._klass.getprops(protected=0).keys()
367             properties.sort()
368             properties = ','.join(properties)
369         return '<a href="javascript:help_window(\'%s?:template=help&' \
370             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
371             '(%s)</b></a>'%(self.classname, properties, width, height, label)
373     def submit(self, label="Submit New Entry"):
374         ''' Generate a submit button (and action hidden element)
375         '''
376         return '  <input type="hidden" name=":action" value="new">\n'\
377         '  <input type="submit" name="submit" value="%s">'%label
379     def history(self):
380         return 'New node - no history'
382     def renderWith(self, name, **kwargs):
383         ''' Render this class with the given template.
384         '''
385         # create a new request and override the specified args
386         req = HTMLRequest(self._client)
387         req.classname = self.classname
388         req.update(kwargs)
390         # new template, using the specified classname and request
391         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
393         # use our fabricated request
394         return pt.render(self._client, self.classname, req)
396 class HTMLItem:
397     ''' Accesses through an *item*
398     '''
399     def __init__(self, client, classname, nodeid):
400         self._client = client
401         self._db = client.db
402         self._classname = classname
403         self._nodeid = nodeid
404         self._klass = self._db.getclass(classname)
405         self._props = self._klass.getprops()
407     def __repr__(self):
408         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
409             self._nodeid)
411     def __getitem__(self, item):
412         ''' return an HTMLProperty instance
413         '''
414        #print 'HTMLItem.getitem', (self, item)
415         if item == 'id':
416             return self._nodeid
418         # get the property
419         prop = self._props[item]
421         # get the value, handling missing values
422         value = self._klass.get(self._nodeid, item, None)
423         if value is None:
424             if isinstance(self._props[item], hyperdb.Multilink):
425                 value = []
427         # look up the correct HTMLProperty class
428         for klass, htmlklass in propclasses:
429             if isinstance(prop, klass):
430                 return htmlklass(self._client, self._nodeid, prop, item, value)
432         raise KeyErorr, item
434     def __getattr__(self, attr):
435         ''' convenience access to properties '''
436         try:
437             return self[attr]
438         except KeyError:
439             raise AttributeError, attr
440     
441     def submit(self, label="Submit Changes"):
442         ''' Generate a submit button (and action hidden element)
443         '''
444         return '  <input type="hidden" name=":action" value="edit">\n'\
445         '  <input type="submit" name="submit" value="%s">'%label
447     def journal(self, direction='descending'):
448         ''' Return a list of HTMLJournalEntry instances.
449         '''
450         # XXX do this
451         return []
453     def history(self, direction='descending'):
454         l = ['<table class="history">'
455              '<tr><th colspan="4" class="header">',
456              _('History'),
457              '</th></tr><tr>',
458              _('<th>Date</th>'),
459              _('<th>User</th>'),
460              _('<th>Action</th>'),
461              _('<th>Args</th>'),
462             '</tr>']
463         comments = {}
464         history = self._klass.history(self._nodeid)
465         history.sort()
466         if direction == 'descending':
467             history.reverse()
468         for id, evt_date, user, action, args in history:
469             date_s = str(evt_date).replace("."," ")
470             arg_s = ''
471             if action == 'link' and type(args) == type(()):
472                 if len(args) == 3:
473                     linkcl, linkid, key = args
474                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
475                         linkcl, linkid, key)
476                 else:
477                     arg_s = str(args)
479             elif action == 'unlink' and type(args) == type(()):
480                 if len(args) == 3:
481                     linkcl, linkid, key = args
482                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
483                         linkcl, linkid, key)
484                 else:
485                     arg_s = str(args)
487             elif type(args) == type({}):
488                 cell = []
489                 for k in args.keys():
490                     # try to get the relevant property and treat it
491                     # specially
492                     try:
493                         prop = self._props[k]
494                     except KeyError:
495                         prop = None
496                     if prop is not None:
497                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
498                                 isinstance(prop, hyperdb.Link)):
499                             # figure what the link class is
500                             classname = prop.classname
501                             try:
502                                 linkcl = self._db.getclass(classname)
503                             except KeyError:
504                                 labelprop = None
505                                 comments[classname] = _('''The linked class
506                                     %(classname)s no longer exists''')%locals()
507                             labelprop = linkcl.labelprop(1)
508                             hrefable = os.path.exists(
509                                 os.path.join(self._db.config.TEMPLATES,
510                                 classname+'.item'))
512                         if isinstance(prop, hyperdb.Multilink) and \
513                                 len(args[k]) > 0:
514                             ml = []
515                             for linkid in args[k]:
516                                 if isinstance(linkid, type(())):
517                                     sublabel = linkid[0] + ' '
518                                     linkids = linkid[1]
519                                 else:
520                                     sublabel = ''
521                                     linkids = [linkid]
522                                 subml = []
523                                 for linkid in linkids:
524                                     label = classname + linkid
525                                     # if we have a label property, try to use it
526                                     # TODO: test for node existence even when
527                                     # there's no labelprop!
528                                     try:
529                                         if labelprop is not None:
530                                             label = linkcl.get(linkid, labelprop)
531                                     except IndexError:
532                                         comments['no_link'] = _('''<strike>The
533                                             linked node no longer
534                                             exists</strike>''')
535                                         subml.append('<strike>%s</strike>'%label)
536                                     else:
537                                         if hrefable:
538                                             subml.append('<a href="%s%s">%s</a>'%(
539                                                 classname, linkid, label))
540                                 ml.append(sublabel + ', '.join(subml))
541                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
542                         elif isinstance(prop, hyperdb.Link) and args[k]:
543                             label = classname + args[k]
544                             # if we have a label property, try to use it
545                             # TODO: test for node existence even when
546                             # there's no labelprop!
547                             if labelprop is not None:
548                                 try:
549                                     label = linkcl.get(args[k], labelprop)
550                                 except IndexError:
551                                     comments['no_link'] = _('''<strike>The
552                                         linked node no longer
553                                         exists</strike>''')
554                                     cell.append(' <strike>%s</strike>,\n'%label)
555                                     # "flag" this is done .... euwww
556                                     label = None
557                             if label is not None:
558                                 if hrefable:
559                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
560                                         classname, args[k], label))
561                                 else:
562                                     cell.append('%s: %s' % (k,label))
564                         elif isinstance(prop, hyperdb.Date) and args[k]:
565                             d = date.Date(args[k])
566                             cell.append('%s: %s'%(k, str(d)))
568                         elif isinstance(prop, hyperdb.Interval) and args[k]:
569                             d = date.Interval(args[k])
570                             cell.append('%s: %s'%(k, str(d)))
572                         elif isinstance(prop, hyperdb.String) and args[k]:
573                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
575                         elif not args[k]:
576                             cell.append('%s: (no value)\n'%k)
578                         else:
579                             cell.append('%s: %s\n'%(k, str(args[k])))
580                     else:
581                         # property no longer exists
582                         comments['no_exist'] = _('''<em>The indicated property
583                             no longer exists</em>''')
584                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
585                 arg_s = '<br />'.join(cell)
586             else:
587                 # unkown event!!
588                 comments['unknown'] = _('''<strong><em>This event is not
589                     handled by the history display!</em></strong>''')
590                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
591             date_s = date_s.replace(' ', '&nbsp;')
592             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
593                 date_s, user, action, arg_s))
594         if comments:
595             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
596         for entry in comments.values():
597             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
598         l.append('</table>')
599         return '\n'.join(l)
601     def renderQueryForm(self):
602         ''' Render this item, which is a query, as a search form.
603         '''
604         # create a new request and override the specified args
605         req = HTMLRequest(self._client)
606         req.classname = self._klass.get(self._nodeid, 'klass')
607         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
609         # new template, using the specified classname and request
610         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
612         # use our fabricated request
613         return pt.render(self._client, req.classname, req)
615 class HTMLUser(HTMLItem):
616     ''' Accesses through the *user* (a special case of item)
617     '''
618     def __init__(self, client, classname, nodeid):
619         HTMLItem.__init__(self, client, 'user', nodeid)
620         self._default_classname = client.classname
622         # used for security checks
623         self._security = client.db.security
624     _marker = []
625     def hasPermission(self, role, classname=_marker):
626         ''' Determine if the user has the Role.
628             The class being tested defaults to the template's class, but may
629             be overidden for this test by suppling an alternate classname.
630         '''
631         if classname is self._marker:
632             classname = self._default_classname
633         return self._security.hasPermission(role, self._nodeid, classname)
635 class HTMLProperty:
636     ''' String, Number, Date, Interval HTMLProperty
638         Hase useful attributes:
640          _name  the name of the property
641          _value the value of the property if any
643         A wrapper object which may be stringified for the plain() behaviour.
644     '''
645     def __init__(self, client, nodeid, prop, name, value):
646         self._client = client
647         self._db = client.db
648         self._nodeid = nodeid
649         self._prop = prop
650         self._name = name
651         self._value = value
652     def __repr__(self):
653         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
654     def __str__(self):
655         return self.plain()
656     def __cmp__(self, other):
657         if isinstance(other, HTMLProperty):
658             return cmp(self._value, other._value)
659         return cmp(self._value, other)
661 class StringHTMLProperty(HTMLProperty):
662     def plain(self, escape=0):
663         if self._value is None:
664             return ''
665         if escape:
666             return cgi.escape(str(self._value))
667         return str(self._value)
669     def stext(self, escape=0):
670         s = self.plain(escape=escape)
671         if not StructuredText:
672             return s
673         return StructuredText(s,level=1,header=0)
675     def field(self, size = 30):
676         if self._value is None:
677             value = ''
678         else:
679             value = cgi.escape(str(self._value))
680             value = '&quot;'.join(value.split('"'))
681         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
683     def multiline(self, escape=0, rows=5, cols=40):
684         if self._value is None:
685             value = ''
686         else:
687             value = cgi.escape(str(self._value))
688             value = '&quot;'.join(value.split('"'))
689         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
690             self._name, rows, cols, value)
692     def email(self, escape=1):
693         ''' fudge email '''
694         if self._value is None: value = ''
695         else: value = str(self._value)
696         value = value.replace('@', ' at ')
697         value = value.replace('.', ' ')
698         if escape:
699             value = cgi.escape(value)
700         return value
702 class PasswordHTMLProperty(HTMLProperty):
703     def plain(self):
704         if self._value is None:
705             return ''
706         return _('*encrypted*')
708     def field(self, size = 30):
709         return '<input type="password" name="%s" size="%s">'%(self._name, size)
711 class NumberHTMLProperty(HTMLProperty):
712     def plain(self):
713         return str(self._value)
715     def field(self, size = 30):
716         if self._value is None:
717             value = ''
718         else:
719             value = cgi.escape(str(self._value))
720             value = '&quot;'.join(value.split('"'))
721         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
723 class BooleanHTMLProperty(HTMLProperty):
724     def plain(self):
725         if self.value is None:
726             return ''
727         return self._value and "Yes" or "No"
729     def field(self):
730         checked = self._value and "checked" or ""
731         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
732             checked)
733         if checked:
734             checked = ""
735         else:
736             checked = "checked"
737         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
738             checked)
739         return s
741 class DateHTMLProperty(HTMLProperty):
742     def plain(self):
743         if self._value is None:
744             return ''
745         return str(self._value)
747     def field(self, size = 30):
748         if self._value is None:
749             value = ''
750         else:
751             value = cgi.escape(str(self._value))
752             value = '&quot;'.join(value.split('"'))
753         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
755     def reldate(self, pretty=1):
756         if not self._value:
757             return ''
759         # figure the interval
760         interval = date.Date('.') - self._value
761         if pretty:
762             return interval.pretty()
763         return str(interval)
765 class IntervalHTMLProperty(HTMLProperty):
766     def plain(self):
767         if self._value is None:
768             return ''
769         return str(self._value)
771     def pretty(self):
772         return self._value.pretty()
774     def field(self, size = 30):
775         if self._value is None:
776             value = ''
777         else:
778             value = cgi.escape(str(self._value))
779             value = '&quot;'.join(value.split('"'))
780         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
782 class LinkHTMLProperty(HTMLProperty):
783     ''' Link HTMLProperty
784         Include the above as well as being able to access the class
785         information. Stringifying the object itself results in the value
786         from the item being displayed. Accessing attributes of this object
787         result in the appropriate entry from the class being queried for the
788         property accessed (so item/assignedto/name would look up the user
789         entry identified by the assignedto property on item, and then the
790         name property of that user)
791     '''
792     def __getattr__(self, attr):
793         ''' return a new HTMLItem '''
794        #print 'Link.getattr', (self, attr, self._value)
795         if not self._value:
796             raise AttributeError, "Can't access missing value"
797         if self._prop.classname == 'user':
798             klass = HTMLUser
799         else:
800             klass = HTMLItem
801         i = klass(self._client, self._prop.classname, self._value)
802         return getattr(i, attr)
804     def plain(self, escape=0):
805         if self._value is None:
806             return ''
807         linkcl = self._db.classes[self._prop.classname]
808         k = linkcl.labelprop(1)
809         value = str(linkcl.get(self._value, k))
810         if escape:
811             value = cgi.escape(value)
812         return value
814     def field(self):
815         linkcl = self._db.getclass(self._prop.classname)
816         if linkcl.getprops().has_key('order'):  
817             sort_on = 'order'  
818         else:  
819             sort_on = linkcl.labelprop()  
820         options = linkcl.filter(None, {}, [sort_on], []) 
821         # TODO: make this a field display, not a menu one!
822         l = ['<select name="%s">'%property]
823         k = linkcl.labelprop(1)
824         if value is None:
825             s = 'selected '
826         else:
827             s = ''
828         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
829         for optionid in options:
830             option = linkcl.get(optionid, k)
831             s = ''
832             if optionid == value:
833                 s = 'selected '
834             if showid:
835                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
836             else:
837                 lab = option
838             if size is not None and len(lab) > size:
839                 lab = lab[:size-3] + '...'
840             lab = cgi.escape(lab)
841             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
842         l.append('</select>')
843         return '\n'.join(l)
845     def menu(self, size=None, height=None, showid=0, additional=[],
846             **conditions):
847         value = self._value
849         # sort function
850         sortfunc = make_sort_function(self._db, self._prop.classname)
852         # force the value to be a single choice
853         if isinstance(value, type('')):
854             value = value[0]
855         linkcl = self._db.getclass(self._prop.classname)
856         l = ['<select name="%s">'%self._name]
857         k = linkcl.labelprop(1)
858         s = ''
859         if value is None:
860             s = 'selected '
861         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
862         if linkcl.getprops().has_key('order'):  
863             sort_on = ('+', 'order')
864         else:  
865             sort_on = ('+', linkcl.labelprop())
866         options = linkcl.filter(None, conditions, sort_on, (None, None))
867         for optionid in options:
868             option = linkcl.get(optionid, k)
869             s = ''
870             if value in [optionid, option]:
871                 s = 'selected '
872             if showid:
873                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
874             else:
875                 lab = option
876             if size is not None and len(lab) > size:
877                 lab = lab[:size-3] + '...'
878             if additional:
879                 m = []
880                 for propname in additional:
881                     m.append(linkcl.get(optionid, propname))
882                 lab = lab + ' (%s)'%', '.join(map(str, m))
883             lab = cgi.escape(lab)
884             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
885         l.append('</select>')
886         return '\n'.join(l)
887 #    def checklist(self, ...)
889 class MultilinkHTMLProperty(HTMLProperty):
890     ''' Multilink HTMLProperty
892         Also be iterable, returning a wrapper object like the Link case for
893         each entry in the multilink.
894     '''
895     def __len__(self):
896         ''' length of the multilink '''
897         return len(self._value)
899     def __getattr__(self, attr):
900         ''' no extended attribute accesses make sense here '''
901         raise AttributeError, attr
903     def __getitem__(self, num):
904         ''' iterate and return a new HTMLItem
905         '''
906        #print 'Multi.getitem', (self, num)
907         value = self._value[num]
908         if self._prop.classname == 'user':
909             klass = HTMLUser
910         else:
911             klass = HTMLItem
912         return klass(self._client, self._prop.classname, value)
914     def __contains__(self, value):
915         ''' Support the "in" operator
916         '''
917         return value in self._value
919     def reverse(self):
920         ''' return the list in reverse order
921         '''
922         l = self._value[:]
923         l.reverse()
924         if self._prop.classname == 'user':
925             klass = HTMLUser
926         else:
927             klass = HTMLItem
928         return [klass(self._client, self._prop.classname, value) for value in l]
930     def plain(self, escape=0):
931         linkcl = self._db.classes[self._prop.classname]
932         k = linkcl.labelprop(1)
933         labels = []
934         for v in self._value:
935             labels.append(linkcl.get(v, k))
936         value = ', '.join(labels)
937         if escape:
938             value = cgi.escape(value)
939         return value
941     def field(self, size=30, showid=0):
942         sortfunc = make_sort_function(self._db, self._prop.classname)
943         linkcl = self._db.getclass(self._prop.classname)
944         value = self._value[:]
945         if value:
946             value.sort(sortfunc)
947         # map the id to the label property
948         if not showid:
949             k = linkcl.labelprop(1)
950             value = [linkcl.get(v, k) for v in value]
951         value = cgi.escape(','.join(value))
952         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
954     def menu(self, size=None, height=None, showid=0, additional=[],
955             **conditions):
956         value = self._value
958         # sort function
959         sortfunc = make_sort_function(self._db, self._prop.classname)
961         linkcl = self._db.getclass(self._prop.classname)
962         if linkcl.getprops().has_key('order'):  
963             sort_on = ('+', 'order')
964         else:  
965             sort_on = ('+', linkcl.labelprop())
966         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
967         height = height or min(len(options), 7)
968         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
969         k = linkcl.labelprop(1)
970         for optionid in options:
971             option = linkcl.get(optionid, k)
972             s = ''
973             if optionid in value or option in value:
974                 s = 'selected '
975             if showid:
976                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
977             else:
978                 lab = option
979             if size is not None and len(lab) > size:
980                 lab = lab[:size-3] + '...'
981             if additional:
982                 m = []
983                 for propname in additional:
984                     m.append(linkcl.get(optionid, propname))
985                 lab = lab + ' (%s)'%', '.join(m)
986             lab = cgi.escape(lab)
987             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
988                 lab))
989         l.append('</select>')
990         return '\n'.join(l)
992 # set the propclasses for HTMLItem
993 propclasses = (
994     (hyperdb.String, StringHTMLProperty),
995     (hyperdb.Number, NumberHTMLProperty),
996     (hyperdb.Boolean, BooleanHTMLProperty),
997     (hyperdb.Date, DateHTMLProperty),
998     (hyperdb.Interval, IntervalHTMLProperty),
999     (hyperdb.Password, PasswordHTMLProperty),
1000     (hyperdb.Link, LinkHTMLProperty),
1001     (hyperdb.Multilink, MultilinkHTMLProperty),
1004 def make_sort_function(db, classname):
1005     '''Make a sort function for a given class
1006     '''
1007     linkcl = db.getclass(classname)
1008     if linkcl.getprops().has_key('order'):
1009         sort_on = 'order'
1010     else:
1011         sort_on = linkcl.labelprop()
1012     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1013         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1014     return sortfunc
1016 def handleListCGIValue(value):
1017     ''' Value is either a single item or a list of items. Each item has a
1018         .value that we're actually interested in.
1019     '''
1020     if isinstance(value, type([])):
1021         return [value.value for value in value]
1022     else:
1023         value = value.value.strip()
1024         if not value:
1025             return []
1026         return value.split(',')
1028 class ShowDict:
1029     ''' A convenience access to the :columns index parameters
1030     '''
1031     def __init__(self, columns):
1032         self.columns = {}
1033         for col in columns:
1034             self.columns[col] = 1
1035     def __getitem__(self, name):
1036         return self.columns.has_key(name)
1038 class HTMLRequest:
1039     ''' The *request*, holding the CGI form and environment.
1041         "form" the CGI form as a cgi.FieldStorage
1042         "env" the CGI environment variables
1043         "url" the current URL path for this request
1044         "base" the base URL for this instance
1045         "user" a HTMLUser instance for this user
1046         "classname" the current classname (possibly None)
1047         "template" the current template (suffix, also possibly None)
1049         Index args:
1050         "columns" dictionary of the columns to display in an index page
1051         "show" a convenience access to columns - request/show/colname will
1052                be true if the columns should be displayed, false otherwise
1053         "sort" index sort column (direction, column name)
1054         "group" index grouping property (direction, column name)
1055         "filter" properties to filter the index on
1056         "filterspec" values to filter the index on
1057         "search_text" text to perform a full-text search on for an index
1059     '''
1060     def __init__(self, client):
1061         self.client = client
1063         # easier access vars
1064         self.form = client.form
1065         self.env = client.env
1066         self.base = client.base
1067         self.url = client.url
1068         self.user = HTMLUser(client, 'user', client.userid)
1070         # store the current class name and action
1071         self.classname = client.classname
1072         self.template = client.template
1074         self._post_init()
1076     def _post_init(self):
1077         ''' Set attributes based on self.form
1078         '''
1079         # extract the index display information from the form
1080         self.columns = []
1081         if self.form.has_key(':columns'):
1082             self.columns = handleListCGIValue(self.form[':columns'])
1083         self.show = ShowDict(self.columns)
1085         # sorting
1086         self.sort = (None, None)
1087         if self.form.has_key(':sort'):
1088             sort = self.form[':sort'].value
1089             if sort.startswith('-'):
1090                 self.sort = ('-', sort[1:])
1091             else:
1092                 self.sort = ('+', sort)
1093         if self.form.has_key(':sortdir'):
1094             self.sort = ('-', self.sort[1])
1096         # grouping
1097         self.group = (None, None)
1098         if self.form.has_key(':group'):
1099             group = self.form[':group'].value
1100             if group.startswith('-'):
1101                 self.group = ('-', group[1:])
1102             else:
1103                 self.group = ('+', group)
1104         if self.form.has_key(':groupdir'):
1105             self.group = ('-', self.group[1])
1107         # filtering
1108         self.filter = []
1109         if self.form.has_key(':filter'):
1110             self.filter = handleListCGIValue(self.form[':filter'])
1111         self.filterspec = {}
1112         if self.classname is not None:
1113             props = self.client.db.getclass(self.classname).getprops()
1114             for name in self.filter:
1115                 if self.form.has_key(name):
1116                     prop = props[name]
1117                     fv = self.form[name]
1118                     if (isinstance(prop, hyperdb.Link) or
1119                             isinstance(prop, hyperdb.Multilink)):
1120                         self.filterspec[name] = handleListCGIValue(fv)
1121                     else:
1122                         self.filterspec[name] = fv.value
1124         # full-text search argument
1125         self.search_text = None
1126         if self.form.has_key(':search_text'):
1127             self.search_text = self.form[':search_text'].value
1129         # pagination - size and start index
1130         # figure batch args
1131         if self.form.has_key(':pagesize'):
1132             self.pagesize = int(self.form[':pagesize'].value)
1133         else:
1134             self.pagesize = 50
1135         if self.form.has_key(':startwith'):
1136             self.startwith = int(self.form[':startwith'].value)
1137         else:
1138             self.startwith = 0
1140     def updateFromURL(self, url):
1141         ''' Parse the URL for query args, and update my attributes using the
1142             values.
1143         ''' 
1144         self.form = {}
1145         for name, value in cgi.parse_qsl(url):
1146             if self.form.has_key(name):
1147                 if isinstance(self.form[name], type([])):
1148                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1149                 else:
1150                     self.form[name] = [self.form[name],
1151                         cgi.MiniFieldStorage(name, value)]
1152             else:
1153                 self.form[name] = cgi.MiniFieldStorage(name, value)
1154         self._post_init()
1156     def update(self, kwargs):
1157         ''' Update my attributes using the keyword args
1158         '''
1159         self.__dict__.update(kwargs)
1160         if kwargs.has_key('columns'):
1161             self.show = ShowDict(self.columns)
1163     def description(self):
1164         ''' Return a description of the request - handle for the page title.
1165         '''
1166         s = [self.client.db.config.TRACKER_NAME]
1167         if self.classname:
1168             if self.client.nodeid:
1169                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1170             else:
1171                 s.append('- index of '+self.classname)
1172         else:
1173             s.append('- home')
1174         return ' '.join(s)
1176     def __str__(self):
1177         d = {}
1178         d.update(self.__dict__)
1179         f = ''
1180         for k in self.form.keys():
1181             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1182         d['form'] = f
1183         e = ''
1184         for k,v in self.env.items():
1185             e += '\n     %r=%r'%(k, v)
1186         d['env'] = e
1187         return '''
1188 form: %(form)s
1189 url: %(url)r
1190 base: %(base)r
1191 classname: %(classname)r
1192 template: %(template)r
1193 columns: %(columns)r
1194 sort: %(sort)r
1195 group: %(group)r
1196 filter: %(filter)r
1197 search_text: %(search_text)r
1198 pagesize: %(pagesize)r
1199 startwith: %(startwith)r
1200 env: %(env)s
1201 '''%d
1203     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1204             filterspec=1):
1205         ''' return the current index args as form elements '''
1206         l = []
1207         s = '<input type="hidden" name="%s" value="%s">'
1208         if columns and self.columns:
1209             l.append(s%(':columns', ','.join(self.columns)))
1210         if sort and self.sort[1] is not None:
1211             if self.sort[0] == '-':
1212                 val = '-'+self.sort[1]
1213             else:
1214                 val = self.sort[1]
1215             l.append(s%(':sort', val))
1216         if group and self.group[1] is not None:
1217             if self.group[0] == '-':
1218                 val = '-'+self.group[1]
1219             else:
1220                 val = self.group[1]
1221             l.append(s%(':group', val))
1222         if filter and self.filter:
1223             l.append(s%(':filter', ','.join(self.filter)))
1224         if filterspec:
1225             for k,v in self.filterspec.items():
1226                 l.append(s%(k, ','.join(v)))
1227         if self.search_text:
1228             l.append(s%(':search_text', self.search_text))
1229         l.append(s%(':pagesize', self.pagesize))
1230         l.append(s%(':startwith', self.startwith))
1231         return '\n'.join(l)
1233     def indexargs_href(self, url, args):
1234         ''' embed the current index args in a URL '''
1235         l = ['%s=%s'%(k,v) for k,v in args.items()]
1236         if self.columns and not args.has_key(':columns'):
1237             l.append(':columns=%s'%(','.join(self.columns)))
1238         if self.sort[1] is not None and not args.has_key(':sort'):
1239             if self.sort[0] == '-':
1240                 val = '-'+self.sort[1]
1241             else:
1242                 val = self.sort[1]
1243             l.append(':sort=%s'%val)
1244         if self.group[1] is not None and not args.has_key(':group'):
1245             if self.group[0] == '-':
1246                 val = '-'+self.group[1]
1247             else:
1248                 val = self.group[1]
1249             l.append(':group=%s'%val)
1250         if self.filter and not args.has_key(':columns'):
1251             l.append(':filter=%s'%(','.join(self.filter)))
1252         for k,v in self.filterspec.items():
1253             if not args.has_key(k):
1254                 l.append('%s=%s'%(k, ','.join(v)))
1255         if self.search_text and not args.has_key(':search_text'):
1256             l.append(':search_text=%s'%self.search_text)
1257         if not args.has_key(':pagesize'):
1258             l.append(':pagesize=%s'%self.pagesize)
1259         if not args.has_key(':startwith'):
1260             l.append(':startwith=%s'%self.startwith)
1261         return '%s?%s'%(url, '&'.join(l))
1263     def base_javascript(self):
1264         return '''
1265 <script language="javascript">
1266 submitted = false;
1267 function submit_once() {
1268     if (submitted) {
1269         alert("Your request is being processed.\\nPlease be patient.");
1270         return 0;
1271     }
1272     submitted = true;
1273     return 1;
1276 function help_window(helpurl, width, height) {
1277     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1279 </script>
1280 '''%self.base
1282     def batch(self):
1283         ''' Return a batch object for results from the "current search"
1284         '''
1285         filterspec = self.filterspec
1286         sort = self.sort
1287         group = self.group
1289         # get the list of ids we're batching over
1290         klass = self.client.db.getclass(self.classname)
1291         if self.search_text:
1292             matches = self.client.db.indexer.search(
1293                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1294         else:
1295             matches = None
1296         l = klass.filter(matches, filterspec, sort, group)
1298         # return the batch object
1299         return Batch(self.client, self.classname, l, self.pagesize,
1300             self.startwith)
1303 # extend the standard ZTUtils Batch object to remove dependency on
1304 # Acquisition and add a couple of useful methods
1305 class Batch(ZTUtils.Batch):
1306     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1307         self.client = client
1308         self.classname = classname
1309         self.last_index = self.last_item = None
1310         self.current_item = None
1311         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1313     # overwrite so we can late-instantiate the HTMLItem instance
1314     def __getitem__(self, index):
1315         if index < 0:
1316             if index + self.end < self.first: raise IndexError, index
1317             return self._sequence[index + self.end]
1318         
1319         if index >= self.length: raise IndexError, index
1321         # move the last_item along - but only if the fetched index changes
1322         # (for some reason, index 0 is fetched twice)
1323         if index != self.last_index:
1324             self.last_item = self.current_item
1325             self.last_index = index
1327         # wrap the return in an HTMLItem
1328         if self.classname == 'user':
1329             klass = HTMLUser
1330         else:
1331             klass = HTMLItem
1332         self.current_item = klass(self.client, self.classname,
1333             self._sequence[index+self.first])
1334         return self.current_item
1336     def propchanged(self, property):
1337         ''' Detect if the property marked as being the group property
1338             changed in the last iteration fetch
1339         '''
1340         if (self.last_item is None or
1341                 self.last_item[property] != self.current_item[property]):
1342             return 1
1343         return 0
1345     # override these 'cos we don't have access to acquisition
1346     def previous(self):
1347         if self.start == 1:
1348             return None
1349         return Batch(self.client, self.classname, self._sequence, self._size,
1350             self.first - self._size + self.overlap, 0, self.orphan,
1351             self.overlap)
1353     def next(self):
1354         try:
1355             self._sequence[self.end]
1356         except IndexError:
1357             return None
1358         return Batch(self.client, self.classname, self._sequence, self._size,
1359             self.end - self.overlap, 0, self.orphan, self.overlap)
1361     def length(self):
1362         self.sequence_length = l = len(self._sequence)
1363         return l