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(src) and \
88 stime < self.templates[src].mtime:
89 # compiled template is up to date
90 return self.templates[src]
92 # compile the template
93 self.templates[src] = 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 def local(self, offset):
895 ''' Return the date/time as a local (timezone offset) date/time.
896 '''
897 return DateHTMLProperty(self._client, self._nodeid, self._prop,
898 self._name, self._value.local())
900 class IntervalHTMLProperty(HTMLProperty):
901 def plain(self):
902 ''' Render a "plain" representation of the property
903 '''
904 if self._value is None:
905 return ''
906 return str(self._value)
908 def pretty(self):
909 ''' Render the interval in a pretty format (eg. "yesterday")
910 '''
911 return self._value.pretty()
913 def field(self, size = 30):
914 ''' Render a form edit field for the property
915 '''
916 if self._value is None:
917 value = ''
918 else:
919 value = cgi.escape(str(self._value))
920 value = '"'.join(value.split('"'))
921 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
923 class LinkHTMLProperty(HTMLProperty):
924 ''' Link HTMLProperty
925 Include the above as well as being able to access the class
926 information. Stringifying the object itself results in the value
927 from the item being displayed. Accessing attributes of this object
928 result in the appropriate entry from the class being queried for the
929 property accessed (so item/assignedto/name would look up the user
930 entry identified by the assignedto property on item, and then the
931 name property of that user)
932 '''
933 def __init__(self, *args):
934 HTMLProperty.__init__(self, *args)
935 # if we're representing a form value, then the -1 from the form really
936 # should be a None
937 if str(self._value) == '-1':
938 self._value = None
940 def __getattr__(self, attr):
941 ''' return a new HTMLItem '''
942 #print 'Link.getattr', (self, attr, self._value)
943 if not self._value:
944 raise AttributeError, "Can't access missing value"
945 if self._prop.classname == 'user':
946 klass = HTMLUser
947 else:
948 klass = HTMLItem
949 i = klass(self._client, self._prop.classname, self._value)
950 return getattr(i, attr)
952 def plain(self, escape=0):
953 ''' Render a "plain" representation of the property
954 '''
955 if self._value is None:
956 return ''
957 linkcl = self._db.classes[self._prop.classname]
958 k = linkcl.labelprop(1)
959 value = str(linkcl.get(self._value, k))
960 if escape:
961 value = cgi.escape(value)
962 return value
964 def field(self, showid=0, size=None):
965 ''' Render a form edit field for the property
966 '''
967 linkcl = self._db.getclass(self._prop.classname)
968 if linkcl.getprops().has_key('order'):
969 sort_on = 'order'
970 else:
971 sort_on = linkcl.labelprop()
972 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
973 # TODO: make this a field display, not a menu one!
974 l = ['<select name="%s">'%self._name]
975 k = linkcl.labelprop(1)
976 if self._value is None:
977 s = 'selected '
978 else:
979 s = ''
980 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
982 # make sure we list the current value if it's retired
983 if self._value and self._value not in options:
984 options.insert(0, self._value)
986 for optionid in options:
987 # get the option value, and if it's None use an empty string
988 option = linkcl.get(optionid, k) or ''
990 # figure if this option is selected
991 s = ''
992 if optionid == self._value:
993 s = 'selected '
995 # figure the label
996 if showid:
997 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
998 else:
999 lab = option
1001 # truncate if it's too long
1002 if size is not None and len(lab) > size:
1003 lab = lab[:size-3] + '...'
1005 # and generate
1006 lab = cgi.escape(lab)
1007 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1008 l.append('</select>')
1009 return '\n'.join(l)
1011 def menu(self, size=None, height=None, showid=0, additional=[],
1012 **conditions):
1013 ''' Render a form select list for this property
1014 '''
1015 value = self._value
1017 # sort function
1018 sortfunc = make_sort_function(self._db, self._prop.classname)
1020 linkcl = self._db.getclass(self._prop.classname)
1021 l = ['<select name="%s">'%self._name]
1022 k = linkcl.labelprop(1)
1023 s = ''
1024 if value is None:
1025 s = 'selected '
1026 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1027 if linkcl.getprops().has_key('order'):
1028 sort_on = ('+', 'order')
1029 else:
1030 sort_on = ('+', linkcl.labelprop())
1031 options = linkcl.filter(None, conditions, sort_on, (None, None))
1033 # make sure we list the current value if it's retired
1034 if self._value and self._value not in options:
1035 options.insert(0, self._value)
1037 for optionid in options:
1038 # get the option value, and if it's None use an empty string
1039 option = linkcl.get(optionid, k) or ''
1041 # figure if this option is selected
1042 s = ''
1043 if value in [optionid, option]:
1044 s = 'selected '
1046 # figure the label
1047 if showid:
1048 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1049 else:
1050 lab = option
1052 # truncate if it's too long
1053 if size is not None and len(lab) > size:
1054 lab = lab[:size-3] + '...'
1055 if additional:
1056 m = []
1057 for propname in additional:
1058 m.append(linkcl.get(optionid, propname))
1059 lab = lab + ' (%s)'%', '.join(map(str, m))
1061 # and generate
1062 lab = cgi.escape(lab)
1063 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1064 l.append('</select>')
1065 return '\n'.join(l)
1066 # def checklist(self, ...)
1068 class MultilinkHTMLProperty(HTMLProperty):
1069 ''' Multilink HTMLProperty
1071 Also be iterable, returning a wrapper object like the Link case for
1072 each entry in the multilink.
1073 '''
1074 def __len__(self):
1075 ''' length of the multilink '''
1076 return len(self._value)
1078 def __getattr__(self, attr):
1079 ''' no extended attribute accesses make sense here '''
1080 raise AttributeError, attr
1082 def __getitem__(self, num):
1083 ''' iterate and return a new HTMLItem
1084 '''
1085 #print 'Multi.getitem', (self, num)
1086 value = self._value[num]
1087 if self._prop.classname == 'user':
1088 klass = HTMLUser
1089 else:
1090 klass = HTMLItem
1091 return klass(self._client, self._prop.classname, value)
1093 def __contains__(self, value):
1094 ''' Support the "in" operator. We have to make sure the passed-in
1095 value is a string first, not a *HTMLProperty.
1096 '''
1097 return str(value) in self._value
1099 def reverse(self):
1100 ''' return the list in reverse order
1101 '''
1102 l = self._value[:]
1103 l.reverse()
1104 if self._prop.classname == 'user':
1105 klass = HTMLUser
1106 else:
1107 klass = HTMLItem
1108 return [klass(self._client, self._prop.classname, value) for value in l]
1110 def plain(self, escape=0):
1111 ''' Render a "plain" representation of the property
1112 '''
1113 linkcl = self._db.classes[self._prop.classname]
1114 k = linkcl.labelprop(1)
1115 labels = []
1116 for v in self._value:
1117 labels.append(linkcl.get(v, k))
1118 value = ', '.join(labels)
1119 if escape:
1120 value = cgi.escape(value)
1121 return value
1123 def field(self, size=30, showid=0):
1124 ''' Render a form edit field for the property
1125 '''
1126 sortfunc = make_sort_function(self._db, self._prop.classname)
1127 linkcl = self._db.getclass(self._prop.classname)
1128 value = self._value[:]
1129 if value:
1130 value.sort(sortfunc)
1131 # map the id to the label property
1132 if not linkcl.getkey():
1133 showid=1
1134 if not showid:
1135 k = linkcl.labelprop(1)
1136 value = [linkcl.get(v, k) for v in value]
1137 value = cgi.escape(','.join(value))
1138 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1140 def menu(self, size=None, height=None, showid=0, additional=[],
1141 **conditions):
1142 ''' Render a form select list for this property
1143 '''
1144 value = self._value
1146 # sort function
1147 sortfunc = make_sort_function(self._db, self._prop.classname)
1149 linkcl = self._db.getclass(self._prop.classname)
1150 if linkcl.getprops().has_key('order'):
1151 sort_on = ('+', 'order')
1152 else:
1153 sort_on = ('+', linkcl.labelprop())
1154 options = linkcl.filter(None, conditions, sort_on, (None,None))
1155 height = height or min(len(options), 7)
1156 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1157 k = linkcl.labelprop(1)
1159 # make sure we list the current values if they're retired
1160 for value in self._value:
1161 if value not in options:
1162 options.insert(0, value)
1164 for optionid in options:
1165 # get the option value, and if it's None use an empty string
1166 option = linkcl.get(optionid, k) or ''
1168 # figure if this option is selected
1169 s = ''
1170 if optionid in value or option in value:
1171 s = 'selected '
1173 # figure the label
1174 if showid:
1175 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1176 else:
1177 lab = option
1178 # truncate if it's too long
1179 if size is not None and len(lab) > size:
1180 lab = lab[:size-3] + '...'
1181 if additional:
1182 m = []
1183 for propname in additional:
1184 m.append(linkcl.get(optionid, propname))
1185 lab = lab + ' (%s)'%', '.join(m)
1187 # and generate
1188 lab = cgi.escape(lab)
1189 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1190 lab))
1191 l.append('</select>')
1192 return '\n'.join(l)
1194 # set the propclasses for HTMLItem
1195 propclasses = (
1196 (hyperdb.String, StringHTMLProperty),
1197 (hyperdb.Number, NumberHTMLProperty),
1198 (hyperdb.Boolean, BooleanHTMLProperty),
1199 (hyperdb.Date, DateHTMLProperty),
1200 (hyperdb.Interval, IntervalHTMLProperty),
1201 (hyperdb.Password, PasswordHTMLProperty),
1202 (hyperdb.Link, LinkHTMLProperty),
1203 (hyperdb.Multilink, MultilinkHTMLProperty),
1204 )
1206 def make_sort_function(db, classname):
1207 '''Make a sort function for a given class
1208 '''
1209 linkcl = db.getclass(classname)
1210 if linkcl.getprops().has_key('order'):
1211 sort_on = 'order'
1212 else:
1213 sort_on = linkcl.labelprop()
1214 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1215 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1216 return sortfunc
1218 def handleListCGIValue(value):
1219 ''' Value is either a single item or a list of items. Each item has a
1220 .value that we're actually interested in.
1221 '''
1222 if isinstance(value, type([])):
1223 return [value.value for value in value]
1224 else:
1225 value = value.value.strip()
1226 if not value:
1227 return []
1228 return value.split(',')
1230 class ShowDict:
1231 ''' A convenience access to the :columns index parameters
1232 '''
1233 def __init__(self, columns):
1234 self.columns = {}
1235 for col in columns:
1236 self.columns[col] = 1
1237 def __getitem__(self, name):
1238 return self.columns.has_key(name)
1240 class HTMLRequest:
1241 ''' The *request*, holding the CGI form and environment.
1243 "form" the CGI form as a cgi.FieldStorage
1244 "env" the CGI environment variables
1245 "base" the base URL for this instance
1246 "user" a HTMLUser instance for this user
1247 "classname" the current classname (possibly None)
1248 "template" the current template (suffix, also possibly None)
1250 Index args:
1251 "columns" dictionary of the columns to display in an index page
1252 "show" a convenience access to columns - request/show/colname will
1253 be true if the columns should be displayed, false otherwise
1254 "sort" index sort column (direction, column name)
1255 "group" index grouping property (direction, column name)
1256 "filter" properties to filter the index on
1257 "filterspec" values to filter the index on
1258 "search_text" text to perform a full-text search on for an index
1260 '''
1261 def __init__(self, client):
1262 self.client = client
1264 # easier access vars
1265 self.form = client.form
1266 self.env = client.env
1267 self.base = client.base
1268 self.user = HTMLUser(client, 'user', client.userid)
1270 # store the current class name and action
1271 self.classname = client.classname
1272 self.template = client.template
1274 self._post_init()
1276 def _post_init(self):
1277 ''' Set attributes based on self.form
1278 '''
1279 # extract the index display information from the form
1280 self.columns = []
1281 if self.form.has_key(':columns'):
1282 self.columns = handleListCGIValue(self.form[':columns'])
1283 self.show = ShowDict(self.columns)
1285 # sorting
1286 self.sort = (None, None)
1287 if self.form.has_key(':sort'):
1288 sort = self.form[':sort'].value
1289 if sort.startswith('-'):
1290 self.sort = ('-', sort[1:])
1291 else:
1292 self.sort = ('+', sort)
1293 if self.form.has_key(':sortdir'):
1294 self.sort = ('-', self.sort[1])
1296 # grouping
1297 self.group = (None, None)
1298 if self.form.has_key(':group'):
1299 group = self.form[':group'].value
1300 if group.startswith('-'):
1301 self.group = ('-', group[1:])
1302 else:
1303 self.group = ('+', group)
1304 if self.form.has_key(':groupdir'):
1305 self.group = ('-', self.group[1])
1307 # filtering
1308 self.filter = []
1309 if self.form.has_key(':filter'):
1310 self.filter = handleListCGIValue(self.form[':filter'])
1311 self.filterspec = {}
1312 db = self.client.db
1313 if self.classname is not None:
1314 props = db.getclass(self.classname).getprops()
1315 for name in self.filter:
1316 if self.form.has_key(name):
1317 prop = props[name]
1318 fv = self.form[name]
1319 if (isinstance(prop, hyperdb.Link) or
1320 isinstance(prop, hyperdb.Multilink)):
1321 self.filterspec[name] = lookupIds(db, prop,
1322 handleListCGIValue(fv))
1323 else:
1324 self.filterspec[name] = fv.value
1326 # full-text search argument
1327 self.search_text = None
1328 if self.form.has_key(':search_text'):
1329 self.search_text = self.form[':search_text'].value
1331 # pagination - size and start index
1332 # figure batch args
1333 if self.form.has_key(':pagesize'):
1334 self.pagesize = int(self.form[':pagesize'].value)
1335 else:
1336 self.pagesize = 50
1337 if self.form.has_key(':startwith'):
1338 self.startwith = int(self.form[':startwith'].value)
1339 else:
1340 self.startwith = 0
1342 def updateFromURL(self, url):
1343 ''' Parse the URL for query args, and update my attributes using the
1344 values.
1345 '''
1346 self.form = {}
1347 for name, value in cgi.parse_qsl(url):
1348 if self.form.has_key(name):
1349 if isinstance(self.form[name], type([])):
1350 self.form[name].append(cgi.MiniFieldStorage(name, value))
1351 else:
1352 self.form[name] = [self.form[name],
1353 cgi.MiniFieldStorage(name, value)]
1354 else:
1355 self.form[name] = cgi.MiniFieldStorage(name, value)
1356 self._post_init()
1358 def update(self, kwargs):
1359 ''' Update my attributes using the keyword args
1360 '''
1361 self.__dict__.update(kwargs)
1362 if kwargs.has_key('columns'):
1363 self.show = ShowDict(self.columns)
1365 def description(self):
1366 ''' Return a description of the request - handle for the page title.
1367 '''
1368 s = [self.client.db.config.TRACKER_NAME]
1369 if self.classname:
1370 if self.client.nodeid:
1371 s.append('- %s%s'%(self.classname, self.client.nodeid))
1372 else:
1373 if self.template == 'item':
1374 s.append('- new %s'%self.classname)
1375 elif self.template == 'index':
1376 s.append('- %s index'%self.classname)
1377 else:
1378 s.append('- %s %s'%(self.classname, self.template))
1379 else:
1380 s.append('- home')
1381 return ' '.join(s)
1383 def __str__(self):
1384 d = {}
1385 d.update(self.__dict__)
1386 f = ''
1387 for k in self.form.keys():
1388 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1389 d['form'] = f
1390 e = ''
1391 for k,v in self.env.items():
1392 e += '\n %r=%r'%(k, v)
1393 d['env'] = e
1394 return '''
1395 form: %(form)s
1396 base: %(base)r
1397 classname: %(classname)r
1398 template: %(template)r
1399 columns: %(columns)r
1400 sort: %(sort)r
1401 group: %(group)r
1402 filter: %(filter)r
1403 search_text: %(search_text)r
1404 pagesize: %(pagesize)r
1405 startwith: %(startwith)r
1406 env: %(env)s
1407 '''%d
1409 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1410 filterspec=1):
1411 ''' return the current index args as form elements '''
1412 l = []
1413 s = '<input type="hidden" name="%s" value="%s">'
1414 if columns and self.columns:
1415 l.append(s%(':columns', ','.join(self.columns)))
1416 if sort and self.sort[1] is not None:
1417 if self.sort[0] == '-':
1418 val = '-'+self.sort[1]
1419 else:
1420 val = self.sort[1]
1421 l.append(s%(':sort', val))
1422 if group and self.group[1] is not None:
1423 if self.group[0] == '-':
1424 val = '-'+self.group[1]
1425 else:
1426 val = self.group[1]
1427 l.append(s%(':group', val))
1428 if filter and self.filter:
1429 l.append(s%(':filter', ','.join(self.filter)))
1430 if filterspec:
1431 for k,v in self.filterspec.items():
1432 l.append(s%(k, ','.join(v)))
1433 if self.search_text:
1434 l.append(s%(':search_text', self.search_text))
1435 l.append(s%(':pagesize', self.pagesize))
1436 l.append(s%(':startwith', self.startwith))
1437 return '\n'.join(l)
1439 def indexargs_url(self, url, args):
1440 ''' embed the current index args in a URL '''
1441 l = ['%s=%s'%(k,v) for k,v in args.items()]
1442 if self.columns and not args.has_key(':columns'):
1443 l.append(':columns=%s'%(','.join(self.columns)))
1444 if self.sort[1] is not None and not args.has_key(':sort'):
1445 if self.sort[0] == '-':
1446 val = '-'+self.sort[1]
1447 else:
1448 val = self.sort[1]
1449 l.append(':sort=%s'%val)
1450 if self.group[1] is not None and not args.has_key(':group'):
1451 if self.group[0] == '-':
1452 val = '-'+self.group[1]
1453 else:
1454 val = self.group[1]
1455 l.append(':group=%s'%val)
1456 if self.filter and not args.has_key(':columns'):
1457 l.append(':filter=%s'%(','.join(self.filter)))
1458 for k,v in self.filterspec.items():
1459 if not args.has_key(k):
1460 l.append('%s=%s'%(k, ','.join(v)))
1461 if self.search_text and not args.has_key(':search_text'):
1462 l.append(':search_text=%s'%self.search_text)
1463 if not args.has_key(':pagesize'):
1464 l.append(':pagesize=%s'%self.pagesize)
1465 if not args.has_key(':startwith'):
1466 l.append(':startwith=%s'%self.startwith)
1467 return '%s?%s'%(url, '&'.join(l))
1468 indexargs_href = indexargs_url
1470 def base_javascript(self):
1471 return '''
1472 <script language="javascript">
1473 submitted = false;
1474 function submit_once() {
1475 if (submitted) {
1476 alert("Your request is being processed.\\nPlease be patient.");
1477 return 0;
1478 }
1479 submitted = true;
1480 return 1;
1481 }
1483 function help_window(helpurl, width, height) {
1484 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1485 }
1486 </script>
1487 '''%self.base
1489 def batch(self):
1490 ''' Return a batch object for results from the "current search"
1491 '''
1492 filterspec = self.filterspec
1493 sort = self.sort
1494 group = self.group
1496 # get the list of ids we're batching over
1497 klass = self.client.db.getclass(self.classname)
1498 if self.search_text:
1499 matches = self.client.db.indexer.search(
1500 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1501 else:
1502 matches = None
1503 l = klass.filter(matches, filterspec, sort, group)
1505 # return the batch object, using IDs only
1506 return Batch(self.client, l, self.pagesize, self.startwith,
1507 classname=self.classname)
1509 # extend the standard ZTUtils Batch object to remove dependency on
1510 # Acquisition and add a couple of useful methods
1511 class Batch(ZTUtils.Batch):
1512 ''' Use me to turn a list of items, or item ids of a given class, into a
1513 series of batches.
1515 ========= ========================================================
1516 Parameter Usage
1517 ========= ========================================================
1518 sequence a list of HTMLItems or item ids
1519 classname if sequence is a list of ids, this is the class of item
1520 size how big to make the sequence.
1521 start where to start (0-indexed) in the sequence.
1522 end where to end (0-indexed) in the sequence.
1523 orphan if the next batch would contain less items than this
1524 value, then it is combined with this batch
1525 overlap the number of items shared between adjacent batches
1526 ========= ========================================================
1528 Attributes: Note that the "start" attribute, unlike the
1529 argument, is a 1-based index (I know, lame). "first" is the
1530 0-based index. "length" is the actual number of elements in
1531 the batch.
1533 "sequence_length" is the length of the original, unbatched, sequence.
1534 '''
1535 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1536 overlap=0, classname=None):
1537 self.client = client
1538 self.last_index = self.last_item = None
1539 self.current_item = None
1540 self.classname = classname
1541 self.sequence_length = len(sequence)
1542 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1543 overlap)
1545 # overwrite so we can late-instantiate the HTMLItem instance
1546 def __getitem__(self, index):
1547 if index < 0:
1548 if index + self.end < self.first: raise IndexError, index
1549 return self._sequence[index + self.end]
1551 if index >= self.length:
1552 raise IndexError, index
1554 # move the last_item along - but only if the fetched index changes
1555 # (for some reason, index 0 is fetched twice)
1556 if index != self.last_index:
1557 self.last_item = self.current_item
1558 self.last_index = index
1560 item = self._sequence[index + self.first]
1561 if self.classname:
1562 # map the item ids to instances
1563 if self.classname == 'user':
1564 item = HTMLUser(self.client, self.classname, item)
1565 else:
1566 item = HTMLItem(self.client, self.classname, item)
1567 self.current_item = item
1568 return item
1570 def propchanged(self, property):
1571 ''' Detect if the property marked as being the group property
1572 changed in the last iteration fetch
1573 '''
1574 if (self.last_item is None or
1575 self.last_item[property] != self.current_item[property]):
1576 return 1
1577 return 0
1579 # override these 'cos we don't have access to acquisition
1580 def previous(self):
1581 if self.start == 1:
1582 return None
1583 return Batch(self.client, self._sequence, self._size,
1584 self.first - self._size + self.overlap, 0, self.orphan,
1585 self.overlap)
1587 def next(self):
1588 try:
1589 self._sequence[self.end]
1590 except IndexError:
1591 return None
1592 return Batch(self.client, self._sequence, self._size,
1593 self.end - self.overlap, 0, self.orphan, self.overlap)
1595 class TemplatingUtils:
1596 ''' Utilities for templating
1597 '''
1598 def __init__(self, client):
1599 self.client = client
1600 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1601 return Batch(self.client, sequence, size, start, end, orphan,
1602 overlap)