Code

better handling of format param
[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=None):
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'
58         elif extension is None and '.' in name:
59             # split name
60             name, extension = name.split('.')
62         # find the source, figure the time it was last modified
63         if extension:
64             filename = '%s.%s'%(name, extension)
65         else:
66             filename = name
68         src = os.path.join(self.dir, filename)
69         try:
70             stime = os.stat(src)[os.path.stat.ST_MTIME]
71         except os.error, error:
72             if error.errno != errno.ENOENT:
73                 raise
74             if not extension:
75                 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
77             # try for a generic template
78             generic = '_generic.%s'%extension
79             src = os.path.join(self.dir, generic)
80             try:
81                 stime = os.stat(src)[os.path.stat.ST_MTIME]
82             except os.error, error:
83                 if error.errno != errno.ENOENT:
84                     raise
85                 # nicer error
86                 raise NoTemplate, 'No template file exists for templating '\
87                     '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
88                     extension, filename, generic)
89             filename = generic
91         if self.templates.has_key(src) and \
92                 stime < self.templates[src].mtime:
93             # compiled template is up to date
94             return self.templates[src]
96         # compile the template
97         self.templates[src] = pt = RoundupPageTemplate()
98         pt.write(open(src).read())
99         pt.id = filename
100         pt.mtime = time.time()
101         return pt
103     def __getitem__(self, name):
104         name, extension = os.path.splitext(name)
105         if extension:
106             extension = extension[1:]
107         try:
108             return self.get(name, extension)
109         except NoTemplate, message:
110             raise KeyError, message
112 class RoundupPageTemplate(PageTemplate.PageTemplate):
113     ''' A Roundup-specific PageTemplate.
115         Interrogate the client to set up the various template variables to
116         be available:
118         *context*
119          this is one of three things:
120          1. None - we're viewing a "home" page
121          2. The current class of item being displayed. This is an HTMLClass
122             instance.
123          3. The current item from the database, if we're viewing a specific
124             item, as an HTMLItem instance.
125         *request*
126           Includes information about the current request, including:
127            - the url
128            - the current index information (``filterspec``, ``filter`` args,
129              ``properties``, etc) parsed out of the form. 
130            - methods for easy filterspec link generation
131            - *user*, the current user node as an HTMLItem instance
132            - *form*, the current CGI form information as a FieldStorage
133         *config*
134           The current tracker config.
135         *db*
136           The current database, used to access arbitrary database items.
137         *utils*
138           This is a special class that has its base in the TemplatingUtils
139           class in this file. If the tracker interfaces module defines a
140           TemplatingUtils class then it is mixed in, overriding the methods
141           in the base class.
142     '''
143     def getContext(self, client, classname, request):
144         # construct the TemplatingUtils class
145         utils = TemplatingUtils
146         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
147             class utils(client.instance.interfaces.TemplatingUtils, utils):
148                 pass
150         c = {
151              'options': {},
152              'nothing': None,
153              'request': request,
154              'db': HTMLDatabase(client),
155              'config': client.instance.config,
156              'tracker': client.instance,
157              'utils': utils(client),
158              'templates': Templates(client.instance.config.TEMPLATES),
159         }
160         # add in the item if there is one
161         if client.nodeid:
162             if classname == 'user':
163                 c['context'] = HTMLUser(client, classname, client.nodeid,
164                     anonymous=1)
165             else:
166                 c['context'] = HTMLItem(client, classname, client.nodeid,
167                     anonymous=1)
168         elif client.db.classes.has_key(classname):
169             c['context'] = HTMLClass(client, classname, anonymous=1)
170         return c
172     def render(self, client, classname, request, **options):
173         """Render this Page Template"""
175         if not self._v_cooked:
176             self._cook()
178         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
180         if self._v_errors:
181             raise PageTemplate.PTRuntimeError, \
182                 'Page Template %s has errors.'%self.id
184         # figure the context
185         classname = classname or client.classname
186         request = request or HTMLRequest(client)
187         c = self.getContext(client, classname, request)
188         c.update({'options': options})
190         # and go
191         output = StringIO.StringIO()
192         TALInterpreter(self._v_program, self.macros,
193             getEngine().getContext(c), output, tal=1, strictinsert=0)()
194         return output.getvalue()
196 class HTMLDatabase:
197     ''' Return HTMLClasses for valid class fetches
198     '''
199     def __init__(self, client):
200         self._client = client
202         # we want config to be exposed
203         self.config = client.db.config
205     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
206         # check to see if we're actually accessing an item
207         m = desre.match(item)
208         if m:
209             self._client.db.getclass(m.group('cl'))
210             return HTMLItem(self._client, m.group('cl'), m.group('id'))
211         else:
212             self._client.db.getclass(item)
213             return HTMLClass(self._client, item)
215     def __getattr__(self, attr):
216         try:
217             return self[attr]
218         except KeyError:
219             raise AttributeError, attr
221     def classes(self):
222         l = self._client.db.classes.keys()
223         l.sort()
224         return [HTMLClass(self._client, cn) for cn in l]
226 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
227     cl = db.getclass(prop.classname)
228     l = []
229     for entry in ids:
230         if num_re.match(entry):
231             l.append(entry)
232         else:
233             try:
234                 l.append(cl.lookup(entry))
235             except KeyError:
236                 # ignore invalid keys
237                 pass
238     return l
240 class HTMLPermissions:
241     ''' Helpers that provide answers to commonly asked Permission questions.
242     '''
243     def is_edit_ok(self):
244         ''' Is the user allowed to Edit the current class?
245         '''
246         return self._db.security.hasPermission('Edit', self._client.userid,
247             self._classname)
248     def is_view_ok(self):
249         ''' Is the user allowed to View the current class?
250         '''
251         return self._db.security.hasPermission('View', self._client.userid,
252             self._classname)
253     def is_only_view_ok(self):
254         ''' Is the user only allowed to View (ie. not Edit) the current class?
255         '''
256         return self.is_view_ok() and not self.is_edit_ok()
258 class HTMLClass(HTMLPermissions):
259     ''' Accesses through a class (either through *class* or *db.<classname>*)
260     '''
261     def __init__(self, client, classname, anonymous=0):
262         self._client = client
263         self._db = client.db
264         self._anonymous = anonymous
266         # we want classname to be exposed, but _classname gives a
267         # consistent API for extending Class/Item
268         self._classname = self.classname = classname
269         self._klass = self._db.getclass(self.classname)
270         self._props = self._klass.getprops()
272     def __repr__(self):
273         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
275     def __getitem__(self, item):
276         ''' return an HTMLProperty instance
277         '''
278        #print 'HTMLClass.getitem', (self, item)
280         # we don't exist
281         if item == 'id':
282             return None
284         # get the property
285         prop = self._props[item]
287         # look up the correct HTMLProperty class
288         form = self._client.form
289         for klass, htmlklass in propclasses:
290             if not isinstance(prop, klass):
291                 continue
292             if form.has_key(item):
293                 if isinstance(prop, hyperdb.Multilink):
294                     value = lookupIds(self._db, prop,
295                         handleListCGIValue(form[item]))
296                 elif isinstance(prop, hyperdb.Link):
297                     value = form[item].value.strip()
298                     if value:
299                         value = lookupIds(self._db, prop, [value])[0]
300                     else:
301                         value = None
302                 else:
303                     value = form[item].value.strip() or None
304             else:
305                 if isinstance(prop, hyperdb.Multilink):
306                     value = []
307                 else:
308                     value = None
309             return htmlklass(self._client, self._classname, '', prop, item,
310                 value, self._anonymous)
312         # no good
313         raise KeyError, item
315     def __getattr__(self, attr):
316         ''' convenience access '''
317         try:
318             return self[attr]
319         except KeyError:
320             raise AttributeError, attr
322     def getItem(self, itemid, num_re=re.compile('\d+')):
323         ''' Get an item of this class by its item id.
324         '''
325         # make sure we're looking at an itemid
326         if not num_re.match(itemid):
327             itemid = self._klass.lookup(itemid)
329         if self.classname == 'user':
330             klass = HTMLUser
331         else:
332             klass = HTMLItem
334         return klass(self._client, self.classname, itemid)
336     def properties(self):
337         ''' Return HTMLProperty for all of this class' properties.
338         '''
339         l = []
340         for name, prop in self._props.items():
341             for klass, htmlklass in propclasses:
342                 if isinstance(prop, hyperdb.Multilink):
343                     value = []
344                 else:
345                     value = None
346                 if isinstance(prop, klass):
347                     l.append(htmlklass(self._client, self._classname, '',
348                         prop, name, value, self._anonymous))
349         return l
351     def list(self):
352         ''' List all items in this class.
353         '''
354         if self.classname == 'user':
355             klass = HTMLUser
356         else:
357             klass = HTMLItem
359         # get the list and sort it nicely
360         l = self._klass.list()
361         sortfunc = make_sort_function(self._db, self.classname)
362         l.sort(sortfunc)
364         l = [klass(self._client, self.classname, x) for x in l]
365         return l
367     def csv(self):
368         ''' Return the items of this class as a chunk of CSV text.
369         '''
370         # get the CSV module
371         try:
372             import csv
373         except ImportError:
374             return 'Sorry, you need the csv module to use this function.\n'\
375                 'Get it from: http://www.object-craft.com.au/projects/csv/'
377         props = self.propnames()
378         p = csv.parser()
379         s = StringIO.StringIO()
380         s.write(p.join(props) + '\n')
381         for nodeid in self._klass.list():
382             l = []
383             for name in props:
384                 value = self._klass.get(nodeid, name)
385                 if value is None:
386                     l.append('')
387                 elif isinstance(value, type([])):
388                     l.append(':'.join(map(str, value)))
389                 else:
390                     l.append(str(self._klass.get(nodeid, name)))
391             s.write(p.join(l) + '\n')
392         return s.getvalue()
394     def propnames(self):
395         ''' Return the list of the names of the properties of this class.
396         '''
397         idlessprops = self._klass.getprops(protected=0).keys()
398         idlessprops.sort()
399         return ['id'] + idlessprops
401     def filter(self, request=None):
402         ''' Return a list of items from this class, filtered and sorted
403             by the current requested filterspec/filter/sort/group args
404         '''
405         if request is not None:
406             filterspec = request.filterspec
407             sort = request.sort
408             group = request.group
409         else:
410             filterspec = {}
411             sort = (None,None)
412             group = (None,None)
413         if self.classname == 'user':
414             klass = HTMLUser
415         else:
416             klass = HTMLItem
417         l = [klass(self._client, self.classname, x)
418              for x in self._klass.filter(None, filterspec, sort, group)]
419         return l
421     def classhelp(self, properties=None, label='list', width='500',
422             height='400'):
423         ''' Pop up a javascript window with class help
425             This generates a link to a popup window which displays the 
426             properties indicated by "properties" of the class named by
427             "classname". The "properties" should be a comma-separated list
428             (eg. 'id,name,description'). Properties defaults to all the
429             properties of a class (excluding id, creator, created and
430             activity).
432             You may optionally override the label displayed, the width and
433             height. The popup window will be resizable and scrollable.
434         '''
435         if properties is None:
436             properties = self._klass.getprops(protected=0).keys()
437             properties.sort()
438             properties = ','.join(properties)
439         return '<a href="javascript:help_window(\'%s?:template=help&' \
440             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
441             self.classname, properties, width, height, label)
443     def submit(self, label="Submit New Entry"):
444         ''' Generate a submit button (and action hidden element)
445         '''
446         return '  <input type="hidden" name=":action" value="new">\n'\
447         '  <input type="submit" name="submit" value="%s">'%label
449     def history(self):
450         return 'New node - no history'
452     def renderWith(self, name, **kwargs):
453         ''' Render this class with the given template.
454         '''
455         # create a new request and override the specified args
456         req = HTMLRequest(self._client)
457         req.classname = self.classname
458         req.update(kwargs)
460         # new template, using the specified classname and request
461         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
463         # use our fabricated request
464         return pt.render(self._client, self.classname, req)
466 class HTMLItem(HTMLPermissions):
467     ''' Accesses through an *item*
468     '''
469     def __init__(self, client, classname, nodeid, anonymous=0):
470         self._client = client
471         self._db = client.db
472         self._classname = classname
473         self._nodeid = nodeid
474         self._klass = self._db.getclass(classname)
475         self._props = self._klass.getprops()
477         # do we prefix the form items with the item's identification?
478         self._anonymous = anonymous
480     def __repr__(self):
481         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
482             self._nodeid)
484     def __getitem__(self, item):
485         ''' return an HTMLProperty instance
486         '''
487         #print 'HTMLItem.getitem', (self, item)
488         if item == 'id':
489             return self._nodeid
491         # get the property
492         prop = self._props[item]
494         # get the value, handling missing values
495         value = None
496         if int(self._nodeid) > 0:
497             value = self._klass.get(self._nodeid, item, None)
498         if value is None:
499             if isinstance(self._props[item], hyperdb.Multilink):
500                 value = []
502         # look up the correct HTMLProperty class
503         for klass, htmlklass in propclasses:
504             if isinstance(prop, klass):
505                 return htmlklass(self._client, self._classname,
506                     self._nodeid, prop, item, value, self._anonymous)
508         raise KeyError, item
510     def __getattr__(self, attr):
511         ''' convenience access to properties '''
512         try:
513             return self[attr]
514         except KeyError:
515             raise AttributeError, attr
516     
517     def submit(self, label="Submit Changes"):
518         ''' Generate a submit button (and action hidden element)
519         '''
520         return '  <input type="hidden" name=":action" value="edit">\n'\
521         '  <input type="submit" name="submit" value="%s">'%label
523     def journal(self, direction='descending'):
524         ''' Return a list of HTMLJournalEntry instances.
525         '''
526         # XXX do this
527         return []
529     def history(self, direction='descending', dre=re.compile('\d+')):
530         l = ['<table class="history">'
531              '<tr><th colspan="4" class="header">',
532              _('History'),
533              '</th></tr><tr>',
534              _('<th>Date</th>'),
535              _('<th>User</th>'),
536              _('<th>Action</th>'),
537              _('<th>Args</th>'),
538             '</tr>']
539         current = {}
540         comments = {}
541         history = self._klass.history(self._nodeid)
542         history.sort()
543         timezone = self._db.getUserTimezone()
544         if direction == 'descending':
545             history.reverse()
546             for prop_n in self._props.keys():
547                 prop = self[prop_n]
548                 if isinstance(prop, HTMLProperty):
549                     current[prop_n] = prop.plain()
550                     # make link if hrefable
551                     if (self._props.has_key(prop_n) and
552                             isinstance(self._props[prop_n], hyperdb.Link)):
553                         classname = self._props[prop_n].classname
554                         if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
555                             current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
556                                 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
557  
558         for id, evt_date, user, action, args in history:
559             date_s = str(evt_date.local(timezone)).replace("."," ")
560             arg_s = ''
561             if action == 'link' and type(args) == type(()):
562                 if len(args) == 3:
563                     linkcl, linkid, key = args
564                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
565                         linkcl, linkid, key)
566                 else:
567                     arg_s = str(args)
569             elif action == 'unlink' and type(args) == type(()):
570                 if len(args) == 3:
571                     linkcl, linkid, key = args
572                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
573                         linkcl, linkid, key)
574                 else:
575                     arg_s = str(args)
577             elif type(args) == type({}):
578                 cell = []
579                 for k in args.keys():
580                     # try to get the relevant property and treat it
581                     # specially
582                     try:
583                         prop = self._props[k]
584                     except KeyError:
585                         prop = None
586                     if prop is not None:
587                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
588                                 isinstance(prop, hyperdb.Link)):
589                             # figure what the link class is
590                             classname = prop.classname
591                             try:
592                                 linkcl = self._db.getclass(classname)
593                             except KeyError:
594                                 labelprop = None
595                                 comments[classname] = _('''The linked class
596                                     %(classname)s no longer exists''')%locals()
597                             labelprop = linkcl.labelprop(1)
598                             hrefable = os.path.exists(
599                                 os.path.join(self._db.config.TEMPLATES,
600                                 classname+'.item'))
602                         if isinstance(prop, hyperdb.Multilink) and \
603                                 len(args[k]) > 0:
604                             ml = []
605                             for linkid in args[k]:
606                                 if isinstance(linkid, type(())):
607                                     sublabel = linkid[0] + ' '
608                                     linkids = linkid[1]
609                                 else:
610                                     sublabel = ''
611                                     linkids = [linkid]
612                                 subml = []
613                                 for linkid in linkids:
614                                     label = classname + linkid
615                                     # if we have a label property, try to use it
616                                     # TODO: test for node existence even when
617                                     # there's no labelprop!
618                                     try:
619                                         if labelprop is not None and \
620                                                 labelprop != 'id':
621                                             label = linkcl.get(linkid, labelprop)
622                                     except IndexError:
623                                         comments['no_link'] = _('''<strike>The
624                                             linked node no longer
625                                             exists</strike>''')
626                                         subml.append('<strike>%s</strike>'%label)
627                                     else:
628                                         if hrefable:
629                                             subml.append('<a href="%s%s">%s</a>'%(
630                                                 classname, linkid, label))
631                                         else:
632                                             subml.append(label)
633                                 ml.append(sublabel + ', '.join(subml))
634                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
635                         elif isinstance(prop, hyperdb.Link) and args[k]:
636                             label = classname + args[k]
637                             # if we have a label property, try to use it
638                             # TODO: test for node existence even when
639                             # there's no labelprop!
640                             if labelprop is not None and labelprop != 'id':
641                                 try:
642                                     label = linkcl.get(args[k], labelprop)
643                                 except IndexError:
644                                     comments['no_link'] = _('''<strike>The
645                                         linked node no longer
646                                         exists</strike>''')
647                                     cell.append(' <strike>%s</strike>,\n'%label)
648                                     # "flag" this is done .... euwww
649                                     label = None
650                             if label is not None:
651                                 if hrefable:
652                                     old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
653                                 else:
654                                     old = label;
655                                 cell.append('%s: %s' % (k,old))
656                                 if current.has_key(k):
657                                     cell[-1] += ' -> %s'%current[k]
658                                     current[k] = old
660                         elif isinstance(prop, hyperdb.Date) and args[k]:
661                             d = date.Date(args[k]).local(timezone)
662                             cell.append('%s: %s'%(k, str(d)))
663                             if current.has_key(k):
664                                 if not current[k] == '(no value)' and current[k]:
665                                     current[k] = date.Date(current[k]).local(timezone)
666                                 cell[-1] += ' -> %s' % current[k]
667                                 current[k] = str(d)
669                         elif isinstance(prop, hyperdb.Interval) and args[k]:
670                             d = date.Interval(args[k])
671                             cell.append('%s: %s'%(k, str(d)))
672                             if current.has_key(k):
673                                 cell[-1] += ' -> %s'%current[k]
674                                 current[k] = str(d)
676                         elif isinstance(prop, hyperdb.String) and args[k]:
677                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
678                             if current.has_key(k):
679                                 cell[-1] += ' -> %s'%current[k]
680                                 current[k] = cgi.escape(args[k])
682                         elif not args[k]:
683                             if current.has_key(k):
684                                 cell.append('%s: %s'%(k, current[k]))
685                                 current[k] = '(no value)'
686                             else:
687                                 cell.append('%s: (no value)'%k)
689                         else:
690                             cell.append('%s: %s'%(k, str(args[k])))
691                             if current.has_key(k):
692                                 cell[-1] += ' -> %s'%current[k]
693                                 current[k] = str(args[k])
694                     else:
695                         # property no longer exists
696                         comments['no_exist'] = _('''<em>The indicated property
697                             no longer exists</em>''')
698                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
699                 arg_s = '<br />'.join(cell)
700             else:
701                 # unkown event!!
702                 comments['unknown'] = _('''<strong><em>This event is not
703                     handled by the history display!</em></strong>''')
704                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
705             date_s = date_s.replace(' ', '&nbsp;')
706             # if the user's an itemid, figure the username (older journals
707             # have the username)
708             if dre.match(user):
709                 user = self._db.user.get(user, 'username')
710             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
711                 date_s, user, action, arg_s))
712         if comments:
713             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
714         for entry in comments.values():
715             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
716         l.append('</table>')
717         return '\n'.join(l)
719     def renderQueryForm(self):
720         ''' Render this item, which is a query, as a search form.
721         '''
722         # create a new request and override the specified args
723         req = HTMLRequest(self._client)
724         req.classname = self._klass.get(self._nodeid, 'klass')
725         name = self._klass.get(self._nodeid, 'name')
726         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
727             '&:queryname=%s'%urllib.quote(name))
729         # new template, using the specified classname and request
730         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
732         # use our fabricated request
733         return pt.render(self._client, req.classname, req)
735 class HTMLUser(HTMLItem):
736     ''' Accesses through the *user* (a special case of item)
737     '''
738     def __init__(self, client, classname, nodeid, anonymous=0):
739         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
740         self._default_classname = client.classname
742         # used for security checks
743         self._security = client.db.security
745     _marker = []
746     def hasPermission(self, permission, classname=_marker):
747         ''' Determine if the user has the Permission.
749             The class being tested defaults to the template's class, but may
750             be overidden for this test by suppling an alternate classname.
751         '''
752         if classname is self._marker:
753             classname = self._default_classname
754         return self._security.hasPermission(permission, self._nodeid, classname)
756     def is_edit_ok(self):
757         ''' Is the user allowed to Edit the current class?
758             Also check whether this is the current user's info.
759         '''
760         return self._db.security.hasPermission('Edit', self._client.userid,
761             self._classname) or self._nodeid == self._client.userid
763     def is_view_ok(self):
764         ''' Is the user allowed to View the current class?
765             Also check whether this is the current user's info.
766         '''
767         return self._db.security.hasPermission('Edit', self._client.userid,
768             self._classname) or self._nodeid == self._client.userid
770 class HTMLProperty:
771     ''' String, Number, Date, Interval HTMLProperty
773         Has useful attributes:
775          _name  the name of the property
776          _value the value of the property if any
778         A wrapper object which may be stringified for the plain() behaviour.
779     '''
780     def __init__(self, client, classname, nodeid, prop, name, value,
781             anonymous=0):
782         self._client = client
783         self._db = client.db
784         self._classname = classname
785         self._nodeid = nodeid
786         self._prop = prop
787         self._value = value
788         self._anonymous = anonymous
789         if not anonymous:
790             self._name = '%s%s@%s'%(classname, nodeid, name)
791         else:
792             self._name = name
793     def __repr__(self):
794         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name,
795             self._prop, self._value)
796     def __str__(self):
797         return self.plain()
798     def __cmp__(self, other):
799         if isinstance(other, HTMLProperty):
800             return cmp(self._value, other._value)
801         return cmp(self._value, other)
803 class StringHTMLProperty(HTMLProperty):
804     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
805                           r'(?P<email>[\w\.]+@[\w\.\-]+)|'
806                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
807     def _hyper_repl(self, match):
808         if match.group('url'):
809             s = match.group('url')
810             return '<a href="%s">%s</a>'%(s, s)
811         elif match.group('email'):
812             s = match.group('email')
813             return '<a href="mailto:%s">%s</a>'%(s, s)
814         else:
815             s = match.group('item')
816             s1 = match.group('class')
817             s2 = match.group('id')
818             try:
819                 # make sure s1 is a valid tracker classname
820                 self._db.getclass(s1)
821                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
822             except KeyError:
823                 return '%s%s'%(s1, s2)
825     def plain(self, escape=0, hyperlink=0):
826         ''' Render a "plain" representation of the property
827             
828             "escape" turns on/off HTML quoting
829             "hyperlink" turns on/off in-text hyperlinking of URLs, email
830                 addresses and designators
831         '''
832         if self._value is None:
833             return ''
834         if escape:
835             s = cgi.escape(str(self._value))
836         else:
837             s = str(self._value)
838         if hyperlink:
839             if not escape:
840                 s = cgi.escape(s)
841             s = self.hyper_re.sub(self._hyper_repl, s)
842         return s
844     def stext(self, escape=0):
845         ''' Render the value of the property as StructuredText.
847             This requires the StructureText module to be installed separately.
848         '''
849         s = self.plain(escape=escape)
850         if not StructuredText:
851             return s
852         return StructuredText(s,level=1,header=0)
854     def field(self, size = 30):
855         ''' Render a form edit field for the property
856         '''
857         if self._value is None:
858             value = ''
859         else:
860             value = cgi.escape(str(self._value))
861             value = '&quot;'.join(value.split('"'))
862         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
864     def multiline(self, escape=0, rows=5, cols=40):
865         ''' Render a multiline form edit field for the property
866         '''
867         if self._value is None:
868             value = ''
869         else:
870             value = cgi.escape(str(self._value))
871             value = '&quot;'.join(value.split('"'))
872         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
873             self._name, rows, cols, value)
875     def email(self, escape=1):
876         ''' Render the value of the property as an obscured email address
877         '''
878         if self._value is None: value = ''
879         else: value = str(self._value)
880         if value.find('@') != -1:
881             name, domain = value.split('@')
882             domain = ' '.join(domain.split('.')[:-1])
883             name = name.replace('.', ' ')
884             value = '%s at %s ...'%(name, domain)
885         else:
886             value = value.replace('.', ' ')
887         if escape:
888             value = cgi.escape(value)
889         return value
891 class PasswordHTMLProperty(HTMLProperty):
892     def plain(self):
893         ''' Render a "plain" representation of the property
894         '''
895         if self._value is None:
896             return ''
897         return _('*encrypted*')
899     def field(self, size = 30):
900         ''' Render a form edit field for the property.
901         '''
902         return '<input type="password" name="%s" size="%s">'%(self._name, size)
904     def confirm(self, size = 30):
905         ''' Render a second form edit field for the property, used for 
906             confirmation that the user typed the password correctly. Generates
907             a field with name ":confirm:name".
908         '''
909         return '<input type="password" name=":confirm:%s" size="%s">'%(
910             self._name, size)
912 class NumberHTMLProperty(HTMLProperty):
913     def plain(self):
914         ''' Render a "plain" representation of the property
915         '''
916         return str(self._value)
918     def field(self, size = 30):
919         ''' Render a form edit field for the property
920         '''
921         if self._value is None:
922             value = ''
923         else:
924             value = cgi.escape(str(self._value))
925             value = '&quot;'.join(value.split('"'))
926         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
928 class BooleanHTMLProperty(HTMLProperty):
929     def plain(self):
930         ''' Render a "plain" representation of the property
931         '''
932         if self._value is None:
933             return ''
934         return self._value and "Yes" or "No"
936     def field(self):
937         ''' Render a form edit field for the property
938         '''
939         checked = self._value and "checked" or ""
940         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
941             checked)
942         if checked:
943             checked = ""
944         else:
945             checked = "checked"
946         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
947             checked)
948         return s
950 class DateHTMLProperty(HTMLProperty):
951     def plain(self):
952         ''' Render a "plain" representation of the property
953         '''
954         if self._value is None:
955             return ''
956         return str(self._value.local(self._db.getUserTimezone()))
958     def now(self):
959         ''' Return the current time.
961             This is useful for defaulting a new value. Returns a
962             DateHTMLProperty.
963         '''
964         return DateHTMLProperty(self._client, self._nodeid, self._prop,
965             self._name, date.Date('.'))
967     def field(self, size = 30):
968         ''' Render a form edit field for the property
969         '''
970         if self._value is None:
971             value = ''
972         else:
973             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
974             value = '&quot;'.join(value.split('"'))
975         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
977     def reldate(self, pretty=1):
978         ''' Render the interval between the date and now.
980             If the "pretty" flag is true, then make the display pretty.
981         '''
982         if not self._value:
983             return ''
985         # figure the interval
986         interval = date.Date('.') - self._value
987         if pretty:
988             return interval.pretty()
989         return str(interval)
991     _marker = []
992     def pretty(self, format=_marker):
993         ''' Render the date in a pretty format (eg. month names, spaces).
995             The format string is a standard python strftime format string.
996             Note that if the day is zero, and appears at the start of the
997             string, then it'll be stripped from the output. This is handy
998             for the situatin when a date only specifies a month and a year.
999         '''
1000         if format is not self._marker:
1001             return self._value.pretty(format)
1002         else:
1003             return self._value.pretty()
1005     def local(self, offset):
1006         ''' Return the date/time as a local (timezone offset) date/time.
1007         '''
1008         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1009             self._name, self._value.local(offset))
1011 class IntervalHTMLProperty(HTMLProperty):
1012     def plain(self):
1013         ''' Render a "plain" representation of the property
1014         '''
1015         if self._value is None:
1016             return ''
1017         return str(self._value)
1019     def pretty(self):
1020         ''' Render the interval in a pretty format (eg. "yesterday")
1021         '''
1022         return self._value.pretty()
1024     def field(self, size = 30):
1025         ''' Render a form edit field for the property
1026         '''
1027         if self._value is None:
1028             value = ''
1029         else:
1030             value = cgi.escape(str(self._value))
1031             value = '&quot;'.join(value.split('"'))
1032         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
1034 class LinkHTMLProperty(HTMLProperty):
1035     ''' Link HTMLProperty
1036         Include the above as well as being able to access the class
1037         information. Stringifying the object itself results in the value
1038         from the item being displayed. Accessing attributes of this object
1039         result in the appropriate entry from the class being queried for the
1040         property accessed (so item/assignedto/name would look up the user
1041         entry identified by the assignedto property on item, and then the
1042         name property of that user)
1043     '''
1044     def __init__(self, *args, **kw):
1045         HTMLProperty.__init__(self, *args, **kw)
1046         # if we're representing a form value, then the -1 from the form really
1047         # should be a None
1048         if str(self._value) == '-1':
1049             self._value = None
1051     def __getattr__(self, attr):
1052         ''' return a new HTMLItem '''
1053        #print 'Link.getattr', (self, attr, self._value)
1054         if not self._value:
1055             raise AttributeError, "Can't access missing value"
1056         if self._prop.classname == 'user':
1057             klass = HTMLUser
1058         else:
1059             klass = HTMLItem
1060         i = klass(self._client, self._prop.classname, self._value)
1061         return getattr(i, attr)
1063     def plain(self, escape=0):
1064         ''' Render a "plain" representation of the property
1065         '''
1066         if self._value is None:
1067             return ''
1068         linkcl = self._db.classes[self._prop.classname]
1069         k = linkcl.labelprop(1)
1070         value = str(linkcl.get(self._value, k))
1071         if escape:
1072             value = cgi.escape(value)
1073         return value
1075     def field(self, showid=0, size=None):
1076         ''' Render a form edit field for the property
1077         '''
1078         linkcl = self._db.getclass(self._prop.classname)
1079         if linkcl.getprops().has_key('order'):  
1080             sort_on = 'order'  
1081         else:  
1082             sort_on = linkcl.labelprop()  
1083         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1084         # TODO: make this a field display, not a menu one!
1085         l = ['<select name="%s">'%self._name]
1086         k = linkcl.labelprop(1)
1087         if self._value is None:
1088             s = 'selected '
1089         else:
1090             s = ''
1091         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1093         # make sure we list the current value if it's retired
1094         if self._value and self._value not in options:
1095             options.insert(0, self._value)
1097         for optionid in options:
1098             # get the option value, and if it's None use an empty string
1099             option = linkcl.get(optionid, k) or ''
1101             # figure if this option is selected
1102             s = ''
1103             if optionid == self._value:
1104                 s = 'selected '
1106             # figure the label
1107             if showid:
1108                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1109             else:
1110                 lab = option
1112             # truncate if it's too long
1113             if size is not None and len(lab) > size:
1114                 lab = lab[:size-3] + '...'
1116             # and generate
1117             lab = cgi.escape(lab)
1118             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1119         l.append('</select>')
1120         return '\n'.join(l)
1122     def menu(self, size=None, height=None, showid=0, additional=[],
1123             **conditions):
1124         ''' Render a form select list for this property
1125         '''
1126         value = self._value
1128         # sort function
1129         sortfunc = make_sort_function(self._db, self._prop.classname)
1131         linkcl = self._db.getclass(self._prop.classname)
1132         l = ['<select name="%s">'%self._name]
1133         k = linkcl.labelprop(1)
1134         s = ''
1135         if value is None:
1136             s = 'selected '
1137         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1138         if linkcl.getprops().has_key('order'):  
1139             sort_on = ('+', 'order')
1140         else:  
1141             sort_on = ('+', linkcl.labelprop())
1142         options = linkcl.filter(None, conditions, sort_on, (None, None))
1144         # make sure we list the current value if it's retired
1145         if self._value and self._value not in options:
1146             options.insert(0, self._value)
1148         for optionid in options:
1149             # get the option value, and if it's None use an empty string
1150             option = linkcl.get(optionid, k) or ''
1152             # figure if this option is selected
1153             s = ''
1154             if value in [optionid, option]:
1155                 s = 'selected '
1157             # figure the label
1158             if showid:
1159                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1160             else:
1161                 lab = option
1163             # truncate if it's too long
1164             if size is not None and len(lab) > size:
1165                 lab = lab[:size-3] + '...'
1166             if additional:
1167                 m = []
1168                 for propname in additional:
1169                     m.append(linkcl.get(optionid, propname))
1170                 lab = lab + ' (%s)'%', '.join(map(str, m))
1172             # and generate
1173             lab = cgi.escape(lab)
1174             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1175         l.append('</select>')
1176         return '\n'.join(l)
1177 #    def checklist(self, ...)
1179 class MultilinkHTMLProperty(HTMLProperty):
1180     ''' Multilink HTMLProperty
1182         Also be iterable, returning a wrapper object like the Link case for
1183         each entry in the multilink.
1184     '''
1185     def __len__(self):
1186         ''' length of the multilink '''
1187         return len(self._value)
1189     def __getattr__(self, attr):
1190         ''' no extended attribute accesses make sense here '''
1191         raise AttributeError, attr
1193     def __getitem__(self, num):
1194         ''' iterate and return a new HTMLItem
1195         '''
1196        #print 'Multi.getitem', (self, num)
1197         value = self._value[num]
1198         if self._prop.classname == 'user':
1199             klass = HTMLUser
1200         else:
1201             klass = HTMLItem
1202         return klass(self._client, self._prop.classname, value)
1204     def __contains__(self, value):
1205         ''' Support the "in" operator. We have to make sure the passed-in
1206             value is a string first, not a *HTMLProperty.
1207         '''
1208         return str(value) in self._value
1210     def reverse(self):
1211         ''' return the list in reverse order
1212         '''
1213         l = self._value[:]
1214         l.reverse()
1215         if self._prop.classname == 'user':
1216             klass = HTMLUser
1217         else:
1218             klass = HTMLItem
1219         return [klass(self._client, self._prop.classname, value) for value in l]
1221     def plain(self, escape=0):
1222         ''' Render a "plain" representation of the property
1223         '''
1224         linkcl = self._db.classes[self._prop.classname]
1225         k = linkcl.labelprop(1)
1226         labels = []
1227         for v in self._value:
1228             labels.append(linkcl.get(v, k))
1229         value = ', '.join(labels)
1230         if escape:
1231             value = cgi.escape(value)
1232         return value
1234     def field(self, size=30, showid=0):
1235         ''' Render a form edit field for the property
1236         '''
1237         sortfunc = make_sort_function(self._db, self._prop.classname)
1238         linkcl = self._db.getclass(self._prop.classname)
1239         value = self._value[:]
1240         if value:
1241             value.sort(sortfunc)
1242         # map the id to the label property
1243         if not linkcl.getkey():
1244             showid=1
1245         if not showid:
1246             k = linkcl.labelprop(1)
1247             value = [linkcl.get(v, k) for v in value]
1248         value = cgi.escape(','.join(value))
1249         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1251     def menu(self, size=None, height=None, showid=0, additional=[],
1252             **conditions):
1253         ''' Render a form select list for this property
1254         '''
1255         value = self._value
1257         # sort function
1258         sortfunc = make_sort_function(self._db, self._prop.classname)
1260         linkcl = self._db.getclass(self._prop.classname)
1261         if linkcl.getprops().has_key('order'):  
1262             sort_on = ('+', 'order')
1263         else:  
1264             sort_on = ('+', linkcl.labelprop())
1265         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1266         height = height or min(len(options), 7)
1267         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1268         k = linkcl.labelprop(1)
1270         # make sure we list the current values if they're retired
1271         for val in value:
1272             if val not in options:
1273                 options.insert(0, val)
1275         for optionid in options:
1276             # get the option value, and if it's None use an empty string
1277             option = linkcl.get(optionid, k) or ''
1279             # figure if this option is selected
1280             s = ''
1281             if optionid in value or option in value:
1282                 s = 'selected '
1284             # figure the label
1285             if showid:
1286                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1287             else:
1288                 lab = option
1289             # truncate if it's too long
1290             if size is not None and len(lab) > size:
1291                 lab = lab[:size-3] + '...'
1292             if additional:
1293                 m = []
1294                 for propname in additional:
1295                     m.append(linkcl.get(optionid, propname))
1296                 lab = lab + ' (%s)'%', '.join(m)
1298             # and generate
1299             lab = cgi.escape(lab)
1300             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1301                 lab))
1302         l.append('</select>')
1303         return '\n'.join(l)
1305 # set the propclasses for HTMLItem
1306 propclasses = (
1307     (hyperdb.String, StringHTMLProperty),
1308     (hyperdb.Number, NumberHTMLProperty),
1309     (hyperdb.Boolean, BooleanHTMLProperty),
1310     (hyperdb.Date, DateHTMLProperty),
1311     (hyperdb.Interval, IntervalHTMLProperty),
1312     (hyperdb.Password, PasswordHTMLProperty),
1313     (hyperdb.Link, LinkHTMLProperty),
1314     (hyperdb.Multilink, MultilinkHTMLProperty),
1317 def make_sort_function(db, classname):
1318     '''Make a sort function for a given class
1319     '''
1320     linkcl = db.getclass(classname)
1321     if linkcl.getprops().has_key('order'):
1322         sort_on = 'order'
1323     else:
1324         sort_on = linkcl.labelprop()
1325     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1326         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1327     return sortfunc
1329 def handleListCGIValue(value):
1330     ''' Value is either a single item or a list of items. Each item has a
1331         .value that we're actually interested in.
1332     '''
1333     if isinstance(value, type([])):
1334         return [value.value for value in value]
1335     else:
1336         value = value.value.strip()
1337         if not value:
1338             return []
1339         return value.split(',')
1341 class ShowDict:
1342     ''' A convenience access to the :columns index parameters
1343     '''
1344     def __init__(self, columns):
1345         self.columns = {}
1346         for col in columns:
1347             self.columns[col] = 1
1348     def __getitem__(self, name):
1349         return self.columns.has_key(name)
1351 class HTMLRequest:
1352     ''' The *request*, holding the CGI form and environment.
1354         "form" the CGI form as a cgi.FieldStorage
1355         "env" the CGI environment variables
1356         "base" the base URL for this instance
1357         "user" a HTMLUser instance for this user
1358         "classname" the current classname (possibly None)
1359         "template" the current template (suffix, also possibly None)
1361         Index args:
1362         "columns" dictionary of the columns to display in an index page
1363         "show" a convenience access to columns - request/show/colname will
1364                be true if the columns should be displayed, false otherwise
1365         "sort" index sort column (direction, column name)
1366         "group" index grouping property (direction, column name)
1367         "filter" properties to filter the index on
1368         "filterspec" values to filter the index on
1369         "search_text" text to perform a full-text search on for an index
1371     '''
1372     def __init__(self, client):
1373         self.client = client
1375         # easier access vars
1376         self.form = client.form
1377         self.env = client.env
1378         self.base = client.base
1379         self.user = HTMLUser(client, 'user', client.userid)
1381         # store the current class name and action
1382         self.classname = client.classname
1383         self.template = client.template
1385         # the special char to use for special vars
1386         self.special_char = '@'
1388         self._post_init()
1390     def _post_init(self):
1391         ''' Set attributes based on self.form
1392         '''
1393         # extract the index display information from the form
1394         self.columns = []
1395         for name in ':columns @columns'.split():
1396             if self.form.has_key(name):
1397                 self.special_char = name[0]
1398                 self.columns = handleListCGIValue(self.form[name])
1399                 break
1400         self.show = ShowDict(self.columns)
1402         # sorting
1403         self.sort = (None, None)
1404         for name in ':sort @sort'.split():
1405             if self.form.has_key(name):
1406                 self.special_char = name[0]
1407                 sort = self.form[name].value
1408                 if sort.startswith('-'):
1409                     self.sort = ('-', sort[1:])
1410                 else:
1411                     self.sort = ('+', sort)
1412                 if self.form.has_key(self.special_char+'sortdir'):
1413                     self.sort = ('-', self.sort[1])
1415         # grouping
1416         self.group = (None, None)
1417         for name in ':group @group'.split():
1418             if self.form.has_key(name):
1419                 self.special_char = name[0]
1420                 group = self.form[name].value
1421                 if group.startswith('-'):
1422                     self.group = ('-', group[1:])
1423                 else:
1424                     self.group = ('+', group)
1425                 if self.form.has_key(self.special_char+'groupdir'):
1426                     self.group = ('-', self.group[1])
1428         # filtering
1429         self.filter = []
1430         for name in ':filter @filter'.split():
1431             if self.form.has_key(name):
1432                 self.special_char = name[0]
1433                 self.filter = handleListCGIValue(self.form[name])
1435         self.filterspec = {}
1436         db = self.client.db
1437         if self.classname is not None:
1438             props = db.getclass(self.classname).getprops()
1439             for name in self.filter:
1440                 if self.form.has_key(name):
1441                     prop = props[name]
1442                     fv = self.form[name]
1443                     if (isinstance(prop, hyperdb.Link) or
1444                             isinstance(prop, hyperdb.Multilink)):
1445                         self.filterspec[name] = lookupIds(db, prop,
1446                             handleListCGIValue(fv))
1447                     else:
1448                         self.filterspec[name] = fv.value
1450         # full-text search argument
1451         self.search_text = None
1452         for name in ':search_text @search_text'.split():
1453             if self.form.has_key(name):
1454                 self.special_char = name[0]
1455                 self.search_text = self.form[name].value
1457         # pagination - size and start index
1458         # figure batch args
1459         self.pagesize = 50
1460         for name in ':pagesize @pagesize'.split():
1461             if self.form.has_key(name):
1462                 self.special_char = name[0]
1463                 self.pagesize = int(self.form[name].value)
1465         self.startwith = 0
1466         for name in ':startwith @startwith'.split():
1467             if self.form.has_key(name):
1468                 self.special_char = name[0]
1469                 self.startwith = int(self.form[name].value)
1471     def updateFromURL(self, url):
1472         ''' Parse the URL for query args, and update my attributes using the
1473             values.
1474         ''' 
1475         self.form = {}
1476         for name, value in cgi.parse_qsl(url):
1477             if self.form.has_key(name):
1478                 if isinstance(self.form[name], type([])):
1479                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1480                 else:
1481                     self.form[name] = [self.form[name],
1482                         cgi.MiniFieldStorage(name, value)]
1483             else:
1484                 self.form[name] = cgi.MiniFieldStorage(name, value)
1485         self._post_init()
1487     def update(self, kwargs):
1488         ''' Update my attributes using the keyword args
1489         '''
1490         self.__dict__.update(kwargs)
1491         if kwargs.has_key('columns'):
1492             self.show = ShowDict(self.columns)
1494     def description(self):
1495         ''' Return a description of the request - handle for the page title.
1496         '''
1497         s = [self.client.db.config.TRACKER_NAME]
1498         if self.classname:
1499             if self.client.nodeid:
1500                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1501             else:
1502                 if self.template == 'item':
1503                     s.append('- new %s'%self.classname)
1504                 elif self.template == 'index':
1505                     s.append('- %s index'%self.classname)
1506                 else:
1507                     s.append('- %s %s'%(self.classname, self.template))
1508         else:
1509             s.append('- home')
1510         return ' '.join(s)
1512     def __str__(self):
1513         d = {}
1514         d.update(self.__dict__)
1515         f = ''
1516         for k in self.form.keys():
1517             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1518         d['form'] = f
1519         e = ''
1520         for k,v in self.env.items():
1521             e += '\n     %r=%r'%(k, v)
1522         d['env'] = e
1523         return '''
1524 form: %(form)s
1525 base: %(base)r
1526 classname: %(classname)r
1527 template: %(template)r
1528 columns: %(columns)r
1529 sort: %(sort)r
1530 group: %(group)r
1531 filter: %(filter)r
1532 search_text: %(search_text)r
1533 pagesize: %(pagesize)r
1534 startwith: %(startwith)r
1535 env: %(env)s
1536 '''%d
1538     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1539             filterspec=1):
1540         ''' return the current index args as form elements '''
1541         l = []
1542         sc = self.special_char
1543         s = '<input type="hidden" name="%s" value="%s">'
1544         if columns and self.columns:
1545             l.append(s%(sc+'columns', ','.join(self.columns)))
1546         if sort and self.sort[1] is not None:
1547             if self.sort[0] == '-':
1548                 val = '-'+self.sort[1]
1549             else:
1550                 val = self.sort[1]
1551             l.append(s%(sc+'sort', val))
1552         if group and self.group[1] is not None:
1553             if self.group[0] == '-':
1554                 val = '-'+self.group[1]
1555             else:
1556                 val = self.group[1]
1557             l.append(s%(sc+'group', val))
1558         if filter and self.filter:
1559             l.append(s%(sc+'filter', ','.join(self.filter)))
1560         if filterspec:
1561             for k,v in self.filterspec.items():
1562                 if type(v) == type([]):
1563                     l.append(s%(k, ','.join(v)))
1564                 else:
1565                     l.append(s%(k, v))
1566         if self.search_text:
1567             l.append(s%(sc+'search_text', self.search_text))
1568         l.append(s%(sc+'pagesize', self.pagesize))
1569         l.append(s%(sc+'startwith', self.startwith))
1570         return '\n'.join(l)
1572     def indexargs_url(self, url, args):
1573         ''' embed the current index args in a URL '''
1574         sc = self.special_char
1575         l = ['%s=%s'%(k,v) for k,v in args.items()]
1576         if self.columns and not args.has_key(':columns'):
1577             l.append(sc+'columns=%s'%(','.join(self.columns)))
1578         if self.sort[1] is not None and not args.has_key(':sort'):
1579             if self.sort[0] == '-':
1580                 val = '-'+self.sort[1]
1581             else:
1582                 val = self.sort[1]
1583             l.append(sc+'sort=%s'%val)
1584         if self.group[1] is not None and not args.has_key(':group'):
1585             if self.group[0] == '-':
1586                 val = '-'+self.group[1]
1587             else:
1588                 val = self.group[1]
1589             l.append(sc+'group=%s'%val)
1590         if self.filter and not args.has_key(':filter'):
1591             l.append(sc+'filter=%s'%(','.join(self.filter)))
1592         for k,v in self.filterspec.items():
1593             if not args.has_key(k):
1594                 if type(v) == type([]):
1595                     l.append('%s=%s'%(k, ','.join(v)))
1596                 else:
1597                     l.append('%s=%s'%(k, v))
1598         if self.search_text and not args.has_key(':search_text'):
1599             l.append(sc+'search_text=%s'%self.search_text)
1600         if not args.has_key(':pagesize'):
1601             l.append(sc+'pagesize=%s'%self.pagesize)
1602         if not args.has_key(':startwith'):
1603             l.append(sc+'startwith=%s'%self.startwith)
1604         return '%s?%s'%(url, '&'.join(l))
1605     indexargs_href = indexargs_url
1607     def base_javascript(self):
1608         return '''
1609 <script language="javascript">
1610 submitted = false;
1611 function submit_once() {
1612     if (submitted) {
1613         alert("Your request is being processed.\\nPlease be patient.");
1614         return 0;
1615     }
1616     submitted = true;
1617     return 1;
1620 function help_window(helpurl, width, height) {
1621     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1623 </script>
1624 '''%self.base
1626     def batch(self):
1627         ''' Return a batch object for results from the "current search"
1628         '''
1629         filterspec = self.filterspec
1630         sort = self.sort
1631         group = self.group
1633         # get the list of ids we're batching over
1634         klass = self.client.db.getclass(self.classname)
1635         if self.search_text:
1636             matches = self.client.db.indexer.search(
1637                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1638         else:
1639             matches = None
1640         l = klass.filter(matches, filterspec, sort, group)
1642         # return the batch object, using IDs only
1643         return Batch(self.client, l, self.pagesize, self.startwith,
1644             classname=self.classname)
1646 # extend the standard ZTUtils Batch object to remove dependency on
1647 # Acquisition and add a couple of useful methods
1648 class Batch(ZTUtils.Batch):
1649     ''' Use me to turn a list of items, or item ids of a given class, into a
1650         series of batches.
1652         ========= ========================================================
1653         Parameter  Usage
1654         ========= ========================================================
1655         sequence  a list of HTMLItems or item ids
1656         classname if sequence is a list of ids, this is the class of item
1657         size      how big to make the sequence.
1658         start     where to start (0-indexed) in the sequence.
1659         end       where to end (0-indexed) in the sequence.
1660         orphan    if the next batch would contain less items than this
1661                   value, then it is combined with this batch
1662         overlap   the number of items shared between adjacent batches
1663         ========= ========================================================
1665         Attributes: Note that the "start" attribute, unlike the
1666         argument, is a 1-based index (I know, lame).  "first" is the
1667         0-based index.  "length" is the actual number of elements in
1668         the batch.
1670         "sequence_length" is the length of the original, unbatched, sequence.
1671     '''
1672     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1673             overlap=0, classname=None):
1674         self.client = client
1675         self.last_index = self.last_item = None
1676         self.current_item = None
1677         self.classname = classname
1678         self.sequence_length = len(sequence)
1679         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1680             overlap)
1682     # overwrite so we can late-instantiate the HTMLItem instance
1683     def __getitem__(self, index):
1684         if index < 0:
1685             if index + self.end < self.first: raise IndexError, index
1686             return self._sequence[index + self.end]
1687         
1688         if index >= self.length:
1689             raise IndexError, index
1691         # move the last_item along - but only if the fetched index changes
1692         # (for some reason, index 0 is fetched twice)
1693         if index != self.last_index:
1694             self.last_item = self.current_item
1695             self.last_index = index
1697         item = self._sequence[index + self.first]
1698         if self.classname:
1699             # map the item ids to instances
1700             if self.classname == 'user':
1701                 item = HTMLUser(self.client, self.classname, item)
1702             else:
1703                 item = HTMLItem(self.client, self.classname, item)
1704         self.current_item = item
1705         return item
1707     def propchanged(self, property):
1708         ''' Detect if the property marked as being the group property
1709             changed in the last iteration fetch
1710         '''
1711         if (self.last_item is None or
1712                 self.last_item[property] != self.current_item[property]):
1713             return 1
1714         return 0
1716     # override these 'cos we don't have access to acquisition
1717     def previous(self):
1718         if self.start == 1:
1719             return None
1720         return Batch(self.client, self._sequence, self._size,
1721             self.first - self._size + self.overlap, 0, self.orphan,
1722             self.overlap)
1724     def next(self):
1725         try:
1726             self._sequence[self.end]
1727         except IndexError:
1728             return None
1729         return Batch(self.client, self._sequence, self._size,
1730             self.end - self.overlap, 0, self.orphan, self.overlap)
1732 class TemplatingUtils:
1733     ''' Utilities for templating
1734     '''
1735     def __init__(self, client):
1736         self.client = client
1737     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1738         return Batch(self.client, sequence, size, start, end, orphan,
1739             overlap)