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 KeyError, 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 current = {}
515 comments = {}
516 history = self._klass.history(self._nodeid)
517 history.sort()
518 if direction == 'descending':
519 history.reverse()
520 for prop_n in self._props.keys():
521 prop = self[prop_n]
522 if isinstance(prop, HTMLProperty):
523 current[prop_n] = prop.plain()
524 # make link if hrefable
525 if (self._props.has_key(prop_n) and
526 isinstance(self._props[prop_n], hyperdb.Link)):
527 classname = self._props[prop_n].classname
528 if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
529 current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
530 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
532 for id, evt_date, user, action, args in history:
533 date_s = str(evt_date).replace("."," ")
534 arg_s = ''
535 if action == 'link' and type(args) == type(()):
536 if len(args) == 3:
537 linkcl, linkid, key = args
538 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
539 linkcl, linkid, key)
540 else:
541 arg_s = str(args)
543 elif action == 'unlink' and type(args) == type(()):
544 if len(args) == 3:
545 linkcl, linkid, key = args
546 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
547 linkcl, linkid, key)
548 else:
549 arg_s = str(args)
551 elif type(args) == type({}):
552 cell = []
553 for k in args.keys():
554 # try to get the relevant property and treat it
555 # specially
556 try:
557 prop = self._props[k]
558 except KeyError:
559 prop = None
560 if prop is not None:
561 if args[k] and (isinstance(prop, hyperdb.Multilink) or
562 isinstance(prop, hyperdb.Link)):
563 # figure what the link class is
564 classname = prop.classname
565 try:
566 linkcl = self._db.getclass(classname)
567 except KeyError:
568 labelprop = None
569 comments[classname] = _('''The linked class
570 %(classname)s no longer exists''')%locals()
571 labelprop = linkcl.labelprop(1)
572 hrefable = os.path.exists(
573 os.path.join(self._db.config.TEMPLATES,
574 classname+'.item'))
576 if isinstance(prop, hyperdb.Multilink) and \
577 len(args[k]) > 0:
578 ml = []
579 for linkid in args[k]:
580 if isinstance(linkid, type(())):
581 sublabel = linkid[0] + ' '
582 linkids = linkid[1]
583 else:
584 sublabel = ''
585 linkids = [linkid]
586 subml = []
587 for linkid in linkids:
588 label = classname + linkid
589 # if we have a label property, try to use it
590 # TODO: test for node existence even when
591 # there's no labelprop!
592 try:
593 if labelprop is not None and \
594 labelprop != 'id':
595 label = linkcl.get(linkid, labelprop)
596 except IndexError:
597 comments['no_link'] = _('''<strike>The
598 linked node no longer
599 exists</strike>''')
600 subml.append('<strike>%s</strike>'%label)
601 else:
602 if hrefable:
603 subml.append('<a href="%s%s">%s</a>'%(
604 classname, linkid, label))
605 else:
606 subml.append(label)
607 ml.append(sublabel + ', '.join(subml))
608 cell.append('%s:\n %s'%(k, ', '.join(ml)))
609 elif isinstance(prop, hyperdb.Link) and args[k]:
610 label = classname + args[k]
611 # if we have a label property, try to use it
612 # TODO: test for node existence even when
613 # there's no labelprop!
614 if labelprop is not None and labelprop != 'id':
615 try:
616 label = linkcl.get(args[k], labelprop)
617 except IndexError:
618 comments['no_link'] = _('''<strike>The
619 linked node no longer
620 exists</strike>''')
621 cell.append(' <strike>%s</strike>,\n'%label)
622 # "flag" this is done .... euwww
623 label = None
624 if label is not None:
625 if hrefable:
626 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
627 else:
628 old = label;
629 cell.append('%s: %s' % (k,old))
630 if current.has_key(k):
631 cell[-1] += ' -> %s'%current[k]
632 current[k] = old
634 elif isinstance(prop, hyperdb.Date) and args[k]:
635 d = date.Date(args[k])
636 cell.append('%s: %s'%(k, str(d)))
637 if current.has_key(k):
638 cell[-1] += ' -> %s'%current[k]
639 current[k] = str(d)
641 elif isinstance(prop, hyperdb.Interval) and args[k]:
642 d = date.Interval(args[k])
643 cell.append('%s: %s'%(k, str(d)))
644 if current.has_key(k):
645 cell[-1] += ' -> %s'%current[k]
646 current[k] = str(d)
648 elif isinstance(prop, hyperdb.String) and args[k]:
649 cell.append('%s: %s'%(k, cgi.escape(args[k])))
650 if current.has_key(k):
651 cell[-1] += ' -> %s'%current[k]
652 current[k] = cgi.escape(args[k])
654 elif not args[k]:
655 if current.has_key(k):
656 cell.append('%s: %s'%(k, current[k]))
657 current[k] = '(no value)'
658 else:
659 cell.append('%s: (no value)'%k)
661 else:
662 cell.append('%s: %s'%(k, str(args[k])))
663 if current.has_key(k):
664 cell[-1] += ' -> %s'%current[k]
665 current[k] = str(args[k])
666 else:
667 # property no longer exists
668 comments['no_exist'] = _('''<em>The indicated property
669 no longer exists</em>''')
670 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
671 arg_s = '<br />'.join(cell)
672 else:
673 # unkown event!!
674 comments['unknown'] = _('''<strong><em>This event is not
675 handled by the history display!</em></strong>''')
676 arg_s = '<strong><em>' + str(args) + '</em></strong>'
677 date_s = date_s.replace(' ', ' ')
678 # if the user's an itemid, figure the username (older journals
679 # have the username)
680 if dre.match(user):
681 user = self._db.user.get(user, 'username')
682 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
683 date_s, user, action, arg_s))
684 if comments:
685 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
686 for entry in comments.values():
687 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
688 l.append('</table>')
689 return '\n'.join(l)
691 def renderQueryForm(self):
692 ''' Render this item, which is a query, as a search form.
693 '''
694 # create a new request and override the specified args
695 req = HTMLRequest(self._client)
696 req.classname = self._klass.get(self._nodeid, 'klass')
697 name = self._klass.get(self._nodeid, 'name')
698 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
699 '&:queryname=%s'%urllib.quote(name))
701 # new template, using the specified classname and request
702 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
704 # use our fabricated request
705 return pt.render(self._client, req.classname, req)
707 class HTMLUser(HTMLItem):
708 ''' Accesses through the *user* (a special case of item)
709 '''
710 def __init__(self, client, classname, nodeid):
711 HTMLItem.__init__(self, client, 'user', nodeid)
712 self._default_classname = client.classname
714 # used for security checks
715 self._security = client.db.security
717 _marker = []
718 def hasPermission(self, role, classname=_marker):
719 ''' Determine if the user has the Role.
721 The class being tested defaults to the template's class, but may
722 be overidden for this test by suppling an alternate classname.
723 '''
724 if classname is self._marker:
725 classname = self._default_classname
726 return self._security.hasPermission(role, self._nodeid, classname)
728 def is_edit_ok(self):
729 ''' Is the user allowed to Edit the current class?
730 Also check whether this is the current user's info.
731 '''
732 return self._db.security.hasPermission('Edit', self._client.userid,
733 self._classname) or self._nodeid == self._client.userid
735 def is_view_ok(self):
736 ''' Is the user allowed to View the current class?
737 Also check whether this is the current user's info.
738 '''
739 return self._db.security.hasPermission('Edit', self._client.userid,
740 self._classname) or self._nodeid == self._client.userid
742 class HTMLProperty:
743 ''' String, Number, Date, Interval HTMLProperty
745 Has useful attributes:
747 _name the name of the property
748 _value the value of the property if any
750 A wrapper object which may be stringified for the plain() behaviour.
751 '''
752 def __init__(self, client, nodeid, prop, name, value):
753 self._client = client
754 self._db = client.db
755 self._nodeid = nodeid
756 self._prop = prop
757 self._name = name
758 self._value = value
759 def __repr__(self):
760 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
761 def __str__(self):
762 return self.plain()
763 def __cmp__(self, other):
764 if isinstance(other, HTMLProperty):
765 return cmp(self._value, other._value)
766 return cmp(self._value, other)
768 class StringHTMLProperty(HTMLProperty):
769 url_re = re.compile(r'\w{3,6}://\S+')
770 email_re = re.compile(r'[\w\.]+@[\w\.\-]+')
771 designator_re = re.compile(r'([a-z_]+)(\d+)')
772 def _url_repl(self, match):
773 s = match.group(0)
774 return '<a href="%s">%s</a>'%(s, s)
775 def _email_repl(self, match):
776 s = match.group(0)
777 return '<a href="mailto:%s">%s</a>'%(s, s)
778 def _designator_repl(self, match):
779 s = match.group(0)
780 s1 = match.group(1)
781 s2 = match.group(2)
782 try:
783 # make sure s1 is a valid tracker classname
784 self._db.getclass(s1)
785 return '<a href="%s">%s %s</a>'%(s, s1, s2)
786 except KeyError:
787 return '%s%s'%(s1, s2)
789 def plain(self, escape=0, hyperlink=0):
790 ''' Render a "plain" representation of the property
792 "escape" turns on/off HTML quoting
793 "hyperlink" turns on/off in-text hyperlinking of URLs, email
794 addresses and designators
795 '''
796 if self._value is None:
797 return ''
798 if escape:
799 s = cgi.escape(str(self._value))
800 else:
801 s = str(self._value)
802 if hyperlink:
803 if not escape:
804 s = cgi.escape(s)
805 s = self.url_re.sub(self._url_repl, s)
806 s = self.email_re.sub(self._email_repl, s)
807 s = self.designator_re.sub(self._designator_repl, s)
808 return s
810 def stext(self, escape=0):
811 ''' Render the value of the property as StructuredText.
813 This requires the StructureText module to be installed separately.
814 '''
815 s = self.plain(escape=escape)
816 if not StructuredText:
817 return s
818 return StructuredText(s,level=1,header=0)
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 def multiline(self, escape=0, rows=5, cols=40):
831 ''' Render a multiline form edit field for the property
832 '''
833 if self._value is None:
834 value = ''
835 else:
836 value = cgi.escape(str(self._value))
837 value = '"'.join(value.split('"'))
838 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
839 self._name, rows, cols, value)
841 def email(self, escape=1):
842 ''' Render the value of the property as an obscured email address
843 '''
844 if self._value is None: value = ''
845 else: value = str(self._value)
846 if value.find('@') != -1:
847 name, domain = value.split('@')
848 domain = ' '.join(domain.split('.')[:-1])
849 name = name.replace('.', ' ')
850 value = '%s at %s ...'%(name, domain)
851 else:
852 value = value.replace('.', ' ')
853 if escape:
854 value = cgi.escape(value)
855 return value
857 class PasswordHTMLProperty(HTMLProperty):
858 def plain(self):
859 ''' Render a "plain" representation of the property
860 '''
861 if self._value is None:
862 return ''
863 return _('*encrypted*')
865 def field(self, size = 30):
866 ''' Render a form edit field for the property.
867 '''
868 return '<input type="password" name="%s" size="%s">'%(self._name, size)
870 def confirm(self, size = 30):
871 ''' Render a second form edit field for the property, used for
872 confirmation that the user typed the password correctly. Generates
873 a field with name "name:confirm".
874 '''
875 return '<input type="password" name="%s:confirm" size="%s">'%(
876 self._name, size)
878 class NumberHTMLProperty(HTMLProperty):
879 def plain(self):
880 ''' Render a "plain" representation of the property
881 '''
882 return str(self._value)
884 def field(self, size = 30):
885 ''' Render a form edit field for the property
886 '''
887 if self._value is None:
888 value = ''
889 else:
890 value = cgi.escape(str(self._value))
891 value = '"'.join(value.split('"'))
892 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
894 class BooleanHTMLProperty(HTMLProperty):
895 def plain(self):
896 ''' Render a "plain" representation of the property
897 '''
898 if self._value is None:
899 return ''
900 return self._value and "Yes" or "No"
902 def field(self):
903 ''' Render a form edit field for the property
904 '''
905 checked = self._value and "checked" or ""
906 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
907 checked)
908 if checked:
909 checked = ""
910 else:
911 checked = "checked"
912 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
913 checked)
914 return s
916 class DateHTMLProperty(HTMLProperty):
917 def plain(self):
918 ''' Render a "plain" representation of the property
919 '''
920 if self._value is None:
921 return ''
922 return str(self._value)
924 def field(self, size = 30):
925 ''' Render a form edit field for the property
926 '''
927 if self._value is None:
928 value = ''
929 else:
930 value = cgi.escape(str(self._value))
931 value = '"'.join(value.split('"'))
932 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
934 def reldate(self, pretty=1):
935 ''' Render the interval between the date and now.
937 If the "pretty" flag is true, then make the display pretty.
938 '''
939 if not self._value:
940 return ''
942 # figure the interval
943 interval = date.Date('.') - self._value
944 if pretty:
945 return interval.pretty()
946 return str(interval)
948 def pretty(self, format='%d %B %Y'):
949 ''' Render the date in a pretty format (eg. month names, spaces).
951 The format string is a standard python strftime format string.
952 Note that if the day is zero, and appears at the start of the
953 string, then it'll be stripped from the output. This is handy
954 for the situatin when a date only specifies a month and a year.
955 '''
956 return self._value.pretty()
958 def local(self, offset):
959 ''' Return the date/time as a local (timezone offset) date/time.
960 '''
961 return DateHTMLProperty(self._client, self._nodeid, self._prop,
962 self._name, self._value.local())
964 class IntervalHTMLProperty(HTMLProperty):
965 def plain(self):
966 ''' Render a "plain" representation of the property
967 '''
968 if self._value is None:
969 return ''
970 return str(self._value)
972 def pretty(self):
973 ''' Render the interval in a pretty format (eg. "yesterday")
974 '''
975 return self._value.pretty()
977 def field(self, size = 30):
978 ''' Render a form edit field for the property
979 '''
980 if self._value is None:
981 value = ''
982 else:
983 value = cgi.escape(str(self._value))
984 value = '"'.join(value.split('"'))
985 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
987 class LinkHTMLProperty(HTMLProperty):
988 ''' Link HTMLProperty
989 Include the above as well as being able to access the class
990 information. Stringifying the object itself results in the value
991 from the item being displayed. Accessing attributes of this object
992 result in the appropriate entry from the class being queried for the
993 property accessed (so item/assignedto/name would look up the user
994 entry identified by the assignedto property on item, and then the
995 name property of that user)
996 '''
997 def __init__(self, *args):
998 HTMLProperty.__init__(self, *args)
999 # if we're representing a form value, then the -1 from the form really
1000 # should be a None
1001 if str(self._value) == '-1':
1002 self._value = None
1004 def __getattr__(self, attr):
1005 ''' return a new HTMLItem '''
1006 #print 'Link.getattr', (self, attr, self._value)
1007 if not self._value:
1008 raise AttributeError, "Can't access missing value"
1009 if self._prop.classname == 'user':
1010 klass = HTMLUser
1011 else:
1012 klass = HTMLItem
1013 i = klass(self._client, self._prop.classname, self._value)
1014 return getattr(i, attr)
1016 def plain(self, escape=0):
1017 ''' Render a "plain" representation of the property
1018 '''
1019 if self._value is None:
1020 return ''
1021 linkcl = self._db.classes[self._prop.classname]
1022 k = linkcl.labelprop(1)
1023 value = str(linkcl.get(self._value, k))
1024 if escape:
1025 value = cgi.escape(value)
1026 return value
1028 def field(self, showid=0, size=None):
1029 ''' Render a form edit field for the property
1030 '''
1031 linkcl = self._db.getclass(self._prop.classname)
1032 if linkcl.getprops().has_key('order'):
1033 sort_on = 'order'
1034 else:
1035 sort_on = linkcl.labelprop()
1036 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1037 # TODO: make this a field display, not a menu one!
1038 l = ['<select name="%s">'%self._name]
1039 k = linkcl.labelprop(1)
1040 if self._value is None:
1041 s = 'selected '
1042 else:
1043 s = ''
1044 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1046 # make sure we list the current value if it's retired
1047 if self._value and self._value not in options:
1048 options.insert(0, self._value)
1050 for optionid in options:
1051 # get the option value, and if it's None use an empty string
1052 option = linkcl.get(optionid, k) or ''
1054 # figure if this option is selected
1055 s = ''
1056 if optionid == self._value:
1057 s = 'selected '
1059 # figure the label
1060 if showid:
1061 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1062 else:
1063 lab = option
1065 # truncate if it's too long
1066 if size is not None and len(lab) > size:
1067 lab = lab[:size-3] + '...'
1069 # and generate
1070 lab = cgi.escape(lab)
1071 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1072 l.append('</select>')
1073 return '\n'.join(l)
1075 def menu(self, size=None, height=None, showid=0, additional=[],
1076 **conditions):
1077 ''' Render a form select list for this property
1078 '''
1079 value = self._value
1081 # sort function
1082 sortfunc = make_sort_function(self._db, self._prop.classname)
1084 linkcl = self._db.getclass(self._prop.classname)
1085 l = ['<select name="%s">'%self._name]
1086 k = linkcl.labelprop(1)
1087 s = ''
1088 if value is None:
1089 s = 'selected '
1090 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1091 if linkcl.getprops().has_key('order'):
1092 sort_on = ('+', 'order')
1093 else:
1094 sort_on = ('+', linkcl.labelprop())
1095 options = linkcl.filter(None, conditions, sort_on, (None, None))
1097 # make sure we list the current value if it's retired
1098 if self._value and self._value not in options:
1099 options.insert(0, self._value)
1101 for optionid in options:
1102 # get the option value, and if it's None use an empty string
1103 option = linkcl.get(optionid, k) or ''
1105 # figure if this option is selected
1106 s = ''
1107 if value in [optionid, option]:
1108 s = 'selected '
1110 # figure the label
1111 if showid:
1112 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1113 else:
1114 lab = option
1116 # truncate if it's too long
1117 if size is not None and len(lab) > size:
1118 lab = lab[:size-3] + '...'
1119 if additional:
1120 m = []
1121 for propname in additional:
1122 m.append(linkcl.get(optionid, propname))
1123 lab = lab + ' (%s)'%', '.join(map(str, m))
1125 # and generate
1126 lab = cgi.escape(lab)
1127 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1128 l.append('</select>')
1129 return '\n'.join(l)
1130 # def checklist(self, ...)
1132 class MultilinkHTMLProperty(HTMLProperty):
1133 ''' Multilink HTMLProperty
1135 Also be iterable, returning a wrapper object like the Link case for
1136 each entry in the multilink.
1137 '''
1138 def __len__(self):
1139 ''' length of the multilink '''
1140 return len(self._value)
1142 def __getattr__(self, attr):
1143 ''' no extended attribute accesses make sense here '''
1144 raise AttributeError, attr
1146 def __getitem__(self, num):
1147 ''' iterate and return a new HTMLItem
1148 '''
1149 #print 'Multi.getitem', (self, num)
1150 value = self._value[num]
1151 if self._prop.classname == 'user':
1152 klass = HTMLUser
1153 else:
1154 klass = HTMLItem
1155 return klass(self._client, self._prop.classname, value)
1157 def __contains__(self, value):
1158 ''' Support the "in" operator. We have to make sure the passed-in
1159 value is a string first, not a *HTMLProperty.
1160 '''
1161 return str(value) in self._value
1163 def reverse(self):
1164 ''' return the list in reverse order
1165 '''
1166 l = self._value[:]
1167 l.reverse()
1168 if self._prop.classname == 'user':
1169 klass = HTMLUser
1170 else:
1171 klass = HTMLItem
1172 return [klass(self._client, self._prop.classname, value) for value in l]
1174 def plain(self, escape=0):
1175 ''' Render a "plain" representation of the property
1176 '''
1177 linkcl = self._db.classes[self._prop.classname]
1178 k = linkcl.labelprop(1)
1179 labels = []
1180 for v in self._value:
1181 labels.append(linkcl.get(v, k))
1182 value = ', '.join(labels)
1183 if escape:
1184 value = cgi.escape(value)
1185 return value
1187 def field(self, size=30, showid=0):
1188 ''' Render a form edit field for the property
1189 '''
1190 sortfunc = make_sort_function(self._db, self._prop.classname)
1191 linkcl = self._db.getclass(self._prop.classname)
1192 value = self._value[:]
1193 if value:
1194 value.sort(sortfunc)
1195 # map the id to the label property
1196 if not linkcl.getkey():
1197 showid=1
1198 if not showid:
1199 k = linkcl.labelprop(1)
1200 value = [linkcl.get(v, k) for v in value]
1201 value = cgi.escape(','.join(value))
1202 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1204 def menu(self, size=None, height=None, showid=0, additional=[],
1205 **conditions):
1206 ''' Render a form select list for this property
1207 '''
1208 value = self._value
1210 # sort function
1211 sortfunc = make_sort_function(self._db, self._prop.classname)
1213 linkcl = self._db.getclass(self._prop.classname)
1214 if linkcl.getprops().has_key('order'):
1215 sort_on = ('+', 'order')
1216 else:
1217 sort_on = ('+', linkcl.labelprop())
1218 options = linkcl.filter(None, conditions, sort_on, (None,None))
1219 height = height or min(len(options), 7)
1220 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1221 k = linkcl.labelprop(1)
1223 # make sure we list the current values if they're retired
1224 for val in value:
1225 if val not in options:
1226 options.insert(0, val)
1228 for optionid in options:
1229 # get the option value, and if it's None use an empty string
1230 option = linkcl.get(optionid, k) or ''
1232 # figure if this option is selected
1233 s = ''
1234 if optionid in value or option in value:
1235 s = 'selected '
1237 # figure the label
1238 if showid:
1239 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1240 else:
1241 lab = option
1242 # truncate if it's too long
1243 if size is not None and len(lab) > size:
1244 lab = lab[:size-3] + '...'
1245 if additional:
1246 m = []
1247 for propname in additional:
1248 m.append(linkcl.get(optionid, propname))
1249 lab = lab + ' (%s)'%', '.join(m)
1251 # and generate
1252 lab = cgi.escape(lab)
1253 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1254 lab))
1255 l.append('</select>')
1256 return '\n'.join(l)
1258 # set the propclasses for HTMLItem
1259 propclasses = (
1260 (hyperdb.String, StringHTMLProperty),
1261 (hyperdb.Number, NumberHTMLProperty),
1262 (hyperdb.Boolean, BooleanHTMLProperty),
1263 (hyperdb.Date, DateHTMLProperty),
1264 (hyperdb.Interval, IntervalHTMLProperty),
1265 (hyperdb.Password, PasswordHTMLProperty),
1266 (hyperdb.Link, LinkHTMLProperty),
1267 (hyperdb.Multilink, MultilinkHTMLProperty),
1268 )
1270 def make_sort_function(db, classname):
1271 '''Make a sort function for a given class
1272 '''
1273 linkcl = db.getclass(classname)
1274 if linkcl.getprops().has_key('order'):
1275 sort_on = 'order'
1276 else:
1277 sort_on = linkcl.labelprop()
1278 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1279 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1280 return sortfunc
1282 def handleListCGIValue(value):
1283 ''' Value is either a single item or a list of items. Each item has a
1284 .value that we're actually interested in.
1285 '''
1286 if isinstance(value, type([])):
1287 return [value.value for value in value]
1288 else:
1289 value = value.value.strip()
1290 if not value:
1291 return []
1292 return value.split(',')
1294 class ShowDict:
1295 ''' A convenience access to the :columns index parameters
1296 '''
1297 def __init__(self, columns):
1298 self.columns = {}
1299 for col in columns:
1300 self.columns[col] = 1
1301 def __getitem__(self, name):
1302 return self.columns.has_key(name)
1304 class HTMLRequest:
1305 ''' The *request*, holding the CGI form and environment.
1307 "form" the CGI form as a cgi.FieldStorage
1308 "env" the CGI environment variables
1309 "base" the base URL for this instance
1310 "user" a HTMLUser instance for this user
1311 "classname" the current classname (possibly None)
1312 "template" the current template (suffix, also possibly None)
1314 Index args:
1315 "columns" dictionary of the columns to display in an index page
1316 "show" a convenience access to columns - request/show/colname will
1317 be true if the columns should be displayed, false otherwise
1318 "sort" index sort column (direction, column name)
1319 "group" index grouping property (direction, column name)
1320 "filter" properties to filter the index on
1321 "filterspec" values to filter the index on
1322 "search_text" text to perform a full-text search on for an index
1324 '''
1325 def __init__(self, client):
1326 self.client = client
1328 # easier access vars
1329 self.form = client.form
1330 self.env = client.env
1331 self.base = client.base
1332 self.user = HTMLUser(client, 'user', client.userid)
1334 # store the current class name and action
1335 self.classname = client.classname
1336 self.template = client.template
1338 self._post_init()
1340 def _post_init(self):
1341 ''' Set attributes based on self.form
1342 '''
1343 # extract the index display information from the form
1344 self.columns = []
1345 if self.form.has_key(':columns'):
1346 self.columns = handleListCGIValue(self.form[':columns'])
1347 self.show = ShowDict(self.columns)
1349 # sorting
1350 self.sort = (None, None)
1351 if self.form.has_key(':sort'):
1352 sort = self.form[':sort'].value
1353 if sort.startswith('-'):
1354 self.sort = ('-', sort[1:])
1355 else:
1356 self.sort = ('+', sort)
1357 if self.form.has_key(':sortdir'):
1358 self.sort = ('-', self.sort[1])
1360 # grouping
1361 self.group = (None, None)
1362 if self.form.has_key(':group'):
1363 group = self.form[':group'].value
1364 if group.startswith('-'):
1365 self.group = ('-', group[1:])
1366 else:
1367 self.group = ('+', group)
1368 if self.form.has_key(':groupdir'):
1369 self.group = ('-', self.group[1])
1371 # filtering
1372 self.filter = []
1373 if self.form.has_key(':filter'):
1374 self.filter = handleListCGIValue(self.form[':filter'])
1375 self.filterspec = {}
1376 db = self.client.db
1377 if self.classname is not None:
1378 props = db.getclass(self.classname).getprops()
1379 for name in self.filter:
1380 if self.form.has_key(name):
1381 prop = props[name]
1382 fv = self.form[name]
1383 if (isinstance(prop, hyperdb.Link) or
1384 isinstance(prop, hyperdb.Multilink)):
1385 self.filterspec[name] = lookupIds(db, prop,
1386 handleListCGIValue(fv))
1387 else:
1388 self.filterspec[name] = fv.value
1390 # full-text search argument
1391 self.search_text = None
1392 if self.form.has_key(':search_text'):
1393 self.search_text = self.form[':search_text'].value
1395 # pagination - size and start index
1396 # figure batch args
1397 if self.form.has_key(':pagesize'):
1398 self.pagesize = int(self.form[':pagesize'].value)
1399 else:
1400 self.pagesize = 50
1401 if self.form.has_key(':startwith'):
1402 self.startwith = int(self.form[':startwith'].value)
1403 else:
1404 self.startwith = 0
1406 def updateFromURL(self, url):
1407 ''' Parse the URL for query args, and update my attributes using the
1408 values.
1409 '''
1410 self.form = {}
1411 for name, value in cgi.parse_qsl(url):
1412 if self.form.has_key(name):
1413 if isinstance(self.form[name], type([])):
1414 self.form[name].append(cgi.MiniFieldStorage(name, value))
1415 else:
1416 self.form[name] = [self.form[name],
1417 cgi.MiniFieldStorage(name, value)]
1418 else:
1419 self.form[name] = cgi.MiniFieldStorage(name, value)
1420 self._post_init()
1422 def update(self, kwargs):
1423 ''' Update my attributes using the keyword args
1424 '''
1425 self.__dict__.update(kwargs)
1426 if kwargs.has_key('columns'):
1427 self.show = ShowDict(self.columns)
1429 def description(self):
1430 ''' Return a description of the request - handle for the page title.
1431 '''
1432 s = [self.client.db.config.TRACKER_NAME]
1433 if self.classname:
1434 if self.client.nodeid:
1435 s.append('- %s%s'%(self.classname, self.client.nodeid))
1436 else:
1437 if self.template == 'item':
1438 s.append('- new %s'%self.classname)
1439 elif self.template == 'index':
1440 s.append('- %s index'%self.classname)
1441 else:
1442 s.append('- %s %s'%(self.classname, self.template))
1443 else:
1444 s.append('- home')
1445 return ' '.join(s)
1447 def __str__(self):
1448 d = {}
1449 d.update(self.__dict__)
1450 f = ''
1451 for k in self.form.keys():
1452 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1453 d['form'] = f
1454 e = ''
1455 for k,v in self.env.items():
1456 e += '\n %r=%r'%(k, v)
1457 d['env'] = e
1458 return '''
1459 form: %(form)s
1460 base: %(base)r
1461 classname: %(classname)r
1462 template: %(template)r
1463 columns: %(columns)r
1464 sort: %(sort)r
1465 group: %(group)r
1466 filter: %(filter)r
1467 search_text: %(search_text)r
1468 pagesize: %(pagesize)r
1469 startwith: %(startwith)r
1470 env: %(env)s
1471 '''%d
1473 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1474 filterspec=1):
1475 ''' return the current index args as form elements '''
1476 l = []
1477 s = '<input type="hidden" name="%s" value="%s">'
1478 if columns and self.columns:
1479 l.append(s%(':columns', ','.join(self.columns)))
1480 if sort and self.sort[1] is not None:
1481 if self.sort[0] == '-':
1482 val = '-'+self.sort[1]
1483 else:
1484 val = self.sort[1]
1485 l.append(s%(':sort', val))
1486 if group and self.group[1] is not None:
1487 if self.group[0] == '-':
1488 val = '-'+self.group[1]
1489 else:
1490 val = self.group[1]
1491 l.append(s%(':group', val))
1492 if filter and self.filter:
1493 l.append(s%(':filter', ','.join(self.filter)))
1494 if filterspec:
1495 for k,v in self.filterspec.items():
1496 if type(v) == type([]):
1497 l.append(s%(k, ','.join(v)))
1498 else:
1499 l.append(s%(k, v))
1500 if self.search_text:
1501 l.append(s%(':search_text', self.search_text))
1502 l.append(s%(':pagesize', self.pagesize))
1503 l.append(s%(':startwith', self.startwith))
1504 return '\n'.join(l)
1506 def indexargs_url(self, url, args):
1507 ''' embed the current index args in a URL '''
1508 l = ['%s=%s'%(k,v) for k,v in args.items()]
1509 if self.columns and not args.has_key(':columns'):
1510 l.append(':columns=%s'%(','.join(self.columns)))
1511 if self.sort[1] is not None and not args.has_key(':sort'):
1512 if self.sort[0] == '-':
1513 val = '-'+self.sort[1]
1514 else:
1515 val = self.sort[1]
1516 l.append(':sort=%s'%val)
1517 if self.group[1] is not None and not args.has_key(':group'):
1518 if self.group[0] == '-':
1519 val = '-'+self.group[1]
1520 else:
1521 val = self.group[1]
1522 l.append(':group=%s'%val)
1523 if self.filter and not args.has_key(':columns'):
1524 l.append(':filter=%s'%(','.join(self.filter)))
1525 for k,v in self.filterspec.items():
1526 if not args.has_key(k):
1527 if type(v) == type([]):
1528 l.append('%s=%s'%(k, ','.join(v)))
1529 else:
1530 l.append('%s=%s'%(k, v))
1531 if self.search_text and not args.has_key(':search_text'):
1532 l.append(':search_text=%s'%self.search_text)
1533 if not args.has_key(':pagesize'):
1534 l.append(':pagesize=%s'%self.pagesize)
1535 if not args.has_key(':startwith'):
1536 l.append(':startwith=%s'%self.startwith)
1537 return '%s?%s'%(url, '&'.join(l))
1538 indexargs_href = indexargs_url
1540 def base_javascript(self):
1541 return '''
1542 <script language="javascript">
1543 submitted = false;
1544 function submit_once() {
1545 if (submitted) {
1546 alert("Your request is being processed.\\nPlease be patient.");
1547 return 0;
1548 }
1549 submitted = true;
1550 return 1;
1551 }
1553 function help_window(helpurl, width, height) {
1554 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1555 }
1556 </script>
1557 '''%self.base
1559 def batch(self):
1560 ''' Return a batch object for results from the "current search"
1561 '''
1562 filterspec = self.filterspec
1563 sort = self.sort
1564 group = self.group
1566 # get the list of ids we're batching over
1567 klass = self.client.db.getclass(self.classname)
1568 if self.search_text:
1569 matches = self.client.db.indexer.search(
1570 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1571 else:
1572 matches = None
1573 l = klass.filter(matches, filterspec, sort, group)
1575 # return the batch object, using IDs only
1576 return Batch(self.client, l, self.pagesize, self.startwith,
1577 classname=self.classname)
1579 # extend the standard ZTUtils Batch object to remove dependency on
1580 # Acquisition and add a couple of useful methods
1581 class Batch(ZTUtils.Batch):
1582 ''' Use me to turn a list of items, or item ids of a given class, into a
1583 series of batches.
1585 ========= ========================================================
1586 Parameter Usage
1587 ========= ========================================================
1588 sequence a list of HTMLItems or item ids
1589 classname if sequence is a list of ids, this is the class of item
1590 size how big to make the sequence.
1591 start where to start (0-indexed) in the sequence.
1592 end where to end (0-indexed) in the sequence.
1593 orphan if the next batch would contain less items than this
1594 value, then it is combined with this batch
1595 overlap the number of items shared between adjacent batches
1596 ========= ========================================================
1598 Attributes: Note that the "start" attribute, unlike the
1599 argument, is a 1-based index (I know, lame). "first" is the
1600 0-based index. "length" is the actual number of elements in
1601 the batch.
1603 "sequence_length" is the length of the original, unbatched, sequence.
1604 '''
1605 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1606 overlap=0, classname=None):
1607 self.client = client
1608 self.last_index = self.last_item = None
1609 self.current_item = None
1610 self.classname = classname
1611 self.sequence_length = len(sequence)
1612 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1613 overlap)
1615 # overwrite so we can late-instantiate the HTMLItem instance
1616 def __getitem__(self, index):
1617 if index < 0:
1618 if index + self.end < self.first: raise IndexError, index
1619 return self._sequence[index + self.end]
1621 if index >= self.length:
1622 raise IndexError, index
1624 # move the last_item along - but only if the fetched index changes
1625 # (for some reason, index 0 is fetched twice)
1626 if index != self.last_index:
1627 self.last_item = self.current_item
1628 self.last_index = index
1630 item = self._sequence[index + self.first]
1631 if self.classname:
1632 # map the item ids to instances
1633 if self.classname == 'user':
1634 item = HTMLUser(self.client, self.classname, item)
1635 else:
1636 item = HTMLItem(self.client, self.classname, item)
1637 self.current_item = item
1638 return item
1640 def propchanged(self, property):
1641 ''' Detect if the property marked as being the group property
1642 changed in the last iteration fetch
1643 '''
1644 if (self.last_item is None or
1645 self.last_item[property] != self.current_item[property]):
1646 return 1
1647 return 0
1649 # override these 'cos we don't have access to acquisition
1650 def previous(self):
1651 if self.start == 1:
1652 return None
1653 return Batch(self.client, self._sequence, self._size,
1654 self.first - self._size + self.overlap, 0, self.orphan,
1655 self.overlap)
1657 def next(self):
1658 try:
1659 self._sequence[self.end]
1660 except IndexError:
1661 return None
1662 return Batch(self.client, self._sequence, self._size,
1663 self.end - self.overlap, 0, self.orphan, self.overlap)
1665 class TemplatingUtils:
1666 ''' Utilities for templating
1667 '''
1668 def __init__(self, client):
1669 self.client = client
1670 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1671 return Batch(self.client, sequence, size, start, end, orphan,
1672 overlap)