3b846337b242561574aa1b944ed9edabe3ef78b9
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 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
770 r'(?P<email>[\w\.]+@[\w\.\-]+)|'
771 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
772 def _hyper_repl(self, match):
773 if match.group('url'):
774 s = match.group('url')
775 return '<a href="%s">%s</a>'%(s, s)
776 elif match.group('email'):
777 s = match.group('email')
778 return '<a href="mailto:%s">%s</a>'%(s, s)
779 else:
780 s = match.group('item')
781 s1 = match.group('class')
782 s2 = match.group('id')
783 try:
784 # make sure s1 is a valid tracker classname
785 self._db.getclass(s1)
786 return '<a href="%s">%s %s</a>'%(s, s1, s2)
787 except KeyError:
788 return '%s%s'%(s1, s2)
790 def plain(self, escape=0, hyperlink=0):
791 ''' Render a "plain" representation of the property
793 "escape" turns on/off HTML quoting
794 "hyperlink" turns on/off in-text hyperlinking of URLs, email
795 addresses and designators
796 '''
797 if self._value is None:
798 return ''
799 if escape:
800 s = cgi.escape(str(self._value))
801 else:
802 s = str(self._value)
803 if hyperlink:
804 if not escape:
805 s = cgi.escape(s)
806 s = self.hyper_re.sub(self._hyper_repl, s)
807 return s
809 def stext(self, escape=0):
810 ''' Render the value of the property as StructuredText.
812 This requires the StructureText module to be installed separately.
813 '''
814 s = self.plain(escape=escape)
815 if not StructuredText:
816 return s
817 return StructuredText(s,level=1,header=0)
819 def field(self, size = 30):
820 ''' Render a form edit field for the property
821 '''
822 if self._value is None:
823 value = ''
824 else:
825 value = cgi.escape(str(self._value))
826 value = '"'.join(value.split('"'))
827 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
829 def multiline(self, escape=0, rows=5, cols=40):
830 ''' Render a multiline form edit field for the property
831 '''
832 if self._value is None:
833 value = ''
834 else:
835 value = cgi.escape(str(self._value))
836 value = '"'.join(value.split('"'))
837 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
838 self._name, rows, cols, value)
840 def email(self, escape=1):
841 ''' Render the value of the property as an obscured email address
842 '''
843 if self._value is None: value = ''
844 else: value = str(self._value)
845 if value.find('@') != -1:
846 name, domain = value.split('@')
847 domain = ' '.join(domain.split('.')[:-1])
848 name = name.replace('.', ' ')
849 value = '%s at %s ...'%(name, domain)
850 else:
851 value = value.replace('.', ' ')
852 if escape:
853 value = cgi.escape(value)
854 return value
856 class PasswordHTMLProperty(HTMLProperty):
857 def plain(self):
858 ''' Render a "plain" representation of the property
859 '''
860 if self._value is None:
861 return ''
862 return _('*encrypted*')
864 def field(self, size = 30):
865 ''' Render a form edit field for the property.
866 '''
867 return '<input type="password" name="%s" size="%s">'%(self._name, size)
869 def confirm(self, size = 30):
870 ''' Render a second form edit field for the property, used for
871 confirmation that the user typed the password correctly. Generates
872 a field with name "name:confirm".
873 '''
874 return '<input type="password" name="%s:confirm" size="%s">'%(
875 self._name, size)
877 class NumberHTMLProperty(HTMLProperty):
878 def plain(self):
879 ''' Render a "plain" representation of the property
880 '''
881 return str(self._value)
883 def field(self, size = 30):
884 ''' Render a form edit field for the property
885 '''
886 if self._value is None:
887 value = ''
888 else:
889 value = cgi.escape(str(self._value))
890 value = '"'.join(value.split('"'))
891 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
893 class BooleanHTMLProperty(HTMLProperty):
894 def plain(self):
895 ''' Render a "plain" representation of the property
896 '''
897 if self._value is None:
898 return ''
899 return self._value and "Yes" or "No"
901 def field(self):
902 ''' Render a form edit field for the property
903 '''
904 checked = self._value and "checked" or ""
905 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
906 checked)
907 if checked:
908 checked = ""
909 else:
910 checked = "checked"
911 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
912 checked)
913 return s
915 class DateHTMLProperty(HTMLProperty):
916 def plain(self):
917 ''' Render a "plain" representation of the property
918 '''
919 if self._value is None:
920 return ''
921 return str(self._value)
923 def field(self, size = 30):
924 ''' Render a form edit field for the property
925 '''
926 if self._value is None:
927 value = ''
928 else:
929 value = cgi.escape(str(self._value))
930 value = '"'.join(value.split('"'))
931 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
933 def reldate(self, pretty=1):
934 ''' Render the interval between the date and now.
936 If the "pretty" flag is true, then make the display pretty.
937 '''
938 if not self._value:
939 return ''
941 # figure the interval
942 interval = date.Date('.') - self._value
943 if pretty:
944 return interval.pretty()
945 return str(interval)
947 def pretty(self, format='%d %B %Y'):
948 ''' Render the date in a pretty format (eg. month names, spaces).
950 The format string is a standard python strftime format string.
951 Note that if the day is zero, and appears at the start of the
952 string, then it'll be stripped from the output. This is handy
953 for the situatin when a date only specifies a month and a year.
954 '''
955 return self._value.pretty()
957 def local(self, offset):
958 ''' Return the date/time as a local (timezone offset) date/time.
959 '''
960 return DateHTMLProperty(self._client, self._nodeid, self._prop,
961 self._name, self._value.local())
963 class IntervalHTMLProperty(HTMLProperty):
964 def plain(self):
965 ''' Render a "plain" representation of the property
966 '''
967 if self._value is None:
968 return ''
969 return str(self._value)
971 def pretty(self):
972 ''' Render the interval in a pretty format (eg. "yesterday")
973 '''
974 return self._value.pretty()
976 def field(self, size = 30):
977 ''' Render a form edit field for the property
978 '''
979 if self._value is None:
980 value = ''
981 else:
982 value = cgi.escape(str(self._value))
983 value = '"'.join(value.split('"'))
984 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
986 class LinkHTMLProperty(HTMLProperty):
987 ''' Link HTMLProperty
988 Include the above as well as being able to access the class
989 information. Stringifying the object itself results in the value
990 from the item being displayed. Accessing attributes of this object
991 result in the appropriate entry from the class being queried for the
992 property accessed (so item/assignedto/name would look up the user
993 entry identified by the assignedto property on item, and then the
994 name property of that user)
995 '''
996 def __init__(self, *args):
997 HTMLProperty.__init__(self, *args)
998 # if we're representing a form value, then the -1 from the form really
999 # should be a None
1000 if str(self._value) == '-1':
1001 self._value = None
1003 def __getattr__(self, attr):
1004 ''' return a new HTMLItem '''
1005 #print 'Link.getattr', (self, attr, self._value)
1006 if not self._value:
1007 raise AttributeError, "Can't access missing value"
1008 if self._prop.classname == 'user':
1009 klass = HTMLUser
1010 else:
1011 klass = HTMLItem
1012 i = klass(self._client, self._prop.classname, self._value)
1013 return getattr(i, attr)
1015 def plain(self, escape=0):
1016 ''' Render a "plain" representation of the property
1017 '''
1018 if self._value is None:
1019 return ''
1020 linkcl = self._db.classes[self._prop.classname]
1021 k = linkcl.labelprop(1)
1022 value = str(linkcl.get(self._value, k))
1023 if escape:
1024 value = cgi.escape(value)
1025 return value
1027 def field(self, showid=0, size=None):
1028 ''' Render a form edit field for the property
1029 '''
1030 linkcl = self._db.getclass(self._prop.classname)
1031 if linkcl.getprops().has_key('order'):
1032 sort_on = 'order'
1033 else:
1034 sort_on = linkcl.labelprop()
1035 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1036 # TODO: make this a field display, not a menu one!
1037 l = ['<select name="%s">'%self._name]
1038 k = linkcl.labelprop(1)
1039 if self._value is None:
1040 s = 'selected '
1041 else:
1042 s = ''
1043 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1045 # make sure we list the current value if it's retired
1046 if self._value and self._value not in options:
1047 options.insert(0, self._value)
1049 for optionid in options:
1050 # get the option value, and if it's None use an empty string
1051 option = linkcl.get(optionid, k) or ''
1053 # figure if this option is selected
1054 s = ''
1055 if optionid == self._value:
1056 s = 'selected '
1058 # figure the label
1059 if showid:
1060 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1061 else:
1062 lab = option
1064 # truncate if it's too long
1065 if size is not None and len(lab) > size:
1066 lab = lab[:size-3] + '...'
1068 # and generate
1069 lab = cgi.escape(lab)
1070 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1071 l.append('</select>')
1072 return '\n'.join(l)
1074 def menu(self, size=None, height=None, showid=0, additional=[],
1075 **conditions):
1076 ''' Render a form select list for this property
1077 '''
1078 value = self._value
1080 # sort function
1081 sortfunc = make_sort_function(self._db, self._prop.classname)
1083 linkcl = self._db.getclass(self._prop.classname)
1084 l = ['<select name="%s">'%self._name]
1085 k = linkcl.labelprop(1)
1086 s = ''
1087 if value is None:
1088 s = 'selected '
1089 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1090 if linkcl.getprops().has_key('order'):
1091 sort_on = ('+', 'order')
1092 else:
1093 sort_on = ('+', linkcl.labelprop())
1094 options = linkcl.filter(None, conditions, sort_on, (None, None))
1096 # make sure we list the current value if it's retired
1097 if self._value and self._value not in options:
1098 options.insert(0, self._value)
1100 for optionid in options:
1101 # get the option value, and if it's None use an empty string
1102 option = linkcl.get(optionid, k) or ''
1104 # figure if this option is selected
1105 s = ''
1106 if value in [optionid, option]:
1107 s = 'selected '
1109 # figure the label
1110 if showid:
1111 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1112 else:
1113 lab = option
1115 # truncate if it's too long
1116 if size is not None and len(lab) > size:
1117 lab = lab[:size-3] + '...'
1118 if additional:
1119 m = []
1120 for propname in additional:
1121 m.append(linkcl.get(optionid, propname))
1122 lab = lab + ' (%s)'%', '.join(map(str, m))
1124 # and generate
1125 lab = cgi.escape(lab)
1126 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1127 l.append('</select>')
1128 return '\n'.join(l)
1129 # def checklist(self, ...)
1131 class MultilinkHTMLProperty(HTMLProperty):
1132 ''' Multilink HTMLProperty
1134 Also be iterable, returning a wrapper object like the Link case for
1135 each entry in the multilink.
1136 '''
1137 def __len__(self):
1138 ''' length of the multilink '''
1139 return len(self._value)
1141 def __getattr__(self, attr):
1142 ''' no extended attribute accesses make sense here '''
1143 raise AttributeError, attr
1145 def __getitem__(self, num):
1146 ''' iterate and return a new HTMLItem
1147 '''
1148 #print 'Multi.getitem', (self, num)
1149 value = self._value[num]
1150 if self._prop.classname == 'user':
1151 klass = HTMLUser
1152 else:
1153 klass = HTMLItem
1154 return klass(self._client, self._prop.classname, value)
1156 def __contains__(self, value):
1157 ''' Support the "in" operator. We have to make sure the passed-in
1158 value is a string first, not a *HTMLProperty.
1159 '''
1160 return str(value) in self._value
1162 def reverse(self):
1163 ''' return the list in reverse order
1164 '''
1165 l = self._value[:]
1166 l.reverse()
1167 if self._prop.classname == 'user':
1168 klass = HTMLUser
1169 else:
1170 klass = HTMLItem
1171 return [klass(self._client, self._prop.classname, value) for value in l]
1173 def plain(self, escape=0):
1174 ''' Render a "plain" representation of the property
1175 '''
1176 linkcl = self._db.classes[self._prop.classname]
1177 k = linkcl.labelprop(1)
1178 labels = []
1179 for v in self._value:
1180 labels.append(linkcl.get(v, k))
1181 value = ', '.join(labels)
1182 if escape:
1183 value = cgi.escape(value)
1184 return value
1186 def field(self, size=30, showid=0):
1187 ''' Render a form edit field for the property
1188 '''
1189 sortfunc = make_sort_function(self._db, self._prop.classname)
1190 linkcl = self._db.getclass(self._prop.classname)
1191 value = self._value[:]
1192 if value:
1193 value.sort(sortfunc)
1194 # map the id to the label property
1195 if not linkcl.getkey():
1196 showid=1
1197 if not showid:
1198 k = linkcl.labelprop(1)
1199 value = [linkcl.get(v, k) for v in value]
1200 value = cgi.escape(','.join(value))
1201 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1203 def menu(self, size=None, height=None, showid=0, additional=[],
1204 **conditions):
1205 ''' Render a form select list for this property
1206 '''
1207 value = self._value
1209 # sort function
1210 sortfunc = make_sort_function(self._db, self._prop.classname)
1212 linkcl = self._db.getclass(self._prop.classname)
1213 if linkcl.getprops().has_key('order'):
1214 sort_on = ('+', 'order')
1215 else:
1216 sort_on = ('+', linkcl.labelprop())
1217 options = linkcl.filter(None, conditions, sort_on, (None,None))
1218 height = height or min(len(options), 7)
1219 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1220 k = linkcl.labelprop(1)
1222 # make sure we list the current values if they're retired
1223 for val in value:
1224 if val not in options:
1225 options.insert(0, val)
1227 for optionid in options:
1228 # get the option value, and if it's None use an empty string
1229 option = linkcl.get(optionid, k) or ''
1231 # figure if this option is selected
1232 s = ''
1233 if optionid in value or option in value:
1234 s = 'selected '
1236 # figure the label
1237 if showid:
1238 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1239 else:
1240 lab = option
1241 # truncate if it's too long
1242 if size is not None and len(lab) > size:
1243 lab = lab[:size-3] + '...'
1244 if additional:
1245 m = []
1246 for propname in additional:
1247 m.append(linkcl.get(optionid, propname))
1248 lab = lab + ' (%s)'%', '.join(m)
1250 # and generate
1251 lab = cgi.escape(lab)
1252 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1253 lab))
1254 l.append('</select>')
1255 return '\n'.join(l)
1257 # set the propclasses for HTMLItem
1258 propclasses = (
1259 (hyperdb.String, StringHTMLProperty),
1260 (hyperdb.Number, NumberHTMLProperty),
1261 (hyperdb.Boolean, BooleanHTMLProperty),
1262 (hyperdb.Date, DateHTMLProperty),
1263 (hyperdb.Interval, IntervalHTMLProperty),
1264 (hyperdb.Password, PasswordHTMLProperty),
1265 (hyperdb.Link, LinkHTMLProperty),
1266 (hyperdb.Multilink, MultilinkHTMLProperty),
1267 )
1269 def make_sort_function(db, classname):
1270 '''Make a sort function for a given class
1271 '''
1272 linkcl = db.getclass(classname)
1273 if linkcl.getprops().has_key('order'):
1274 sort_on = 'order'
1275 else:
1276 sort_on = linkcl.labelprop()
1277 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1278 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1279 return sortfunc
1281 def handleListCGIValue(value):
1282 ''' Value is either a single item or a list of items. Each item has a
1283 .value that we're actually interested in.
1284 '''
1285 if isinstance(value, type([])):
1286 return [value.value for value in value]
1287 else:
1288 value = value.value.strip()
1289 if not value:
1290 return []
1291 return value.split(',')
1293 class ShowDict:
1294 ''' A convenience access to the :columns index parameters
1295 '''
1296 def __init__(self, columns):
1297 self.columns = {}
1298 for col in columns:
1299 self.columns[col] = 1
1300 def __getitem__(self, name):
1301 return self.columns.has_key(name)
1303 class HTMLRequest:
1304 ''' The *request*, holding the CGI form and environment.
1306 "form" the CGI form as a cgi.FieldStorage
1307 "env" the CGI environment variables
1308 "base" the base URL for this instance
1309 "user" a HTMLUser instance for this user
1310 "classname" the current classname (possibly None)
1311 "template" the current template (suffix, also possibly None)
1313 Index args:
1314 "columns" dictionary of the columns to display in an index page
1315 "show" a convenience access to columns - request/show/colname will
1316 be true if the columns should be displayed, false otherwise
1317 "sort" index sort column (direction, column name)
1318 "group" index grouping property (direction, column name)
1319 "filter" properties to filter the index on
1320 "filterspec" values to filter the index on
1321 "search_text" text to perform a full-text search on for an index
1323 '''
1324 def __init__(self, client):
1325 self.client = client
1327 # easier access vars
1328 self.form = client.form
1329 self.env = client.env
1330 self.base = client.base
1331 self.user = HTMLUser(client, 'user', client.userid)
1333 # store the current class name and action
1334 self.classname = client.classname
1335 self.template = client.template
1337 self._post_init()
1339 def _post_init(self):
1340 ''' Set attributes based on self.form
1341 '''
1342 # extract the index display information from the form
1343 self.columns = []
1344 if self.form.has_key(':columns'):
1345 self.columns = handleListCGIValue(self.form[':columns'])
1346 self.show = ShowDict(self.columns)
1348 # sorting
1349 self.sort = (None, None)
1350 if self.form.has_key(':sort'):
1351 sort = self.form[':sort'].value
1352 if sort.startswith('-'):
1353 self.sort = ('-', sort[1:])
1354 else:
1355 self.sort = ('+', sort)
1356 if self.form.has_key(':sortdir'):
1357 self.sort = ('-', self.sort[1])
1359 # grouping
1360 self.group = (None, None)
1361 if self.form.has_key(':group'):
1362 group = self.form[':group'].value
1363 if group.startswith('-'):
1364 self.group = ('-', group[1:])
1365 else:
1366 self.group = ('+', group)
1367 if self.form.has_key(':groupdir'):
1368 self.group = ('-', self.group[1])
1370 # filtering
1371 self.filter = []
1372 if self.form.has_key(':filter'):
1373 self.filter = handleListCGIValue(self.form[':filter'])
1374 self.filterspec = {}
1375 db = self.client.db
1376 if self.classname is not None:
1377 props = db.getclass(self.classname).getprops()
1378 for name in self.filter:
1379 if self.form.has_key(name):
1380 prop = props[name]
1381 fv = self.form[name]
1382 if (isinstance(prop, hyperdb.Link) or
1383 isinstance(prop, hyperdb.Multilink)):
1384 self.filterspec[name] = lookupIds(db, prop,
1385 handleListCGIValue(fv))
1386 else:
1387 self.filterspec[name] = fv.value
1389 # full-text search argument
1390 self.search_text = None
1391 if self.form.has_key(':search_text'):
1392 self.search_text = self.form[':search_text'].value
1394 # pagination - size and start index
1395 # figure batch args
1396 if self.form.has_key(':pagesize'):
1397 self.pagesize = int(self.form[':pagesize'].value)
1398 else:
1399 self.pagesize = 50
1400 if self.form.has_key(':startwith'):
1401 self.startwith = int(self.form[':startwith'].value)
1402 else:
1403 self.startwith = 0
1405 def updateFromURL(self, url):
1406 ''' Parse the URL for query args, and update my attributes using the
1407 values.
1408 '''
1409 self.form = {}
1410 for name, value in cgi.parse_qsl(url):
1411 if self.form.has_key(name):
1412 if isinstance(self.form[name], type([])):
1413 self.form[name].append(cgi.MiniFieldStorage(name, value))
1414 else:
1415 self.form[name] = [self.form[name],
1416 cgi.MiniFieldStorage(name, value)]
1417 else:
1418 self.form[name] = cgi.MiniFieldStorage(name, value)
1419 self._post_init()
1421 def update(self, kwargs):
1422 ''' Update my attributes using the keyword args
1423 '''
1424 self.__dict__.update(kwargs)
1425 if kwargs.has_key('columns'):
1426 self.show = ShowDict(self.columns)
1428 def description(self):
1429 ''' Return a description of the request - handle for the page title.
1430 '''
1431 s = [self.client.db.config.TRACKER_NAME]
1432 if self.classname:
1433 if self.client.nodeid:
1434 s.append('- %s%s'%(self.classname, self.client.nodeid))
1435 else:
1436 if self.template == 'item':
1437 s.append('- new %s'%self.classname)
1438 elif self.template == 'index':
1439 s.append('- %s index'%self.classname)
1440 else:
1441 s.append('- %s %s'%(self.classname, self.template))
1442 else:
1443 s.append('- home')
1444 return ' '.join(s)
1446 def __str__(self):
1447 d = {}
1448 d.update(self.__dict__)
1449 f = ''
1450 for k in self.form.keys():
1451 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1452 d['form'] = f
1453 e = ''
1454 for k,v in self.env.items():
1455 e += '\n %r=%r'%(k, v)
1456 d['env'] = e
1457 return '''
1458 form: %(form)s
1459 base: %(base)r
1460 classname: %(classname)r
1461 template: %(template)r
1462 columns: %(columns)r
1463 sort: %(sort)r
1464 group: %(group)r
1465 filter: %(filter)r
1466 search_text: %(search_text)r
1467 pagesize: %(pagesize)r
1468 startwith: %(startwith)r
1469 env: %(env)s
1470 '''%d
1472 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1473 filterspec=1):
1474 ''' return the current index args as form elements '''
1475 l = []
1476 s = '<input type="hidden" name="%s" value="%s">'
1477 if columns and self.columns:
1478 l.append(s%(':columns', ','.join(self.columns)))
1479 if sort and self.sort[1] is not None:
1480 if self.sort[0] == '-':
1481 val = '-'+self.sort[1]
1482 else:
1483 val = self.sort[1]
1484 l.append(s%(':sort', val))
1485 if group and self.group[1] is not None:
1486 if self.group[0] == '-':
1487 val = '-'+self.group[1]
1488 else:
1489 val = self.group[1]
1490 l.append(s%(':group', val))
1491 if filter and self.filter:
1492 l.append(s%(':filter', ','.join(self.filter)))
1493 if filterspec:
1494 for k,v in self.filterspec.items():
1495 if type(v) == type([]):
1496 l.append(s%(k, ','.join(v)))
1497 else:
1498 l.append(s%(k, v))
1499 if self.search_text:
1500 l.append(s%(':search_text', self.search_text))
1501 l.append(s%(':pagesize', self.pagesize))
1502 l.append(s%(':startwith', self.startwith))
1503 return '\n'.join(l)
1505 def indexargs_url(self, url, args):
1506 ''' embed the current index args in a URL '''
1507 l = ['%s=%s'%(k,v) for k,v in args.items()]
1508 if self.columns and not args.has_key(':columns'):
1509 l.append(':columns=%s'%(','.join(self.columns)))
1510 if self.sort[1] is not None and not args.has_key(':sort'):
1511 if self.sort[0] == '-':
1512 val = '-'+self.sort[1]
1513 else:
1514 val = self.sort[1]
1515 l.append(':sort=%s'%val)
1516 if self.group[1] is not None and not args.has_key(':group'):
1517 if self.group[0] == '-':
1518 val = '-'+self.group[1]
1519 else:
1520 val = self.group[1]
1521 l.append(':group=%s'%val)
1522 if self.filter and not args.has_key(':columns'):
1523 l.append(':filter=%s'%(','.join(self.filter)))
1524 for k,v in self.filterspec.items():
1525 if not args.has_key(k):
1526 if type(v) == type([]):
1527 l.append('%s=%s'%(k, ','.join(v)))
1528 else:
1529 l.append('%s=%s'%(k, v))
1530 if self.search_text and not args.has_key(':search_text'):
1531 l.append(':search_text=%s'%self.search_text)
1532 if not args.has_key(':pagesize'):
1533 l.append(':pagesize=%s'%self.pagesize)
1534 if not args.has_key(':startwith'):
1535 l.append(':startwith=%s'%self.startwith)
1536 return '%s?%s'%(url, '&'.join(l))
1537 indexargs_href = indexargs_url
1539 def base_javascript(self):
1540 return '''
1541 <script language="javascript">
1542 submitted = false;
1543 function submit_once() {
1544 if (submitted) {
1545 alert("Your request is being processed.\\nPlease be patient.");
1546 return 0;
1547 }
1548 submitted = true;
1549 return 1;
1550 }
1552 function help_window(helpurl, width, height) {
1553 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1554 }
1555 </script>
1556 '''%self.base
1558 def batch(self):
1559 ''' Return a batch object for results from the "current search"
1560 '''
1561 filterspec = self.filterspec
1562 sort = self.sort
1563 group = self.group
1565 # get the list of ids we're batching over
1566 klass = self.client.db.getclass(self.classname)
1567 if self.search_text:
1568 matches = self.client.db.indexer.search(
1569 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1570 else:
1571 matches = None
1572 l = klass.filter(matches, filterspec, sort, group)
1574 # return the batch object, using IDs only
1575 return Batch(self.client, l, self.pagesize, self.startwith,
1576 classname=self.classname)
1578 # extend the standard ZTUtils Batch object to remove dependency on
1579 # Acquisition and add a couple of useful methods
1580 class Batch(ZTUtils.Batch):
1581 ''' Use me to turn a list of items, or item ids of a given class, into a
1582 series of batches.
1584 ========= ========================================================
1585 Parameter Usage
1586 ========= ========================================================
1587 sequence a list of HTMLItems or item ids
1588 classname if sequence is a list of ids, this is the class of item
1589 size how big to make the sequence.
1590 start where to start (0-indexed) in the sequence.
1591 end where to end (0-indexed) in the sequence.
1592 orphan if the next batch would contain less items than this
1593 value, then it is combined with this batch
1594 overlap the number of items shared between adjacent batches
1595 ========= ========================================================
1597 Attributes: Note that the "start" attribute, unlike the
1598 argument, is a 1-based index (I know, lame). "first" is the
1599 0-based index. "length" is the actual number of elements in
1600 the batch.
1602 "sequence_length" is the length of the original, unbatched, sequence.
1603 '''
1604 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1605 overlap=0, classname=None):
1606 self.client = client
1607 self.last_index = self.last_item = None
1608 self.current_item = None
1609 self.classname = classname
1610 self.sequence_length = len(sequence)
1611 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1612 overlap)
1614 # overwrite so we can late-instantiate the HTMLItem instance
1615 def __getitem__(self, index):
1616 if index < 0:
1617 if index + self.end < self.first: raise IndexError, index
1618 return self._sequence[index + self.end]
1620 if index >= self.length:
1621 raise IndexError, index
1623 # move the last_item along - but only if the fetched index changes
1624 # (for some reason, index 0 is fetched twice)
1625 if index != self.last_index:
1626 self.last_item = self.current_item
1627 self.last_index = index
1629 item = self._sequence[index + self.first]
1630 if self.classname:
1631 # map the item ids to instances
1632 if self.classname == 'user':
1633 item = HTMLUser(self.client, self.classname, item)
1634 else:
1635 item = HTMLItem(self.client, self.classname, item)
1636 self.current_item = item
1637 return item
1639 def propchanged(self, property):
1640 ''' Detect if the property marked as being the group property
1641 changed in the last iteration fetch
1642 '''
1643 if (self.last_item is None or
1644 self.last_item[property] != self.current_item[property]):
1645 return 1
1646 return 0
1648 # override these 'cos we don't have access to acquisition
1649 def previous(self):
1650 if self.start == 1:
1651 return None
1652 return Batch(self.client, self._sequence, self._size,
1653 self.first - self._size + self.overlap, 0, self.orphan,
1654 self.overlap)
1656 def next(self):
1657 try:
1658 self._sequence[self.end]
1659 except IndexError:
1660 return None
1661 return Batch(self.client, self._sequence, self._size,
1662 self.end - self.overlap, 0, self.orphan, self.overlap)
1664 class TemplatingUtils:
1665 ''' Utilities for templating
1666 '''
1667 def __init__(self, client):
1668 self.client = client
1669 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1670 return Batch(self.client, sequence, size, start, end, orphan,
1671 overlap)