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