Code

no idea why this code existed, but bye bye
[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              'utils': TemplatingUtils(client),
155         }
156         # add in the item if there is one
157         if client.nodeid:
158             if classname == 'user':
159                 c['context'] = HTMLUser(client, classname, client.nodeid)
160             else:
161                 c['context'] = HTMLItem(client, classname, client.nodeid)
162         else:
163             c['context'] = HTMLClass(client, classname)
164         return c
166     def render(self, client, classname, request, **options):
167         """Render this Page Template"""
169         if not self._v_cooked:
170             self._cook()
172         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
174         if self._v_errors:
175             raise PageTemplate.PTRuntimeError, \
176                 'Page Template %s has errors.'%self.id
178         # figure the context
179         classname = classname or client.classname
180         request = request or HTMLRequest(client)
181         c = self.getContext(client, classname, request)
182         c.update({'options': options})
184         # and go
185         output = StringIO.StringIO()
186         TALInterpreter(self._v_program, self._v_macros,
187             getEngine().getContext(c), output, tal=1, strictinsert=0)()
188         return output.getvalue()
190 class HTMLDatabase:
191     ''' Return HTMLClasses for valid class fetches
192     '''
193     def __init__(self, client):
194         self._client = client
196         # we want config to be exposed
197         self.config = client.db.config
199     def __getitem__(self, item):
200         self._client.db.getclass(item)
201         return HTMLClass(self._client, item)
203     def __getattr__(self, attr):
204         try:
205             return self[attr]
206         except KeyError:
207             raise AttributeError, attr
209     def classes(self):
210         l = self._client.db.classes.keys()
211         l.sort()
212         return [HTMLClass(self._client, cn) for cn in l]
214 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
215     cl = db.getclass(prop.classname)
216     l = []
217     for entry in ids:
218         if num_re.match(entry):
219             l.append(entry)
220         else:
221             l.append(cl.lookup(entry))
222     return l
224 class HTMLPermissions:
225     ''' Helpers that provide answers to commonly asked Permission questions.
226     '''
227     def is_edit_ok(self):
228         ''' Is the user allowed to Edit the current class?
229         '''
230         return self._db.security.hasPermission('Edit', self._client.userid,
231             self._classname)
232     def is_view_ok(self):
233         ''' Is the user allowed to View the current class?
234         '''
235         return self._db.security.hasPermission('View', self._client.userid,
236             self._classname)
237     def is_only_view_ok(self):
238         ''' Is the user only allowed to View (ie. not Edit) the current class?
239         '''
240         return self.is_view_ok() and not self.is_edit_ok()
242 class HTMLClass(HTMLPermissions):
243     ''' Accesses through a class (either through *class* or *db.<classname>*)
244     '''
245     def __init__(self, client, classname):
246         self._client = client
247         self._db = client.db
249         # we want classname to be exposed, but _classname gives a
250         # consistent API for extending Class/Item
251         self._classname = self.classname = classname
252         if classname is not None:
253             self._klass = self._db.getclass(self.classname)
254             self._props = self._klass.getprops()
256     def __repr__(self):
257         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
259     def __getitem__(self, item):
260         ''' return an HTMLProperty instance
261         '''
262        #print 'HTMLClass.getitem', (self, item)
264         # we don't exist
265         if item == 'id':
266             return None
268         # get the property
269         prop = self._props[item]
271         # look up the correct HTMLProperty class
272         form = self._client.form
273         for klass, htmlklass in propclasses:
274             if not isinstance(prop, klass):
275                 continue
276             if form.has_key(item):
277                 if isinstance(prop, hyperdb.Multilink):
278                     value = lookupIds(self._db, prop,
279                         handleListCGIValue(form[item]))
280                 elif isinstance(prop, hyperdb.Link):
281                     value = form[item].value.strip()
282                     if value:
283                         value = lookupIds(self._db, prop, [value])[0]
284                     else:
285                         value = None
286                 else:
287                     value = form[item].value.strip() or None
288             else:
289                 if isinstance(prop, hyperdb.Multilink):
290                     value = []
291                 else:
292                     value = None
293             return htmlklass(self._client, '', prop, item, value)
295         # no good
296         raise KeyError, item
298     def __getattr__(self, attr):
299         ''' convenience access '''
300         try:
301             return self[attr]
302         except KeyError:
303             raise AttributeError, attr
305     def properties(self):
306         ''' Return HTMLProperty for all of this class' properties.
307         '''
308         l = []
309         for name, prop in self._props.items():
310             for klass, htmlklass in propclasses:
311                 if isinstance(prop, hyperdb.Multilink):
312                     value = []
313                 else:
314                     value = None
315                 if isinstance(prop, klass):
316                     l.append(htmlklass(self._client, '', prop, name, value))
317         return l
319     def list(self):
320         ''' List all items in this class.
321         '''
322         if self.classname == 'user':
323             klass = HTMLUser
324         else:
325             klass = HTMLItem
327         # get the list and sort it nicely
328         l = self._klass.list()
329         sortfunc = make_sort_function(self._db, self._prop.classname)
330         l.sort(sortfunc)
332         l = [klass(self._client, self.classname, x) for x in l]
333         return l
335     def csv(self):
336         ''' Return the items of this class as a chunk of CSV text.
337         '''
338         # get the CSV module
339         try:
340             import csv
341         except ImportError:
342             return 'Sorry, you need the csv module to use this function.\n'\
343                 'Get it from: http://www.object-craft.com.au/projects/csv/'
345         props = self.propnames()
346         p = csv.parser()
347         s = StringIO.StringIO()
348         s.write(p.join(props) + '\n')
349         for nodeid in self._klass.list():
350             l = []
351             for name in props:
352                 value = self._klass.get(nodeid, name)
353                 if value is None:
354                     l.append('')
355                 elif isinstance(value, type([])):
356                     l.append(':'.join(map(str, value)))
357                 else:
358                     l.append(str(self._klass.get(nodeid, name)))
359             s.write(p.join(l) + '\n')
360         return s.getvalue()
362     def propnames(self):
363         ''' Return the list of the names of the properties of this class.
364         '''
365         idlessprops = self._klass.getprops(protected=0).keys()
366         idlessprops.sort()
367         return ['id'] + idlessprops
369     def filter(self, request=None):
370         ''' Return a list of items from this class, filtered and sorted
371             by the current requested filterspec/filter/sort/group args
372         '''
373         if request is not None:
374             filterspec = request.filterspec
375             sort = request.sort
376             group = request.group
377         if self.classname == 'user':
378             klass = HTMLUser
379         else:
380             klass = HTMLItem
381         l = [klass(self._client, self.classname, x)
382              for x in self._klass.filter(None, filterspec, sort, group)]
383         return l
385     def classhelp(self, properties=None, label='list', width='500',
386             height='400'):
387         ''' Pop up a javascript window with class help
389             This generates a link to a popup window which displays the 
390             properties indicated by "properties" of the class named by
391             "classname". The "properties" should be a comma-separated list
392             (eg. 'id,name,description'). Properties defaults to all the
393             properties of a class (excluding id, creator, created and
394             activity).
396             You may optionally override the label displayed, the width and
397             height. The popup window will be resizable and scrollable.
398         '''
399         if properties is None:
400             properties = self._klass.getprops(protected=0).keys()
401             properties.sort()
402             properties = ','.join(properties)
403         return '<a href="javascript:help_window(\'%s?:template=help&' \
404             ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
405             '(%s)</b></a>'%(self.classname, properties, width, height, label)
407     def submit(self, label="Submit New Entry"):
408         ''' Generate a submit button (and action hidden element)
409         '''
410         return '  <input type="hidden" name=":action" value="new">\n'\
411         '  <input type="submit" name="submit" value="%s">'%label
413     def history(self):
414         return 'New node - no history'
416     def renderWith(self, name, **kwargs):
417         ''' Render this class with the given template.
418         '''
419         # create a new request and override the specified args
420         req = HTMLRequest(self._client)
421         req.classname = self.classname
422         req.update(kwargs)
424         # new template, using the specified classname and request
425         pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
427         # use our fabricated request
428         return pt.render(self._client, self.classname, req)
430 class HTMLItem(HTMLPermissions):
431     ''' Accesses through an *item*
432     '''
433     def __init__(self, client, classname, nodeid):
434         self._client = client
435         self._db = client.db
436         self._classname = classname
437         self._nodeid = nodeid
438         self._klass = self._db.getclass(classname)
439         self._props = self._klass.getprops()
441     def __repr__(self):
442         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
443             self._nodeid)
445     def __getitem__(self, item):
446         ''' return an HTMLProperty instance
447         '''
448        #print 'HTMLItem.getitem', (self, item)
449         if item == 'id':
450             return self._nodeid
452         # get the property
453         prop = self._props[item]
455         # get the value, handling missing values
456         value = self._klass.get(self._nodeid, item, None)
457         if value is None:
458             if isinstance(self._props[item], hyperdb.Multilink):
459                 value = []
461         # look up the correct HTMLProperty class
462         for klass, htmlklass in propclasses:
463             if isinstance(prop, klass):
464                 return htmlklass(self._client, self._nodeid, prop, item, value)
466         raise KeyErorr, item
468     def __getattr__(self, attr):
469         ''' convenience access to properties '''
470         try:
471             return self[attr]
472         except KeyError:
473             raise AttributeError, attr
474     
475     def submit(self, label="Submit Changes"):
476         ''' Generate a submit button (and action hidden element)
477         '''
478         return '  <input type="hidden" name=":action" value="edit">\n'\
479         '  <input type="submit" name="submit" value="%s">'%label
481     def journal(self, direction='descending'):
482         ''' Return a list of HTMLJournalEntry instances.
483         '''
484         # XXX do this
485         return []
487     def history(self, direction='descending'):
488         l = ['<table class="history">'
489              '<tr><th colspan="4" class="header">',
490              _('History'),
491              '</th></tr><tr>',
492              _('<th>Date</th>'),
493              _('<th>User</th>'),
494              _('<th>Action</th>'),
495              _('<th>Args</th>'),
496             '</tr>']
497         comments = {}
498         history = self._klass.history(self._nodeid)
499         history.sort()
500         if direction == 'descending':
501             history.reverse()
502         for id, evt_date, user, action, args in history:
503             date_s = str(evt_date).replace("."," ")
504             arg_s = ''
505             if action == 'link' and type(args) == type(()):
506                 if len(args) == 3:
507                     linkcl, linkid, key = args
508                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
509                         linkcl, linkid, key)
510                 else:
511                     arg_s = str(args)
513             elif action == 'unlink' and type(args) == type(()):
514                 if len(args) == 3:
515                     linkcl, linkid, key = args
516                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
517                         linkcl, linkid, key)
518                 else:
519                     arg_s = str(args)
521             elif type(args) == type({}):
522                 cell = []
523                 for k in args.keys():
524                     # try to get the relevant property and treat it
525                     # specially
526                     try:
527                         prop = self._props[k]
528                     except KeyError:
529                         prop = None
530                     if prop is not None:
531                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
532                                 isinstance(prop, hyperdb.Link)):
533                             # figure what the link class is
534                             classname = prop.classname
535                             try:
536                                 linkcl = self._db.getclass(classname)
537                             except KeyError:
538                                 labelprop = None
539                                 comments[classname] = _('''The linked class
540                                     %(classname)s no longer exists''')%locals()
541                             labelprop = linkcl.labelprop(1)
542                             hrefable = os.path.exists(
543                                 os.path.join(self._db.config.TEMPLATES,
544                                 classname+'.item'))
546                         if isinstance(prop, hyperdb.Multilink) and \
547                                 len(args[k]) > 0:
548                             ml = []
549                             for linkid in args[k]:
550                                 if isinstance(linkid, type(())):
551                                     sublabel = linkid[0] + ' '
552                                     linkids = linkid[1]
553                                 else:
554                                     sublabel = ''
555                                     linkids = [linkid]
556                                 subml = []
557                                 for linkid in linkids:
558                                     label = classname + linkid
559                                     # if we have a label property, try to use it
560                                     # TODO: test for node existence even when
561                                     # there's no labelprop!
562                                     try:
563                                         if labelprop is not None:
564                                             label = linkcl.get(linkid, labelprop)
565                                     except IndexError:
566                                         comments['no_link'] = _('''<strike>The
567                                             linked node no longer
568                                             exists</strike>''')
569                                         subml.append('<strike>%s</strike>'%label)
570                                     else:
571                                         if hrefable:
572                                             subml.append('<a href="%s%s">%s</a>'%(
573                                                 classname, linkid, label))
574                                 ml.append(sublabel + ', '.join(subml))
575                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
576                         elif isinstance(prop, hyperdb.Link) and args[k]:
577                             label = classname + args[k]
578                             # if we have a label property, try to use it
579                             # TODO: test for node existence even when
580                             # there's no labelprop!
581                             if labelprop is not None:
582                                 try:
583                                     label = linkcl.get(args[k], labelprop)
584                                 except IndexError:
585                                     comments['no_link'] = _('''<strike>The
586                                         linked node no longer
587                                         exists</strike>''')
588                                     cell.append(' <strike>%s</strike>,\n'%label)
589                                     # "flag" this is done .... euwww
590                                     label = None
591                             if label is not None:
592                                 if hrefable:
593                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
594                                         classname, args[k], label))
595                                 else:
596                                     cell.append('%s: %s' % (k,label))
598                         elif isinstance(prop, hyperdb.Date) and args[k]:
599                             d = date.Date(args[k])
600                             cell.append('%s: %s'%(k, str(d)))
602                         elif isinstance(prop, hyperdb.Interval) and args[k]:
603                             d = date.Interval(args[k])
604                             cell.append('%s: %s'%(k, str(d)))
606                         elif isinstance(prop, hyperdb.String) and args[k]:
607                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
609                         elif not args[k]:
610                             cell.append('%s: (no value)\n'%k)
612                         else:
613                             cell.append('%s: %s\n'%(k, str(args[k])))
614                     else:
615                         # property no longer exists
616                         comments['no_exist'] = _('''<em>The indicated property
617                             no longer exists</em>''')
618                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
619                 arg_s = '<br />'.join(cell)
620             else:
621                 # unkown event!!
622                 comments['unknown'] = _('''<strong><em>This event is not
623                     handled by the history display!</em></strong>''')
624                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
625             date_s = date_s.replace(' ', '&nbsp;')
626             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
627                 date_s, user, action, arg_s))
628         if comments:
629             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
630         for entry in comments.values():
631             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
632         l.append('</table>')
633         return '\n'.join(l)
635     def renderQueryForm(self):
636         ''' Render this item, which is a query, as a search form.
637         '''
638         # create a new request and override the specified args
639         req = HTMLRequest(self._client)
640         req.classname = self._klass.get(self._nodeid, 'klass')
641         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
643         # new template, using the specified classname and request
644         pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
646         # use our fabricated request
647         return pt.render(self._client, req.classname, req)
649 class HTMLUser(HTMLItem):
650     ''' Accesses through the *user* (a special case of item)
651     '''
652     def __init__(self, client, classname, nodeid):
653         HTMLItem.__init__(self, client, 'user', nodeid)
654         self._default_classname = client.classname
656         # used for security checks
657         self._security = client.db.security
659     _marker = []
660     def hasPermission(self, role, classname=_marker):
661         ''' Determine if the user has the Role.
663             The class being tested defaults to the template's class, but may
664             be overidden for this test by suppling an alternate classname.
665         '''
666         if classname is self._marker:
667             classname = self._default_classname
668         return self._security.hasPermission(role, self._nodeid, classname)
670     def is_edit_ok(self):
671         ''' Is the user allowed to Edit the current class?
672             Also check whether this is the current user's info.
673         '''
674         return self._db.security.hasPermission('Edit', self._client.userid,
675             self._classname) or self._nodeid == self._client.userid
677     def is_view_ok(self):
678         ''' Is the user allowed to View the current class?
679             Also check whether this is the current user's info.
680         '''
681         return self._db.security.hasPermission('Edit', self._client.userid,
682             self._classname) or self._nodeid == self._client.userid
684 class HTMLProperty:
685     ''' String, Number, Date, Interval HTMLProperty
687         Has useful attributes:
689          _name  the name of the property
690          _value the value of the property if any
692         A wrapper object which may be stringified for the plain() behaviour.
693     '''
694     def __init__(self, client, nodeid, prop, name, value):
695         self._client = client
696         self._db = client.db
697         self._nodeid = nodeid
698         self._prop = prop
699         self._name = name
700         self._value = value
701     def __repr__(self):
702         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
703     def __str__(self):
704         return self.plain()
705     def __cmp__(self, other):
706         if isinstance(other, HTMLProperty):
707             return cmp(self._value, other._value)
708         return cmp(self._value, other)
710 class StringHTMLProperty(HTMLProperty):
711     def plain(self, escape=0):
712         ''' Render a "plain" representation of the property
713         '''
714         if self._value is None:
715             return ''
716         if escape:
717             return cgi.escape(str(self._value))
718         return str(self._value)
720     def stext(self, escape=0):
721         ''' Render the value of the property as StructuredText.
723             This requires the StructureText module to be installed separately.
724         '''
725         s = self.plain(escape=escape)
726         if not StructuredText:
727             return s
728         return StructuredText(s,level=1,header=0)
730     def field(self, size = 30):
731         ''' Render a form edit field for the property
732         '''
733         if self._value is None:
734             value = ''
735         else:
736             value = cgi.escape(str(self._value))
737             value = '&quot;'.join(value.split('"'))
738         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
740     def multiline(self, escape=0, rows=5, cols=40):
741         ''' Render a multiline form edit field for the property
742         '''
743         if self._value is None:
744             value = ''
745         else:
746             value = cgi.escape(str(self._value))
747             value = '&quot;'.join(value.split('"'))
748         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
749             self._name, rows, cols, value)
751     def email(self, escape=1):
752         ''' Render the value of the property as an obscured email address
753         '''
754         if self._value is None: value = ''
755         else: value = str(self._value)
756         if value.find('@') != -1:
757             name, domain = value.split('@')
758             domain = ' '.join(domain.split('.')[:-1])
759             name = name.replace('.', ' ')
760             value = '%s at %s ...'%(name, domain)
761         else:
762             value = value.replace('.', ' ')
763         if escape:
764             value = cgi.escape(value)
765         return value
767 class PasswordHTMLProperty(HTMLProperty):
768     def plain(self):
769         ''' Render a "plain" representation of the property
770         '''
771         if self._value is None:
772             return ''
773         return _('*encrypted*')
775     def field(self, size = 30):
776         ''' Render a form edit field for the property.
777         '''
778         return '<input type="password" name="%s" size="%s">'%(self._name, size)
780     def confirm(self, size = 30):
781         ''' Render a second form edit field for the property, used for 
782             confirmation that the user typed the password correctly. Generates
783             a field with name "name:confirm".
784         '''
785         return '<input type="password" name="%s:confirm" size="%s">'%(
786             self._name, size)
788 class NumberHTMLProperty(HTMLProperty):
789     def plain(self):
790         ''' Render a "plain" representation of the property
791         '''
792         return str(self._value)
794     def field(self, size = 30):
795         ''' Render a form edit field for the property
796         '''
797         if self._value is None:
798             value = ''
799         else:
800             value = cgi.escape(str(self._value))
801             value = '&quot;'.join(value.split('"'))
802         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
804 class BooleanHTMLProperty(HTMLProperty):
805     def plain(self):
806         ''' Render a "plain" representation of the property
807         '''
808         if self.value is None:
809             return ''
810         return self._value and "Yes" or "No"
812     def field(self):
813         ''' Render a form edit field for the property
814         '''
815         checked = self._value and "checked" or ""
816         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
817             checked)
818         if checked:
819             checked = ""
820         else:
821             checked = "checked"
822         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
823             checked)
824         return s
826 class DateHTMLProperty(HTMLProperty):
827     def plain(self):
828         ''' Render a "plain" representation of the property
829         '''
830         if self._value is None:
831             return ''
832         return str(self._value)
834     def field(self, size = 30):
835         ''' Render a form edit field for the property
836         '''
837         if self._value is None:
838             value = ''
839         else:
840             value = cgi.escape(str(self._value))
841             value = '&quot;'.join(value.split('"'))
842         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
844     def reldate(self, pretty=1):
845         ''' Render the interval between the date and now.
847             If the "pretty" flag is true, then make the display pretty.
848         '''
849         if not self._value:
850             return ''
852         # figure the interval
853         interval = date.Date('.') - self._value
854         if pretty:
855             return interval.pretty()
856         return str(interval)
858 class IntervalHTMLProperty(HTMLProperty):
859     def plain(self):
860         ''' Render a "plain" representation of the property
861         '''
862         if self._value is None:
863             return ''
864         return str(self._value)
866     def pretty(self):
867         ''' Render the interval in a pretty format (eg. "yesterday")
868         '''
869         return self._value.pretty()
871     def field(self, size = 30):
872         ''' Render a form edit field for the property
873         '''
874         if self._value is None:
875             value = ''
876         else:
877             value = cgi.escape(str(self._value))
878             value = '&quot;'.join(value.split('"'))
879         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
881 class LinkHTMLProperty(HTMLProperty):
882     ''' Link HTMLProperty
883         Include the above as well as being able to access the class
884         information. Stringifying the object itself results in the value
885         from the item being displayed. Accessing attributes of this object
886         result in the appropriate entry from the class being queried for the
887         property accessed (so item/assignedto/name would look up the user
888         entry identified by the assignedto property on item, and then the
889         name property of that user)
890     '''
891     def __getattr__(self, attr):
892         ''' return a new HTMLItem '''
893        #print 'Link.getattr', (self, attr, self._value)
894         if not self._value:
895             raise AttributeError, "Can't access missing value"
896         if self._prop.classname == 'user':
897             klass = HTMLUser
898         else:
899             klass = HTMLItem
900         i = klass(self._client, self._prop.classname, self._value)
901         return getattr(i, attr)
903     def plain(self, escape=0):
904         ''' Render a "plain" representation of the property
905         '''
906         if self._value is None:
907             return ''
908         linkcl = self._db.classes[self._prop.classname]
909         k = linkcl.labelprop(1)
910         value = str(linkcl.get(self._value, k))
911         if escape:
912             value = cgi.escape(value)
913         return value
915     def field(self, showid=0, size=None):
916         ''' Render a form edit field for the property
917         '''
918         linkcl = self._db.getclass(self._prop.classname)
919         if linkcl.getprops().has_key('order'):  
920             sort_on = 'order'  
921         else:  
922             sort_on = linkcl.labelprop()  
923         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
924         # TODO: make this a field display, not a menu one!
925         l = ['<select name="%s">'%self._name]
926         k = linkcl.labelprop(1)
927         if self._value is None:
928             s = 'selected '
929         else:
930             s = ''
931         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
932         for optionid in options:
933             option = linkcl.get(optionid, k)
934             s = ''
935             if optionid == self._value:
936                 s = 'selected '
937             if showid:
938                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
939             else:
940                 lab = option
941             if size is not None and len(lab) > size:
942                 lab = lab[:size-3] + '...'
943             lab = cgi.escape(lab)
944             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
945         l.append('</select>')
946         return '\n'.join(l)
948     def menu(self, size=None, height=None, showid=0, additional=[],
949             **conditions):
950         ''' Render a form select list for this property
951         '''
952         value = self._value
954         # sort function
955         sortfunc = make_sort_function(self._db, self._prop.classname)
957         linkcl = self._db.getclass(self._prop.classname)
958         l = ['<select name="%s">'%self._name]
959         k = linkcl.labelprop(1)
960         s = ''
961         if value is None:
962             s = 'selected '
963         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
964         if linkcl.getprops().has_key('order'):  
965             sort_on = ('+', 'order')
966         else:  
967             sort_on = ('+', linkcl.labelprop())
968         options = linkcl.filter(None, conditions, sort_on, (None, None))
969         for optionid in options:
970             option = linkcl.get(optionid, k)
971             s = ''
972             if value in [optionid, option]:
973                 s = 'selected '
974             if showid:
975                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
976             else:
977                 lab = option
978             if size is not None and len(lab) > size:
979                 lab = lab[:size-3] + '...'
980             if additional:
981                 m = []
982                 for propname in additional:
983                     m.append(linkcl.get(optionid, propname))
984                 lab = lab + ' (%s)'%', '.join(map(str, m))
985             lab = cgi.escape(lab)
986             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
987         l.append('</select>')
988         return '\n'.join(l)
989 #    def checklist(self, ...)
991 class MultilinkHTMLProperty(HTMLProperty):
992     ''' Multilink HTMLProperty
994         Also be iterable, returning a wrapper object like the Link case for
995         each entry in the multilink.
996     '''
997     def __len__(self):
998         ''' length of the multilink '''
999         return len(self._value)
1001     def __getattr__(self, attr):
1002         ''' no extended attribute accesses make sense here '''
1003         raise AttributeError, attr
1005     def __getitem__(self, num):
1006         ''' iterate and return a new HTMLItem
1007         '''
1008        #print 'Multi.getitem', (self, num)
1009         value = self._value[num]
1010         if self._prop.classname == 'user':
1011             klass = HTMLUser
1012         else:
1013             klass = HTMLItem
1014         return klass(self._client, self._prop.classname, value)
1016     def __contains__(self, value):
1017         ''' Support the "in" operator
1018         '''
1019         return value in self._value
1021     def reverse(self):
1022         ''' return the list in reverse order
1023         '''
1024         l = self._value[:]
1025         l.reverse()
1026         if self._prop.classname == 'user':
1027             klass = HTMLUser
1028         else:
1029             klass = HTMLItem
1030         return [klass(self._client, self._prop.classname, value) for value in l]
1032     def plain(self, escape=0):
1033         ''' Render a "plain" representation of the property
1034         '''
1035         linkcl = self._db.classes[self._prop.classname]
1036         k = linkcl.labelprop(1)
1037         labels = []
1038         for v in self._value:
1039             labels.append(linkcl.get(v, k))
1040         value = ', '.join(labels)
1041         if escape:
1042             value = cgi.escape(value)
1043         return value
1045     def field(self, size=30, showid=0):
1046         ''' Render a form edit field for the property
1047         '''
1048         sortfunc = make_sort_function(self._db, self._prop.classname)
1049         linkcl = self._db.getclass(self._prop.classname)
1050         value = self._value[:]
1051         if value:
1052             value.sort(sortfunc)
1053         # map the id to the label property
1054         if not linkcl.getkey():
1055             showid=1
1056         if not showid:
1057             k = linkcl.labelprop(1)
1058             value = [linkcl.get(v, k) for v in value]
1059         value = cgi.escape(','.join(value))
1060         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1062     def menu(self, size=None, height=None, showid=0, additional=[],
1063             **conditions):
1064         ''' Render a form select list for this property
1065         '''
1066         value = self._value
1068         # sort function
1069         sortfunc = make_sort_function(self._db, self._prop.classname)
1071         linkcl = self._db.getclass(self._prop.classname)
1072         if linkcl.getprops().has_key('order'):  
1073             sort_on = ('+', 'order')
1074         else:  
1075             sort_on = ('+', linkcl.labelprop())
1076         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1077         height = height or min(len(options), 7)
1078         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1079         k = linkcl.labelprop(1)
1080         for optionid in options:
1081             option = linkcl.get(optionid, k)
1082             s = ''
1083             if optionid in value or option in value:
1084                 s = 'selected '
1085             if showid:
1086                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1087             else:
1088                 lab = option
1089             if size is not None and len(lab) > size:
1090                 lab = lab[:size-3] + '...'
1091             if additional:
1092                 m = []
1093                 for propname in additional:
1094                     m.append(linkcl.get(optionid, propname))
1095                 lab = lab + ' (%s)'%', '.join(m)
1096             lab = cgi.escape(lab)
1097             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1098                 lab))
1099         l.append('</select>')
1100         return '\n'.join(l)
1102 # set the propclasses for HTMLItem
1103 propclasses = (
1104     (hyperdb.String, StringHTMLProperty),
1105     (hyperdb.Number, NumberHTMLProperty),
1106     (hyperdb.Boolean, BooleanHTMLProperty),
1107     (hyperdb.Date, DateHTMLProperty),
1108     (hyperdb.Interval, IntervalHTMLProperty),
1109     (hyperdb.Password, PasswordHTMLProperty),
1110     (hyperdb.Link, LinkHTMLProperty),
1111     (hyperdb.Multilink, MultilinkHTMLProperty),
1114 def make_sort_function(db, classname):
1115     '''Make a sort function for a given class
1116     '''
1117     linkcl = db.getclass(classname)
1118     if linkcl.getprops().has_key('order'):
1119         sort_on = 'order'
1120     else:
1121         sort_on = linkcl.labelprop()
1122     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1123         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1124     return sortfunc
1126 def handleListCGIValue(value):
1127     ''' Value is either a single item or a list of items. Each item has a
1128         .value that we're actually interested in.
1129     '''
1130     if isinstance(value, type([])):
1131         return [value.value for value in value]
1132     else:
1133         value = value.value.strip()
1134         if not value:
1135             return []
1136         return value.split(',')
1138 class ShowDict:
1139     ''' A convenience access to the :columns index parameters
1140     '''
1141     def __init__(self, columns):
1142         self.columns = {}
1143         for col in columns:
1144             self.columns[col] = 1
1145     def __getitem__(self, name):
1146         return self.columns.has_key(name)
1148 class HTMLRequest:
1149     ''' The *request*, holding the CGI form and environment.
1151         "form" the CGI form as a cgi.FieldStorage
1152         "env" the CGI environment variables
1153         "base" the base URL for this instance
1154         "user" a HTMLUser instance for this user
1155         "classname" the current classname (possibly None)
1156         "template" the current template (suffix, also possibly None)
1158         Index args:
1159         "columns" dictionary of the columns to display in an index page
1160         "show" a convenience access to columns - request/show/colname will
1161                be true if the columns should be displayed, false otherwise
1162         "sort" index sort column (direction, column name)
1163         "group" index grouping property (direction, column name)
1164         "filter" properties to filter the index on
1165         "filterspec" values to filter the index on
1166         "search_text" text to perform a full-text search on for an index
1168     '''
1169     def __init__(self, client):
1170         self.client = client
1172         # easier access vars
1173         self.form = client.form
1174         self.env = client.env
1175         self.base = client.base
1176         self.user = HTMLUser(client, 'user', client.userid)
1178         # store the current class name and action
1179         self.classname = client.classname
1180         self.template = client.template
1182         self._post_init()
1184     def _post_init(self):
1185         ''' Set attributes based on self.form
1186         '''
1187         # extract the index display information from the form
1188         self.columns = []
1189         if self.form.has_key(':columns'):
1190             self.columns = handleListCGIValue(self.form[':columns'])
1191         self.show = ShowDict(self.columns)
1193         # sorting
1194         self.sort = (None, None)
1195         if self.form.has_key(':sort'):
1196             sort = self.form[':sort'].value
1197             if sort.startswith('-'):
1198                 self.sort = ('-', sort[1:])
1199             else:
1200                 self.sort = ('+', sort)
1201         if self.form.has_key(':sortdir'):
1202             self.sort = ('-', self.sort[1])
1204         # grouping
1205         self.group = (None, None)
1206         if self.form.has_key(':group'):
1207             group = self.form[':group'].value
1208             if group.startswith('-'):
1209                 self.group = ('-', group[1:])
1210             else:
1211                 self.group = ('+', group)
1212         if self.form.has_key(':groupdir'):
1213             self.group = ('-', self.group[1])
1215         # filtering
1216         self.filter = []
1217         if self.form.has_key(':filter'):
1218             self.filter = handleListCGIValue(self.form[':filter'])
1219         self.filterspec = {}
1220         if self.classname is not None:
1221             props = self.client.db.getclass(self.classname).getprops()
1222             for name in self.filter:
1223                 if self.form.has_key(name):
1224                     prop = props[name]
1225                     fv = self.form[name]
1226                     if (isinstance(prop, hyperdb.Link) or
1227                             isinstance(prop, hyperdb.Multilink)):
1228                         self.filterspec[name] = handleListCGIValue(fv)
1229                     else:
1230                         self.filterspec[name] = fv.value
1232         # full-text search argument
1233         self.search_text = None
1234         if self.form.has_key(':search_text'):
1235             self.search_text = self.form[':search_text'].value
1237         # pagination - size and start index
1238         # figure batch args
1239         if self.form.has_key(':pagesize'):
1240             self.pagesize = int(self.form[':pagesize'].value)
1241         else:
1242             self.pagesize = 50
1243         if self.form.has_key(':startwith'):
1244             self.startwith = int(self.form[':startwith'].value)
1245         else:
1246             self.startwith = 0
1248     def updateFromURL(self, url):
1249         ''' Parse the URL for query args, and update my attributes using the
1250             values.
1251         ''' 
1252         self.form = {}
1253         for name, value in cgi.parse_qsl(url):
1254             if self.form.has_key(name):
1255                 if isinstance(self.form[name], type([])):
1256                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1257                 else:
1258                     self.form[name] = [self.form[name],
1259                         cgi.MiniFieldStorage(name, value)]
1260             else:
1261                 self.form[name] = cgi.MiniFieldStorage(name, value)
1262         self._post_init()
1264     def update(self, kwargs):
1265         ''' Update my attributes using the keyword args
1266         '''
1267         self.__dict__.update(kwargs)
1268         if kwargs.has_key('columns'):
1269             self.show = ShowDict(self.columns)
1271     def description(self):
1272         ''' Return a description of the request - handle for the page title.
1273         '''
1274         s = [self.client.db.config.TRACKER_NAME]
1275         if self.classname:
1276             if self.client.nodeid:
1277                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1278             else:
1279                 if self.template == 'item':
1280                     s.append('- new %s'%self.classname)
1281                 elif self.template == 'index':
1282                     s.append('- %s index'%self.classname)
1283                 else:
1284                     s.append('- %s %s'%(self.classname, self.template))
1285         else:
1286             s.append('- home')
1287         return ' '.join(s)
1289     def __str__(self):
1290         d = {}
1291         d.update(self.__dict__)
1292         f = ''
1293         for k in self.form.keys():
1294             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1295         d['form'] = f
1296         e = ''
1297         for k,v in self.env.items():
1298             e += '\n     %r=%r'%(k, v)
1299         d['env'] = e
1300         return '''
1301 form: %(form)s
1302 url: %(url)r
1303 base: %(base)r
1304 classname: %(classname)r
1305 template: %(template)r
1306 columns: %(columns)r
1307 sort: %(sort)r
1308 group: %(group)r
1309 filter: %(filter)r
1310 search_text: %(search_text)r
1311 pagesize: %(pagesize)r
1312 startwith: %(startwith)r
1313 env: %(env)s
1314 '''%d
1316     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1317             filterspec=1):
1318         ''' return the current index args as form elements '''
1319         l = []
1320         s = '<input type="hidden" name="%s" value="%s">'
1321         if columns and self.columns:
1322             l.append(s%(':columns', ','.join(self.columns)))
1323         if sort and self.sort[1] is not None:
1324             if self.sort[0] == '-':
1325                 val = '-'+self.sort[1]
1326             else:
1327                 val = self.sort[1]
1328             l.append(s%(':sort', val))
1329         if group and self.group[1] is not None:
1330             if self.group[0] == '-':
1331                 val = '-'+self.group[1]
1332             else:
1333                 val = self.group[1]
1334             l.append(s%(':group', val))
1335         if filter and self.filter:
1336             l.append(s%(':filter', ','.join(self.filter)))
1337         if filterspec:
1338             for k,v in self.filterspec.items():
1339                 l.append(s%(k, ','.join(v)))
1340         if self.search_text:
1341             l.append(s%(':search_text', self.search_text))
1342         l.append(s%(':pagesize', self.pagesize))
1343         l.append(s%(':startwith', self.startwith))
1344         return '\n'.join(l)
1346     def indexargs_url(self, url, args):
1347         ''' embed the current index args in a URL '''
1348         l = ['%s=%s'%(k,v) for k,v in args.items()]
1349         if self.columns and not args.has_key(':columns'):
1350             l.append(':columns=%s'%(','.join(self.columns)))
1351         if self.sort[1] is not None and not args.has_key(':sort'):
1352             if self.sort[0] == '-':
1353                 val = '-'+self.sort[1]
1354             else:
1355                 val = self.sort[1]
1356             l.append(':sort=%s'%val)
1357         if self.group[1] is not None and not args.has_key(':group'):
1358             if self.group[0] == '-':
1359                 val = '-'+self.group[1]
1360             else:
1361                 val = self.group[1]
1362             l.append(':group=%s'%val)
1363         if self.filter and not args.has_key(':columns'):
1364             l.append(':filter=%s'%(','.join(self.filter)))
1365         for k,v in self.filterspec.items():
1366             if not args.has_key(k):
1367                 l.append('%s=%s'%(k, ','.join(v)))
1368         if self.search_text and not args.has_key(':search_text'):
1369             l.append(':search_text=%s'%self.search_text)
1370         if not args.has_key(':pagesize'):
1371             l.append(':pagesize=%s'%self.pagesize)
1372         if not args.has_key(':startwith'):
1373             l.append(':startwith=%s'%self.startwith)
1374         return '%s?%s'%(url, '&'.join(l))
1375     indexargs_href = indexargs_url
1377     def base_javascript(self):
1378         return '''
1379 <script language="javascript">
1380 submitted = false;
1381 function submit_once() {
1382     if (submitted) {
1383         alert("Your request is being processed.\\nPlease be patient.");
1384         return 0;
1385     }
1386     submitted = true;
1387     return 1;
1390 function help_window(helpurl, width, height) {
1391     HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1393 </script>
1394 '''%self.base
1396     def batch(self):
1397         ''' Return a batch object for results from the "current search"
1398         '''
1399         filterspec = self.filterspec
1400         sort = self.sort
1401         group = self.group
1403         # get the list of ids we're batching over
1404         klass = self.client.db.getclass(self.classname)
1405         if self.search_text:
1406             matches = self.client.db.indexer.search(
1407                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1408         else:
1409             matches = None
1410         l = klass.filter(matches, filterspec, sort, group)
1412         # map the item ids to instances
1413         if self.classname == 'user':
1414             klass = HTMLUser
1415         else:
1416             klass = HTMLItem
1417         l = [klass(self.client, self.classname, item) for item in l]
1419         # return the batch object
1420         return Batch(self.client, l, self.pagesize, self.startwith)
1422 # extend the standard ZTUtils Batch object to remove dependency on
1423 # Acquisition and add a couple of useful methods
1424 class Batch(ZTUtils.Batch):
1425     ''' Use me to turn a list of items, or item ids of a given class, into a
1426         series of batches.
1428         ========= ========================================================
1429         Parameter  Usage
1430         ========= ========================================================
1431         sequence  a list of HTMLItems
1432         size      how big to make the sequence.
1433         start     where to start (0-indexed) in the sequence.
1434         end       where to end (0-indexed) in the sequence.
1435         orphan    if the next batch would contain less items than this
1436                   value, then it is combined with this batch
1437         overlap   the number of items shared between adjacent batches
1438         ========= ========================================================
1440         Attributes: Note that the "start" attribute, unlike the
1441         argument, is a 1-based index (I know, lame).  "first" is the
1442         0-based index.  "length" is the actual number of elements in
1443         the batch.
1445         "sequence_length" is the length of the original, unbatched, sequence.
1446     '''
1447     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1448             overlap=0):
1449         self.client = client
1450         self.last_index = self.last_item = None
1451         self.current_item = None
1452         self.sequence_length = len(sequence)
1453         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1454             overlap)
1456     # overwrite so we can late-instantiate the HTMLItem instance
1457     def __getitem__(self, index):
1458         if index < 0:
1459             if index + self.end < self.first: raise IndexError, index
1460             return self._sequence[index + self.end]
1461         
1462         if index >= self.length:
1463             raise IndexError, index
1465         # move the last_item along - but only if the fetched index changes
1466         # (for some reason, index 0 is fetched twice)
1467         if index != self.last_index:
1468             self.last_item = self.current_item
1469             self.last_index = index
1471         self.current_item = self._sequence[index + self.first]
1472         return self.current_item
1474     def propchanged(self, property):
1475         ''' Detect if the property marked as being the group property
1476             changed in the last iteration fetch
1477         '''
1478         if (self.last_item is None or
1479                 self.last_item[property] != self.current_item[property]):
1480             return 1
1481         return 0
1483     # override these 'cos we don't have access to acquisition
1484     def previous(self):
1485         if self.start == 1:
1486             return None
1487         return Batch(self.client, self._sequence, self._size,
1488             self.first - self._size + self.overlap, 0, self.orphan,
1489             self.overlap)
1491     def next(self):
1492         try:
1493             self._sequence[self.end]
1494         except IndexError:
1495             return None
1496         return Batch(self.client, self._sequence, self._size,
1497             self.end - self.overlap, 0, self.orphan, self.overlap)
1499 class TemplatingUtils:
1500     ''' Utilities for templating
1501     '''
1502     def __init__(self, client):
1503         self.client = client
1504     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1505         return Batch(self.client, sequence, size, start, end, orphan,
1506             overlap)