Code

fixed error in cgi/templates.py (sf bug 652089)
[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 class NoTemplate(Exception):
26     pass
28 class Templates:
29     templates = {}
31     def __init__(self, dir):
32         self.dir = dir
34     def precompileTemplates(self):
35         ''' Go through a directory and precompile all the templates therein
36         '''
37         for filename in os.listdir(self.dir):
38             if os.path.isdir(filename): continue
39             if '.' in filename:
40                 name, extension = filename.split('.')
41                 self.getTemplate(name, extension)
42             else:
43                 self.getTemplate(filename, None)
45     def get(self, name, extension):
46         ''' Interface to get a template, possibly loading a compiled template.
48             "name" and "extension" indicate the template we're after, which in
49             most cases will be "name.extension". If "extension" is None, then
50             we look for a template just called "name" with no extension.
52             If the file "name.extension" doesn't exist, we look for
53             "_generic.extension" as a fallback.
54         '''
55         # default the name to "home"
56         if name is None:
57             name = 'home'
59         # find the source, figure the time it was last modified
60         if extension:
61             filename = '%s.%s'%(name, extension)
62         else:
63             filename = name
64         src = os.path.join(self.dir, filename)
65         try:
66             stime = os.stat(src)[os.path.stat.ST_MTIME]
67         except os.error, error:
68             if error.errno != errno.ENOENT:
69                 raise
70             if not extension:
71                 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
73             # try for a generic template
74             generic = '_generic.%s'%extension
75             src = os.path.join(self.dir, generic)
76             try:
77                 stime = os.stat(src)[os.path.stat.ST_MTIME]
78             except os.error, error:
79                 if error.errno != errno.ENOENT:
80                     raise
81                 # nicer error
82                 raise NoTemplate, 'No template file exists for templating '\
83                     '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
84                     extension, filename, generic)
85             filename = generic
87         if self.templates.has_key(src) and \
88                 stime < self.templates[src].mtime:
89             # compiled template is up to date
90             return self.templates[src]
92         # compile the template
93         self.templates[src] = pt = RoundupPageTemplate()
94         pt.write(open(src).read())
95         pt.id = filename
96         pt.mtime = time.time()
97         return pt
99     def __getitem__(self, name):
100         name, extension = os.path.splitext(name)
101         if extension:
102             extension = extension[1:]
103         try:
104             return self.get(name, extension)
105         except NoTemplate, message:
106             raise KeyError, message
108 class RoundupPageTemplate(PageTemplate.PageTemplate):
109     ''' A Roundup-specific PageTemplate.
111         Interrogate the client to set up the various template variables to
112         be available:
114         *context*
115          this is one of three things:
116          1. None - we're viewing a "home" page
117          2. The current class of item being displayed. This is an HTMLClass
118             instance.
119          3. The current item from the database, if we're viewing a specific
120             item, as an HTMLItem instance.
121         *request*
122           Includes information about the current request, including:
123            - the url
124            - the current index information (``filterspec``, ``filter`` args,
125              ``properties``, etc) parsed out of the form. 
126            - methods for easy filterspec link generation
127            - *user*, the current user node as an HTMLItem instance
128            - *form*, the current CGI form information as a FieldStorage
129         *config*
130           The current tracker config.
131         *db*
132           The current database, used to access arbitrary database items.
133         *utils*
134           This is a special class that has its base in the TemplatingUtils
135           class in this file. If the tracker interfaces module defines a
136           TemplatingUtils class then it is mixed in, overriding the methods
137           in the base class.
138     '''
139     def getContext(self, client, classname, request):
140         # construct the TemplatingUtils class
141         utils = TemplatingUtils
142         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
143             class utils(client.instance.interfaces.TemplatingUtils, utils):
144                 pass
146         c = {
147              'options': {},
148              'nothing': None,
149              'request': request,
150              'db': HTMLDatabase(client),
151              'config': client.instance.config,
152              'tracker': client.instance,
153              'utils': utils(client),
154              'templates': Templates(client.instance.config.TEMPLATES),
155         }
156         # add in the item if there is one
157         if client.nodeid:
158             if classname == 'user':
159                 c['context'] = HTMLUser(client, classname, client.nodeid)
160             else:
161                 c['context'] = HTMLItem(client, classname, client.nodeid)
162         elif client.db.classes.has_key(classname):
163             c['context'] = HTMLClass(client, classname)
164         return c
166     def render(self, client, classname, request, **options):
167         """Render this Page Template"""
169         if not self._v_cooked:
170             self._cook()
172         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
174         if self._v_errors:
175             raise PageTemplate.PTRuntimeError, \
176                 'Page Template %s has errors.'%self.id
178         # figure the context
179         classname = classname or client.classname
180         request = request or HTMLRequest(client)
181         c = self.getContext(client, classname, request)
182         c.update({'options': options})
184         # and go
185         output = StringIO.StringIO()
186         TALInterpreter(self._v_program, self.macros,
187             getEngine().getContext(c), output, tal=1, strictinsert=0)()
188         return output.getvalue()
190 class HTMLDatabase:
191     ''' Return HTMLClasses for valid class fetches
192     '''
193     def __init__(self, client):
194         self._client = client
196         # we want config to be exposed
197         self.config = client.db.config
199     def __getitem__(self, item):
200         self._client.db.getclass(item)
201         return HTMLClass(self._client, item)
203     def __getattr__(self, attr):
204         try:
205             return self[attr]
206         except KeyError:
207             raise AttributeError, attr
209     def classes(self):
210         l = self._client.db.classes.keys()
211         l.sort()
212         return [HTMLClass(self._client, cn) for cn in l]
214 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
215     cl = db.getclass(prop.classname)
216     l = []
217     for entry in ids:
218         if num_re.match(entry):
219             l.append(entry)
220         else:
221             try:
222                 l.append(cl.lookup(entry))
223             except KeyError:
224                 # ignore invalid keys
225                 pass
226     return l
228 class HTMLPermissions:
229     ''' Helpers that provide answers to commonly asked Permission questions.
230     '''
231     def is_edit_ok(self):
232         ''' Is the user allowed to Edit the current class?
233         '''
234         return self._db.security.hasPermission('Edit', self._client.userid,
235             self._classname)
236     def is_view_ok(self):
237         ''' Is the user allowed to View the current class?
238         '''
239         return self._db.security.hasPermission('View', self._client.userid,
240             self._classname)
241     def is_only_view_ok(self):
242         ''' Is the user only allowed to View (ie. not Edit) the current class?
243         '''
244         return self.is_view_ok() and not self.is_edit_ok()
246 class HTMLClass(HTMLPermissions):
247     ''' Accesses through a class (either through *class* or *db.<classname>*)
248     '''
249     def __init__(self, client, classname):
250         self._client = client
251         self._db = client.db
253         # we want classname to be exposed, but _classname gives a
254         # consistent API for extending Class/Item
255         self._classname = self.classname = classname
256         self._klass = self._db.getclass(self.classname)
257         self._props = self._klass.getprops()
259     def __repr__(self):
260         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
262     def __getitem__(self, item):
263         ''' return an HTMLProperty instance
264         '''
265        #print 'HTMLClass.getitem', (self, item)
267         # we don't exist
268         if item == 'id':
269             return None
271         # get the property
272         prop = self._props[item]
274         # look up the correct HTMLProperty class
275         form = self._client.form
276         for klass, htmlklass in propclasses:
277             if not isinstance(prop, klass):
278                 continue
279             if form.has_key(item):
280                 if isinstance(prop, hyperdb.Multilink):
281                     value = lookupIds(self._db, prop,
282                         handleListCGIValue(form[item]))
283                 elif isinstance(prop, hyperdb.Link):
284                     value = form[item].value.strip()
285                     if value:
286                         value = lookupIds(self._db, prop, [value])[0]
287                     else:
288                         value = None
289                 else:
290                     value = form[item].value.strip() or None
291             else:
292                 if isinstance(prop, hyperdb.Multilink):
293                     value = []
294                 else:
295                     value = None
296             return htmlklass(self._client, '', prop, item, value)
298         # no good
299         raise KeyError, item
301     def __getattr__(self, attr):
302         ''' convenience access '''
303         try:
304             return self[attr]
305         except KeyError:
306             raise AttributeError, attr
308     def getItem(self, itemid, num_re=re.compile('\d+')):
309         ''' Get an item of this class by its item id.
310         '''
311         # make sure we're looking at an itemid
312         if not num_re.match(itemid):
313             itemid = self._klass.lookup(itemid)
315         if self.classname == 'user':
316             klass = HTMLUser
317         else:
318             klass = HTMLItem
320         return klass(self._client, self.classname, itemid)
322     def properties(self):
323         ''' Return HTMLProperty for all of this class' properties.
324         '''
325         l = []
326         for name, prop in self._props.items():
327             for klass, htmlklass in propclasses:
328                 if isinstance(prop, hyperdb.Multilink):
329                     value = []
330                 else:
331                     value = None
332                 if isinstance(prop, klass):
333                     l.append(htmlklass(self._client, '', prop, name, value))
334         return l
336     def list(self):
337         ''' List all items in this class.
338         '''
339         if self.classname == 'user':
340             klass = HTMLUser
341         else:
342             klass = HTMLItem
344         # get the list and sort it nicely
345         l = self._klass.list()
346         sortfunc = make_sort_function(self._db, self.classname)
347         l.sort(sortfunc)
349         l = [klass(self._client, self.classname, x) for x in l]
350         return l
352     def csv(self):
353         ''' Return the items of this class as a chunk of CSV text.
354         '''
355         # get the CSV module
356         try:
357             import csv
358         except ImportError:
359             return 'Sorry, you need the csv module to use this function.\n'\
360                 'Get it from: http://www.object-craft.com.au/projects/csv/'
362         props = self.propnames()
363         p = csv.parser()
364         s = StringIO.StringIO()
365         s.write(p.join(props) + '\n')
366         for nodeid in self._klass.list():
367             l = []
368             for name in props:
369                 value = self._klass.get(nodeid, name)
370                 if value is None:
371                     l.append('')
372                 elif isinstance(value, type([])):
373                     l.append(':'.join(map(str, value)))
374                 else:
375                     l.append(str(self._klass.get(nodeid, name)))
376             s.write(p.join(l) + '\n')
377         return s.getvalue()
379     def propnames(self):
380         ''' Return the list of the names of the properties of this class.
381         '''
382         idlessprops = self._klass.getprops(protected=0).keys()
383         idlessprops.sort()
384         return ['id'] + idlessprops
386     def filter(self, request=None):
387         ''' Return a list of items from this class, filtered and sorted
388             by the current requested filterspec/filter/sort/group args
389         '''
390         if request is not None:
391             filterspec = request.filterspec
392             sort = request.sort
393             group = request.group
394         if self.classname == 'user':
395             klass = HTMLUser
396         else:
397             klass = HTMLItem
398         l = [klass(self._client, self.classname, x)
399              for x in self._klass.filter(None, filterspec, sort, group)]
400         return l
402     def classhelp(self, properties=None, label='list', width='500',
403             height='400'):
404         ''' Pop up a javascript window with class help
406             This generates a link to a popup window which displays the 
407             properties indicated by "properties" of the class named by
408             "classname". The "properties" should be a comma-separated list
409             (eg. 'id,name,description'). Properties defaults to all the
410             properties of a class (excluding id, creator, created and
411             activity).
413             You may optionally override the label displayed, the width and
414             height. The popup window will be resizable and scrollable.
415         '''
416         if properties is None:
417             properties = self._klass.getprops(protected=0).keys()
418             properties.sort()
419             properties = ','.join(properties)
420         return '<a href="javascript:help_window(\'%s?:template=help&' \
421             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
422             self.classname, properties, width, height, label)
424     def submit(self, label="Submit New Entry"):
425         ''' Generate a submit button (and action hidden element)
426         '''
427         return '  <input type="hidden" name=":action" value="new">\n'\
428         '  <input type="submit" name="submit" value="%s">'%label
430     def history(self):
431         return 'New node - no history'
433     def renderWith(self, name, **kwargs):
434         ''' Render this class with the given template.
435         '''
436         # create a new request and override the specified args
437         req = HTMLRequest(self._client)
438         req.classname = self.classname
439         req.update(kwargs)
441         # new template, using the specified classname and request
442         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
444         # use our fabricated request
445         return pt.render(self._client, self.classname, req)
447 class HTMLItem(HTMLPermissions):
448     ''' Accesses through an *item*
449     '''
450     def __init__(self, client, classname, nodeid):
451         self._client = client
452         self._db = client.db
453         self._classname = classname
454         self._nodeid = nodeid
455         self._klass = self._db.getclass(classname)
456         self._props = self._klass.getprops()
458     def __repr__(self):
459         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
460             self._nodeid)
462     def __getitem__(self, item):
463         ''' return an HTMLProperty instance
464         '''
465         #print 'HTMLItem.getitem', (self, item)
466         if item == 'id':
467             return self._nodeid
469         # get the property
470         prop = self._props[item]
472         # get the value, handling missing values
473         value = self._klass.get(self._nodeid, item, None)
474         if value is None:
475             if isinstance(self._props[item], hyperdb.Multilink):
476                 value = []
478         # look up the correct HTMLProperty class
479         for klass, htmlklass in propclasses:
480             if isinstance(prop, klass):
481                 return htmlklass(self._client, self._nodeid, prop, item, value)
483         raise KeyErorr, item
485     def __getattr__(self, attr):
486         ''' convenience access to properties '''
487         try:
488             return self[attr]
489         except KeyError:
490             raise AttributeError, attr
491     
492     def submit(self, label="Submit Changes"):
493         ''' Generate a submit button (and action hidden element)
494         '''
495         return '  <input type="hidden" name=":action" value="edit">\n'\
496         '  <input type="submit" name="submit" value="%s">'%label
498     def journal(self, direction='descending'):
499         ''' Return a list of HTMLJournalEntry instances.
500         '''
501         # XXX do this
502         return []
504     def history(self, direction='descending', dre=re.compile('\d+')):
505         l = ['<table class="history">'
506              '<tr><th colspan="4" class="header">',
507              _('History'),
508              '</th></tr><tr>',
509              _('<th>Date</th>'),
510              _('<th>User</th>'),
511              _('<th>Action</th>'),
512              _('<th>Args</th>'),
513             '</tr>']
514         comments = {}
515         history = self._klass.history(self._nodeid)
516         history.sort()
517         if direction == 'descending':
518             history.reverse()
519         for id, evt_date, user, action, args in history:
520             date_s = str(evt_date).replace("."," ")
521             arg_s = ''
522             if action == 'link' and type(args) == type(()):
523                 if len(args) == 3:
524                     linkcl, linkid, key = args
525                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
526                         linkcl, linkid, key)
527                 else:
528                     arg_s = str(args)
530             elif action == 'unlink' and type(args) == type(()):
531                 if len(args) == 3:
532                     linkcl, linkid, key = args
533                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
534                         linkcl, linkid, key)
535                 else:
536                     arg_s = str(args)
538             elif type(args) == type({}):
539                 cell = []
540                 for k in args.keys():
541                     # try to get the relevant property and treat it
542                     # specially
543                     try:
544                         prop = self._props[k]
545                     except KeyError:
546                         prop = None
547                     if prop is not None:
548                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
549                                 isinstance(prop, hyperdb.Link)):
550                             # figure what the link class is
551                             classname = prop.classname
552                             try:
553                                 linkcl = self._db.getclass(classname)
554                             except KeyError:
555                                 labelprop = None
556                                 comments[classname] = _('''The linked class
557                                     %(classname)s no longer exists''')%locals()
558                             labelprop = linkcl.labelprop(1)
559                             hrefable = os.path.exists(
560                                 os.path.join(self._db.config.TEMPLATES,
561                                 classname+'.item'))
563                         if isinstance(prop, hyperdb.Multilink) and \
564                                 len(args[k]) > 0:
565                             ml = []
566                             for linkid in args[k]:
567                                 if isinstance(linkid, type(())):
568                                     sublabel = linkid[0] + ' '
569                                     linkids = linkid[1]
570                                 else:
571                                     sublabel = ''
572                                     linkids = [linkid]
573                                 subml = []
574                                 for linkid in linkids:
575                                     label = classname + linkid
576                                     # if we have a label property, try to use it
577                                     # TODO: test for node existence even when
578                                     # there's no labelprop!
579                                     try:
580                                         if labelprop is not None and \
581                                                 labelprop != 'id':
582                                             label = linkcl.get(linkid, labelprop)
583                                     except IndexError:
584                                         comments['no_link'] = _('''<strike>The
585                                             linked node no longer
586                                             exists</strike>''')
587                                         subml.append('<strike>%s</strike>'%label)
588                                     else:
589                                         if hrefable:
590                                             subml.append('<a href="%s%s">%s</a>'%(
591                                                 classname, linkid, label))
592                                         else:
593                                             subml.append(label)
594                                 ml.append(sublabel + ', '.join(subml))
595                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
596                         elif isinstance(prop, hyperdb.Link) and args[k]:
597                             label = classname + args[k]
598                             # if we have a label property, try to use it
599                             # TODO: test for node existence even when
600                             # there's no labelprop!
601                             if labelprop is not None and labelprop != 'id':
602                                 try:
603                                     label = linkcl.get(args[k], labelprop)
604                                 except IndexError:
605                                     comments['no_link'] = _('''<strike>The
606                                         linked node no longer
607                                         exists</strike>''')
608                                     cell.append(' <strike>%s</strike>,\n'%label)
609                                     # "flag" this is done .... euwww
610                                     label = None
611                             if label is not None:
612                                 if hrefable:
613                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
614                                         classname, args[k], label))
615                                 else:
616                                     cell.append('%s: %s' % (k,label))
618                         elif isinstance(prop, hyperdb.Date) and args[k]:
619                             d = date.Date(args[k])
620                             cell.append('%s: %s'%(k, str(d)))
622                         elif isinstance(prop, hyperdb.Interval) and args[k]:
623                             d = date.Interval(args[k])
624                             cell.append('%s: %s'%(k, str(d)))
626                         elif isinstance(prop, hyperdb.String) and args[k]:
627                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
629                         elif not args[k]:
630                             cell.append('%s: (no value)\n'%k)
632                         else:
633                             cell.append('%s: %s\n'%(k, str(args[k])))
634                     else:
635                         # property no longer exists
636                         comments['no_exist'] = _('''<em>The indicated property
637                             no longer exists</em>''')
638                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
639                 arg_s = '<br />'.join(cell)
640             else:
641                 # unkown event!!
642                 comments['unknown'] = _('''<strong><em>This event is not
643                     handled by the history display!</em></strong>''')
644                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
645             date_s = date_s.replace(' ', '&nbsp;')
646             # if the user's an itemid, figure the username (older journals
647             # have the username)
648             if dre.match(user):
649                 user = self._db.user.get(user, 'username')
650             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
651                 date_s, user, action, arg_s))
652         if comments:
653             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
654         for entry in comments.values():
655             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
656         l.append('</table>')
657         return '\n'.join(l)
659     def renderQueryForm(self):
660         ''' Render this item, which is a query, as a search form.
661         '''
662         # create a new request and override the specified args
663         req = HTMLRequest(self._client)
664         req.classname = self._klass.get(self._nodeid, 'klass')
665         name = self._klass.get(self._nodeid, 'name')
666         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
667             '&:queryname=%s'%urllib.quote(name))
669         # new template, using the specified classname and request
670         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
672         # use our fabricated request
673         return pt.render(self._client, req.classname, req)
675 class HTMLUser(HTMLItem):
676     ''' Accesses through the *user* (a special case of item)
677     '''
678     def __init__(self, client, classname, nodeid):
679         HTMLItem.__init__(self, client, 'user', nodeid)
680         self._default_classname = client.classname
682         # used for security checks
683         self._security = client.db.security
685     _marker = []
686     def hasPermission(self, role, classname=_marker):
687         ''' Determine if the user has the Role.
689             The class being tested defaults to the template's class, but may
690             be overidden for this test by suppling an alternate classname.
691         '''
692         if classname is self._marker:
693             classname = self._default_classname
694         return self._security.hasPermission(role, self._nodeid, classname)
696     def is_edit_ok(self):
697         ''' Is the user allowed to Edit the current class?
698             Also check whether this is the current user's info.
699         '''
700         return self._db.security.hasPermission('Edit', self._client.userid,
701             self._classname) or self._nodeid == self._client.userid
703     def is_view_ok(self):
704         ''' Is the user allowed to View the current class?
705             Also check whether this is the current user's info.
706         '''
707         return self._db.security.hasPermission('Edit', self._client.userid,
708             self._classname) or self._nodeid == self._client.userid
710 class HTMLProperty:
711     ''' String, Number, Date, Interval HTMLProperty
713         Has useful attributes:
715          _name  the name of the property
716          _value the value of the property if any
718         A wrapper object which may be stringified for the plain() behaviour.
719     '''
720     def __init__(self, client, nodeid, prop, name, value):
721         self._client = client
722         self._db = client.db
723         self._nodeid = nodeid
724         self._prop = prop
725         self._name = name
726         self._value = value
727     def __repr__(self):
728         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
729     def __str__(self):
730         return self.plain()
731     def __cmp__(self, other):
732         if isinstance(other, HTMLProperty):
733             return cmp(self._value, other._value)
734         return cmp(self._value, other)
736 class StringHTMLProperty(HTMLProperty):
737     url_re = re.compile(r'\w{3,6}://\S+')
738     email_re = re.compile(r'\w+@[\w\.\-]+')
739     designator_re = re.compile(r'([a-z_]+)(\d+)')
740     def _url_repl(self, match):
741         s = match.group(0)
742         return '<a href="%s">%s</a>'%(s, s)
743     def _email_repl(self, match):
744         s = match.group(0)
745         return '<a href="mailto:%s">%s</a>'%(s, s)
746     def _designator_repl(self, match):
747         s = match.group(0)
748         s1 = match.group(1)
749         s2 = match.group(2)
750         try:
751             # make sure s1 is a valid tracker classname
752             self._db.getclass(s1)
753             return '<a href="%s">%s %s</a>'%(s, s1, s2)
754         except KeyError:
755             return '%s%s'%(s1, s2)
757     def plain(self, escape=0, hyperlink=1):
758         ''' Render a "plain" representation of the property
759             
760             "escape" turns on/off HTML quoting
761             "hyperlink" turns on/off in-text hyperlinking of URLs, email
762                 addresses and designators
763         '''
764         if self._value is None:
765             return ''
766         if escape:
767             s = cgi.escape(str(self._value))
768         else:
769             s = self._value
770         if hyperlink:
771             s = self.url_re.sub(self._url_repl, s)
772             s = self.email_re.sub(self._email_repl, s)
773             s = self.designator_re.sub(self._designator_repl, s)
774         return s
776     def stext(self, escape=0):
777         ''' Render the value of the property as StructuredText.
779             This requires the StructureText module to be installed separately.
780         '''
781         s = self.plain(escape=escape)
782         if not StructuredText:
783             return s
784         return StructuredText(s,level=1,header=0)
786     def field(self, size = 30):
787         ''' Render a form edit field for the property
788         '''
789         if self._value is None:
790             value = ''
791         else:
792             value = cgi.escape(str(self._value))
793             value = '&quot;'.join(value.split('"'))
794         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
796     def multiline(self, escape=0, rows=5, cols=40):
797         ''' Render a multiline form edit field for the property
798         '''
799         if self._value is None:
800             value = ''
801         else:
802             value = cgi.escape(str(self._value))
803             value = '&quot;'.join(value.split('"'))
804         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
805             self._name, rows, cols, value)
807     def email(self, escape=1):
808         ''' Render the value of the property as an obscured email address
809         '''
810         if self._value is None: value = ''
811         else: value = str(self._value)
812         if value.find('@') != -1:
813             name, domain = value.split('@')
814             domain = ' '.join(domain.split('.')[:-1])
815             name = name.replace('.', ' ')
816             value = '%s at %s ...'%(name, domain)
817         else:
818             value = value.replace('.', ' ')
819         if escape:
820             value = cgi.escape(value)
821         return value
823 class PasswordHTMLProperty(HTMLProperty):
824     def plain(self):
825         ''' Render a "plain" representation of the property
826         '''
827         if self._value is None:
828             return ''
829         return _('*encrypted*')
831     def field(self, size = 30):
832         ''' Render a form edit field for the property.
833         '''
834         return '<input type="password" name="%s" size="%s">'%(self._name, size)
836     def confirm(self, size = 30):
837         ''' Render a second form edit field for the property, used for 
838             confirmation that the user typed the password correctly. Generates
839             a field with name "name:confirm".
840         '''
841         return '<input type="password" name="%s:confirm" size="%s">'%(
842             self._name, size)
844 class NumberHTMLProperty(HTMLProperty):
845     def plain(self):
846         ''' Render a "plain" representation of the property
847         '''
848         return str(self._value)
850     def field(self, size = 30):
851         ''' Render a form edit field for the property
852         '''
853         if self._value is None:
854             value = ''
855         else:
856             value = cgi.escape(str(self._value))
857             value = '&quot;'.join(value.split('"'))
858         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
860 class BooleanHTMLProperty(HTMLProperty):
861     def plain(self):
862         ''' Render a "plain" representation of the property
863         '''
864         if self._value is None:
865             return ''
866         return self._value and "Yes" or "No"
868     def field(self):
869         ''' Render a form edit field for the property
870         '''
871         checked = self._value and "checked" or ""
872         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
873             checked)
874         if checked:
875             checked = ""
876         else:
877             checked = "checked"
878         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
879             checked)
880         return s
882 class DateHTMLProperty(HTMLProperty):
883     def plain(self):
884         ''' Render a "plain" representation of the property
885         '''
886         if self._value is None:
887             return ''
888         return str(self._value)
890     def field(self, size = 30):
891         ''' Render a form edit field for the property
892         '''
893         if self._value is None:
894             value = ''
895         else:
896             value = cgi.escape(str(self._value))
897             value = '&quot;'.join(value.split('"'))
898         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
900     def reldate(self, pretty=1):
901         ''' Render the interval between the date and now.
903             If the "pretty" flag is true, then make the display pretty.
904         '''
905         if not self._value:
906             return ''
908         # figure the interval
909         interval = date.Date('.') - self._value
910         if pretty:
911             return interval.pretty()
912         return str(interval)
914     def pretty(self, format='%d %B %Y'):
915         ''' Render the date in a pretty format (eg. month names, spaces).
917             The format string is a standard python strftime format string.
918             Note that if the day is zero, and appears at the start of the
919             string, then it'll be stripped from the output. This is handy
920             for the situatin when a date only specifies a month and a year.
921         '''
922         return self._value.pretty()
924     def local(self, offset):
925         ''' Return the date/time as a local (timezone offset) date/time.
926         '''
927         return DateHTMLProperty(self._client, self._nodeid, self._prop,
928             self._name, self._value.local())
930 class IntervalHTMLProperty(HTMLProperty):
931     def plain(self):
932         ''' Render a "plain" representation of the property
933         '''
934         if self._value is None:
935             return ''
936         return str(self._value)
938     def pretty(self):
939         ''' Render the interval in a pretty format (eg. "yesterday")
940         '''
941         return self._value.pretty()
943     def field(self, size = 30):
944         ''' Render a form edit field for the property
945         '''
946         if self._value is None:
947             value = ''
948         else:
949             value = cgi.escape(str(self._value))
950             value = '&quot;'.join(value.split('"'))
951         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
953 class LinkHTMLProperty(HTMLProperty):
954     ''' Link HTMLProperty
955         Include the above as well as being able to access the class
956         information. Stringifying the object itself results in the value
957         from the item being displayed. Accessing attributes of this object
958         result in the appropriate entry from the class being queried for the
959         property accessed (so item/assignedto/name would look up the user
960         entry identified by the assignedto property on item, and then the
961         name property of that user)
962     '''
963     def __init__(self, *args):
964         HTMLProperty.__init__(self, *args)
965         # if we're representing a form value, then the -1 from the form really
966         # should be a None
967         if str(self._value) == '-1':
968             self._value = None
970     def __getattr__(self, attr):
971         ''' return a new HTMLItem '''
972        #print 'Link.getattr', (self, attr, self._value)
973         if not self._value:
974             raise AttributeError, "Can't access missing value"
975         if self._prop.classname == 'user':
976             klass = HTMLUser
977         else:
978             klass = HTMLItem
979         i = klass(self._client, self._prop.classname, self._value)
980         return getattr(i, attr)
982     def plain(self, escape=0):
983         ''' Render a "plain" representation of the property
984         '''
985         if self._value is None:
986             return ''
987         linkcl = self._db.classes[self._prop.classname]
988         k = linkcl.labelprop(1)
989         value = str(linkcl.get(self._value, k))
990         if escape:
991             value = cgi.escape(value)
992         return value
994     def field(self, showid=0, size=None):
995         ''' Render a form edit field for the property
996         '''
997         linkcl = self._db.getclass(self._prop.classname)
998         if linkcl.getprops().has_key('order'):  
999             sort_on = 'order'  
1000         else:  
1001             sort_on = linkcl.labelprop()  
1002         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1003         # TODO: make this a field display, not a menu one!
1004         l = ['<select name="%s">'%self._name]
1005         k = linkcl.labelprop(1)
1006         if self._value is None:
1007             s = 'selected '
1008         else:
1009             s = ''
1010         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1012         # make sure we list the current value if it's retired
1013         if self._value and self._value not in options:
1014             options.insert(0, self._value)
1016         for optionid in options:
1017             # get the option value, and if it's None use an empty string
1018             option = linkcl.get(optionid, k) or ''
1020             # figure if this option is selected
1021             s = ''
1022             if optionid == self._value:
1023                 s = 'selected '
1025             # figure the label
1026             if showid:
1027                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1028             else:
1029                 lab = option
1031             # truncate if it's too long
1032             if size is not None and len(lab) > size:
1033                 lab = lab[:size-3] + '...'
1035             # and generate
1036             lab = cgi.escape(lab)
1037             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1038         l.append('</select>')
1039         return '\n'.join(l)
1041     def menu(self, size=None, height=None, showid=0, additional=[],
1042             **conditions):
1043         ''' Render a form select list for this property
1044         '''
1045         value = self._value
1047         # sort function
1048         sortfunc = make_sort_function(self._db, self._prop.classname)
1050         linkcl = self._db.getclass(self._prop.classname)
1051         l = ['<select name="%s">'%self._name]
1052         k = linkcl.labelprop(1)
1053         s = ''
1054         if value is None:
1055             s = 'selected '
1056         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1057         if linkcl.getprops().has_key('order'):  
1058             sort_on = ('+', 'order')
1059         else:  
1060             sort_on = ('+', linkcl.labelprop())
1061         options = linkcl.filter(None, conditions, sort_on, (None, None))
1063         # make sure we list the current value if it's retired
1064         if self._value and self._value not in options:
1065             options.insert(0, self._value)
1067         for optionid in options:
1068             # get the option value, and if it's None use an empty string
1069             option = linkcl.get(optionid, k) or ''
1071             # figure if this option is selected
1072             s = ''
1073             if value in [optionid, option]:
1074                 s = 'selected '
1076             # figure the label
1077             if showid:
1078                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1079             else:
1080                 lab = option
1082             # truncate if it's too long
1083             if size is not None and len(lab) > size:
1084                 lab = lab[:size-3] + '...'
1085             if additional:
1086                 m = []
1087                 for propname in additional:
1088                     m.append(linkcl.get(optionid, propname))
1089                 lab = lab + ' (%s)'%', '.join(map(str, m))
1091             # and generate
1092             lab = cgi.escape(lab)
1093             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1094         l.append('</select>')
1095         return '\n'.join(l)
1096 #    def checklist(self, ...)
1098 class MultilinkHTMLProperty(HTMLProperty):
1099     ''' Multilink HTMLProperty
1101         Also be iterable, returning a wrapper object like the Link case for
1102         each entry in the multilink.
1103     '''
1104     def __len__(self):
1105         ''' length of the multilink '''
1106         return len(self._value)
1108     def __getattr__(self, attr):
1109         ''' no extended attribute accesses make sense here '''
1110         raise AttributeError, attr
1112     def __getitem__(self, num):
1113         ''' iterate and return a new HTMLItem
1114         '''
1115        #print 'Multi.getitem', (self, num)
1116         value = self._value[num]
1117         if self._prop.classname == 'user':
1118             klass = HTMLUser
1119         else:
1120             klass = HTMLItem
1121         return klass(self._client, self._prop.classname, value)
1123     def __contains__(self, value):
1124         ''' Support the "in" operator. We have to make sure the passed-in
1125             value is a string first, not a *HTMLProperty.
1126         '''
1127         return str(value) in self._value
1129     def reverse(self):
1130         ''' return the list in reverse order
1131         '''
1132         l = self._value[:]
1133         l.reverse()
1134         if self._prop.classname == 'user':
1135             klass = HTMLUser
1136         else:
1137             klass = HTMLItem
1138         return [klass(self._client, self._prop.classname, value) for value in l]
1140     def plain(self, escape=0):
1141         ''' Render a "plain" representation of the property
1142         '''
1143         linkcl = self._db.classes[self._prop.classname]
1144         k = linkcl.labelprop(1)
1145         labels = []
1146         for v in self._value:
1147             labels.append(linkcl.get(v, k))
1148         value = ', '.join(labels)
1149         if escape:
1150             value = cgi.escape(value)
1151         return value
1153     def field(self, size=30, showid=0):
1154         ''' Render a form edit field for the property
1155         '''
1156         sortfunc = make_sort_function(self._db, self._prop.classname)
1157         linkcl = self._db.getclass(self._prop.classname)
1158         value = self._value[:]
1159         if value:
1160             value.sort(sortfunc)
1161         # map the id to the label property
1162         if not linkcl.getkey():
1163             showid=1
1164         if not showid:
1165             k = linkcl.labelprop(1)
1166             value = [linkcl.get(v, k) for v in value]
1167         value = cgi.escape(','.join(value))
1168         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1170     def menu(self, size=None, height=None, showid=0, additional=[],
1171             **conditions):
1172         ''' Render a form select list for this property
1173         '''
1174         value = self._value
1176         # sort function
1177         sortfunc = make_sort_function(self._db, self._prop.classname)
1179         linkcl = self._db.getclass(self._prop.classname)
1180         if linkcl.getprops().has_key('order'):  
1181             sort_on = ('+', 'order')
1182         else:  
1183             sort_on = ('+', linkcl.labelprop())
1184         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1185         height = height or min(len(options), 7)
1186         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1187         k = linkcl.labelprop(1)
1189         # make sure we list the current values if they're retired
1190         for val in value:
1191             if val not in options:
1192                 options.insert(0, val)
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 in value or option in value:
1201                 s = 'selected '
1203             # figure the label
1204             if showid:
1205                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1206             else:
1207                 lab = option
1208             # truncate if it's too long
1209             if size is not None and len(lab) > size:
1210                 lab = lab[:size-3] + '...'
1211             if additional:
1212                 m = []
1213                 for propname in additional:
1214                     m.append(linkcl.get(optionid, propname))
1215                 lab = lab + ' (%s)'%', '.join(m)
1217             # and generate
1218             lab = cgi.escape(lab)
1219             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1220                 lab))
1221         l.append('</select>')
1222         return '\n'.join(l)
1224 # set the propclasses for HTMLItem
1225 propclasses = (
1226     (hyperdb.String, StringHTMLProperty),
1227     (hyperdb.Number, NumberHTMLProperty),
1228     (hyperdb.Boolean, BooleanHTMLProperty),
1229     (hyperdb.Date, DateHTMLProperty),
1230     (hyperdb.Interval, IntervalHTMLProperty),
1231     (hyperdb.Password, PasswordHTMLProperty),
1232     (hyperdb.Link, LinkHTMLProperty),
1233     (hyperdb.Multilink, MultilinkHTMLProperty),
1236 def make_sort_function(db, classname):
1237     '''Make a sort function for a given class
1238     '''
1239     linkcl = db.getclass(classname)
1240     if linkcl.getprops().has_key('order'):
1241         sort_on = 'order'
1242     else:
1243         sort_on = linkcl.labelprop()
1244     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1245         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1246     return sortfunc
1248 def handleListCGIValue(value):
1249     ''' Value is either a single item or a list of items. Each item has a
1250         .value that we're actually interested in.
1251     '''
1252     if isinstance(value, type([])):
1253         return [value.value for value in value]
1254     else:
1255         value = value.value.strip()
1256         if not value:
1257             return []
1258         return value.split(',')
1260 class ShowDict:
1261     ''' A convenience access to the :columns index parameters
1262     '''
1263     def __init__(self, columns):
1264         self.columns = {}
1265         for col in columns:
1266             self.columns[col] = 1
1267     def __getitem__(self, name):
1268         return self.columns.has_key(name)
1270 class HTMLRequest:
1271     ''' The *request*, holding the CGI form and environment.
1273         "form" the CGI form as a cgi.FieldStorage
1274         "env" the CGI environment variables
1275         "base" the base URL for this instance
1276         "user" a HTMLUser instance for this user
1277         "classname" the current classname (possibly None)
1278         "template" the current template (suffix, also possibly None)
1280         Index args:
1281         "columns" dictionary of the columns to display in an index page
1282         "show" a convenience access to columns - request/show/colname will
1283                be true if the columns should be displayed, false otherwise
1284         "sort" index sort column (direction, column name)
1285         "group" index grouping property (direction, column name)
1286         "filter" properties to filter the index on
1287         "filterspec" values to filter the index on
1288         "search_text" text to perform a full-text search on for an index
1290     '''
1291     def __init__(self, client):
1292         self.client = client
1294         # easier access vars
1295         self.form = client.form
1296         self.env = client.env
1297         self.base = client.base
1298         self.user = HTMLUser(client, 'user', client.userid)
1300         # store the current class name and action
1301         self.classname = client.classname
1302         self.template = client.template
1304         self._post_init()
1306     def _post_init(self):
1307         ''' Set attributes based on self.form
1308         '''
1309         # extract the index display information from the form
1310         self.columns = []
1311         if self.form.has_key(':columns'):
1312             self.columns = handleListCGIValue(self.form[':columns'])
1313         self.show = ShowDict(self.columns)
1315         # sorting
1316         self.sort = (None, None)
1317         if self.form.has_key(':sort'):
1318             sort = self.form[':sort'].value
1319             if sort.startswith('-'):
1320                 self.sort = ('-', sort[1:])
1321             else:
1322                 self.sort = ('+', sort)
1323         if self.form.has_key(':sortdir'):
1324             self.sort = ('-', self.sort[1])
1326         # grouping
1327         self.group = (None, None)
1328         if self.form.has_key(':group'):
1329             group = self.form[':group'].value
1330             if group.startswith('-'):
1331                 self.group = ('-', group[1:])
1332             else:
1333                 self.group = ('+', group)
1334         if self.form.has_key(':groupdir'):
1335             self.group = ('-', self.group[1])
1337         # filtering
1338         self.filter = []
1339         if self.form.has_key(':filter'):
1340             self.filter = handleListCGIValue(self.form[':filter'])
1341         self.filterspec = {}
1342         db = self.client.db
1343         if self.classname is not None:
1344             props = db.getclass(self.classname).getprops()
1345             for name in self.filter:
1346                 if self.form.has_key(name):
1347                     prop = props[name]
1348                     fv = self.form[name]
1349                     if (isinstance(prop, hyperdb.Link) or
1350                             isinstance(prop, hyperdb.Multilink)):
1351                         self.filterspec[name] = lookupIds(db, prop,
1352                             handleListCGIValue(fv))
1353                     else:
1354                         self.filterspec[name] = fv.value
1356         # full-text search argument
1357         self.search_text = None
1358         if self.form.has_key(':search_text'):
1359             self.search_text = self.form[':search_text'].value
1361         # pagination - size and start index
1362         # figure batch args
1363         if self.form.has_key(':pagesize'):
1364             self.pagesize = int(self.form[':pagesize'].value)
1365         else:
1366             self.pagesize = 50
1367         if self.form.has_key(':startwith'):
1368             self.startwith = int(self.form[':startwith'].value)
1369         else:
1370             self.startwith = 0
1372     def updateFromURL(self, url):
1373         ''' Parse the URL for query args, and update my attributes using the
1374             values.
1375         ''' 
1376         self.form = {}
1377         for name, value in cgi.parse_qsl(url):
1378             if self.form.has_key(name):
1379                 if isinstance(self.form[name], type([])):
1380                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1381                 else:
1382                     self.form[name] = [self.form[name],
1383                         cgi.MiniFieldStorage(name, value)]
1384             else:
1385                 self.form[name] = cgi.MiniFieldStorage(name, value)
1386         self._post_init()
1388     def update(self, kwargs):
1389         ''' Update my attributes using the keyword args
1390         '''
1391         self.__dict__.update(kwargs)
1392         if kwargs.has_key('columns'):
1393             self.show = ShowDict(self.columns)
1395     def description(self):
1396         ''' Return a description of the request - handle for the page title.
1397         '''
1398         s = [self.client.db.config.TRACKER_NAME]
1399         if self.classname:
1400             if self.client.nodeid:
1401                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1402             else:
1403                 if self.template == 'item':
1404                     s.append('- new %s'%self.classname)
1405                 elif self.template == 'index':
1406                     s.append('- %s index'%self.classname)
1407                 else:
1408                     s.append('- %s %s'%(self.classname, self.template))
1409         else:
1410             s.append('- home')
1411         return ' '.join(s)
1413     def __str__(self):
1414         d = {}
1415         d.update(self.__dict__)
1416         f = ''
1417         for k in self.form.keys():
1418             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1419         d['form'] = f
1420         e = ''
1421         for k,v in self.env.items():
1422             e += '\n     %r=%r'%(k, v)
1423         d['env'] = e
1424         return '''
1425 form: %(form)s
1426 base: %(base)r
1427 classname: %(classname)r
1428 template: %(template)r
1429 columns: %(columns)r
1430 sort: %(sort)r
1431 group: %(group)r
1432 filter: %(filter)r
1433 search_text: %(search_text)r
1434 pagesize: %(pagesize)r
1435 startwith: %(startwith)r
1436 env: %(env)s
1437 '''%d
1439     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1440             filterspec=1):
1441         ''' return the current index args as form elements '''
1442         l = []
1443         s = '<input type="hidden" name="%s" value="%s">'
1444         if columns and self.columns:
1445             l.append(s%(':columns', ','.join(self.columns)))
1446         if sort and self.sort[1] is not None:
1447             if self.sort[0] == '-':
1448                 val = '-'+self.sort[1]
1449             else:
1450                 val = self.sort[1]
1451             l.append(s%(':sort', val))
1452         if group and self.group[1] is not None:
1453             if self.group[0] == '-':
1454                 val = '-'+self.group[1]
1455             else:
1456                 val = self.group[1]
1457             l.append(s%(':group', val))
1458         if filter and self.filter:
1459             l.append(s%(':filter', ','.join(self.filter)))
1460         if filterspec:
1461             for k,v in self.filterspec.items():
1462                 l.append(s%(k, ','.join(v)))
1463         if self.search_text:
1464             l.append(s%(':search_text', self.search_text))
1465         l.append(s%(':pagesize', self.pagesize))
1466         l.append(s%(':startwith', self.startwith))
1467         return '\n'.join(l)
1469     def indexargs_url(self, url, args):
1470         ''' embed the current index args in a URL '''
1471         l = ['%s=%s'%(k,v) for k,v in args.items()]
1472         if self.columns and not args.has_key(':columns'):
1473             l.append(':columns=%s'%(','.join(self.columns)))
1474         if self.sort[1] is not None and not args.has_key(':sort'):
1475             if self.sort[0] == '-':
1476                 val = '-'+self.sort[1]
1477             else:
1478                 val = self.sort[1]
1479             l.append(':sort=%s'%val)
1480         if self.group[1] is not None and not args.has_key(':group'):
1481             if self.group[0] == '-':
1482                 val = '-'+self.group[1]
1483             else:
1484                 val = self.group[1]
1485             l.append(':group=%s'%val)
1486         if self.filter and not args.has_key(':columns'):
1487             l.append(':filter=%s'%(','.join(self.filter)))
1488         for k,v in self.filterspec.items():
1489             if not args.has_key(k):
1490                 l.append('%s=%s'%(k, ','.join(v)))
1491         if self.search_text and not args.has_key(':search_text'):
1492             l.append(':search_text=%s'%self.search_text)
1493         if not args.has_key(':pagesize'):
1494             l.append(':pagesize=%s'%self.pagesize)
1495         if not args.has_key(':startwith'):
1496             l.append(':startwith=%s'%self.startwith)
1497         return '%s?%s'%(url, '&'.join(l))
1498     indexargs_href = indexargs_url
1500     def base_javascript(self):
1501         return '''
1502 <script language="javascript">
1503 submitted = false;
1504 function submit_once() {
1505     if (submitted) {
1506         alert("Your request is being processed.\\nPlease be patient.");
1507         return 0;
1508     }
1509     submitted = true;
1510     return 1;
1513 function help_window(helpurl, width, height) {
1514     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1516 </script>
1517 '''%self.base
1519     def batch(self):
1520         ''' Return a batch object for results from the "current search"
1521         '''
1522         filterspec = self.filterspec
1523         sort = self.sort
1524         group = self.group
1526         # get the list of ids we're batching over
1527         klass = self.client.db.getclass(self.classname)
1528         if self.search_text:
1529             matches = self.client.db.indexer.search(
1530                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1531         else:
1532             matches = None
1533         l = klass.filter(matches, filterspec, sort, group)
1535         # return the batch object, using IDs only
1536         return Batch(self.client, l, self.pagesize, self.startwith,
1537             classname=self.classname)
1539 # extend the standard ZTUtils Batch object to remove dependency on
1540 # Acquisition and add a couple of useful methods
1541 class Batch(ZTUtils.Batch):
1542     ''' Use me to turn a list of items, or item ids of a given class, into a
1543         series of batches.
1545         ========= ========================================================
1546         Parameter  Usage
1547         ========= ========================================================
1548         sequence  a list of HTMLItems or item ids
1549         classname if sequence is a list of ids, this is the class of item
1550         size      how big to make the sequence.
1551         start     where to start (0-indexed) in the sequence.
1552         end       where to end (0-indexed) in the sequence.
1553         orphan    if the next batch would contain less items than this
1554                   value, then it is combined with this batch
1555         overlap   the number of items shared between adjacent batches
1556         ========= ========================================================
1558         Attributes: Note that the "start" attribute, unlike the
1559         argument, is a 1-based index (I know, lame).  "first" is the
1560         0-based index.  "length" is the actual number of elements in
1561         the batch.
1563         "sequence_length" is the length of the original, unbatched, sequence.
1564     '''
1565     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1566             overlap=0, classname=None):
1567         self.client = client
1568         self.last_index = self.last_item = None
1569         self.current_item = None
1570         self.classname = classname
1571         self.sequence_length = len(sequence)
1572         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1573             overlap)
1575     # overwrite so we can late-instantiate the HTMLItem instance
1576     def __getitem__(self, index):
1577         if index < 0:
1578             if index + self.end < self.first: raise IndexError, index
1579             return self._sequence[index + self.end]
1580         
1581         if index >= self.length:
1582             raise IndexError, index
1584         # move the last_item along - but only if the fetched index changes
1585         # (for some reason, index 0 is fetched twice)
1586         if index != self.last_index:
1587             self.last_item = self.current_item
1588             self.last_index = index
1590         item = self._sequence[index + self.first]
1591         if self.classname:
1592             # map the item ids to instances
1593             if self.classname == 'user':
1594                 item = HTMLUser(self.client, self.classname, item)
1595             else:
1596                 item = HTMLItem(self.client, self.classname, item)
1597         self.current_item = item
1598         return item
1600     def propchanged(self, property):
1601         ''' Detect if the property marked as being the group property
1602             changed in the last iteration fetch
1603         '''
1604         if (self.last_item is None or
1605                 self.last_item[property] != self.current_item[property]):
1606             return 1
1607         return 0
1609     # override these 'cos we don't have access to acquisition
1610     def previous(self):
1611         if self.start == 1:
1612             return None
1613         return Batch(self.client, self._sequence, self._size,
1614             self.first - self._size + self.overlap, 0, self.orphan,
1615             self.overlap)
1617     def next(self):
1618         try:
1619             self._sequence[self.end]
1620         except IndexError:
1621             return None
1622         return Batch(self.client, self._sequence, self._size,
1623             self.end - self.overlap, 0, self.orphan, self.overlap)
1625 class TemplatingUtils:
1626     ''' Utilities for templating
1627     '''
1628     def __init__(self, client):
1629         self.client = client
1630     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1631         return Batch(self.client, sequence, size, start, end, orphan,
1632             overlap)