Code

fix incorrect hyperlinking markup
[roundup.git] / roundup / cgi / templating.py
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
491     
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])
531  
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(' ', '&nbsp;')
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
792             
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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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),
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;
1552 function help_window(helpurl, width, height) {
1553     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
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]
1619         
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)