Code

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