Code

9df83d2a01f5f0fe66687dde89a85090b99af805
[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
201         self._db = client.db
203         # we want config to be exposed
204         self.config = client.db.config
206     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
207         # check to see if we're actually accessing an item
208         m = desre.match(item)
209         if m:
210             self._client.db.getclass(m.group('cl'))
211             return HTMLItem(self._client, m.group('cl'), m.group('id'))
212         else:
213             self._client.db.getclass(item)
214             return HTMLClass(self._client, item)
216     def __getattr__(self, attr):
217         try:
218             return self[attr]
219         except KeyError:
220             raise AttributeError, attr
222     def classes(self):
223         l = self._client.db.classes.keys()
224         l.sort()
225         return [HTMLClass(self._client, cn) for cn in l]
227 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
228     cl = db.getclass(prop.classname)
229     l = []
230     for entry in ids:
231         if num_re.match(entry):
232             l.append(entry)
233         else:
234             try:
235                 l.append(cl.lookup(entry))
236             except KeyError:
237                 # ignore invalid keys
238                 pass
239     return l
241 class HTMLPermissions:
242     ''' Helpers that provide answers to commonly asked Permission questions.
243     '''
244     def is_edit_ok(self):
245         ''' Is the user allowed to Edit the current class?
246         '''
247         return self._db.security.hasPermission('Edit', self._client.userid,
248             self._classname)
249     def is_view_ok(self):
250         ''' Is the user allowed to View the current class?
251         '''
252         return self._db.security.hasPermission('View', self._client.userid,
253             self._classname)
254     def is_only_view_ok(self):
255         ''' Is the user only allowed to View (ie. not Edit) the current class?
256         '''
257         return self.is_view_ok() and not self.is_edit_ok()
259 class HTMLClass(HTMLPermissions):
260     ''' Accesses through a class (either through *class* or *db.<classname>*)
261     '''
262     def __init__(self, client, classname, anonymous=0):
263         self._client = client
264         self._db = client.db
265         self._anonymous = anonymous
267         # we want classname to be exposed, but _classname gives a
268         # consistent API for extending Class/Item
269         self._classname = self.classname = classname
270         self._klass = self._db.getclass(self.classname)
271         self._props = self._klass.getprops()
273     def __repr__(self):
274         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
276     def __getitem__(self, item):
277         ''' return an HTMLProperty instance
278         '''
279        #print 'HTMLClass.getitem', (self, item)
281         # we don't exist
282         if item == 'id':
283             return None
285         # get the property
286         prop = self._props[item]
288         # look up the correct HTMLProperty class
289         form = self._client.form
290         for klass, htmlklass in propclasses:
291             if not isinstance(prop, klass):
292                 continue
293             if form.has_key(item):
294                 if isinstance(prop, hyperdb.Multilink):
295                     value = lookupIds(self._db, prop,
296                         handleListCGIValue(form[item]))
297                 elif isinstance(prop, hyperdb.Link):
298                     value = form[item].value.strip()
299                     if value:
300                         value = lookupIds(self._db, prop, [value])[0]
301                     else:
302                         value = None
303                 else:
304                     value = form[item].value.strip() or None
305             else:
306                 if isinstance(prop, hyperdb.Multilink):
307                     value = []
308                 else:
309                     value = None
310             return htmlklass(self._client, self._classname, '', prop, item,
311                 value, self._anonymous)
313         # no good
314         raise KeyError, item
316     def __getattr__(self, attr):
317         ''' convenience access '''
318         try:
319             return self[attr]
320         except KeyError:
321             raise AttributeError, attr
323     def getItem(self, itemid, num_re=re.compile('\d+')):
324         ''' Get an item of this class by its item id.
325         '''
326         # make sure we're looking at an itemid
327         if not num_re.match(itemid):
328             itemid = self._klass.lookup(itemid)
330         if self.classname == 'user':
331             klass = HTMLUser
332         else:
333             klass = HTMLItem
335         return klass(self._client, self.classname, itemid)
337     def properties(self):
338         ''' Return HTMLProperty for all of this class' properties.
339         '''
340         l = []
341         for name, prop in self._props.items():
342             for klass, htmlklass in propclasses:
343                 if isinstance(prop, hyperdb.Multilink):
344                     value = []
345                 else:
346                     value = None
347                 if isinstance(prop, klass):
348                     l.append(htmlklass(self._client, self._classname, '',
349                         prop, name, value, self._anonymous))
350         return l
352     def list(self):
353         ''' List all items in this class.
354         '''
355         if self.classname == 'user':
356             klass = HTMLUser
357         else:
358             klass = HTMLItem
360         # get the list and sort it nicely
361         l = self._klass.list()
362         sortfunc = make_sort_function(self._db, self.classname)
363         l.sort(sortfunc)
365         l = [klass(self._client, self.classname, x) for x in l]
366         return l
368     def csv(self):
369         ''' Return the items of this class as a chunk of CSV text.
370         '''
371         # get the CSV module
372         try:
373             import csv
374         except ImportError:
375             return 'Sorry, you need the csv module to use this function.\n'\
376                 'Get it from: http://www.object-craft.com.au/projects/csv/'
378         props = self.propnames()
379         p = csv.parser()
380         s = StringIO.StringIO()
381         s.write(p.join(props) + '\n')
382         for nodeid in self._klass.list():
383             l = []
384             for name in props:
385                 value = self._klass.get(nodeid, name)
386                 if value is None:
387                     l.append('')
388                 elif isinstance(value, type([])):
389                     l.append(':'.join(map(str, value)))
390                 else:
391                     l.append(str(self._klass.get(nodeid, name)))
392             s.write(p.join(l) + '\n')
393         return s.getvalue()
395     def propnames(self):
396         ''' Return the list of the names of the properties of this class.
397         '''
398         idlessprops = self._klass.getprops(protected=0).keys()
399         idlessprops.sort()
400         return ['id'] + idlessprops
402     def filter(self, request=None):
403         ''' Return a list of items from this class, filtered and sorted
404             by the current requested filterspec/filter/sort/group args
405         '''
406         # XXX allow direct specification of the filterspec etc.
407         if request is not None:
408             filterspec = request.filterspec
409             sort = request.sort
410             group = request.group
411         else:
412             filterspec = {}
413             sort = (None,None)
414             group = (None,None)
415         if self.classname == 'user':
416             klass = HTMLUser
417         else:
418             klass = HTMLItem
419         l = [klass(self._client, self.classname, x)
420              for x in self._klass.filter(None, filterspec, sort, group)]
421         return l
423     def classhelp(self, properties=None, label='list', width='500',
424             height='400'):
425         ''' Pop up a javascript window with class help
427             This generates a link to a popup window which displays the 
428             properties indicated by "properties" of the class named by
429             "classname". The "properties" should be a comma-separated list
430             (eg. 'id,name,description'). Properties defaults to all the
431             properties of a class (excluding id, creator, created and
432             activity).
434             You may optionally override the label displayed, the width and
435             height. The popup window will be resizable and scrollable.
436         '''
437         if properties is None:
438             properties = self._klass.getprops(protected=0).keys()
439             properties.sort()
440             properties = ','.join(properties)
441         return '<a href="javascript:help_window(\'%s?:template=help&' \
442             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
443             self.classname, properties, width, height, label)
445     def submit(self, label="Submit New Entry"):
446         ''' Generate a submit button (and action hidden element)
447         '''
448         return '  <input type="hidden" name=":action" value="new">\n'\
449         '  <input type="submit" name="submit" value="%s">'%label
451     def history(self):
452         return 'New node - no history'
454     def renderWith(self, name, **kwargs):
455         ''' Render this class with the given template.
456         '''
457         # create a new request and override the specified args
458         req = HTMLRequest(self._client)
459         req.classname = self.classname
460         req.update(kwargs)
462         # new template, using the specified classname and request
463         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
465         # use our fabricated request
466         return pt.render(self._client, self.classname, req)
468 class HTMLItem(HTMLPermissions):
469     ''' Accesses through an *item*
470     '''
471     def __init__(self, client, classname, nodeid, anonymous=0):
472         self._client = client
473         self._db = client.db
474         self._classname = classname
475         self._nodeid = nodeid
476         self._klass = self._db.getclass(classname)
477         self._props = self._klass.getprops()
479         # do we prefix the form items with the item's identification?
480         self._anonymous = anonymous
482     def __repr__(self):
483         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
484             self._nodeid)
486     def __getitem__(self, item):
487         ''' return an HTMLProperty instance
488         '''
489         #print 'HTMLItem.getitem', (self, item)
490         if item == 'id':
491             return self._nodeid
493         # get the property
494         prop = self._props[item]
496         # get the value, handling missing values
497         value = None
498         if int(self._nodeid) > 0:
499             value = self._klass.get(self._nodeid, item, None)
500         if value is None:
501             if isinstance(self._props[item], hyperdb.Multilink):
502                 value = []
504         # look up the correct HTMLProperty class
505         for klass, htmlklass in propclasses:
506             if isinstance(prop, klass):
507                 return htmlklass(self._client, self._classname,
508                     self._nodeid, prop, item, value, self._anonymous)
510         raise KeyError, item
512     def __getattr__(self, attr):
513         ''' convenience access to properties '''
514         try:
515             return self[attr]
516         except KeyError:
517             raise AttributeError, attr
518     
519     def submit(self, label="Submit Changes"):
520         ''' Generate a submit button (and action hidden element)
521         '''
522         return '  <input type="hidden" name=":action" value="edit">\n'\
523         '  <input type="submit" name="submit" value="%s">'%label
525     def journal(self, direction='descending'):
526         ''' Return a list of HTMLJournalEntry instances.
527         '''
528         # XXX do this
529         return []
531     def history(self, direction='descending', dre=re.compile('\d+')):
532         l = ['<table class="history">'
533              '<tr><th colspan="4" class="header">',
534              _('History'),
535              '</th></tr><tr>',
536              _('<th>Date</th>'),
537              _('<th>User</th>'),
538              _('<th>Action</th>'),
539              _('<th>Args</th>'),
540             '</tr>']
541         current = {}
542         comments = {}
543         history = self._klass.history(self._nodeid)
544         history.sort()
545         timezone = self._db.getUserTimezone()
546         if direction == 'descending':
547             history.reverse()
548             for prop_n in self._props.keys():
549                 prop = self[prop_n]
550                 if isinstance(prop, HTMLProperty):
551                     current[prop_n] = prop.plain()
552                     # make link if hrefable
553                     if (self._props.has_key(prop_n) and
554                             isinstance(self._props[prop_n], hyperdb.Link)):
555                         classname = self._props[prop_n].classname
556                         if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
557                             current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
558                                 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
559  
560         for id, evt_date, user, action, args in history:
561             date_s = str(evt_date.local(timezone)).replace("."," ")
562             arg_s = ''
563             if action == 'link' and type(args) == type(()):
564                 if len(args) == 3:
565                     linkcl, linkid, key = args
566                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
567                         linkcl, linkid, key)
568                 else:
569                     arg_s = str(args)
571             elif action == 'unlink' and type(args) == type(()):
572                 if len(args) == 3:
573                     linkcl, linkid, key = args
574                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
575                         linkcl, linkid, key)
576                 else:
577                     arg_s = str(args)
579             elif type(args) == type({}):
580                 cell = []
581                 for k in args.keys():
582                     # try to get the relevant property and treat it
583                     # specially
584                     try:
585                         prop = self._props[k]
586                     except KeyError:
587                         prop = None
588                     if prop is not None:
589                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
590                                 isinstance(prop, hyperdb.Link)):
591                             # figure what the link class is
592                             classname = prop.classname
593                             try:
594                                 linkcl = self._db.getclass(classname)
595                             except KeyError:
596                                 labelprop = None
597                                 comments[classname] = _('''The linked class
598                                     %(classname)s no longer exists''')%locals()
599                             labelprop = linkcl.labelprop(1)
600                             hrefable = os.path.exists(
601                                 os.path.join(self._db.config.TEMPLATES,
602                                 classname+'.item'))
604                         if isinstance(prop, hyperdb.Multilink) and args[k]:
605                             ml = []
606                             for linkid in args[k]:
607                                 if isinstance(linkid, type(())):
608                                     sublabel = linkid[0] + ' '
609                                     linkids = linkid[1]
610                                 else:
611                                     sublabel = ''
612                                     linkids = [linkid]
613                                 subml = []
614                                 for linkid in linkids:
615                                     label = classname + linkid
616                                     # if we have a label property, try to use it
617                                     # TODO: test for node existence even when
618                                     # there's no labelprop!
619                                     try:
620                                         if labelprop is not None and \
621                                                 labelprop != 'id':
622                                             label = linkcl.get(linkid, labelprop)
623                                     except IndexError:
624                                         comments['no_link'] = _('''<strike>The
625                                             linked node no longer
626                                             exists</strike>''')
627                                         subml.append('<strike>%s</strike>'%label)
628                                     else:
629                                         if hrefable:
630                                             subml.append('<a href="%s%s">%s</a>'%(
631                                                 classname, linkid, label))
632                                         else:
633                                             subml.append(label)
634                                 ml.append(sublabel + ', '.join(subml))
635                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
636                         elif isinstance(prop, hyperdb.Link) and args[k]:
637                             label = classname + args[k]
638                             # if we have a label property, try to use it
639                             # TODO: test for node existence even when
640                             # there's no labelprop!
641                             if labelprop is not None and labelprop != 'id':
642                                 try:
643                                     label = linkcl.get(args[k], labelprop)
644                                 except IndexError:
645                                     comments['no_link'] = _('''<strike>The
646                                         linked node no longer
647                                         exists</strike>''')
648                                     cell.append(' <strike>%s</strike>,\n'%label)
649                                     # "flag" this is done .... euwww
650                                     label = None
651                             if label is not None:
652                                 if hrefable:
653                                     old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
654                                 else:
655                                     old = label;
656                                 cell.append('%s: %s' % (k,old))
657                                 if current.has_key(k):
658                                     cell[-1] += ' -> %s'%current[k]
659                                     current[k] = old
661                         elif isinstance(prop, hyperdb.Date) and args[k]:
662                             d = date.Date(args[k]).local(timezone)
663                             cell.append('%s: %s'%(k, str(d)))
664                             if current.has_key(k):
665                                 cell[-1] += ' -> %s' % current[k]
666                                 current[k] = str(d)
668                         elif isinstance(prop, hyperdb.Interval) and args[k]:
669                             d = date.Interval(args[k])
670                             cell.append('%s: %s'%(k, str(d)))
671                             if current.has_key(k):
672                                 cell[-1] += ' -> %s'%current[k]
673                                 current[k] = str(d)
675                         elif isinstance(prop, hyperdb.String) and args[k]:
676                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
677                             if current.has_key(k):
678                                 cell[-1] += ' -> %s'%current[k]
679                                 current[k] = cgi.escape(args[k])
681                         elif not args[k]:
682                             if current.has_key(k):
683                                 cell.append('%s: %s'%(k, current[k]))
684                                 current[k] = '(no value)'
685                             else:
686                                 cell.append('%s: (no value)'%k)
688                         else:
689                             cell.append('%s: %s'%(k, str(args[k])))
690                             if current.has_key(k):
691                                 cell[-1] += ' -> %s'%current[k]
692                                 current[k] = str(args[k])
693                     else:
694                         # property no longer exists
695                         comments['no_exist'] = _('''<em>The indicated property
696                             no longer exists</em>''')
697                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
698                 arg_s = '<br />'.join(cell)
699             else:
700                 # unkown event!!
701                 comments['unknown'] = _('''<strong><em>This event is not
702                     handled by the history display!</em></strong>''')
703                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
704             date_s = date_s.replace(' ', '&nbsp;')
705             # if the user's an itemid, figure the username (older journals
706             # have the username)
707             if dre.match(user):
708                 user = self._db.user.get(user, 'username')
709             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
710                 date_s, user, action, arg_s))
711         if comments:
712             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
713         for entry in comments.values():
714             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
715         l.append('</table>')
716         return '\n'.join(l)
718     def renderQueryForm(self):
719         ''' Render this item, which is a query, as a search form.
720         '''
721         # create a new request and override the specified args
722         req = HTMLRequest(self._client)
723         req.classname = self._klass.get(self._nodeid, 'klass')
724         name = self._klass.get(self._nodeid, 'name')
725         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
726             '&:queryname=%s'%urllib.quote(name))
728         # new template, using the specified classname and request
729         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
731         # use our fabricated request
732         return pt.render(self._client, req.classname, req)
734 class HTMLUser(HTMLItem):
735     ''' Accesses through the *user* (a special case of item)
736     '''
737     def __init__(self, client, classname, nodeid, anonymous=0):
738         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
739         self._default_classname = client.classname
741         # used for security checks
742         self._security = client.db.security
744     _marker = []
745     def hasPermission(self, permission, classname=_marker):
746         ''' Determine if the user has the Permission.
748             The class being tested defaults to the template's class, but may
749             be overidden for this test by suppling an alternate classname.
750         '''
751         if classname is self._marker:
752             classname = self._default_classname
753         return self._security.hasPermission(permission, self._nodeid, classname)
755     def is_edit_ok(self):
756         ''' Is the user allowed to Edit the current class?
757             Also check whether this is the current user's info.
758         '''
759         return self._db.security.hasPermission('Edit', self._client.userid,
760             self._classname) or self._nodeid == self._client.userid
762     def is_view_ok(self):
763         ''' Is the user allowed to View the current class?
764             Also check whether this is the current user's info.
765         '''
766         return self._db.security.hasPermission('Edit', self._client.userid,
767             self._classname) or self._nodeid == self._client.userid
769 class HTMLProperty:
770     ''' String, Number, Date, Interval HTMLProperty
772         Has useful attributes:
774          _name  the name of the property
775          _value the value of the property if any
777         A wrapper object which may be stringified for the plain() behaviour.
778     '''
779     def __init__(self, client, classname, nodeid, prop, name, value,
780             anonymous=0):
781         self._client = client
782         self._db = client.db
783         self._classname = classname
784         self._nodeid = nodeid
785         self._prop = prop
786         self._value = value
787         self._anonymous = anonymous
788         self._name = name
789         if not anonymous:
790             self._formname = '%s%s@%s'%(classname, nodeid, name)
791         else:
792             self._formname = name
793     def __repr__(self):
794         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
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._formname, 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._formname, 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._formname, 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._formname, 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._formname, value, size)
928     def __int__(self):
929         ''' Return an int of me
930         '''
931         return int(self._value)
933     def __float__(self):
934         ''' Return a float of me
935         '''
936         return float(self._value)
939 class BooleanHTMLProperty(HTMLProperty):
940     def plain(self):
941         ''' Render a "plain" representation of the property
942         '''
943         if self._value is None:
944             return ''
945         return self._value and "Yes" or "No"
947     def field(self):
948         ''' Render a form edit field for the property
949         '''
950         checked = self._value and "checked" or ""
951         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
952             checked)
953         if checked:
954             checked = ""
955         else:
956             checked = "checked"
957         s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
958             checked)
959         return s
961 class DateHTMLProperty(HTMLProperty):
962     def plain(self):
963         ''' Render a "plain" representation of the property
964         '''
965         if self._value is None:
966             return ''
967         return str(self._value.local(self._db.getUserTimezone()))
969     def now(self):
970         ''' Return the current time.
972             This is useful for defaulting a new value. Returns a
973             DateHTMLProperty.
974         '''
975         return DateHTMLProperty(self._client, self._nodeid, self._prop,
976             self._formname, date.Date('.'))
978     def field(self, size = 30):
979         ''' Render a form edit field for the property
980         '''
981         if self._value is None:
982             value = ''
983         else:
984             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
985             value = '&quot;'.join(value.split('"'))
986         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
988     def reldate(self, pretty=1):
989         ''' Render the interval between the date and now.
991             If the "pretty" flag is true, then make the display pretty.
992         '''
993         if not self._value:
994             return ''
996         # figure the interval
997         interval = date.Date('.') - self._value
998         if pretty:
999             return interval.pretty()
1000         return str(interval)
1002     _marker = []
1003     def pretty(self, format=_marker):
1004         ''' Render the date in a pretty format (eg. month names, spaces).
1006             The format string is a standard python strftime format string.
1007             Note that if the day is zero, and appears at the start of the
1008             string, then it'll be stripped from the output. This is handy
1009             for the situatin when a date only specifies a month and a year.
1010         '''
1011         if format is not self._marker:
1012             return self._value.pretty(format)
1013         else:
1014             return self._value.pretty()
1016     def local(self, offset):
1017         ''' Return the date/time as a local (timezone offset) date/time.
1018         '''
1019         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1020             self._formname, self._value.local(offset))
1022 class IntervalHTMLProperty(HTMLProperty):
1023     def plain(self):
1024         ''' Render a "plain" representation of the property
1025         '''
1026         if self._value is None:
1027             return ''
1028         return str(self._value)
1030     def pretty(self):
1031         ''' Render the interval in a pretty format (eg. "yesterday")
1032         '''
1033         return self._value.pretty()
1035     def field(self, size = 30):
1036         ''' Render a form edit field for the property
1037         '''
1038         if self._value is None:
1039             value = ''
1040         else:
1041             value = cgi.escape(str(self._value))
1042             value = '&quot;'.join(value.split('"'))
1043         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1045 class LinkHTMLProperty(HTMLProperty):
1046     ''' Link HTMLProperty
1047         Include the above as well as being able to access the class
1048         information. Stringifying the object itself results in the value
1049         from the item being displayed. Accessing attributes of this object
1050         result in the appropriate entry from the class being queried for the
1051         property accessed (so item/assignedto/name would look up the user
1052         entry identified by the assignedto property on item, and then the
1053         name property of that user)
1054     '''
1055     def __init__(self, *args, **kw):
1056         HTMLProperty.__init__(self, *args, **kw)
1057         # if we're representing a form value, then the -1 from the form really
1058         # should be a None
1059         if str(self._value) == '-1':
1060             self._value = None
1062     def __getattr__(self, attr):
1063         ''' return a new HTMLItem '''
1064        #print 'Link.getattr', (self, attr, self._value)
1065         if not self._value:
1066             raise AttributeError, "Can't access missing value"
1067         if self._prop.classname == 'user':
1068             klass = HTMLUser
1069         else:
1070             klass = HTMLItem
1071         i = klass(self._client, self._prop.classname, self._value)
1072         return getattr(i, attr)
1074     def plain(self, escape=0):
1075         ''' Render a "plain" representation of the property
1076         '''
1077         if self._value is None:
1078             return ''
1079         linkcl = self._db.classes[self._prop.classname]
1080         k = linkcl.labelprop(1)
1081         value = str(linkcl.get(self._value, k))
1082         if escape:
1083             value = cgi.escape(value)
1084         return value
1086     def field(self, showid=0, size=None):
1087         ''' Render a form edit field for the property
1088         '''
1089         linkcl = self._db.getclass(self._prop.classname)
1090         if linkcl.getprops().has_key('order'):  
1091             sort_on = 'order'  
1092         else:  
1093             sort_on = linkcl.labelprop()  
1094         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1095         # TODO: make this a field display, not a menu one!
1096         l = ['<select name="%s">'%self._formname]
1097         k = linkcl.labelprop(1)
1098         if self._value is None:
1099             s = 'selected '
1100         else:
1101             s = ''
1102         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1104         # make sure we list the current value if it's retired
1105         if self._value and self._value not in options:
1106             options.insert(0, self._value)
1108         for optionid in options:
1109             # get the option value, and if it's None use an empty string
1110             option = linkcl.get(optionid, k) or ''
1112             # figure if this option is selected
1113             s = ''
1114             if optionid == self._value:
1115                 s = 'selected '
1117             # figure the label
1118             if showid:
1119                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1120             else:
1121                 lab = option
1123             # truncate if it's too long
1124             if size is not None and len(lab) > size:
1125                 lab = lab[:size-3] + '...'
1127             # and generate
1128             lab = cgi.escape(lab)
1129             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1130         l.append('</select>')
1131         return '\n'.join(l)
1133     def menu(self, size=None, height=None, showid=0, additional=[],
1134             **conditions):
1135         ''' Render a form select list for this property
1136         '''
1137         value = self._value
1139         # sort function
1140         sortfunc = make_sort_function(self._db, self._prop.classname)
1142         linkcl = self._db.getclass(self._prop.classname)
1143         l = ['<select name="%s">'%self._formname]
1144         k = linkcl.labelprop(1)
1145         s = ''
1146         if value is None:
1147             s = 'selected '
1148         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1149         if linkcl.getprops().has_key('order'):  
1150             sort_on = ('+', 'order')
1151         else:  
1152             sort_on = ('+', linkcl.labelprop())
1153         options = linkcl.filter(None, conditions, sort_on, (None, None))
1155         # make sure we list the current value if it's retired
1156         if self._value and self._value not in options:
1157             options.insert(0, self._value)
1159         for optionid in options:
1160             # get the option value, and if it's None use an empty string
1161             option = linkcl.get(optionid, k) or ''
1163             # figure if this option is selected
1164             s = ''
1165             if value in [optionid, option]:
1166                 s = 'selected '
1168             # figure the label
1169             if showid:
1170                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1171             else:
1172                 lab = option
1174             # truncate if it's too long
1175             if size is not None and len(lab) > size:
1176                 lab = lab[:size-3] + '...'
1177             if additional:
1178                 m = []
1179                 for propname in additional:
1180                     m.append(linkcl.get(optionid, propname))
1181                 lab = lab + ' (%s)'%', '.join(map(str, m))
1183             # and generate
1184             lab = cgi.escape(lab)
1185             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1186         l.append('</select>')
1187         return '\n'.join(l)
1188 #    def checklist(self, ...)
1190 class MultilinkHTMLProperty(HTMLProperty):
1191     ''' Multilink HTMLProperty
1193         Also be iterable, returning a wrapper object like the Link case for
1194         each entry in the multilink.
1195     '''
1196     def __len__(self):
1197         ''' length of the multilink '''
1198         return len(self._value)
1200     def __getattr__(self, attr):
1201         ''' no extended attribute accesses make sense here '''
1202         raise AttributeError, attr
1204     def __getitem__(self, num):
1205         ''' iterate and return a new HTMLItem
1206         '''
1207        #print 'Multi.getitem', (self, num)
1208         value = self._value[num]
1209         if self._prop.classname == 'user':
1210             klass = HTMLUser
1211         else:
1212             klass = HTMLItem
1213         return klass(self._client, self._prop.classname, value)
1215     def __contains__(self, value):
1216         ''' Support the "in" operator. We have to make sure the passed-in
1217             value is a string first, not a *HTMLProperty.
1218         '''
1219         return str(value) in self._value
1221     def reverse(self):
1222         ''' return the list in reverse order
1223         '''
1224         l = self._value[:]
1225         l.reverse()
1226         if self._prop.classname == 'user':
1227             klass = HTMLUser
1228         else:
1229             klass = HTMLItem
1230         return [klass(self._client, self._prop.classname, value) for value in l]
1232     def plain(self, escape=0):
1233         ''' Render a "plain" representation of the property
1234         '''
1235         linkcl = self._db.classes[self._prop.classname]
1236         k = linkcl.labelprop(1)
1237         labels = []
1238         for v in self._value:
1239             labels.append(linkcl.get(v, k))
1240         value = ', '.join(labels)
1241         if escape:
1242             value = cgi.escape(value)
1243         return value
1245     def field(self, size=30, showid=0):
1246         ''' Render a form edit field for the property
1247         '''
1248         sortfunc = make_sort_function(self._db, self._prop.classname)
1249         linkcl = self._db.getclass(self._prop.classname)
1250         value = self._value[:]
1251         if value:
1252             value.sort(sortfunc)
1253         # map the id to the label property
1254         if not linkcl.getkey():
1255             showid=1
1256         if not showid:
1257             k = linkcl.labelprop(1)
1258             value = [linkcl.get(v, k) for v in value]
1259         value = cgi.escape(','.join(value))
1260         return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1262     def menu(self, size=None, height=None, showid=0, additional=[],
1263             **conditions):
1264         ''' Render a form select list for this property
1265         '''
1266         value = self._value
1268         # sort function
1269         sortfunc = make_sort_function(self._db, self._prop.classname)
1271         linkcl = self._db.getclass(self._prop.classname)
1272         if linkcl.getprops().has_key('order'):  
1273             sort_on = ('+', 'order')
1274         else:  
1275             sort_on = ('+', linkcl.labelprop())
1276         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1277         height = height or min(len(options), 7)
1278         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1279         k = linkcl.labelprop(1)
1281         # make sure we list the current values if they're retired
1282         for val in value:
1283             if val not in options:
1284                 options.insert(0, val)
1286         for optionid in options:
1287             # get the option value, and if it's None use an empty string
1288             option = linkcl.get(optionid, k) or ''
1290             # figure if this option is selected
1291             s = ''
1292             if optionid in value or option in value:
1293                 s = 'selected '
1295             # figure the label
1296             if showid:
1297                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1298             else:
1299                 lab = option
1300             # truncate if it's too long
1301             if size is not None and len(lab) > size:
1302                 lab = lab[:size-3] + '...'
1303             if additional:
1304                 m = []
1305                 for propname in additional:
1306                     m.append(linkcl.get(optionid, propname))
1307                 lab = lab + ' (%s)'%', '.join(m)
1309             # and generate
1310             lab = cgi.escape(lab)
1311             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1312                 lab))
1313         l.append('</select>')
1314         return '\n'.join(l)
1316 # set the propclasses for HTMLItem
1317 propclasses = (
1318     (hyperdb.String, StringHTMLProperty),
1319     (hyperdb.Number, NumberHTMLProperty),
1320     (hyperdb.Boolean, BooleanHTMLProperty),
1321     (hyperdb.Date, DateHTMLProperty),
1322     (hyperdb.Interval, IntervalHTMLProperty),
1323     (hyperdb.Password, PasswordHTMLProperty),
1324     (hyperdb.Link, LinkHTMLProperty),
1325     (hyperdb.Multilink, MultilinkHTMLProperty),
1328 def make_sort_function(db, classname):
1329     '''Make a sort function for a given class
1330     '''
1331     linkcl = db.getclass(classname)
1332     if linkcl.getprops().has_key('order'):
1333         sort_on = 'order'
1334     else:
1335         sort_on = linkcl.labelprop()
1336     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1337         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1338     return sortfunc
1340 def handleListCGIValue(value):
1341     ''' Value is either a single item or a list of items. Each item has a
1342         .value that we're actually interested in.
1343     '''
1344     if isinstance(value, type([])):
1345         return [value.value for value in value]
1346     else:
1347         value = value.value.strip()
1348         if not value:
1349             return []
1350         return value.split(',')
1352 class ShowDict:
1353     ''' A convenience access to the :columns index parameters
1354     '''
1355     def __init__(self, columns):
1356         self.columns = {}
1357         for col in columns:
1358             self.columns[col] = 1
1359     def __getitem__(self, name):
1360         return self.columns.has_key(name)
1362 class HTMLRequest:
1363     ''' The *request*, holding the CGI form and environment.
1365         "form" the CGI form as a cgi.FieldStorage
1366         "env" the CGI environment variables
1367         "base" the base URL for this instance
1368         "user" a HTMLUser instance for this user
1369         "classname" the current classname (possibly None)
1370         "template" the current template (suffix, also possibly None)
1372         Index args:
1373         "columns" dictionary of the columns to display in an index page
1374         "show" a convenience access to columns - request/show/colname will
1375                be true if the columns should be displayed, false otherwise
1376         "sort" index sort column (direction, column name)
1377         "group" index grouping property (direction, column name)
1378         "filter" properties to filter the index on
1379         "filterspec" values to filter the index on
1380         "search_text" text to perform a full-text search on for an index
1382     '''
1383     def __init__(self, client):
1384         self.client = client
1386         # easier access vars
1387         self.form = client.form
1388         self.env = client.env
1389         self.base = client.base
1390         self.user = HTMLUser(client, 'user', client.userid)
1392         # store the current class name and action
1393         self.classname = client.classname
1394         self.template = client.template
1396         # the special char to use for special vars
1397         self.special_char = '@'
1399         self._post_init()
1401     def _post_init(self):
1402         ''' Set attributes based on self.form
1403         '''
1404         # extract the index display information from the form
1405         self.columns = []
1406         for name in ':columns @columns'.split():
1407             if self.form.has_key(name):
1408                 self.special_char = name[0]
1409                 self.columns = handleListCGIValue(self.form[name])
1410                 break
1411         self.show = ShowDict(self.columns)
1413         # sorting
1414         self.sort = (None, None)
1415         for name in ':sort @sort'.split():
1416             if self.form.has_key(name):
1417                 self.special_char = name[0]
1418                 sort = self.form[name].value
1419                 if sort.startswith('-'):
1420                     self.sort = ('-', sort[1:])
1421                 else:
1422                     self.sort = ('+', sort)
1423                 if self.form.has_key(self.special_char+'sortdir'):
1424                     self.sort = ('-', self.sort[1])
1426         # grouping
1427         self.group = (None, None)
1428         for name in ':group @group'.split():
1429             if self.form.has_key(name):
1430                 self.special_char = name[0]
1431                 group = self.form[name].value
1432                 if group.startswith('-'):
1433                     self.group = ('-', group[1:])
1434                 else:
1435                     self.group = ('+', group)
1436                 if self.form.has_key(self.special_char+'groupdir'):
1437                     self.group = ('-', self.group[1])
1439         # filtering
1440         self.filter = []
1441         for name in ':filter @filter'.split():
1442             if self.form.has_key(name):
1443                 self.special_char = name[0]
1444                 self.filter = handleListCGIValue(self.form[name])
1446         self.filterspec = {}
1447         db = self.client.db
1448         if self.classname is not None:
1449             props = db.getclass(self.classname).getprops()
1450             for name in self.filter:
1451                 if self.form.has_key(name):
1452                     prop = props[name]
1453                     fv = self.form[name]
1454                     if (isinstance(prop, hyperdb.Link) or
1455                             isinstance(prop, hyperdb.Multilink)):
1456                         self.filterspec[name] = lookupIds(db, prop,
1457                             handleListCGIValue(fv))
1458                     else:
1459                         self.filterspec[name] = fv.value
1461         # full-text search argument
1462         self.search_text = None
1463         for name in ':search_text @search_text'.split():
1464             if self.form.has_key(name):
1465                 self.special_char = name[0]
1466                 self.search_text = self.form[name].value
1468         # pagination - size and start index
1469         # figure batch args
1470         self.pagesize = 50
1471         for name in ':pagesize @pagesize'.split():
1472             if self.form.has_key(name):
1473                 self.special_char = name[0]
1474                 self.pagesize = int(self.form[name].value)
1476         self.startwith = 0
1477         for name in ':startwith @startwith'.split():
1478             if self.form.has_key(name):
1479                 self.special_char = name[0]
1480                 self.startwith = int(self.form[name].value)
1482     def updateFromURL(self, url):
1483         ''' Parse the URL for query args, and update my attributes using the
1484             values.
1485         ''' 
1486         self.form = {}
1487         for name, value in cgi.parse_qsl(url):
1488             if self.form.has_key(name):
1489                 if isinstance(self.form[name], type([])):
1490                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1491                 else:
1492                     self.form[name] = [self.form[name],
1493                         cgi.MiniFieldStorage(name, value)]
1494             else:
1495                 self.form[name] = cgi.MiniFieldStorage(name, value)
1496         self._post_init()
1498     def update(self, kwargs):
1499         ''' Update my attributes using the keyword args
1500         '''
1501         self.__dict__.update(kwargs)
1502         if kwargs.has_key('columns'):
1503             self.show = ShowDict(self.columns)
1505     def description(self):
1506         ''' Return a description of the request - handle for the page title.
1507         '''
1508         s = [self.client.db.config.TRACKER_NAME]
1509         if self.classname:
1510             if self.client.nodeid:
1511                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1512             else:
1513                 if self.template == 'item':
1514                     s.append('- new %s'%self.classname)
1515                 elif self.template == 'index':
1516                     s.append('- %s index'%self.classname)
1517                 else:
1518                     s.append('- %s %s'%(self.classname, self.template))
1519         else:
1520             s.append('- home')
1521         return ' '.join(s)
1523     def __str__(self):
1524         d = {}
1525         d.update(self.__dict__)
1526         f = ''
1527         for k in self.form.keys():
1528             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1529         d['form'] = f
1530         e = ''
1531         for k,v in self.env.items():
1532             e += '\n     %r=%r'%(k, v)
1533         d['env'] = e
1534         return '''
1535 form: %(form)s
1536 base: %(base)r
1537 classname: %(classname)r
1538 template: %(template)r
1539 columns: %(columns)r
1540 sort: %(sort)r
1541 group: %(group)r
1542 filter: %(filter)r
1543 search_text: %(search_text)r
1544 pagesize: %(pagesize)r
1545 startwith: %(startwith)r
1546 env: %(env)s
1547 '''%d
1549     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1550             filterspec=1):
1551         ''' return the current index args as form elements '''
1552         l = []
1553         sc = self.special_char
1554         s = '<input type="hidden" name="%s" value="%s">'
1555         if columns and self.columns:
1556             l.append(s%(sc+'columns', ','.join(self.columns)))
1557         if sort and self.sort[1] is not None:
1558             if self.sort[0] == '-':
1559                 val = '-'+self.sort[1]
1560             else:
1561                 val = self.sort[1]
1562             l.append(s%(sc+'sort', val))
1563         if group and self.group[1] is not None:
1564             if self.group[0] == '-':
1565                 val = '-'+self.group[1]
1566             else:
1567                 val = self.group[1]
1568             l.append(s%(sc+'group', val))
1569         if filter and self.filter:
1570             l.append(s%(sc+'filter', ','.join(self.filter)))
1571         if filterspec:
1572             for k,v in self.filterspec.items():
1573                 if type(v) == type([]):
1574                     l.append(s%(k, ','.join(v)))
1575                 else:
1576                     l.append(s%(k, v))
1577         if self.search_text:
1578             l.append(s%(sc+'search_text', self.search_text))
1579         l.append(s%(sc+'pagesize', self.pagesize))
1580         l.append(s%(sc+'startwith', self.startwith))
1581         return '\n'.join(l)
1583     def indexargs_url(self, url, args):
1584         ''' Embed the current index args in a URL
1585         '''
1586         sc = self.special_char
1587         l = ['%s=%s'%(k,v) for k,v in args.items()]
1589         # pull out the special values (prefixed by @ or :)
1590         specials = {}
1591         for key in args.keys():
1592             if key[0] in '@:':
1593                 specials[key[1:]] = args[key]
1595         # ok, now handle the specials we received in the request
1596         if self.columns and not specials.has_key('columns'):
1597             l.append(sc+'columns=%s'%(','.join(self.columns)))
1598         if self.sort[1] is not None and not specials.has_key('sort'):
1599             if self.sort[0] == '-':
1600                 val = '-'+self.sort[1]
1601             else:
1602                 val = self.sort[1]
1603             l.append(sc+'sort=%s'%val)
1604         if self.group[1] is not None and not specials.has_key('group'):
1605             if self.group[0] == '-':
1606                 val = '-'+self.group[1]
1607             else:
1608                 val = self.group[1]
1609             l.append(sc+'group=%s'%val)
1610         if self.filter and not specials.has_key('filter'):
1611             l.append(sc+'filter=%s'%(','.join(self.filter)))
1612         if self.search_text and not specials.has_key('search_text'):
1613             l.append(sc+'search_text=%s'%self.search_text)
1614         if not specials.has_key('pagesize'):
1615             l.append(sc+'pagesize=%s'%self.pagesize)
1616         if not specials.has_key('startwith'):
1617             l.append(sc+'startwith=%s'%self.startwith)
1619         # finally, the remainder of the filter args in the request
1620         for k,v in self.filterspec.items():
1621             if not args.has_key(k):
1622                 if type(v) == type([]):
1623                     l.append('%s=%s'%(k, ','.join(v)))
1624                 else:
1625                     l.append('%s=%s'%(k, v))
1626         return '%s?%s'%(url, '&'.join(l))
1627     indexargs_href = indexargs_url
1629     def base_javascript(self):
1630         return '''
1631 <script language="javascript">
1632 submitted = false;
1633 function submit_once() {
1634     if (submitted) {
1635         alert("Your request is being processed.\\nPlease be patient.");
1636         return 0;
1637     }
1638     submitted = true;
1639     return 1;
1642 function help_window(helpurl, width, height) {
1643     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1645 </script>
1646 '''%self.base
1648     def batch(self):
1649         ''' Return a batch object for results from the "current search"
1650         '''
1651         filterspec = self.filterspec
1652         sort = self.sort
1653         group = self.group
1655         # get the list of ids we're batching over
1656         klass = self.client.db.getclass(self.classname)
1657         if self.search_text:
1658             matches = self.client.db.indexer.search(
1659                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1660         else:
1661             matches = None
1662         l = klass.filter(matches, filterspec, sort, group)
1664         # return the batch object, using IDs only
1665         return Batch(self.client, l, self.pagesize, self.startwith,
1666             classname=self.classname)
1668 # extend the standard ZTUtils Batch object to remove dependency on
1669 # Acquisition and add a couple of useful methods
1670 class Batch(ZTUtils.Batch):
1671     ''' Use me to turn a list of items, or item ids of a given class, into a
1672         series of batches.
1674         ========= ========================================================
1675         Parameter  Usage
1676         ========= ========================================================
1677         sequence  a list of HTMLItems or item ids
1678         classname if sequence is a list of ids, this is the class of item
1679         size      how big to make the sequence.
1680         start     where to start (0-indexed) in the sequence.
1681         end       where to end (0-indexed) in the sequence.
1682         orphan    if the next batch would contain less items than this
1683                   value, then it is combined with this batch
1684         overlap   the number of items shared between adjacent batches
1685         ========= ========================================================
1687         Attributes: Note that the "start" attribute, unlike the
1688         argument, is a 1-based index (I know, lame).  "first" is the
1689         0-based index.  "length" is the actual number of elements in
1690         the batch.
1692         "sequence_length" is the length of the original, unbatched, sequence.
1693     '''
1694     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1695             overlap=0, classname=None):
1696         self.client = client
1697         self.last_index = self.last_item = None
1698         self.current_item = None
1699         self.classname = classname
1700         self.sequence_length = len(sequence)
1701         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1702             overlap)
1704     # overwrite so we can late-instantiate the HTMLItem instance
1705     def __getitem__(self, index):
1706         if index < 0:
1707             if index + self.end < self.first: raise IndexError, index
1708             return self._sequence[index + self.end]
1709         
1710         if index >= self.length:
1711             raise IndexError, index
1713         # move the last_item along - but only if the fetched index changes
1714         # (for some reason, index 0 is fetched twice)
1715         if index != self.last_index:
1716             self.last_item = self.current_item
1717             self.last_index = index
1719         item = self._sequence[index + self.first]
1720         if self.classname:
1721             # map the item ids to instances
1722             if self.classname == 'user':
1723                 item = HTMLUser(self.client, self.classname, item)
1724             else:
1725                 item = HTMLItem(self.client, self.classname, item)
1726         self.current_item = item
1727         return item
1729     def propchanged(self, property):
1730         ''' Detect if the property marked as being the group property
1731             changed in the last iteration fetch
1732         '''
1733         if (self.last_item is None or
1734                 self.last_item[property] != self.current_item[property]):
1735             return 1
1736         return 0
1738     # override these 'cos we don't have access to acquisition
1739     def previous(self):
1740         if self.start == 1:
1741             return None
1742         return Batch(self.client, self._sequence, self._size,
1743             self.first - self._size + self.overlap, 0, self.orphan,
1744             self.overlap)
1746     def next(self):
1747         try:
1748             self._sequence[self.end]
1749         except IndexError:
1750             return None
1751         return Batch(self.client, self._sequence, self._size,
1752             self.end - self.overlap, 0, self.orphan, self.overlap)
1754 class TemplatingUtils:
1755     ''' Utilities for templating
1756     '''
1757     def __init__(self, client):
1758         self.client = client
1759     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1760         return Batch(self.client, sequence, size, start, end, orphan,
1761             overlap)