Code

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