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