Code

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