Code

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