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