Code

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