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