Code

4c4a13b3656ab9865bd139874e51d0f6cbdeabf5
[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'):
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             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
633                 date_s, user, action, arg_s))
634         if comments:
635             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
636         for entry in comments.values():
637             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
638         l.append('</table>')
639         return '\n'.join(l)
641     def renderQueryForm(self):
642         ''' Render this item, which is a query, as a search form.
643         '''
644         # create a new request and override the specified args
645         req = HTMLRequest(self._client)
646         req.classname = self._klass.get(self._nodeid, 'klass')
647         req.updateFromURL(self._klass.get(self._nodeid, 'url'))
649         # new template, using the specified classname and request
650         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
652         # use our fabricated request
653         return pt.render(self._client, req.classname, req)
655 class HTMLUser(HTMLItem):
656     ''' Accesses through the *user* (a special case of item)
657     '''
658     def __init__(self, client, classname, nodeid):
659         HTMLItem.__init__(self, client, 'user', nodeid)
660         self._default_classname = client.classname
662         # used for security checks
663         self._security = client.db.security
665     _marker = []
666     def hasPermission(self, role, classname=_marker):
667         ''' Determine if the user has the Role.
669             The class being tested defaults to the template's class, but may
670             be overidden for this test by suppling an alternate classname.
671         '''
672         if classname is self._marker:
673             classname = self._default_classname
674         return self._security.hasPermission(role, self._nodeid, classname)
676     def is_edit_ok(self):
677         ''' Is the user allowed to Edit the current class?
678             Also check whether this is the current user's info.
679         '''
680         return self._db.security.hasPermission('Edit', self._client.userid,
681             self._classname) or self._nodeid == self._client.userid
683     def is_view_ok(self):
684         ''' Is the user allowed to View the current class?
685             Also check whether this is the current user's info.
686         '''
687         return self._db.security.hasPermission('Edit', self._client.userid,
688             self._classname) or self._nodeid == self._client.userid
690 class HTMLProperty:
691     ''' String, Number, Date, Interval HTMLProperty
693         Has useful attributes:
695          _name  the name of the property
696          _value the value of the property if any
698         A wrapper object which may be stringified for the plain() behaviour.
699     '''
700     def __init__(self, client, nodeid, prop, name, value):
701         self._client = client
702         self._db = client.db
703         self._nodeid = nodeid
704         self._prop = prop
705         self._name = name
706         self._value = value
707     def __repr__(self):
708         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
709     def __str__(self):
710         return self.plain()
711     def __cmp__(self, other):
712         if isinstance(other, HTMLProperty):
713             return cmp(self._value, other._value)
714         return cmp(self._value, other)
716 class StringHTMLProperty(HTMLProperty):
717     def plain(self, escape=0):
718         ''' Render a "plain" representation of the property
719         '''
720         if self._value is None:
721             return ''
722         if escape:
723             return cgi.escape(str(self._value))
724         return str(self._value)
726     def stext(self, escape=0):
727         ''' Render the value of the property as StructuredText.
729             This requires the StructureText module to be installed separately.
730         '''
731         s = self.plain(escape=escape)
732         if not StructuredText:
733             return s
734         return StructuredText(s,level=1,header=0)
736     def field(self, size = 30):
737         ''' Render a form edit field for the property
738         '''
739         if self._value is None:
740             value = ''
741         else:
742             value = cgi.escape(str(self._value))
743             value = '&quot;'.join(value.split('"'))
744         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
746     def multiline(self, escape=0, rows=5, cols=40):
747         ''' Render a multiline form edit field for the property
748         '''
749         if self._value is None:
750             value = ''
751         else:
752             value = cgi.escape(str(self._value))
753             value = '&quot;'.join(value.split('"'))
754         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
755             self._name, rows, cols, value)
757     def email(self, escape=1):
758         ''' Render the value of the property as an obscured email address
759         '''
760         if self._value is None: value = ''
761         else: value = str(self._value)
762         if value.find('@') != -1:
763             name, domain = value.split('@')
764             domain = ' '.join(domain.split('.')[:-1])
765             name = name.replace('.', ' ')
766             value = '%s at %s ...'%(name, domain)
767         else:
768             value = value.replace('.', ' ')
769         if escape:
770             value = cgi.escape(value)
771         return value
773 class PasswordHTMLProperty(HTMLProperty):
774     def plain(self):
775         ''' Render a "plain" representation of the property
776         '''
777         if self._value is None:
778             return ''
779         return _('*encrypted*')
781     def field(self, size = 30):
782         ''' Render a form edit field for the property.
783         '''
784         return '<input type="password" name="%s" size="%s">'%(self._name, size)
786     def confirm(self, size = 30):
787         ''' Render a second form edit field for the property, used for 
788             confirmation that the user typed the password correctly. Generates
789             a field with name "name:confirm".
790         '''
791         return '<input type="password" name="%s:confirm" size="%s">'%(
792             self._name, size)
794 class NumberHTMLProperty(HTMLProperty):
795     def plain(self):
796         ''' Render a "plain" representation of the property
797         '''
798         return str(self._value)
800     def field(self, size = 30):
801         ''' Render a form edit field for the property
802         '''
803         if self._value is None:
804             value = ''
805         else:
806             value = cgi.escape(str(self._value))
807             value = '&quot;'.join(value.split('"'))
808         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
810 class BooleanHTMLProperty(HTMLProperty):
811     def plain(self):
812         ''' Render a "plain" representation of the property
813         '''
814         if self.value is None:
815             return ''
816         return self._value and "Yes" or "No"
818     def field(self):
819         ''' Render a form edit field for the property
820         '''
821         checked = self._value and "checked" or ""
822         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
823             checked)
824         if checked:
825             checked = ""
826         else:
827             checked = "checked"
828         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
829             checked)
830         return s
832 class DateHTMLProperty(HTMLProperty):
833     def plain(self):
834         ''' Render a "plain" representation of the property
835         '''
836         if self._value is None:
837             return ''
838         return str(self._value)
840     def field(self, size = 30):
841         ''' Render a form edit field for the property
842         '''
843         if self._value is None:
844             value = ''
845         else:
846             value = cgi.escape(str(self._value))
847             value = '&quot;'.join(value.split('"'))
848         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
850     def reldate(self, pretty=1):
851         ''' Render the interval between the date and now.
853             If the "pretty" flag is true, then make the display pretty.
854         '''
855         if not self._value:
856             return ''
858         # figure the interval
859         interval = date.Date('.') - self._value
860         if pretty:
861             return interval.pretty()
862         return str(interval)
864 class IntervalHTMLProperty(HTMLProperty):
865     def plain(self):
866         ''' Render a "plain" representation of the property
867         '''
868         if self._value is None:
869             return ''
870         return str(self._value)
872     def pretty(self):
873         ''' Render the interval in a pretty format (eg. "yesterday")
874         '''
875         return self._value.pretty()
877     def field(self, size = 30):
878         ''' Render a form edit field for the property
879         '''
880         if self._value is None:
881             value = ''
882         else:
883             value = cgi.escape(str(self._value))
884             value = '&quot;'.join(value.split('"'))
885         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
887 class LinkHTMLProperty(HTMLProperty):
888     ''' Link HTMLProperty
889         Include the above as well as being able to access the class
890         information. Stringifying the object itself results in the value
891         from the item being displayed. Accessing attributes of this object
892         result in the appropriate entry from the class being queried for the
893         property accessed (so item/assignedto/name would look up the user
894         entry identified by the assignedto property on item, and then the
895         name property of that user)
896     '''
897     def __getattr__(self, attr):
898         ''' return a new HTMLItem '''
899        #print 'Link.getattr', (self, attr, self._value)
900         if not self._value:
901             raise AttributeError, "Can't access missing value"
902         if self._prop.classname == 'user':
903             klass = HTMLUser
904         else:
905             klass = HTMLItem
906         i = klass(self._client, self._prop.classname, self._value)
907         return getattr(i, attr)
909     def plain(self, escape=0):
910         ''' Render a "plain" representation of the property
911         '''
912         if self._value is None:
913             return ''
914         linkcl = self._db.classes[self._prop.classname]
915         k = linkcl.labelprop(1)
916         value = str(linkcl.get(self._value, k))
917         if escape:
918             value = cgi.escape(value)
919         return value
921     def field(self, showid=0, size=None):
922         ''' Render a form edit field for the property
923         '''
924         linkcl = self._db.getclass(self._prop.classname)
925         if linkcl.getprops().has_key('order'):  
926             sort_on = 'order'  
927         else:  
928             sort_on = linkcl.labelprop()  
929         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
930         # TODO: make this a field display, not a menu one!
931         l = ['<select name="%s">'%self._name]
932         k = linkcl.labelprop(1)
933         if self._value is None:
934             s = 'selected '
935         else:
936             s = ''
937         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
938         for optionid in options:
939             # get the option value, and if it's None use an empty string
940             option = linkcl.get(optionid, k) or ''
942             # figure if this option is selected
943             s = ''
944             if optionid == self._value:
945                 s = 'selected '
947             # figure the label
948             if showid:
949                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
950             else:
951                 lab = option
953             # truncate if it's too long
954             if size is not None and len(lab) > size:
955                 lab = lab[:size-3] + '...'
957             # and generate
958             lab = cgi.escape(lab)
959             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
960         l.append('</select>')
961         return '\n'.join(l)
963     def menu(self, size=None, height=None, showid=0, additional=[],
964             **conditions):
965         ''' Render a form select list for this property
966         '''
967         value = self._value
969         # sort function
970         sortfunc = make_sort_function(self._db, self._prop.classname)
972         linkcl = self._db.getclass(self._prop.classname)
973         l = ['<select name="%s">'%self._name]
974         k = linkcl.labelprop(1)
975         s = ''
976         if value is None:
977             s = 'selected '
978         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
979         if linkcl.getprops().has_key('order'):  
980             sort_on = ('+', 'order')
981         else:  
982             sort_on = ('+', linkcl.labelprop())
983         options = linkcl.filter(None, conditions, sort_on, (None, None))
984         for optionid in options:
985             # get the option value, and if it's None use an empty string
986             option = linkcl.get(optionid, k) or ''
988             # figure if this option is selected
989             s = ''
990             if value in [optionid, option]:
991                 s = 'selected '
993             # figure the label
994             if showid:
995                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
996             else:
997                 lab = option
999             # truncate if it's too long
1000             if size is not None and len(lab) > size:
1001                 lab = lab[:size-3] + '...'
1002             if additional:
1003                 m = []
1004                 for propname in additional:
1005                     m.append(linkcl.get(optionid, propname))
1006                 lab = lab + ' (%s)'%', '.join(map(str, m))
1008             # and generate
1009             lab = cgi.escape(lab)
1010             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1011         l.append('</select>')
1012         return '\n'.join(l)
1013 #    def checklist(self, ...)
1015 class MultilinkHTMLProperty(HTMLProperty):
1016     ''' Multilink HTMLProperty
1018         Also be iterable, returning a wrapper object like the Link case for
1019         each entry in the multilink.
1020     '''
1021     def __len__(self):
1022         ''' length of the multilink '''
1023         return len(self._value)
1025     def __getattr__(self, attr):
1026         ''' no extended attribute accesses make sense here '''
1027         raise AttributeError, attr
1029     def __getitem__(self, num):
1030         ''' iterate and return a new HTMLItem
1031         '''
1032        #print 'Multi.getitem', (self, num)
1033         value = self._value[num]
1034         if self._prop.classname == 'user':
1035             klass = HTMLUser
1036         else:
1037             klass = HTMLItem
1038         return klass(self._client, self._prop.classname, value)
1040     def __contains__(self, value):
1041         ''' Support the "in" operator
1042         '''
1043         return value in self._value
1045     def reverse(self):
1046         ''' return the list in reverse order
1047         '''
1048         l = self._value[:]
1049         l.reverse()
1050         if self._prop.classname == 'user':
1051             klass = HTMLUser
1052         else:
1053             klass = HTMLItem
1054         return [klass(self._client, self._prop.classname, value) for value in l]
1056     def plain(self, escape=0):
1057         ''' Render a "plain" representation of the property
1058         '''
1059         linkcl = self._db.classes[self._prop.classname]
1060         k = linkcl.labelprop(1)
1061         labels = []
1062         for v in self._value:
1063             labels.append(linkcl.get(v, k))
1064         value = ', '.join(labels)
1065         if escape:
1066             value = cgi.escape(value)
1067         return value
1069     def field(self, size=30, showid=0):
1070         ''' Render a form edit field for the property
1071         '''
1072         sortfunc = make_sort_function(self._db, self._prop.classname)
1073         linkcl = self._db.getclass(self._prop.classname)
1074         value = self._value[:]
1075         if value:
1076             value.sort(sortfunc)
1077         # map the id to the label property
1078         if not linkcl.getkey():
1079             showid=1
1080         if not showid:
1081             k = linkcl.labelprop(1)
1082             value = [linkcl.get(v, k) for v in value]
1083         value = cgi.escape(','.join(value))
1084         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1086     def menu(self, size=None, height=None, showid=0, additional=[],
1087             **conditions):
1088         ''' Render a form select list for this property
1089         '''
1090         value = self._value
1092         # sort function
1093         sortfunc = make_sort_function(self._db, self._prop.classname)
1095         linkcl = self._db.getclass(self._prop.classname)
1096         if linkcl.getprops().has_key('order'):  
1097             sort_on = ('+', 'order')
1098         else:  
1099             sort_on = ('+', linkcl.labelprop())
1100         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1101         height = height or min(len(options), 7)
1102         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1103         k = linkcl.labelprop(1)
1104         for optionid in options:
1105             # get the option value, and if it's None use an empty string
1106             option = linkcl.get(optionid, k) or ''
1108             # figure if this option is selected
1109             s = ''
1110             if optionid in value or option in value:
1111                 s = 'selected '
1113             # figure the label
1114             if showid:
1115                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1116             else:
1117                 lab = option
1118             # truncate if it's too long
1119             if size is not None and len(lab) > size:
1120                 lab = lab[:size-3] + '...'
1121             if additional:
1122                 m = []
1123                 for propname in additional:
1124                     m.append(linkcl.get(optionid, propname))
1125                 lab = lab + ' (%s)'%', '.join(m)
1127             # and generate
1128             lab = cgi.escape(lab)
1129             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1130                 lab))
1131         l.append('</select>')
1132         return '\n'.join(l)
1134 # set the propclasses for HTMLItem
1135 propclasses = (
1136     (hyperdb.String, StringHTMLProperty),
1137     (hyperdb.Number, NumberHTMLProperty),
1138     (hyperdb.Boolean, BooleanHTMLProperty),
1139     (hyperdb.Date, DateHTMLProperty),
1140     (hyperdb.Interval, IntervalHTMLProperty),
1141     (hyperdb.Password, PasswordHTMLProperty),
1142     (hyperdb.Link, LinkHTMLProperty),
1143     (hyperdb.Multilink, MultilinkHTMLProperty),
1146 def make_sort_function(db, classname):
1147     '''Make a sort function for a given class
1148     '''
1149     linkcl = db.getclass(classname)
1150     if linkcl.getprops().has_key('order'):
1151         sort_on = 'order'
1152     else:
1153         sort_on = linkcl.labelprop()
1154     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1155         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1156     return sortfunc
1158 def handleListCGIValue(value):
1159     ''' Value is either a single item or a list of items. Each item has a
1160         .value that we're actually interested in.
1161     '''
1162     if isinstance(value, type([])):
1163         return [value.value for value in value]
1164     else:
1165         value = value.value.strip()
1166         if not value:
1167             return []
1168         return value.split(',')
1170 class ShowDict:
1171     ''' A convenience access to the :columns index parameters
1172     '''
1173     def __init__(self, columns):
1174         self.columns = {}
1175         for col in columns:
1176             self.columns[col] = 1
1177     def __getitem__(self, name):
1178         return self.columns.has_key(name)
1180 class HTMLRequest:
1181     ''' The *request*, holding the CGI form and environment.
1183         "form" the CGI form as a cgi.FieldStorage
1184         "env" the CGI environment variables
1185         "base" the base URL for this instance
1186         "user" a HTMLUser instance for this user
1187         "classname" the current classname (possibly None)
1188         "template" the current template (suffix, also possibly None)
1190         Index args:
1191         "columns" dictionary of the columns to display in an index page
1192         "show" a convenience access to columns - request/show/colname will
1193                be true if the columns should be displayed, false otherwise
1194         "sort" index sort column (direction, column name)
1195         "group" index grouping property (direction, column name)
1196         "filter" properties to filter the index on
1197         "filterspec" values to filter the index on
1198         "search_text" text to perform a full-text search on for an index
1200     '''
1201     def __init__(self, client):
1202         self.client = client
1204         # easier access vars
1205         self.form = client.form
1206         self.env = client.env
1207         self.base = client.base
1208         self.user = HTMLUser(client, 'user', client.userid)
1210         # store the current class name and action
1211         self.classname = client.classname
1212         self.template = client.template
1214         self._post_init()
1216     def _post_init(self):
1217         ''' Set attributes based on self.form
1218         '''
1219         # extract the index display information from the form
1220         self.columns = []
1221         if self.form.has_key(':columns'):
1222             self.columns = handleListCGIValue(self.form[':columns'])
1223         self.show = ShowDict(self.columns)
1225         # sorting
1226         self.sort = (None, None)
1227         if self.form.has_key(':sort'):
1228             sort = self.form[':sort'].value
1229             if sort.startswith('-'):
1230                 self.sort = ('-', sort[1:])
1231             else:
1232                 self.sort = ('+', sort)
1233         if self.form.has_key(':sortdir'):
1234             self.sort = ('-', self.sort[1])
1236         # grouping
1237         self.group = (None, None)
1238         if self.form.has_key(':group'):
1239             group = self.form[':group'].value
1240             if group.startswith('-'):
1241                 self.group = ('-', group[1:])
1242             else:
1243                 self.group = ('+', group)
1244         if self.form.has_key(':groupdir'):
1245             self.group = ('-', self.group[1])
1247         # filtering
1248         self.filter = []
1249         if self.form.has_key(':filter'):
1250             self.filter = handleListCGIValue(self.form[':filter'])
1251         self.filterspec = {}
1252         db = self.client.db
1253         if self.classname is not None:
1254             props = db.getclass(self.classname).getprops()
1255             for name in self.filter:
1256                 if self.form.has_key(name):
1257                     prop = props[name]
1258                     fv = self.form[name]
1259                     if (isinstance(prop, hyperdb.Link) or
1260                             isinstance(prop, hyperdb.Multilink)):
1261                         self.filterspec[name] = lookupIds(db, prop,
1262                             handleListCGIValue(fv))
1263                     else:
1264                         self.filterspec[name] = fv.value
1266         # full-text search argument
1267         self.search_text = None
1268         if self.form.has_key(':search_text'):
1269             self.search_text = self.form[':search_text'].value
1271         # pagination - size and start index
1272         # figure batch args
1273         if self.form.has_key(':pagesize'):
1274             self.pagesize = int(self.form[':pagesize'].value)
1275         else:
1276             self.pagesize = 50
1277         if self.form.has_key(':startwith'):
1278             self.startwith = int(self.form[':startwith'].value)
1279         else:
1280             self.startwith = 0
1282     def updateFromURL(self, url):
1283         ''' Parse the URL for query args, and update my attributes using the
1284             values.
1285         ''' 
1286         self.form = {}
1287         for name, value in cgi.parse_qsl(url):
1288             if self.form.has_key(name):
1289                 if isinstance(self.form[name], type([])):
1290                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1291                 else:
1292                     self.form[name] = [self.form[name],
1293                         cgi.MiniFieldStorage(name, value)]
1294             else:
1295                 self.form[name] = cgi.MiniFieldStorage(name, value)
1296         self._post_init()
1298     def update(self, kwargs):
1299         ''' Update my attributes using the keyword args
1300         '''
1301         self.__dict__.update(kwargs)
1302         if kwargs.has_key('columns'):
1303             self.show = ShowDict(self.columns)
1305     def description(self):
1306         ''' Return a description of the request - handle for the page title.
1307         '''
1308         s = [self.client.db.config.TRACKER_NAME]
1309         if self.classname:
1310             if self.client.nodeid:
1311                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1312             else:
1313                 if self.template == 'item':
1314                     s.append('- new %s'%self.classname)
1315                 elif self.template == 'index':
1316                     s.append('- %s index'%self.classname)
1317                 else:
1318                     s.append('- %s %s'%(self.classname, self.template))
1319         else:
1320             s.append('- home')
1321         return ' '.join(s)
1323     def __str__(self):
1324         d = {}
1325         d.update(self.__dict__)
1326         f = ''
1327         for k in self.form.keys():
1328             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1329         d['form'] = f
1330         e = ''
1331         for k,v in self.env.items():
1332             e += '\n     %r=%r'%(k, v)
1333         d['env'] = e
1334         return '''
1335 form: %(form)s
1336 base: %(base)r
1337 classname: %(classname)r
1338 template: %(template)r
1339 columns: %(columns)r
1340 sort: %(sort)r
1341 group: %(group)r
1342 filter: %(filter)r
1343 search_text: %(search_text)r
1344 pagesize: %(pagesize)r
1345 startwith: %(startwith)r
1346 env: %(env)s
1347 '''%d
1349     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1350             filterspec=1):
1351         ''' return the current index args as form elements '''
1352         l = []
1353         s = '<input type="hidden" name="%s" value="%s">'
1354         if columns and self.columns:
1355             l.append(s%(':columns', ','.join(self.columns)))
1356         if sort and self.sort[1] is not None:
1357             if self.sort[0] == '-':
1358                 val = '-'+self.sort[1]
1359             else:
1360                 val = self.sort[1]
1361             l.append(s%(':sort', val))
1362         if group and self.group[1] is not None:
1363             if self.group[0] == '-':
1364                 val = '-'+self.group[1]
1365             else:
1366                 val = self.group[1]
1367             l.append(s%(':group', val))
1368         if filter and self.filter:
1369             l.append(s%(':filter', ','.join(self.filter)))
1370         if filterspec:
1371             for k,v in self.filterspec.items():
1372                 l.append(s%(k, ','.join(v)))
1373         if self.search_text:
1374             l.append(s%(':search_text', self.search_text))
1375         l.append(s%(':pagesize', self.pagesize))
1376         l.append(s%(':startwith', self.startwith))
1377         return '\n'.join(l)
1379     def indexargs_url(self, url, args):
1380         ''' embed the current index args in a URL '''
1381         l = ['%s=%s'%(k,v) for k,v in args.items()]
1382         if self.columns and not args.has_key(':columns'):
1383             l.append(':columns=%s'%(','.join(self.columns)))
1384         if self.sort[1] is not None and not args.has_key(':sort'):
1385             if self.sort[0] == '-':
1386                 val = '-'+self.sort[1]
1387             else:
1388                 val = self.sort[1]
1389             l.append(':sort=%s'%val)
1390         if self.group[1] is not None and not args.has_key(':group'):
1391             if self.group[0] == '-':
1392                 val = '-'+self.group[1]
1393             else:
1394                 val = self.group[1]
1395             l.append(':group=%s'%val)
1396         if self.filter and not args.has_key(':columns'):
1397             l.append(':filter=%s'%(','.join(self.filter)))
1398         for k,v in self.filterspec.items():
1399             if not args.has_key(k):
1400                 l.append('%s=%s'%(k, ','.join(v)))
1401         if self.search_text and not args.has_key(':search_text'):
1402             l.append(':search_text=%s'%self.search_text)
1403         if not args.has_key(':pagesize'):
1404             l.append(':pagesize=%s'%self.pagesize)
1405         if not args.has_key(':startwith'):
1406             l.append(':startwith=%s'%self.startwith)
1407         return '%s?%s'%(url, '&'.join(l))
1408     indexargs_href = indexargs_url
1410     def base_javascript(self):
1411         return '''
1412 <script language="javascript">
1413 submitted = false;
1414 function submit_once() {
1415     if (submitted) {
1416         alert("Your request is being processed.\\nPlease be patient.");
1417         return 0;
1418     }
1419     submitted = true;
1420     return 1;
1423 function help_window(helpurl, width, height) {
1424     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1426 </script>
1427 '''%self.base
1429     def batch(self):
1430         ''' Return a batch object for results from the "current search"
1431         '''
1432         filterspec = self.filterspec
1433         sort = self.sort
1434         group = self.group
1436         # get the list of ids we're batching over
1437         klass = self.client.db.getclass(self.classname)
1438         if self.search_text:
1439             matches = self.client.db.indexer.search(
1440                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1441         else:
1442             matches = None
1443         l = klass.filter(matches, filterspec, sort, group)
1445         # return the batch object, using IDs only
1446         return Batch(self.client, l, self.pagesize, self.startwith,
1447             classname=self.classname)
1449 # extend the standard ZTUtils Batch object to remove dependency on
1450 # Acquisition and add a couple of useful methods
1451 class Batch(ZTUtils.Batch):
1452     ''' Use me to turn a list of items, or item ids of a given class, into a
1453         series of batches.
1455         ========= ========================================================
1456         Parameter  Usage
1457         ========= ========================================================
1458         sequence  a list of HTMLItems or item ids
1459         classname if sequence is a list of ids, this is the class of item
1460         size      how big to make the sequence.
1461         start     where to start (0-indexed) in the sequence.
1462         end       where to end (0-indexed) in the sequence.
1463         orphan    if the next batch would contain less items than this
1464                   value, then it is combined with this batch
1465         overlap   the number of items shared between adjacent batches
1466         ========= ========================================================
1468         Attributes: Note that the "start" attribute, unlike the
1469         argument, is a 1-based index (I know, lame).  "first" is the
1470         0-based index.  "length" is the actual number of elements in
1471         the batch.
1473         "sequence_length" is the length of the original, unbatched, sequence.
1474     '''
1475     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1476             overlap=0, classname=None):
1477         self.client = client
1478         self.last_index = self.last_item = None
1479         self.current_item = None
1480         self.classname = classname
1481         self.sequence_length = len(sequence)
1482         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1483             overlap)
1485     # overwrite so we can late-instantiate the HTMLItem instance
1486     def __getitem__(self, index):
1487         if index < 0:
1488             if index + self.end < self.first: raise IndexError, index
1489             return self._sequence[index + self.end]
1490         
1491         if index >= self.length:
1492             raise IndexError, index
1494         # move the last_item along - but only if the fetched index changes
1495         # (for some reason, index 0 is fetched twice)
1496         if index != self.last_index:
1497             self.last_item = self.current_item
1498             self.last_index = index
1500         item = self._sequence[index + self.first]
1501         if self.classname:
1502             # map the item ids to instances
1503             if self.classname == 'user':
1504                 item = HTMLUser(self.client, self.classname, item)
1505             else:
1506                 item = HTMLItem(self.client, self.classname, item)
1507         self.current_item = item
1508         return item
1510     def propchanged(self, property):
1511         ''' Detect if the property marked as being the group property
1512             changed in the last iteration fetch
1513         '''
1514         if (self.last_item is None or
1515                 self.last_item[property] != self.current_item[property]):
1516             return 1
1517         return 0
1519     # override these 'cos we don't have access to acquisition
1520     def previous(self):
1521         if self.start == 1:
1522             return None
1523         return Batch(self.client, self._sequence, self._size,
1524             self.first - self._size + self.overlap, 0, self.orphan,
1525             self.overlap)
1527     def next(self):
1528         try:
1529             self._sequence[self.end]
1530         except IndexError:
1531             return None
1532         return Batch(self.client, self._sequence, self._size,
1533             self.end - self.overlap, 0, self.orphan, self.overlap)
1535 class TemplatingUtils:
1536     ''' Utilities for templating
1537     '''
1538     def __init__(self, client):
1539         self.client = client
1540     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1541         return Batch(self.client, sequence, size, start, end, orphan,
1542             overlap)