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