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