Code

Added 'Users may only edit their issues' customisation example.
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9     import cPickle as pickle
10 except ImportError:
11     import pickle
12 try:
13     import cStringIO as StringIO
14 except ImportError:
15     import StringIO
16 try:
17     import StructuredText
18 except ImportError:
19     StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 def input_html4(**attrs):
28     """Generate an 'input' (html4) element with given attributes"""
29     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
31 def input_xhtml(**attrs):
32     """Generate an 'input' (xhtml) element with given attributes"""
33     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
35 class NoTemplate(Exception):
36     pass
38 def find_template(dir, name, extension):
39     ''' Find a template in the nominated dir
40     '''
41     # find the source
42     if extension:
43         filename = '%s.%s'%(name, extension)
44     else:
45         filename = name
47     # try old-style
48     src = os.path.join(dir, filename)
49     if os.path.exists(src):
50         return (src, filename)
52     # try with a .html extension (new-style)
53     filename = filename + '.html'
54     src = os.path.join(dir, filename)
55     if os.path.exists(src):
56         return (src, filename)
58     # no extension == no generic template is possible
59     if not extension:
60         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
62     # try for a _generic template
63     generic = '_generic.%s'%extension
64     src = os.path.join(dir, generic)
65     if os.path.exists(src):
66         return (src, generic)
68     # finally, try _generic.html
69     generic = generic + '.html'
70     src = os.path.join(dir, generic)
71     if os.path.exists(src):
72         return (src, generic)
74     raise NoTemplate, 'No template file exists for templating "%s" '\
75         'with template "%s" (neither "%s" nor "%s")'%(name, extension,
76         filename, generic)
78 class Templates:
79     templates = {}
81     def __init__(self, dir):
82         self.dir = dir
84     def precompileTemplates(self):
85         ''' Go through a directory and precompile all the templates therein
86         '''
87         for filename in os.listdir(self.dir):
88             if os.path.isdir(filename): continue
89             if '.' in filename:
90                 name, extension = filename.split('.')
91                 self.get(name, extension)
92             else:
93                 self.get(filename, None)
95     def get(self, name, extension=None):
96         ''' Interface to get a template, possibly loading a compiled template.
98             "name" and "extension" indicate the template we're after, which in
99             most cases will be "name.extension". If "extension" is None, then
100             we look for a template just called "name" with no extension.
102             If the file "name.extension" doesn't exist, we look for
103             "_generic.extension" as a fallback.
104         '''
105         # default the name to "home"
106         if name is None:
107             name = 'home'
108         elif extension is None and '.' in name:
109             # split name
110             name, extension = name.split('.')
112         # find the source
113         src, filename = find_template(self.dir, name, extension)
115         # has it changed?
116         try:
117             stime = os.stat(src)[os.path.stat.ST_MTIME]
118         except os.error, error:
119             if error.errno != errno.ENOENT:
120                 raise
122         if self.templates.has_key(src) and \
123                 stime < self.templates[src].mtime:
124             # compiled template is up to date
125             return self.templates[src]
127         # compile the template
128         self.templates[src] = pt = RoundupPageTemplate()
129         # use pt_edit so we can pass the content_type guess too
130         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
131         pt.pt_edit(open(src).read(), content_type)
132         pt.id = filename
133         pt.mtime = time.time()
134         return pt
136     def __getitem__(self, name):
137         name, extension = os.path.splitext(name)
138         if extension:
139             extension = extension[1:]
140         try:
141             return self.get(name, extension)
142         except NoTemplate, message:
143             raise KeyError, message
145 class RoundupPageTemplate(PageTemplate.PageTemplate):
146     ''' A Roundup-specific PageTemplate.
148         Interrogate the client to set up the various template variables to
149         be available:
151         *context*
152          this is one of three things:
153          1. None - we're viewing a "home" page
154          2. The current class of item being displayed. This is an HTMLClass
155             instance.
156          3. The current item from the database, if we're viewing a specific
157             item, as an HTMLItem instance.
158         *request*
159           Includes information about the current request, including:
160            - the url
161            - the current index information (``filterspec``, ``filter`` args,
162              ``properties``, etc) parsed out of the form. 
163            - methods for easy filterspec link generation
164            - *user*, the current user node as an HTMLItem instance
165            - *form*, the current CGI form information as a FieldStorage
166         *config*
167           The current tracker config.
168         *db*
169           The current database, used to access arbitrary database items.
170         *utils*
171           This is a special class that has its base in the TemplatingUtils
172           class in this file. If the tracker interfaces module defines a
173           TemplatingUtils class then it is mixed in, overriding the methods
174           in the base class.
175     '''
176     def getContext(self, client, classname, request):
177         # construct the TemplatingUtils class
178         utils = TemplatingUtils
179         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
180             class utils(client.instance.interfaces.TemplatingUtils, utils):
181                 pass
183         c = {
184              'options': {},
185              'nothing': None,
186              'request': request,
187              'db': HTMLDatabase(client),
188              'config': client.instance.config,
189              'tracker': client.instance,
190              'utils': utils(client),
191              'templates': Templates(client.instance.config.TEMPLATES),
192         }
193         # add in the item if there is one
194         if client.nodeid:
195             if classname == 'user':
196                 c['context'] = HTMLUser(client, classname, client.nodeid,
197                     anonymous=1)
198             else:
199                 c['context'] = HTMLItem(client, classname, client.nodeid,
200                     anonymous=1)
201         elif client.db.classes.has_key(classname):
202             c['context'] = HTMLClass(client, classname, anonymous=1)
203         return c
205     def render(self, client, classname, request, **options):
206         """Render this Page Template"""
208         if not self._v_cooked:
209             self._cook()
211         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
213         if self._v_errors:
214             raise PageTemplate.PTRuntimeError, \
215                 'Page Template %s has errors.'%self.id
217         # figure the context
218         classname = classname or client.classname
219         request = request or HTMLRequest(client)
220         c = self.getContext(client, classname, request)
221         c.update({'options': options})
223         # and go
224         output = StringIO.StringIO()
225         TALInterpreter(self._v_program, self.macros,
226             getEngine().getContext(c), output, tal=1, strictinsert=0)()
227         return output.getvalue()
229 class HTMLDatabase:
230     ''' Return HTMLClasses for valid class fetches
231     '''
232     def __init__(self, client):
233         self._client = client
234         self._db = client.db
236         # we want config to be exposed
237         self.config = client.db.config
239     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
240         # check to see if we're actually accessing an item
241         m = desre.match(item)
242         if m:
243             self._client.db.getclass(m.group('cl'))
244             return HTMLItem(self._client, m.group('cl'), m.group('id'))
245         else:
246             self._client.db.getclass(item)
247             return HTMLClass(self._client, item)
249     def __getattr__(self, attr):
250         try:
251             return self[attr]
252         except KeyError:
253             raise AttributeError, attr
255     def classes(self):
256         l = self._client.db.classes.keys()
257         l.sort()
258         return [HTMLClass(self._client, cn) for cn in l]
260 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
261     cl = db.getclass(prop.classname)
262     l = []
263     for entry in ids:
264         if num_re.match(entry):
265             l.append(entry)
266         else:
267             try:
268                 l.append(cl.lookup(entry))
269             except KeyError:
270                 # ignore invalid keys
271                 pass
272     return l
274 class HTMLPermissions:
275     ''' Helpers that provide answers to commonly asked Permission questions.
276     '''
277     def is_edit_ok(self):
278         ''' Is the user allowed to Edit the current class?
279         '''
280         return self._db.security.hasPermission('Edit', self._client.userid,
281             self._classname)
282     def is_view_ok(self):
283         ''' Is the user allowed to View the current class?
284         '''
285         return self._db.security.hasPermission('View', self._client.userid,
286             self._classname)
287     def is_only_view_ok(self):
288         ''' Is the user only allowed to View (ie. not Edit) the current class?
289         '''
290         return self.is_view_ok() and not self.is_edit_ok()
292 class HTMLClass(HTMLPermissions):
293     ''' Accesses through a class (either through *class* or *db.<classname>*)
294     '''
295     def __init__(self, client, classname, anonymous=0):
296         self._client = client
297         self._db = client.db
298         self._anonymous = anonymous
300         # we want classname to be exposed, but _classname gives a
301         # consistent API for extending Class/Item
302         self._classname = self.classname = classname
303         self._klass = self._db.getclass(self.classname)
304         self._props = self._klass.getprops()
306         html_version = 'html4'
307         if hasattr(self._client.instance.config, 'HTML_VERSION'):
308             html_version = self._client.instance.config.HTML_VERSION
309         if html_version == 'xhtml':
310             self.input = input_xhtml
311         else:
312             self.input = input_html4
314     def __repr__(self):
315         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
317     def __getitem__(self, item):
318         ''' return an HTMLProperty instance
319         '''
320        #print 'HTMLClass.getitem', (self, item)
322         # we don't exist
323         if item == 'id':
324             return None
326         # get the property
327         prop = self._props[item]
329         # look up the correct HTMLProperty class
330         form = self._client.form
331         for klass, htmlklass in propclasses:
332             if not isinstance(prop, klass):
333                 continue
334             if form.has_key(item):
335                 if isinstance(prop, hyperdb.Multilink):
336                     value = lookupIds(self._db, prop,
337                         handleListCGIValue(form[item]))
338                 elif isinstance(prop, hyperdb.Link):
339                     value = form[item].value.strip()
340                     if value:
341                         value = lookupIds(self._db, prop, [value])[0]
342                     else:
343                         value = None
344                 else:
345                     value = form[item].value.strip() or None
346             else:
347                 if isinstance(prop, hyperdb.Multilink):
348                     value = []
349                 else:
350                     value = None
351             return htmlklass(self._client, self._classname, '', prop, item,
352                 value, self._anonymous)
354         # no good
355         raise KeyError, item
357     def __getattr__(self, attr):
358         ''' convenience access '''
359         try:
360             return self[attr]
361         except KeyError:
362             raise AttributeError, attr
364     def designator(self):
365         ''' Return this class' designator (classname) '''
366         return self._classname
368     def getItem(self, itemid, num_re=re.compile('-?\d+')):
369         ''' Get an item of this class by its item id.
370         '''
371         # make sure we're looking at an itemid
372         if not num_re.match(itemid):
373             itemid = self._klass.lookup(itemid)
375         if self.classname == 'user':
376             klass = HTMLUser
377         else:
378             klass = HTMLItem
380         return klass(self._client, self.classname, itemid)
382     def properties(self, sort=1):
383         ''' Return HTMLProperty for all of this class' properties.
384         '''
385         l = []
386         for name, prop in self._props.items():
387             for klass, htmlklass in propclasses:
388                 if isinstance(prop, hyperdb.Multilink):
389                     value = []
390                 else:
391                     value = None
392                 if isinstance(prop, klass):
393                     l.append(htmlklass(self._client, self._classname, '',
394                         prop, name, value, self._anonymous))
395         if sort:
396             l.sort(lambda a,b:cmp(a._name, b._name))
397         return l
399     def list(self):
400         ''' List all items in this class.
401         '''
402         if self.classname == 'user':
403             klass = HTMLUser
404         else:
405             klass = HTMLItem
407         # get the list and sort it nicely
408         l = self._klass.list()
409         sortfunc = make_sort_function(self._db, self.classname)
410         l.sort(sortfunc)
412         l = [klass(self._client, self.classname, x) for x in l]
413         return l
415     def csv(self):
416         ''' Return the items of this class as a chunk of CSV text.
417         '''
418         if rcsv.error:
419             return rcsv.error
421         props = self.propnames()
422         s = StringIO.StringIO()
423         writer = rcsv.writer(s, rcsv.comma_separated)
424         writer.writerow(props)
425         for nodeid in self._klass.list():
426             l = []
427             for name in props:
428                 value = self._klass.get(nodeid, name)
429                 if value is None:
430                     l.append('')
431                 elif isinstance(value, type([])):
432                     l.append(':'.join(map(str, value)))
433                 else:
434                     l.append(str(self._klass.get(nodeid, name)))
435             writer.writerow(l)
436         return s.getvalue()
438     def propnames(self):
439         ''' Return the list of the names of the properties of this class.
440         '''
441         idlessprops = self._klass.getprops(protected=0).keys()
442         idlessprops.sort()
443         return ['id'] + idlessprops
445     def filter(self, request=None):
446         ''' Return a list of items from this class, filtered and sorted
447             by the current requested filterspec/filter/sort/group args
448         '''
449         # XXX allow direct specification of the filterspec etc.
450         if request is not None:
451             filterspec = request.filterspec
452             sort = request.sort
453             group = request.group
454         else:
455             filterspec = {}
456             sort = (None,None)
457             group = (None,None)
458         if self.classname == 'user':
459             klass = HTMLUser
460         else:
461             klass = HTMLItem
462         l = [klass(self._client, self.classname, x)
463              for x in self._klass.filter(None, filterspec, sort, group)]
464         return l
466     def classhelp(self, properties=None, label='(list)', width='500',
467             height='400', property=''):
468         ''' Pop up a javascript window with class help
470             This generates a link to a popup window which displays the 
471             properties indicated by "properties" of the class named by
472             "classname". The "properties" should be a comma-separated list
473             (eg. 'id,name,description'). Properties defaults to all the
474             properties of a class (excluding id, creator, created and
475             activity).
477             You may optionally override the label displayed, the width and
478             height. The popup window will be resizable and scrollable.
480             If the "property" arg is given, it's passed through to the
481             javascript help_window function.
482         '''
483         if properties is None:
484             properties = self._klass.getprops(protected=0).keys()
485             properties.sort()
486             properties = ','.join(properties)
487         if property:
488             property = '&amp;property=%s'%property
489         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
490             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
491             \'%s\')">%s</a>'%(self.classname, properties, property, width,
492             height, label)
494     def submit(self, label="Submit New Entry"):
495         ''' Generate a submit button (and action hidden element)
496         '''
497         return self.input(type="hidden",name="@action",value="new") + '\n' + \
498                self.input(type="submit",name="submit",value=label)
500     def history(self):
501         return 'New node - no history'
503     def renderWith(self, name, **kwargs):
504         ''' Render this class with the given template.
505         '''
506         # create a new request and override the specified args
507         req = HTMLRequest(self._client)
508         req.classname = self.classname
509         req.update(kwargs)
511         # new template, using the specified classname and request
512         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
514         # use our fabricated request
515         return pt.render(self._client, self.classname, req)
517 class HTMLItem(HTMLPermissions):
518     ''' Accesses through an *item*
519     '''
520     def __init__(self, client, classname, nodeid, anonymous=0):
521         self._client = client
522         self._db = client.db
523         self._classname = classname
524         self._nodeid = nodeid
525         self._klass = self._db.getclass(classname)
526         self._props = self._klass.getprops()
528         # do we prefix the form items with the item's identification?
529         self._anonymous = anonymous
531     def __repr__(self):
532         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
533             self._nodeid)
535     def __getitem__(self, item):
536         ''' return an HTMLProperty instance
537         '''
538         #print 'HTMLItem.getitem', (self, item)
539         if item == 'id':
540             return self._nodeid
542         # get the property
543         prop = self._props[item]
545         # get the value, handling missing values
546         value = None
547         if int(self._nodeid) > 0:
548             value = self._klass.get(self._nodeid, item, None)
549         if value is None:
550             if isinstance(self._props[item], hyperdb.Multilink):
551                 value = []
553         # look up the correct HTMLProperty class
554         for klass, htmlklass in propclasses:
555             if isinstance(prop, klass):
556                 return htmlklass(self._client, self._classname,
557                     self._nodeid, prop, item, value, self._anonymous)
559         raise KeyError, item
561     def __getattr__(self, attr):
562         ''' convenience access to properties '''
563         try:
564             return self[attr]
565         except KeyError:
566             raise AttributeError, attr
568     def designator(self):
569         ''' Return this item's designator (classname + id) '''
570         return '%s%s'%(self._classname, self._nodeid)
571     
572     def submit(self, label="Submit Changes"):
573         ''' Generate a submit button (and action hidden element)
574         '''
575         return self.input(type="hidden",name="@action",value="edit") + '\n' + \
576                self.input(type="submit",name="submit",value=label)
578     def journal(self, direction='descending'):
579         ''' Return a list of HTMLJournalEntry instances.
580         '''
581         # XXX do this
582         return []
584     def history(self, direction='descending', dre=re.compile('\d+')):
585         l = ['<table class="history">'
586              '<tr><th colspan="4" class="header">',
587              _('History'),
588              '</th></tr><tr>',
589              _('<th>Date</th>'),
590              _('<th>User</th>'),
591              _('<th>Action</th>'),
592              _('<th>Args</th>'),
593             '</tr>']
594         current = {}
595         comments = {}
596         history = self._klass.history(self._nodeid)
597         history.sort()
598         timezone = self._db.getUserTimezone()
599         if direction == 'descending':
600             history.reverse()
601             for prop_n in self._props.keys():
602                 prop = self[prop_n]
603                 if isinstance(prop, HTMLProperty):
604                     current[prop_n] = prop.plain()
605                     # make link if hrefable
606                     if (self._props.has_key(prop_n) and
607                             isinstance(self._props[prop_n], hyperdb.Link)):
608                         classname = self._props[prop_n].classname
609                         try:
610                             template = find_template(self._db.config.TEMPLATES,
611                                 classname, 'item')
612                             if template[1].startswith('_generic'):
613                                 raise NoTemplate, 'not really...'
614                         except NoTemplate:
615                             pass
616                         else:
617                             id = self._klass.get(self._nodeid, prop_n, None)
618                             current[prop_n] = '<a href="%s%s">%s</a>'%(
619                                 classname, id, current[prop_n])
620  
621         for id, evt_date, user, action, args in history:
622             date_s = str(evt_date.local(timezone)).replace("."," ")
623             arg_s = ''
624             if action == 'link' and type(args) == type(()):
625                 if len(args) == 3:
626                     linkcl, linkid, key = args
627                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
628                         linkcl, linkid, key)
629                 else:
630                     arg_s = str(args)
632             elif action == 'unlink' and type(args) == type(()):
633                 if len(args) == 3:
634                     linkcl, linkid, key = args
635                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
636                         linkcl, linkid, key)
637                 else:
638                     arg_s = str(args)
640             elif type(args) == type({}):
641                 cell = []
642                 for k in args.keys():
643                     # try to get the relevant property and treat it
644                     # specially
645                     try:
646                         prop = self._props[k]
647                     except KeyError:
648                         prop = None
649                     if prop is None:
650                         # property no longer exists
651                         comments['no_exist'] = _('''<em>The indicated property
652                             no longer exists</em>''')
653                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
654                         continue
656                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
657                             isinstance(prop, hyperdb.Link)):
658                         # figure what the link class is
659                         classname = prop.classname
660                         try:
661                             linkcl = self._db.getclass(classname)
662                         except KeyError:
663                             labelprop = None
664                             comments[classname] = _('''The linked class
665                                 %(classname)s no longer exists''')%locals()
666                         labelprop = linkcl.labelprop(1)
667                         try:
668                             template = find_template(self._db.config.TEMPLATES,
669                                 classname, 'item')
670                             if template[1].startswith('_generic'):
671                                 raise NoTemplate, 'not really...'
672                             hrefable = 1
673                         except NoTemplate:
674                             hrefable = 0
676                     if isinstance(prop, hyperdb.Multilink) and args[k]:
677                         ml = []
678                         for linkid in args[k]:
679                             if isinstance(linkid, type(())):
680                                 sublabel = linkid[0] + ' '
681                                 linkids = linkid[1]
682                             else:
683                                 sublabel = ''
684                                 linkids = [linkid]
685                             subml = []
686                             for linkid in linkids:
687                                 label = classname + linkid
688                                 # if we have a label property, try to use it
689                                 # TODO: test for node existence even when
690                                 # there's no labelprop!
691                                 try:
692                                     if labelprop is not None and \
693                                             labelprop != 'id':
694                                         label = linkcl.get(linkid, labelprop)
695                                 except IndexError:
696                                     comments['no_link'] = _('''<strike>The
697                                         linked node no longer
698                                         exists</strike>''')
699                                     subml.append('<strike>%s</strike>'%label)
700                                 else:
701                                     if hrefable:
702                                         subml.append('<a href="%s%s">%s</a>'%(
703                                             classname, linkid, label))
704                                     else:
705                                         subml.append(label)
706                             ml.append(sublabel + ', '.join(subml))
707                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
708                     elif isinstance(prop, hyperdb.Link) and args[k]:
709                         label = classname + args[k]
710                         # if we have a label property, try to use it
711                         # TODO: test for node existence even when
712                         # there's no labelprop!
713                         if labelprop is not None and labelprop != 'id':
714                             try:
715                                 label = linkcl.get(args[k], labelprop)
716                             except IndexError:
717                                 comments['no_link'] = _('''<strike>The
718                                     linked node no longer
719                                     exists</strike>''')
720                                 cell.append(' <strike>%s</strike>,\n'%label)
721                                 # "flag" this is done .... euwww
722                                 label = None
723                         if label is not None:
724                             if hrefable:
725                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
726                             else:
727                                 old = label;
728                             cell.append('%s: %s' % (k,old))
729                             if current.has_key(k):
730                                 cell[-1] += ' -> %s'%current[k]
731                                 current[k] = old
733                     elif isinstance(prop, hyperdb.Date) and args[k]:
734                         d = date.Date(args[k]).local(timezone)
735                         cell.append('%s: %s'%(k, str(d)))
736                         if current.has_key(k):
737                             cell[-1] += ' -> %s' % current[k]
738                             current[k] = str(d)
740                     elif isinstance(prop, hyperdb.Interval) and args[k]:
741                         d = date.Interval(args[k])
742                         cell.append('%s: %s'%(k, str(d)))
743                         if current.has_key(k):
744                             cell[-1] += ' -> %s'%current[k]
745                             current[k] = str(d)
747                     elif isinstance(prop, hyperdb.String) and args[k]:
748                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
749                         if current.has_key(k):
750                             cell[-1] += ' -> %s'%current[k]
751                             current[k] = cgi.escape(args[k])
753                     elif not args[k]:
754                         if current.has_key(k):
755                             cell.append('%s: %s'%(k, current[k]))
756                             current[k] = '(no value)'
757                         else:
758                             cell.append('%s: (no value)'%k)
760                     else:
761                         cell.append('%s: %s'%(k, str(args[k])))
762                         if current.has_key(k):
763                             cell[-1] += ' -> %s'%current[k]
764                             current[k] = str(args[k])
766                 arg_s = '<br />'.join(cell)
767             else:
768                 # unkown event!!
769                 comments['unknown'] = _('''<strong><em>This event is not
770                     handled by the history display!</em></strong>''')
771                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
772             date_s = date_s.replace(' ', '&nbsp;')
773             # if the user's an itemid, figure the username (older journals
774             # have the username)
775             if dre.match(user):
776                 user = self._db.user.get(user, 'username')
777             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
778                 date_s, user, action, arg_s))
779         if comments:
780             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
781         for entry in comments.values():
782             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
783         l.append('</table>')
784         return '\n'.join(l)
786     def renderQueryForm(self):
787         ''' Render this item, which is a query, as a search form.
788         '''
789         # create a new request and override the specified args
790         req = HTMLRequest(self._client)
791         req.classname = self._klass.get(self._nodeid, 'klass')
792         name = self._klass.get(self._nodeid, 'name')
793         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
794             '&@queryname=%s'%urllib.quote(name))
796         # new template, using the specified classname and request
797         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
799         # use our fabricated request
800         return pt.render(self._client, req.classname, req)
802 class HTMLUser(HTMLItem):
803     ''' Accesses through the *user* (a special case of item)
804     '''
805     def __init__(self, client, classname, nodeid, anonymous=0):
806         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
807         self._default_classname = client.classname
809         # used for security checks
810         self._security = client.db.security
812     _marker = []
813     def hasPermission(self, permission, classname=_marker):
814         ''' Determine if the user has the Permission.
816             The class being tested defaults to the template's class, but may
817             be overidden for this test by suppling an alternate classname.
818         '''
819         if classname is self._marker:
820             classname = self._default_classname
821         return self._security.hasPermission(permission, self._nodeid, classname)
823     def is_edit_ok(self):
824         ''' Is the user allowed to Edit the current class?
825             Also check whether this is the current user's info.
826         '''
827         return self._db.security.hasPermission('Edit', self._client.userid,
828             self._classname) or (self._nodeid == self._client.userid and
829             self._db.user.get(self._client.userid, 'username') != 'anonymous')
831     def is_view_ok(self):
832         ''' Is the user allowed to View the current class?
833             Also check whether this is the current user's info.
834         '''
835         return self._db.security.hasPermission('Edit', self._client.userid,
836             self._classname) or (self._nodeid == self._client.userid and
837             self._db.user.get(self._client.userid, 'username') != 'anonymous')
839 class HTMLProperty:
840     ''' String, Number, Date, Interval HTMLProperty
842         Has useful attributes:
844          _name  the name of the property
845          _value the value of the property if any
847         A wrapper object which may be stringified for the plain() behaviour.
848     '''
849     def __init__(self, client, classname, nodeid, prop, name, value,
850             anonymous=0):
851         self._client = client
852         self._db = client.db
853         self._classname = classname
854         self._nodeid = nodeid
855         self._prop = prop
856         self._value = value
857         self._anonymous = anonymous
858         self._name = name
859         if not anonymous:
860             self._formname = '%s%s@%s'%(classname, nodeid, name)
861         else:
862             self._formname = name
864         html_version = 'html4'
865         if hasattr(self._client.instance.config, 'HTML_VERSION'):
866             html_version = self._client.instance.config.HTML_VERSION
867         if html_version == 'xhtml':
868             self.input = input_xhtml
869         else:
870             self.input = input_html4
871         
872     def __repr__(self):
873         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
874             self._prop, self._value)
875     def __str__(self):
876         return self.plain()
877     def __cmp__(self, other):
878         if isinstance(other, HTMLProperty):
879             return cmp(self._value, other._value)
880         return cmp(self._value, other)
882 class StringHTMLProperty(HTMLProperty):
883     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
884                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
885                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
886     def _hyper_repl(self, match):
887         if match.group('url'):
888             s = match.group('url')
889             return '<a href="%s">%s</a>'%(s, s)
890         elif match.group('email'):
891             s = match.group('email')
892             return '<a href="mailto:%s">%s</a>'%(s, s)
893         else:
894             s = match.group('item')
895             s1 = match.group('class')
896             s2 = match.group('id')
897             try:
898                 # make sure s1 is a valid tracker classname
899                 self._db.getclass(s1)
900                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
901             except KeyError:
902                 return '%s%s'%(s1, s2)
904     def hyperlinked(self):
905         ''' Render a "hyperlinked" version of the text '''
906         return self.plain(hyperlink=1)
908     def plain(self, escape=0, hyperlink=0):
909         ''' Render a "plain" representation of the property
910             
911             "escape" turns on/off HTML quoting
912             "hyperlink" turns on/off in-text hyperlinking of URLs, email
913                 addresses and designators
914         '''
915         if self._value is None:
916             return ''
917         if escape:
918             s = cgi.escape(str(self._value))
919         else:
920             s = str(self._value)
921         if hyperlink:
922             # no, we *must* escape this text
923             if not escape:
924                 s = cgi.escape(s)
925             s = self.hyper_re.sub(self._hyper_repl, s)
926         return s
928     def stext(self, escape=0):
929         ''' Render the value of the property as StructuredText.
931             This requires the StructureText module to be installed separately.
932         '''
933         s = self.plain(escape=escape)
934         if not StructuredText:
935             return s
936         return StructuredText(s,level=1,header=0)
938     def field(self, size = 30):
939         ''' Render a form edit field for the property
940         '''
941         if self._value is None:
942             value = ''
943         else:
944             value = cgi.escape(str(self._value))
945             value = '&quot;'.join(value.split('"'))
946         return self.input(name=self._formname,value=value,size=size)
948     def multiline(self, escape=0, rows=5, cols=40):
949         ''' Render a multiline form edit field for the property
950         '''
951         if self._value is None:
952             value = ''
953         else:
954             value = cgi.escape(str(self._value))
955             value = '&quot;'.join(value.split('"'))
956         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
957             self._formname, rows, cols, value)
959     def email(self, escape=1):
960         ''' Render the value of the property as an obscured email address
961         '''
962         if self._value is None: value = ''
963         else: value = str(self._value)
964         if value.find('@') != -1:
965             name, domain = value.split('@')
966             domain = ' '.join(domain.split('.')[:-1])
967             name = name.replace('.', ' ')
968             value = '%s at %s ...'%(name, domain)
969         else:
970             value = value.replace('.', ' ')
971         if escape:
972             value = cgi.escape(value)
973         return value
975 class PasswordHTMLProperty(HTMLProperty):
976     def plain(self):
977         ''' Render a "plain" representation of the property
978         '''
979         if self._value is None:
980             return ''
981         return _('*encrypted*')
983     def field(self, size = 30):
984         ''' Render a form edit field for the property.
985         '''
986         return self.input(type="password", name=self._formname, size=size)
988     def confirm(self, size = 30):
989         ''' Render a second form edit field for the property, used for 
990             confirmation that the user typed the password correctly. Generates
991             a field with name "@confirm@name".
992         '''
993         return self.input(type="password", name="@confirm@%s"%self._formname,
994             size=size)
996 class NumberHTMLProperty(HTMLProperty):
997     def plain(self):
998         ''' Render a "plain" representation of the property
999         '''
1000         return str(self._value)
1002     def field(self, size = 30):
1003         ''' Render a form edit field for the property
1004         '''
1005         if self._value is None:
1006             value = ''
1007         else:
1008             value = cgi.escape(str(self._value))
1009             value = '&quot;'.join(value.split('"'))
1010         return self.input(name=self._formname,value=value,size=size)
1012     def __int__(self):
1013         ''' Return an int of me
1014         '''
1015         return int(self._value)
1017     def __float__(self):
1018         ''' Return a float of me
1019         '''
1020         return float(self._value)
1023 class BooleanHTMLProperty(HTMLProperty):
1024     def plain(self):
1025         ''' Render a "plain" representation of the property
1026         '''
1027         if self._value is None:
1028             return ''
1029         return self._value and "Yes" or "No"
1031     def field(self):
1032         ''' Render a form edit field for the property
1033         '''
1034         checked = self._value and "checked" or ""
1035         if self._value:
1036             s = self.input(type="radio",name=self._formname,value="yes",checked="checked")
1037             s += 'Yes'
1038             s +=self.input(type="radio",name=self._formname,value="no")
1039             s += 'No'
1040         else:
1041             s = self.input(type="radio",name=self._formname,value="yes")
1042             s += 'Yes'
1043             s +=self.input(type="radio",name=self._formname,value="no",checked="checked")
1044             s += 'No'
1045         return s
1047 class DateHTMLProperty(HTMLProperty):
1048     def plain(self):
1049         ''' Render a "plain" representation of the property
1050         '''
1051         if self._value is None:
1052             return ''
1053         return str(self._value.local(self._db.getUserTimezone()))
1055     def now(self):
1056         ''' Return the current time.
1058             This is useful for defaulting a new value. Returns a
1059             DateHTMLProperty.
1060         '''
1061         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1062             self._formname, date.Date('.'))
1064     def field(self, size = 30):
1065         ''' Render a form edit field for the property
1066         '''
1067         if self._value is None:
1068             value = ''
1069         else:
1070             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1071             value = '&quot;'.join(value.split('"'))
1072         return self.input(name=self._formname,value=value,size=size)
1074     def reldate(self, pretty=1):
1075         ''' Render the interval between the date and now.
1077             If the "pretty" flag is true, then make the display pretty.
1078         '''
1079         if not self._value:
1080             return ''
1082         # figure the interval
1083         interval = self._value - date.Date('.')
1084         if pretty:
1085             return interval.pretty()
1086         return str(interval)
1088     _marker = []
1089     def pretty(self, format=_marker):
1090         ''' Render the date in a pretty format (eg. month names, spaces).
1092             The format string is a standard python strftime format string.
1093             Note that if the day is zero, and appears at the start of the
1094             string, then it'll be stripped from the output. This is handy
1095             for the situatin when a date only specifies a month and a year.
1096         '''
1097         if format is not self._marker:
1098             return self._value.pretty(format)
1099         else:
1100             return self._value.pretty()
1102     def local(self, offset):
1103         ''' Return the date/time as a local (timezone offset) date/time.
1104         '''
1105         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1106             self._formname, self._value.local(offset))
1108 class IntervalHTMLProperty(HTMLProperty):
1109     def plain(self):
1110         ''' Render a "plain" representation of the property
1111         '''
1112         if self._value is None:
1113             return ''
1114         return str(self._value)
1116     def pretty(self):
1117         ''' Render the interval in a pretty format (eg. "yesterday")
1118         '''
1119         return self._value.pretty()
1121     def field(self, size = 30):
1122         ''' Render a form edit field for the property
1123         '''
1124         if self._value is None:
1125             value = ''
1126         else:
1127             value = cgi.escape(str(self._value))
1128             value = '&quot;'.join(value.split('"'))
1129         return self.input(name=self._formname,value=value,size=size)
1131 class LinkHTMLProperty(HTMLProperty):
1132     ''' Link HTMLProperty
1133         Include the above as well as being able to access the class
1134         information. Stringifying the object itself results in the value
1135         from the item being displayed. Accessing attributes of this object
1136         result in the appropriate entry from the class being queried for the
1137         property accessed (so item/assignedto/name would look up the user
1138         entry identified by the assignedto property on item, and then the
1139         name property of that user)
1140     '''
1141     def __init__(self, *args, **kw):
1142         HTMLProperty.__init__(self, *args, **kw)
1143         # if we're representing a form value, then the -1 from the form really
1144         # should be a None
1145         if str(self._value) == '-1':
1146             self._value = None
1148     def __getattr__(self, attr):
1149         ''' return a new HTMLItem '''
1150        #print 'Link.getattr', (self, attr, self._value)
1151         if not self._value:
1152             raise AttributeError, "Can't access missing value"
1153         if self._prop.classname == 'user':
1154             klass = HTMLUser
1155         else:
1156             klass = HTMLItem
1157         i = klass(self._client, self._prop.classname, self._value)
1158         return getattr(i, attr)
1160     def plain(self, escape=0):
1161         ''' Render a "plain" representation of the property
1162         '''
1163         if self._value is None:
1164             return ''
1165         linkcl = self._db.classes[self._prop.classname]
1166         k = linkcl.labelprop(1)
1167         value = str(linkcl.get(self._value, k))
1168         if escape:
1169             value = cgi.escape(value)
1170         return value
1172     def field(self, showid=0, size=None):
1173         ''' Render a form edit field for the property
1174         '''
1175         linkcl = self._db.getclass(self._prop.classname)
1176         if linkcl.getprops().has_key('order'):  
1177             sort_on = 'order'  
1178         else:  
1179             sort_on = linkcl.labelprop()  
1180         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1181         # TODO: make this a field display, not a menu one!
1182         l = ['<select name="%s">'%self._formname]
1183         k = linkcl.labelprop(1)
1184         if self._value is None:
1185             s = 'selected="selected" '
1186         else:
1187             s = ''
1188         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1190         # make sure we list the current value if it's retired
1191         if self._value and self._value not in options:
1192             options.insert(0, self._value)
1194         for optionid in options:
1195             # get the option value, and if it's None use an empty string
1196             option = linkcl.get(optionid, k) or ''
1198             # figure if this option is selected
1199             s = ''
1200             if optionid == self._value:
1201                 s = 'selected="selected" '
1203             # figure the label
1204             if showid:
1205                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1206             else:
1207                 lab = option
1209             # truncate if it's too long
1210             if size is not None and len(lab) > size:
1211                 lab = lab[:size-3] + '...'
1213             # and generate
1214             lab = cgi.escape(lab)
1215             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1216         l.append('</select>')
1217         return '\n'.join(l)
1219     def menu(self, size=None, height=None, showid=0, additional=[],
1220             **conditions):
1221         ''' Render a form select list for this property
1222         '''
1223         value = self._value
1225         linkcl = self._db.getclass(self._prop.classname)
1226         l = ['<select name="%s">'%self._formname]
1227         k = linkcl.labelprop(1)
1228         s = ''
1229         if value is None:
1230             s = 'selected="selected" '
1231         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1232         if linkcl.getprops().has_key('order'):  
1233             sort_on = ('+', 'order')
1234         else:  
1235             sort_on = ('+', linkcl.labelprop())
1236         options = linkcl.filter(None, conditions, sort_on, (None, None))
1238         # make sure we list the current value if it's retired
1239         if self._value and self._value not in options:
1240             options.insert(0, self._value)
1242         for optionid in options:
1243             # get the option value, and if it's None use an empty string
1244             option = linkcl.get(optionid, k) or ''
1246             # figure if this option is selected
1247             s = ''
1248             if value in [optionid, option]:
1249                 s = 'selected="selected" '
1251             # figure the label
1252             if showid:
1253                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1254             else:
1255                 lab = option
1257             # truncate if it's too long
1258             if size is not None and len(lab) > size:
1259                 lab = lab[:size-3] + '...'
1260             if additional:
1261                 m = []
1262                 for propname in additional:
1263                     m.append(linkcl.get(optionid, propname))
1264                 lab = lab + ' (%s)'%', '.join(map(str, m))
1266             # and generate
1267             lab = cgi.escape(lab)
1268             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1269         l.append('</select>')
1270         return '\n'.join(l)
1271 #    def checklist(self, ...)
1273 class MultilinkHTMLProperty(HTMLProperty):
1274     ''' Multilink HTMLProperty
1276         Also be iterable, returning a wrapper object like the Link case for
1277         each entry in the multilink.
1278     '''
1279     def __init__(self, *args, **kwargs):
1280         HTMLProperty.__init__(self, *args, **kwargs)
1281         if self._value:
1282             self._value.sort(make_sort_function(self._db, self._prop.classname))
1283     
1284     def __len__(self):
1285         ''' length of the multilink '''
1286         return len(self._value)
1288     def __getattr__(self, attr):
1289         ''' no extended attribute accesses make sense here '''
1290         raise AttributeError, attr
1292     def __getitem__(self, num):
1293         ''' iterate and return a new HTMLItem
1294         '''
1295        #print 'Multi.getitem', (self, num)
1296         value = self._value[num]
1297         if self._prop.classname == 'user':
1298             klass = HTMLUser
1299         else:
1300             klass = HTMLItem
1301         return klass(self._client, self._prop.classname, value)
1303     def __contains__(self, value):
1304         ''' Support the "in" operator. We have to make sure the passed-in
1305             value is a string first, not a *HTMLProperty.
1306         '''
1307         return str(value) in self._value
1309     def reverse(self):
1310         ''' return the list in reverse order
1311         '''
1312         l = self._value[:]
1313         l.reverse()
1314         if self._prop.classname == 'user':
1315             klass = HTMLUser
1316         else:
1317             klass = HTMLItem
1318         return [klass(self._client, self._prop.classname, value) for value in l]
1320     def plain(self, escape=0):
1321         ''' Render a "plain" representation of the property
1322         '''
1323         linkcl = self._db.classes[self._prop.classname]
1324         k = linkcl.labelprop(1)
1325         labels = []
1326         for v in self._value:
1327             labels.append(linkcl.get(v, k))
1328         value = ', '.join(labels)
1329         if escape:
1330             value = cgi.escape(value)
1331         return value
1333     def field(self, size=30, showid=0):
1334         ''' Render a form edit field for the property
1335         '''
1336         linkcl = self._db.getclass(self._prop.classname)
1337         value = self._value[:]
1338         # map the id to the label property
1339         if not linkcl.getkey():
1340             showid=1
1341         if not showid:
1342             k = linkcl.labelprop(1)
1343             value = [linkcl.get(v, k) for v in value]
1344         value = cgi.escape(','.join(value))
1345         return self.input(name=self._formname,size=size,value=value)
1347     def menu(self, size=None, height=None, showid=0, additional=[],
1348             **conditions):
1349         ''' Render a form select list for this property
1350         '''
1351         value = self._value
1353         linkcl = self._db.getclass(self._prop.classname)
1354         sort_on = ('+', find_sort_key(linkcl))
1355         options = linkcl.filter(None, conditions, sort_on)
1356         height = height or min(len(options), 7)
1357         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1358         k = linkcl.labelprop(1)
1360         # make sure we list the current values if they're retired
1361         for val in value:
1362             if val not in options:
1363                 options.insert(0, val)
1365         for optionid in options:
1366             # get the option value, and if it's None use an empty string
1367             option = linkcl.get(optionid, k) or ''
1369             # figure if this option is selected
1370             s = ''
1371             if optionid in value or option in value:
1372                 s = 'selected="selected" '
1374             # figure the label
1375             if showid:
1376                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1377             else:
1378                 lab = option
1379             # truncate if it's too long
1380             if size is not None and len(lab) > size:
1381                 lab = lab[:size-3] + '...'
1382             if additional:
1383                 m = []
1384                 for propname in additional:
1385                     m.append(linkcl.get(optionid, propname))
1386                 lab = lab + ' (%s)'%', '.join(m)
1388             # and generate
1389             lab = cgi.escape(lab)
1390             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1391                 lab))
1392         l.append('</select>')
1393         return '\n'.join(l)
1395 # set the propclasses for HTMLItem
1396 propclasses = (
1397     (hyperdb.String, StringHTMLProperty),
1398     (hyperdb.Number, NumberHTMLProperty),
1399     (hyperdb.Boolean, BooleanHTMLProperty),
1400     (hyperdb.Date, DateHTMLProperty),
1401     (hyperdb.Interval, IntervalHTMLProperty),
1402     (hyperdb.Password, PasswordHTMLProperty),
1403     (hyperdb.Link, LinkHTMLProperty),
1404     (hyperdb.Multilink, MultilinkHTMLProperty),
1407 def make_sort_function(db, classname):
1408     '''Make a sort function for a given class
1409     '''
1410     linkcl = db.getclass(classname)
1411     sort_on = find_sort_key(linkcl)
1412     def sortfunc(a, b):
1413         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1414     return sortfunc
1416 def find_sort_key(linkcl):
1417     if linkcl.getprops().has_key('order'):
1418         return 'order'
1419     else:
1420         return linkcl.labelprop()
1422 def handleListCGIValue(value):
1423     ''' Value is either a single item or a list of items. Each item has a
1424         .value that we're actually interested in.
1425     '''
1426     if isinstance(value, type([])):
1427         return [value.value for value in value]
1428     else:
1429         value = value.value.strip()
1430         if not value:
1431             return []
1432         return value.split(',')
1434 class ShowDict:
1435     ''' A convenience access to the :columns index parameters
1436     '''
1437     def __init__(self, columns):
1438         self.columns = {}
1439         for col in columns:
1440             self.columns[col] = 1
1441     def __getitem__(self, name):
1442         return self.columns.has_key(name)
1444 class HTMLRequest:
1445     ''' The *request*, holding the CGI form and environment.
1447         "form" the CGI form as a cgi.FieldStorage
1448         "env" the CGI environment variables
1449         "base" the base URL for this instance
1450         "user" a HTMLUser instance for this user
1451         "classname" the current classname (possibly None)
1452         "template" the current template (suffix, also possibly None)
1454         Index args:
1455         "columns" dictionary of the columns to display in an index page
1456         "show" a convenience access to columns - request/show/colname will
1457                be true if the columns should be displayed, false otherwise
1458         "sort" index sort column (direction, column name)
1459         "group" index grouping property (direction, column name)
1460         "filter" properties to filter the index on
1461         "filterspec" values to filter the index on
1462         "search_text" text to perform a full-text search on for an index
1464     '''
1465     def __init__(self, client):
1466         self.client = client
1468         # easier access vars
1469         self.form = client.form
1470         self.env = client.env
1471         self.base = client.base
1472         self.user = HTMLUser(client, 'user', client.userid)
1474         # store the current class name and action
1475         self.classname = client.classname
1476         self.template = client.template
1478         # the special char to use for special vars
1479         self.special_char = '@'
1481         html_version = 'html4'
1482         if hasattr(self.client.instance.config, 'HTML_VERSION'):
1483             html_version = self.client.instance.config.HTML_VERSION
1484         if html_version == 'xhtml':
1485             self.input = input_xhtml
1486         else:
1487             self.input = input_html4
1489         self._post_init()
1491     def _post_init(self):
1492         ''' Set attributes based on self.form
1493         '''
1494         # extract the index display information from the form
1495         self.columns = []
1496         for name in ':columns @columns'.split():
1497             if self.form.has_key(name):
1498                 self.special_char = name[0]
1499                 self.columns = handleListCGIValue(self.form[name])
1500                 break
1501         self.show = ShowDict(self.columns)
1503         # sorting
1504         self.sort = (None, None)
1505         for name in ':sort @sort'.split():
1506             if self.form.has_key(name):
1507                 self.special_char = name[0]
1508                 sort = self.form[name].value
1509                 if sort.startswith('-'):
1510                     self.sort = ('-', sort[1:])
1511                 else:
1512                     self.sort = ('+', sort)
1513                 if self.form.has_key(self.special_char+'sortdir'):
1514                     self.sort = ('-', self.sort[1])
1516         # grouping
1517         self.group = (None, None)
1518         for name in ':group @group'.split():
1519             if self.form.has_key(name):
1520                 self.special_char = name[0]
1521                 group = self.form[name].value
1522                 if group.startswith('-'):
1523                     self.group = ('-', group[1:])
1524                 else:
1525                     self.group = ('+', group)
1526                 if self.form.has_key(self.special_char+'groupdir'):
1527                     self.group = ('-', self.group[1])
1529         # filtering
1530         self.filter = []
1531         for name in ':filter @filter'.split():
1532             if self.form.has_key(name):
1533                 self.special_char = name[0]
1534                 self.filter = handleListCGIValue(self.form[name])
1536         self.filterspec = {}
1537         db = self.client.db
1538         if self.classname is not None:
1539             props = db.getclass(self.classname).getprops()
1540             for name in self.filter:
1541                 if not self.form.has_key(name):
1542                     continue
1543                 prop = props[name]
1544                 fv = self.form[name]
1545                 if (isinstance(prop, hyperdb.Link) or
1546                         isinstance(prop, hyperdb.Multilink)):
1547                     self.filterspec[name] = lookupIds(db, prop,
1548                         handleListCGIValue(fv))
1549                 else:
1550                     if isinstance(fv, type([])):
1551                         self.filterspec[name] = [v.value for v in fv]
1552                     else:
1553                         self.filterspec[name] = fv.value
1555         # full-text search argument
1556         self.search_text = None
1557         for name in ':search_text @search_text'.split():
1558             if self.form.has_key(name):
1559                 self.special_char = name[0]
1560                 self.search_text = self.form[name].value
1562         # pagination - size and start index
1563         # figure batch args
1564         self.pagesize = 50
1565         for name in ':pagesize @pagesize'.split():
1566             if self.form.has_key(name):
1567                 self.special_char = name[0]
1568                 self.pagesize = int(self.form[name].value)
1570         self.startwith = 0
1571         for name in ':startwith @startwith'.split():
1572             if self.form.has_key(name):
1573                 self.special_char = name[0]
1574                 self.startwith = int(self.form[name].value)
1576     def updateFromURL(self, url):
1577         ''' Parse the URL for query args, and update my attributes using the
1578             values.
1579         ''' 
1580         env = {'QUERY_STRING': url}
1581         self.form = cgi.FieldStorage(environ=env)
1583         self._post_init()
1585     def update(self, kwargs):
1586         ''' Update my attributes using the keyword args
1587         '''
1588         self.__dict__.update(kwargs)
1589         if kwargs.has_key('columns'):
1590             self.show = ShowDict(self.columns)
1592     def description(self):
1593         ''' Return a description of the request - handle for the page title.
1594         '''
1595         s = [self.client.db.config.TRACKER_NAME]
1596         if self.classname:
1597             if self.client.nodeid:
1598                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1599             else:
1600                 if self.template == 'item':
1601                     s.append('- new %s'%self.classname)
1602                 elif self.template == 'index':
1603                     s.append('- %s index'%self.classname)
1604                 else:
1605                     s.append('- %s %s'%(self.classname, self.template))
1606         else:
1607             s.append('- home')
1608         return ' '.join(s)
1610     def __str__(self):
1611         d = {}
1612         d.update(self.__dict__)
1613         f = ''
1614         for k in self.form.keys():
1615             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1616         d['form'] = f
1617         e = ''
1618         for k,v in self.env.items():
1619             e += '\n     %r=%r'%(k, v)
1620         d['env'] = e
1621         return '''
1622 form: %(form)s
1623 base: %(base)r
1624 classname: %(classname)r
1625 template: %(template)r
1626 columns: %(columns)r
1627 sort: %(sort)r
1628 group: %(group)r
1629 filter: %(filter)r
1630 search_text: %(search_text)r
1631 pagesize: %(pagesize)r
1632 startwith: %(startwith)r
1633 env: %(env)s
1634 '''%d
1636     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1637             filterspec=1):
1638         ''' return the current index args as form elements '''
1639         l = []
1640         sc = self.special_char
1641         s = self.input(type="hidden",name="%s",value="%s")
1642         if columns and self.columns:
1643             l.append(s%(sc+'columns', ','.join(self.columns)))
1644         if sort and self.sort[1] is not None:
1645             if self.sort[0] == '-':
1646                 val = '-'+self.sort[1]
1647             else:
1648                 val = self.sort[1]
1649             l.append(s%(sc+'sort', val))
1650         if group and self.group[1] is not None:
1651             if self.group[0] == '-':
1652                 val = '-'+self.group[1]
1653             else:
1654                 val = self.group[1]
1655             l.append(s%(sc+'group', val))
1656         if filter and self.filter:
1657             l.append(s%(sc+'filter', ','.join(self.filter)))
1658         if filterspec:
1659             for k,v in self.filterspec.items():
1660                 if type(v) == type([]):
1661                     l.append(s%(k, ','.join(v)))
1662                 else:
1663                     l.append(s%(k, v))
1664         if self.search_text:
1665             l.append(s%(sc+'search_text', self.search_text))
1666         l.append(s%(sc+'pagesize', self.pagesize))
1667         l.append(s%(sc+'startwith', self.startwith))
1668         return '\n'.join(l)
1670     def indexargs_url(self, url, args):
1671         ''' Embed the current index args in a URL
1672         '''
1673         sc = self.special_char
1674         l = ['%s=%s'%(k,v) for k,v in args.items()]
1676         # pull out the special values (prefixed by @ or :)
1677         specials = {}
1678         for key in args.keys():
1679             if key[0] in '@:':
1680                 specials[key[1:]] = args[key]
1682         # ok, now handle the specials we received in the request
1683         if self.columns and not specials.has_key('columns'):
1684             l.append(sc+'columns=%s'%(','.join(self.columns)))
1685         if self.sort[1] is not None and not specials.has_key('sort'):
1686             if self.sort[0] == '-':
1687                 val = '-'+self.sort[1]
1688             else:
1689                 val = self.sort[1]
1690             l.append(sc+'sort=%s'%val)
1691         if self.group[1] is not None and not specials.has_key('group'):
1692             if self.group[0] == '-':
1693                 val = '-'+self.group[1]
1694             else:
1695                 val = self.group[1]
1696             l.append(sc+'group=%s'%val)
1697         if self.filter and not specials.has_key('filter'):
1698             l.append(sc+'filter=%s'%(','.join(self.filter)))
1699         if self.search_text and not specials.has_key('search_text'):
1700             l.append(sc+'search_text=%s'%self.search_text)
1701         if not specials.has_key('pagesize'):
1702             l.append(sc+'pagesize=%s'%self.pagesize)
1703         if not specials.has_key('startwith'):
1704             l.append(sc+'startwith=%s'%self.startwith)
1706         # finally, the remainder of the filter args in the request
1707         for k,v in self.filterspec.items():
1708             if not args.has_key(k):
1709                 if type(v) == type([]):
1710                     l.append('%s=%s'%(k, ','.join(v)))
1711                 else:
1712                     l.append('%s=%s'%(k, v))
1713         return '%s?%s'%(url, '&'.join(l))
1714     indexargs_href = indexargs_url
1716     def base_javascript(self):
1717         return '''
1718 <script type="text/javascript">
1719 submitted = false;
1720 function submit_once() {
1721     if (submitted) {
1722         alert("Your request is being processed.\\nPlease be patient.");
1723         return 0;
1724     }
1725     submitted = true;
1726     return 1;
1729 function help_window(helpurl, width, height) {
1730     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1732 </script>
1733 '''%self.base
1735     def batch(self):
1736         ''' Return a batch object for results from the "current search"
1737         '''
1738         filterspec = self.filterspec
1739         sort = self.sort
1740         group = self.group
1742         # get the list of ids we're batching over
1743         klass = self.client.db.getclass(self.classname)
1744         if self.search_text:
1745             matches = self.client.db.indexer.search(
1746                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1747         else:
1748             matches = None
1749         l = klass.filter(matches, filterspec, sort, group)
1751         # return the batch object, using IDs only
1752         return Batch(self.client, l, self.pagesize, self.startwith,
1753             classname=self.classname)
1755 # extend the standard ZTUtils Batch object to remove dependency on
1756 # Acquisition and add a couple of useful methods
1757 class Batch(ZTUtils.Batch):
1758     ''' Use me to turn a list of items, or item ids of a given class, into a
1759         series of batches.
1761         ========= ========================================================
1762         Parameter  Usage
1763         ========= ========================================================
1764         sequence  a list of HTMLItems or item ids
1765         classname if sequence is a list of ids, this is the class of item
1766         size      how big to make the sequence.
1767         start     where to start (0-indexed) in the sequence.
1768         end       where to end (0-indexed) in the sequence.
1769         orphan    if the next batch would contain less items than this
1770                   value, then it is combined with this batch
1771         overlap   the number of items shared between adjacent batches
1772         ========= ========================================================
1774         Attributes: Note that the "start" attribute, unlike the
1775         argument, is a 1-based index (I know, lame).  "first" is the
1776         0-based index.  "length" is the actual number of elements in
1777         the batch.
1779         "sequence_length" is the length of the original, unbatched, sequence.
1780     '''
1781     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1782             overlap=0, classname=None):
1783         self.client = client
1784         self.last_index = self.last_item = None
1785         self.current_item = None
1786         self.classname = classname
1787         self.sequence_length = len(sequence)
1788         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1789             overlap)
1791     # overwrite so we can late-instantiate the HTMLItem instance
1792     def __getitem__(self, index):
1793         if index < 0:
1794             if index + self.end < self.first: raise IndexError, index
1795             return self._sequence[index + self.end]
1796         
1797         if index >= self.length:
1798             raise IndexError, index
1800         # move the last_item along - but only if the fetched index changes
1801         # (for some reason, index 0 is fetched twice)
1802         if index != self.last_index:
1803             self.last_item = self.current_item
1804             self.last_index = index
1806         item = self._sequence[index + self.first]
1807         if self.classname:
1808             # map the item ids to instances
1809             if self.classname == 'user':
1810                 item = HTMLUser(self.client, self.classname, item)
1811             else:
1812                 item = HTMLItem(self.client, self.classname, item)
1813         self.current_item = item
1814         return item
1816     def propchanged(self, property):
1817         ''' Detect if the property marked as being the group property
1818             changed in the last iteration fetch
1819         '''
1820         if (self.last_item is None or
1821                 self.last_item[property] != self.current_item[property]):
1822             return 1
1823         return 0
1825     # override these 'cos we don't have access to acquisition
1826     def previous(self):
1827         if self.start == 1:
1828             return None
1829         return Batch(self.client, self._sequence, self._size,
1830             self.first - self._size + self.overlap, 0, self.orphan,
1831             self.overlap)
1833     def next(self):
1834         try:
1835             self._sequence[self.end]
1836         except IndexError:
1837             return None
1838         return Batch(self.client, self._sequence, self._size,
1839             self.end - self.overlap, 0, self.orphan, self.overlap)
1841 class TemplatingUtils:
1842     ''' Utilities for templating
1843     '''
1844     def __init__(self, client):
1845         self.client = client
1846     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1847         return Batch(self.client, sequence, size, start, end, orphan,
1848             overlap)