Code

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