Code

oops
[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     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
791             
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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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 = '&quot;'.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),
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;
1551 function help_window(helpurl, width, height) {
1552     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
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]
1618         
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)