Code

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