Code

add access to real db through HTML db
[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         if request is not None:
407             filterspec = request.filterspec
408             sort = request.sort
409             group = request.group
410         else:
411             filterspec = {}
412             sort = (None,None)
413             group = (None,None)
414         if self.classname == 'user':
415             klass = HTMLUser
416         else:
417             klass = HTMLItem
418         l = [klass(self._client, self.classname, x)
419              for x in self._klass.filter(None, filterspec, sort, group)]
420         return l
422     def classhelp(self, properties=None, label='list', width='500',
423             height='400'):
424         ''' Pop up a javascript window with class help
426             This generates a link to a popup window which displays the 
427             properties indicated by "properties" of the class named by
428             "classname". The "properties" should be a comma-separated list
429             (eg. 'id,name,description'). Properties defaults to all the
430             properties of a class (excluding id, creator, created and
431             activity).
433             You may optionally override the label displayed, the width and
434             height. The popup window will be resizable and scrollable.
435         '''
436         if properties is None:
437             properties = self._klass.getprops(protected=0).keys()
438             properties.sort()
439             properties = ','.join(properties)
440         return '<a href="javascript:help_window(\'%s?:template=help&' \
441             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
442             self.classname, properties, width, height, label)
444     def submit(self, label="Submit New Entry"):
445         ''' Generate a submit button (and action hidden element)
446         '''
447         return '  <input type="hidden" name=":action" value="new">\n'\
448         '  <input type="submit" name="submit" value="%s">'%label
450     def history(self):
451         return 'New node - no history'
453     def renderWith(self, name, **kwargs):
454         ''' Render this class with the given template.
455         '''
456         # create a new request and override the specified args
457         req = HTMLRequest(self._client)
458         req.classname = self.classname
459         req.update(kwargs)
461         # new template, using the specified classname and request
462         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
464         # use our fabricated request
465         return pt.render(self._client, self.classname, req)
467 class HTMLItem(HTMLPermissions):
468     ''' Accesses through an *item*
469     '''
470     def __init__(self, client, classname, nodeid, anonymous=0):
471         self._client = client
472         self._db = client.db
473         self._classname = classname
474         self._nodeid = nodeid
475         self._klass = self._db.getclass(classname)
476         self._props = self._klass.getprops()
478         # do we prefix the form items with the item's identification?
479         self._anonymous = anonymous
481     def __repr__(self):
482         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
483             self._nodeid)
485     def __getitem__(self, item):
486         ''' return an HTMLProperty instance
487         '''
488         #print 'HTMLItem.getitem', (self, item)
489         if item == 'id':
490             return self._nodeid
492         # get the property
493         prop = self._props[item]
495         # get the value, handling missing values
496         value = None
497         if int(self._nodeid) > 0:
498             value = self._klass.get(self._nodeid, item, None)
499         if value is None:
500             if isinstance(self._props[item], hyperdb.Multilink):
501                 value = []
503         # look up the correct HTMLProperty class
504         for klass, htmlklass in propclasses:
505             if isinstance(prop, klass):
506                 return htmlklass(self._client, self._classname,
507                     self._nodeid, prop, item, value, self._anonymous)
509         raise KeyError, item
511     def __getattr__(self, attr):
512         ''' convenience access to properties '''
513         try:
514             return self[attr]
515         except KeyError:
516             raise AttributeError, attr
517     
518     def submit(self, label="Submit Changes"):
519         ''' Generate a submit button (and action hidden element)
520         '''
521         return '  <input type="hidden" name=":action" value="edit">\n'\
522         '  <input type="submit" name="submit" value="%s">'%label
524     def journal(self, direction='descending'):
525         ''' Return a list of HTMLJournalEntry instances.
526         '''
527         # XXX do this
528         return []
530     def history(self, direction='descending', dre=re.compile('\d+')):
531         l = ['<table class="history">'
532              '<tr><th colspan="4" class="header">',
533              _('History'),
534              '</th></tr><tr>',
535              _('<th>Date</th>'),
536              _('<th>User</th>'),
537              _('<th>Action</th>'),
538              _('<th>Args</th>'),
539             '</tr>']
540         current = {}
541         comments = {}
542         history = self._klass.history(self._nodeid)
543         history.sort()
544         timezone = self._db.getUserTimezone()
545         if direction == 'descending':
546             history.reverse()
547             for prop_n in self._props.keys():
548                 prop = self[prop_n]
549                 if isinstance(prop, HTMLProperty):
550                     current[prop_n] = prop.plain()
551                     # make link if hrefable
552                     if (self._props.has_key(prop_n) and
553                             isinstance(self._props[prop_n], hyperdb.Link)):
554                         classname = self._props[prop_n].classname
555                         if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
556                             current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
557                                 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
558  
559         for id, evt_date, user, action, args in history:
560             date_s = str(evt_date.local(timezone)).replace("."," ")
561             arg_s = ''
562             if action == 'link' and type(args) == type(()):
563                 if len(args) == 3:
564                     linkcl, linkid, key = args
565                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
566                         linkcl, linkid, key)
567                 else:
568                     arg_s = str(args)
570             elif action == 'unlink' and type(args) == type(()):
571                 if len(args) == 3:
572                     linkcl, linkid, key = args
573                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
574                         linkcl, linkid, key)
575                 else:
576                     arg_s = str(args)
578             elif type(args) == type({}):
579                 cell = []
580                 for k in args.keys():
581                     # try to get the relevant property and treat it
582                     # specially
583                     try:
584                         prop = self._props[k]
585                     except KeyError:
586                         prop = None
587                     if prop is not None:
588                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
589                                 isinstance(prop, hyperdb.Link)):
590                             # figure what the link class is
591                             classname = prop.classname
592                             try:
593                                 linkcl = self._db.getclass(classname)
594                             except KeyError:
595                                 labelprop = None
596                                 comments[classname] = _('''The linked class
597                                     %(classname)s no longer exists''')%locals()
598                             labelprop = linkcl.labelprop(1)
599                             hrefable = os.path.exists(
600                                 os.path.join(self._db.config.TEMPLATES,
601                                 classname+'.item'))
603                         if isinstance(prop, hyperdb.Multilink) and args[k]:
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                                 cell[-1] += ' -> %s' % current[k]
665                                 current[k] = str(d)
667                         elif isinstance(prop, hyperdb.Interval) and args[k]:
668                             d = date.Interval(args[k])
669                             cell.append('%s: %s'%(k, str(d)))
670                             if current.has_key(k):
671                                 cell[-1] += ' -> %s'%current[k]
672                                 current[k] = str(d)
674                         elif isinstance(prop, hyperdb.String) and args[k]:
675                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
676                             if current.has_key(k):
677                                 cell[-1] += ' -> %s'%current[k]
678                                 current[k] = cgi.escape(args[k])
680                         elif not args[k]:
681                             if current.has_key(k):
682                                 cell.append('%s: %s'%(k, current[k]))
683                                 current[k] = '(no value)'
684                             else:
685                                 cell.append('%s: (no value)'%k)
687                         else:
688                             cell.append('%s: %s'%(k, str(args[k])))
689                             if current.has_key(k):
690                                 cell[-1] += ' -> %s'%current[k]
691                                 current[k] = str(args[k])
692                     else:
693                         # property no longer exists
694                         comments['no_exist'] = _('''<em>The indicated property
695                             no longer exists</em>''')
696                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
697                 arg_s = '<br />'.join(cell)
698             else:
699                 # unkown event!!
700                 comments['unknown'] = _('''<strong><em>This event is not
701                     handled by the history display!</em></strong>''')
702                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
703             date_s = date_s.replace(' ', '&nbsp;')
704             # if the user's an itemid, figure the username (older journals
705             # have the username)
706             if dre.match(user):
707                 user = self._db.user.get(user, 'username')
708             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
709                 date_s, user, action, arg_s))
710         if comments:
711             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
712         for entry in comments.values():
713             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
714         l.append('</table>')
715         return '\n'.join(l)
717     def renderQueryForm(self):
718         ''' Render this item, which is a query, as a search form.
719         '''
720         # create a new request and override the specified args
721         req = HTMLRequest(self._client)
722         req.classname = self._klass.get(self._nodeid, 'klass')
723         name = self._klass.get(self._nodeid, 'name')
724         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
725             '&:queryname=%s'%urllib.quote(name))
727         # new template, using the specified classname and request
728         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
730         # use our fabricated request
731         return pt.render(self._client, req.classname, req)
733 class HTMLUser(HTMLItem):
734     ''' Accesses through the *user* (a special case of item)
735     '''
736     def __init__(self, client, classname, nodeid, anonymous=0):
737         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
738         self._default_classname = client.classname
740         # used for security checks
741         self._security = client.db.security
743     _marker = []
744     def hasPermission(self, permission, classname=_marker):
745         ''' Determine if the user has the Permission.
747             The class being tested defaults to the template's class, but may
748             be overidden for this test by suppling an alternate classname.
749         '''
750         if classname is self._marker:
751             classname = self._default_classname
752         return self._security.hasPermission(permission, self._nodeid, classname)
754     def is_edit_ok(self):
755         ''' Is the user allowed to Edit the current class?
756             Also check whether this is the current user's info.
757         '''
758         return self._db.security.hasPermission('Edit', self._client.userid,
759             self._classname) or self._nodeid == self._client.userid
761     def is_view_ok(self):
762         ''' Is the user allowed to View the current class?
763             Also check whether this is the current user's info.
764         '''
765         return self._db.security.hasPermission('Edit', self._client.userid,
766             self._classname) or self._nodeid == self._client.userid
768 class HTMLProperty:
769     ''' String, Number, Date, Interval HTMLProperty
771         Has useful attributes:
773          _name  the name of the property
774          _value the value of the property if any
776         A wrapper object which may be stringified for the plain() behaviour.
777     '''
778     def __init__(self, client, classname, nodeid, prop, name, value,
779             anonymous=0):
780         self._client = client
781         self._db = client.db
782         self._classname = classname
783         self._nodeid = nodeid
784         self._prop = prop
785         self._value = value
786         self._anonymous = anonymous
787         self._name = name
788         if not anonymous:
789             self._formname = '%s%s@%s'%(classname, nodeid, name)
790         else:
791             self._formname = name
792     def __repr__(self):
793         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
794             self._prop, self._value)
795     def __str__(self):
796         return self.plain()
797     def __cmp__(self, other):
798         if isinstance(other, HTMLProperty):
799             return cmp(self._value, other._value)
800         return cmp(self._value, other)
802 class StringHTMLProperty(HTMLProperty):
803     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
804                           r'(?P<email>[\w\.]+@[\w\.\-]+)|'
805                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
806     def _hyper_repl(self, match):
807         if match.group('url'):
808             s = match.group('url')
809             return '<a href="%s">%s</a>'%(s, s)
810         elif match.group('email'):
811             s = match.group('email')
812             return '<a href="mailto:%s">%s</a>'%(s, s)
813         else:
814             s = match.group('item')
815             s1 = match.group('class')
816             s2 = match.group('id')
817             try:
818                 # make sure s1 is a valid tracker classname
819                 self._db.getclass(s1)
820                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
821             except KeyError:
822                 return '%s%s'%(s1, s2)
824     def plain(self, escape=0, hyperlink=0):
825         ''' Render a "plain" representation of the property
826             
827             "escape" turns on/off HTML quoting
828             "hyperlink" turns on/off in-text hyperlinking of URLs, email
829                 addresses and designators
830         '''
831         if self._value is None:
832             return ''
833         if escape:
834             s = cgi.escape(str(self._value))
835         else:
836             s = str(self._value)
837         if hyperlink:
838             if not escape:
839                 s = cgi.escape(s)
840             s = self.hyper_re.sub(self._hyper_repl, s)
841         return s
843     def stext(self, escape=0):
844         ''' Render the value of the property as StructuredText.
846             This requires the StructureText module to be installed separately.
847         '''
848         s = self.plain(escape=escape)
849         if not StructuredText:
850             return s
851         return StructuredText(s,level=1,header=0)
853     def field(self, size = 30):
854         ''' Render a form edit field for the property
855         '''
856         if self._value is None:
857             value = ''
858         else:
859             value = cgi.escape(str(self._value))
860             value = '&quot;'.join(value.split('"'))
861         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
863     def multiline(self, escape=0, rows=5, cols=40):
864         ''' Render a multiline form edit field for the property
865         '''
866         if self._value is None:
867             value = ''
868         else:
869             value = cgi.escape(str(self._value))
870             value = '&quot;'.join(value.split('"'))
871         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
872             self._formname, rows, cols, value)
874     def email(self, escape=1):
875         ''' Render the value of the property as an obscured email address
876         '''
877         if self._value is None: value = ''
878         else: value = str(self._value)
879         if value.find('@') != -1:
880             name, domain = value.split('@')
881             domain = ' '.join(domain.split('.')[:-1])
882             name = name.replace('.', ' ')
883             value = '%s at %s ...'%(name, domain)
884         else:
885             value = value.replace('.', ' ')
886         if escape:
887             value = cgi.escape(value)
888         return value
890 class PasswordHTMLProperty(HTMLProperty):
891     def plain(self):
892         ''' Render a "plain" representation of the property
893         '''
894         if self._value is None:
895             return ''
896         return _('*encrypted*')
898     def field(self, size = 30):
899         ''' Render a form edit field for the property.
900         '''
901         return '<input type="password" name="%s" size="%s">'%(self._formname, size)
903     def confirm(self, size = 30):
904         ''' Render a second form edit field for the property, used for 
905             confirmation that the user typed the password correctly. Generates
906             a field with name ":confirm:name".
907         '''
908         return '<input type="password" name=":confirm:%s" size="%s">'%(
909             self._formname, size)
911 class NumberHTMLProperty(HTMLProperty):
912     def plain(self):
913         ''' Render a "plain" representation of the property
914         '''
915         return str(self._value)
917     def field(self, size = 30):
918         ''' Render a form edit field for the property
919         '''
920         if self._value is None:
921             value = ''
922         else:
923             value = cgi.escape(str(self._value))
924             value = '&quot;'.join(value.split('"'))
925         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
927 class BooleanHTMLProperty(HTMLProperty):
928     def plain(self):
929         ''' Render a "plain" representation of the property
930         '''
931         if self._value is None:
932             return ''
933         return self._value and "Yes" or "No"
935     def field(self):
936         ''' Render a form edit field for the property
937         '''
938         checked = self._value and "checked" or ""
939         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
940             checked)
941         if checked:
942             checked = ""
943         else:
944             checked = "checked"
945         s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
946             checked)
947         return s
949 class DateHTMLProperty(HTMLProperty):
950     def plain(self):
951         ''' Render a "plain" representation of the property
952         '''
953         if self._value is None:
954             return ''
955         return str(self._value.local(self._db.getUserTimezone()))
957     def now(self):
958         ''' Return the current time.
960             This is useful for defaulting a new value. Returns a
961             DateHTMLProperty.
962         '''
963         return DateHTMLProperty(self._client, self._nodeid, self._prop,
964             self._formname, date.Date('.'))
966     def field(self, size = 30):
967         ''' Render a form edit field for the property
968         '''
969         if self._value is None:
970             value = ''
971         else:
972             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
973             value = '&quot;'.join(value.split('"'))
974         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
976     def reldate(self, pretty=1):
977         ''' Render the interval between the date and now.
979             If the "pretty" flag is true, then make the display pretty.
980         '''
981         if not self._value:
982             return ''
984         # figure the interval
985         interval = date.Date('.') - self._value
986         if pretty:
987             return interval.pretty()
988         return str(interval)
990     _marker = []
991     def pretty(self, format=_marker):
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         if format is not self._marker:
1000             return self._value.pretty(format)
1001         else:
1002             return self._value.pretty()
1004     def local(self, offset):
1005         ''' Return the date/time as a local (timezone offset) date/time.
1006         '''
1007         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1008             self._formname, self._value.local(offset))
1010 class IntervalHTMLProperty(HTMLProperty):
1011     def plain(self):
1012         ''' Render a "plain" representation of the property
1013         '''
1014         if self._value is None:
1015             return ''
1016         return str(self._value)
1018     def pretty(self):
1019         ''' Render the interval in a pretty format (eg. "yesterday")
1020         '''
1021         return self._value.pretty()
1023     def field(self, size = 30):
1024         ''' Render a form edit field for the property
1025         '''
1026         if self._value is None:
1027             value = ''
1028         else:
1029             value = cgi.escape(str(self._value))
1030             value = '&quot;'.join(value.split('"'))
1031         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1033 class LinkHTMLProperty(HTMLProperty):
1034     ''' Link HTMLProperty
1035         Include the above as well as being able to access the class
1036         information. Stringifying the object itself results in the value
1037         from the item being displayed. Accessing attributes of this object
1038         result in the appropriate entry from the class being queried for the
1039         property accessed (so item/assignedto/name would look up the user
1040         entry identified by the assignedto property on item, and then the
1041         name property of that user)
1042     '''
1043     def __init__(self, *args, **kw):
1044         HTMLProperty.__init__(self, *args, **kw)
1045         # if we're representing a form value, then the -1 from the form really
1046         # should be a None
1047         if str(self._value) == '-1':
1048             self._value = None
1050     def __getattr__(self, attr):
1051         ''' return a new HTMLItem '''
1052        #print 'Link.getattr', (self, attr, self._value)
1053         if not self._value:
1054             raise AttributeError, "Can't access missing value"
1055         if self._prop.classname == 'user':
1056             klass = HTMLUser
1057         else:
1058             klass = HTMLItem
1059         i = klass(self._client, self._prop.classname, self._value)
1060         return getattr(i, attr)
1062     def plain(self, escape=0):
1063         ''' Render a "plain" representation of the property
1064         '''
1065         if self._value is None:
1066             return ''
1067         linkcl = self._db.classes[self._prop.classname]
1068         k = linkcl.labelprop(1)
1069         value = str(linkcl.get(self._value, k))
1070         if escape:
1071             value = cgi.escape(value)
1072         return value
1074     def field(self, showid=0, size=None):
1075         ''' Render a form edit field for the property
1076         '''
1077         linkcl = self._db.getclass(self._prop.classname)
1078         if linkcl.getprops().has_key('order'):  
1079             sort_on = 'order'  
1080         else:  
1081             sort_on = linkcl.labelprop()  
1082         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1083         # TODO: make this a field display, not a menu one!
1084         l = ['<select name="%s">'%self._formname]
1085         k = linkcl.labelprop(1)
1086         if self._value is None:
1087             s = 'selected '
1088         else:
1089             s = ''
1090         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1092         # make sure we list the current value if it's retired
1093         if self._value and self._value not in options:
1094             options.insert(0, self._value)
1096         for optionid in options:
1097             # get the option value, and if it's None use an empty string
1098             option = linkcl.get(optionid, k) or ''
1100             # figure if this option is selected
1101             s = ''
1102             if optionid == self._value:
1103                 s = 'selected '
1105             # figure the label
1106             if showid:
1107                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1108             else:
1109                 lab = option
1111             # truncate if it's too long
1112             if size is not None and len(lab) > size:
1113                 lab = lab[:size-3] + '...'
1115             # and generate
1116             lab = cgi.escape(lab)
1117             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1118         l.append('</select>')
1119         return '\n'.join(l)
1121     def menu(self, size=None, height=None, showid=0, additional=[],
1122             **conditions):
1123         ''' Render a form select list for this property
1124         '''
1125         value = self._value
1127         # sort function
1128         sortfunc = make_sort_function(self._db, self._prop.classname)
1130         linkcl = self._db.getclass(self._prop.classname)
1131         l = ['<select name="%s">'%self._formname]
1132         k = linkcl.labelprop(1)
1133         s = ''
1134         if value is None:
1135             s = 'selected '
1136         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1137         if linkcl.getprops().has_key('order'):  
1138             sort_on = ('+', 'order')
1139         else:  
1140             sort_on = ('+', linkcl.labelprop())
1141         options = linkcl.filter(None, conditions, sort_on, (None, None))
1143         # make sure we list the current value if it's retired
1144         if self._value and self._value not in options:
1145             options.insert(0, self._value)
1147         for optionid in options:
1148             # get the option value, and if it's None use an empty string
1149             option = linkcl.get(optionid, k) or ''
1151             # figure if this option is selected
1152             s = ''
1153             if value in [optionid, option]:
1154                 s = 'selected '
1156             # figure the label
1157             if showid:
1158                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1159             else:
1160                 lab = option
1162             # truncate if it's too long
1163             if size is not None and len(lab) > size:
1164                 lab = lab[:size-3] + '...'
1165             if additional:
1166                 m = []
1167                 for propname in additional:
1168                     m.append(linkcl.get(optionid, propname))
1169                 lab = lab + ' (%s)'%', '.join(map(str, m))
1171             # and generate
1172             lab = cgi.escape(lab)
1173             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1174         l.append('</select>')
1175         return '\n'.join(l)
1176 #    def checklist(self, ...)
1178 class MultilinkHTMLProperty(HTMLProperty):
1179     ''' Multilink HTMLProperty
1181         Also be iterable, returning a wrapper object like the Link case for
1182         each entry in the multilink.
1183     '''
1184     def __len__(self):
1185         ''' length of the multilink '''
1186         return len(self._value)
1188     def __getattr__(self, attr):
1189         ''' no extended attribute accesses make sense here '''
1190         raise AttributeError, attr
1192     def __getitem__(self, num):
1193         ''' iterate and return a new HTMLItem
1194         '''
1195        #print 'Multi.getitem', (self, num)
1196         value = self._value[num]
1197         if self._prop.classname == 'user':
1198             klass = HTMLUser
1199         else:
1200             klass = HTMLItem
1201         return klass(self._client, self._prop.classname, value)
1203     def __contains__(self, value):
1204         ''' Support the "in" operator. We have to make sure the passed-in
1205             value is a string first, not a *HTMLProperty.
1206         '''
1207         return str(value) in self._value
1209     def reverse(self):
1210         ''' return the list in reverse order
1211         '''
1212         l = self._value[:]
1213         l.reverse()
1214         if self._prop.classname == 'user':
1215             klass = HTMLUser
1216         else:
1217             klass = HTMLItem
1218         return [klass(self._client, self._prop.classname, value) for value in l]
1220     def plain(self, escape=0):
1221         ''' Render a "plain" representation of the property
1222         '''
1223         linkcl = self._db.classes[self._prop.classname]
1224         k = linkcl.labelprop(1)
1225         labels = []
1226         for v in self._value:
1227             labels.append(linkcl.get(v, k))
1228         value = ', '.join(labels)
1229         if escape:
1230             value = cgi.escape(value)
1231         return value
1233     def field(self, size=30, showid=0):
1234         ''' Render a form edit field for the property
1235         '''
1236         sortfunc = make_sort_function(self._db, self._prop.classname)
1237         linkcl = self._db.getclass(self._prop.classname)
1238         value = self._value[:]
1239         if value:
1240             value.sort(sortfunc)
1241         # map the id to the label property
1242         if not linkcl.getkey():
1243             showid=1
1244         if not showid:
1245             k = linkcl.labelprop(1)
1246             value = [linkcl.get(v, k) for v in value]
1247         value = cgi.escape(','.join(value))
1248         return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1250     def menu(self, size=None, height=None, showid=0, additional=[],
1251             **conditions):
1252         ''' Render a form select list for this property
1253         '''
1254         value = self._value
1256         # sort function
1257         sortfunc = make_sort_function(self._db, self._prop.classname)
1259         linkcl = self._db.getclass(self._prop.classname)
1260         if linkcl.getprops().has_key('order'):  
1261             sort_on = ('+', 'order')
1262         else:  
1263             sort_on = ('+', linkcl.labelprop())
1264         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1265         height = height or min(len(options), 7)
1266         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1267         k = linkcl.labelprop(1)
1269         # make sure we list the current values if they're retired
1270         for val in value:
1271             if val not in options:
1272                 options.insert(0, val)
1274         for optionid in options:
1275             # get the option value, and if it's None use an empty string
1276             option = linkcl.get(optionid, k) or ''
1278             # figure if this option is selected
1279             s = ''
1280             if optionid in value or option in value:
1281                 s = 'selected '
1283             # figure the label
1284             if showid:
1285                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1286             else:
1287                 lab = option
1288             # truncate if it's too long
1289             if size is not None and len(lab) > size:
1290                 lab = lab[:size-3] + '...'
1291             if additional:
1292                 m = []
1293                 for propname in additional:
1294                     m.append(linkcl.get(optionid, propname))
1295                 lab = lab + ' (%s)'%', '.join(m)
1297             # and generate
1298             lab = cgi.escape(lab)
1299             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1300                 lab))
1301         l.append('</select>')
1302         return '\n'.join(l)
1304 # set the propclasses for HTMLItem
1305 propclasses = (
1306     (hyperdb.String, StringHTMLProperty),
1307     (hyperdb.Number, NumberHTMLProperty),
1308     (hyperdb.Boolean, BooleanHTMLProperty),
1309     (hyperdb.Date, DateHTMLProperty),
1310     (hyperdb.Interval, IntervalHTMLProperty),
1311     (hyperdb.Password, PasswordHTMLProperty),
1312     (hyperdb.Link, LinkHTMLProperty),
1313     (hyperdb.Multilink, MultilinkHTMLProperty),
1316 def make_sort_function(db, classname):
1317     '''Make a sort function for a given class
1318     '''
1319     linkcl = db.getclass(classname)
1320     if linkcl.getprops().has_key('order'):
1321         sort_on = 'order'
1322     else:
1323         sort_on = linkcl.labelprop()
1324     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1325         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1326     return sortfunc
1328 def handleListCGIValue(value):
1329     ''' Value is either a single item or a list of items. Each item has a
1330         .value that we're actually interested in.
1331     '''
1332     if isinstance(value, type([])):
1333         return [value.value for value in value]
1334     else:
1335         value = value.value.strip()
1336         if not value:
1337             return []
1338         return value.split(',')
1340 class ShowDict:
1341     ''' A convenience access to the :columns index parameters
1342     '''
1343     def __init__(self, columns):
1344         self.columns = {}
1345         for col in columns:
1346             self.columns[col] = 1
1347     def __getitem__(self, name):
1348         return self.columns.has_key(name)
1350 class HTMLRequest:
1351     ''' The *request*, holding the CGI form and environment.
1353         "form" the CGI form as a cgi.FieldStorage
1354         "env" the CGI environment variables
1355         "base" the base URL for this instance
1356         "user" a HTMLUser instance for this user
1357         "classname" the current classname (possibly None)
1358         "template" the current template (suffix, also possibly None)
1360         Index args:
1361         "columns" dictionary of the columns to display in an index page
1362         "show" a convenience access to columns - request/show/colname will
1363                be true if the columns should be displayed, false otherwise
1364         "sort" index sort column (direction, column name)
1365         "group" index grouping property (direction, column name)
1366         "filter" properties to filter the index on
1367         "filterspec" values to filter the index on
1368         "search_text" text to perform a full-text search on for an index
1370     '''
1371     def __init__(self, client):
1372         self.client = client
1374         # easier access vars
1375         self.form = client.form
1376         self.env = client.env
1377         self.base = client.base
1378         self.user = HTMLUser(client, 'user', client.userid)
1380         # store the current class name and action
1381         self.classname = client.classname
1382         self.template = client.template
1384         # the special char to use for special vars
1385         self.special_char = '@'
1387         self._post_init()
1389     def _post_init(self):
1390         ''' Set attributes based on self.form
1391         '''
1392         # extract the index display information from the form
1393         self.columns = []
1394         for name in ':columns @columns'.split():
1395             if self.form.has_key(name):
1396                 self.special_char = name[0]
1397                 self.columns = handleListCGIValue(self.form[name])
1398                 break
1399         self.show = ShowDict(self.columns)
1401         # sorting
1402         self.sort = (None, None)
1403         for name in ':sort @sort'.split():
1404             if self.form.has_key(name):
1405                 self.special_char = name[0]
1406                 sort = self.form[name].value
1407                 if sort.startswith('-'):
1408                     self.sort = ('-', sort[1:])
1409                 else:
1410                     self.sort = ('+', sort)
1411                 if self.form.has_key(self.special_char+'sortdir'):
1412                     self.sort = ('-', self.sort[1])
1414         # grouping
1415         self.group = (None, None)
1416         for name in ':group @group'.split():
1417             if self.form.has_key(name):
1418                 self.special_char = name[0]
1419                 group = self.form[name].value
1420                 if group.startswith('-'):
1421                     self.group = ('-', group[1:])
1422                 else:
1423                     self.group = ('+', group)
1424                 if self.form.has_key(self.special_char+'groupdir'):
1425                     self.group = ('-', self.group[1])
1427         # filtering
1428         self.filter = []
1429         for name in ':filter @filter'.split():
1430             if self.form.has_key(name):
1431                 self.special_char = name[0]
1432                 self.filter = handleListCGIValue(self.form[name])
1434         self.filterspec = {}
1435         db = self.client.db
1436         if self.classname is not None:
1437             props = db.getclass(self.classname).getprops()
1438             for name in self.filter:
1439                 if self.form.has_key(name):
1440                     prop = props[name]
1441                     fv = self.form[name]
1442                     if (isinstance(prop, hyperdb.Link) or
1443                             isinstance(prop, hyperdb.Multilink)):
1444                         self.filterspec[name] = lookupIds(db, prop,
1445                             handleListCGIValue(fv))
1446                     else:
1447                         self.filterspec[name] = fv.value
1449         # full-text search argument
1450         self.search_text = None
1451         for name in ':search_text @search_text'.split():
1452             if self.form.has_key(name):
1453                 self.special_char = name[0]
1454                 self.search_text = self.form[name].value
1456         # pagination - size and start index
1457         # figure batch args
1458         self.pagesize = 50
1459         for name in ':pagesize @pagesize'.split():
1460             if self.form.has_key(name):
1461                 self.special_char = name[0]
1462                 self.pagesize = int(self.form[name].value)
1464         self.startwith = 0
1465         for name in ':startwith @startwith'.split():
1466             if self.form.has_key(name):
1467                 self.special_char = name[0]
1468                 self.startwith = int(self.form[name].value)
1470     def updateFromURL(self, url):
1471         ''' Parse the URL for query args, and update my attributes using the
1472             values.
1473         ''' 
1474         self.form = {}
1475         for name, value in cgi.parse_qsl(url):
1476             if self.form.has_key(name):
1477                 if isinstance(self.form[name], type([])):
1478                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1479                 else:
1480                     self.form[name] = [self.form[name],
1481                         cgi.MiniFieldStorage(name, value)]
1482             else:
1483                 self.form[name] = cgi.MiniFieldStorage(name, value)
1484         self._post_init()
1486     def update(self, kwargs):
1487         ''' Update my attributes using the keyword args
1488         '''
1489         self.__dict__.update(kwargs)
1490         if kwargs.has_key('columns'):
1491             self.show = ShowDict(self.columns)
1493     def description(self):
1494         ''' Return a description of the request - handle for the page title.
1495         '''
1496         s = [self.client.db.config.TRACKER_NAME]
1497         if self.classname:
1498             if self.client.nodeid:
1499                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1500             else:
1501                 if self.template == 'item':
1502                     s.append('- new %s'%self.classname)
1503                 elif self.template == 'index':
1504                     s.append('- %s index'%self.classname)
1505                 else:
1506                     s.append('- %s %s'%(self.classname, self.template))
1507         else:
1508             s.append('- home')
1509         return ' '.join(s)
1511     def __str__(self):
1512         d = {}
1513         d.update(self.__dict__)
1514         f = ''
1515         for k in self.form.keys():
1516             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1517         d['form'] = f
1518         e = ''
1519         for k,v in self.env.items():
1520             e += '\n     %r=%r'%(k, v)
1521         d['env'] = e
1522         return '''
1523 form: %(form)s
1524 base: %(base)r
1525 classname: %(classname)r
1526 template: %(template)r
1527 columns: %(columns)r
1528 sort: %(sort)r
1529 group: %(group)r
1530 filter: %(filter)r
1531 search_text: %(search_text)r
1532 pagesize: %(pagesize)r
1533 startwith: %(startwith)r
1534 env: %(env)s
1535 '''%d
1537     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1538             filterspec=1):
1539         ''' return the current index args as form elements '''
1540         l = []
1541         sc = self.special_char
1542         s = '<input type="hidden" name="%s" value="%s">'
1543         if columns and self.columns:
1544             l.append(s%(sc+'columns', ','.join(self.columns)))
1545         if sort and self.sort[1] is not None:
1546             if self.sort[0] == '-':
1547                 val = '-'+self.sort[1]
1548             else:
1549                 val = self.sort[1]
1550             l.append(s%(sc+'sort', val))
1551         if group and self.group[1] is not None:
1552             if self.group[0] == '-':
1553                 val = '-'+self.group[1]
1554             else:
1555                 val = self.group[1]
1556             l.append(s%(sc+'group', val))
1557         if filter and self.filter:
1558             l.append(s%(sc+'filter', ','.join(self.filter)))
1559         if filterspec:
1560             for k,v in self.filterspec.items():
1561                 if type(v) == type([]):
1562                     l.append(s%(k, ','.join(v)))
1563                 else:
1564                     l.append(s%(k, v))
1565         if self.search_text:
1566             l.append(s%(sc+'search_text', self.search_text))
1567         l.append(s%(sc+'pagesize', self.pagesize))
1568         l.append(s%(sc+'startwith', self.startwith))
1569         return '\n'.join(l)
1571     def indexargs_url(self, url, args):
1572         ''' Embed the current index args in a URL
1573         '''
1574         sc = self.special_char
1575         l = ['%s=%s'%(k,v) for k,v in args.items()]
1577         # pull out the special values (prefixed by @ or :)
1578         specials = {}
1579         for key in args.keys():
1580             if key[0] in '@:':
1581                 specials[key[1:]] = args[key]
1583         # ok, now handle the specials we received in the request
1584         if self.columns and not specials.has_key('columns'):
1585             l.append(sc+'columns=%s'%(','.join(self.columns)))
1586         if self.sort[1] is not None and not specials.has_key('sort'):
1587             if self.sort[0] == '-':
1588                 val = '-'+self.sort[1]
1589             else:
1590                 val = self.sort[1]
1591             l.append(sc+'sort=%s'%val)
1592         if self.group[1] is not None and not specials.has_key('group'):
1593             if self.group[0] == '-':
1594                 val = '-'+self.group[1]
1595             else:
1596                 val = self.group[1]
1597             l.append(sc+'group=%s'%val)
1598         if self.filter and not specials.has_key('filter'):
1599             l.append(sc+'filter=%s'%(','.join(self.filter)))
1600         if self.search_text and not specials.has_key('search_text'):
1601             l.append(sc+'search_text=%s'%self.search_text)
1602         if not specials.has_key('pagesize'):
1603             l.append(sc+'pagesize=%s'%self.pagesize)
1604         if not specials.has_key('startwith'):
1605             l.append(sc+'startwith=%s'%self.startwith)
1607         # finally, the remainder of the filter args in the request
1608         for k,v in self.filterspec.items():
1609             if not args.has_key(k):
1610                 if type(v) == type([]):
1611                     l.append('%s=%s'%(k, ','.join(v)))
1612                 else:
1613                     l.append('%s=%s'%(k, v))
1614         return '%s?%s'%(url, '&'.join(l))
1615     indexargs_href = indexargs_url
1617     def base_javascript(self):
1618         return '''
1619 <script language="javascript">
1620 submitted = false;
1621 function submit_once() {
1622     if (submitted) {
1623         alert("Your request is being processed.\\nPlease be patient.");
1624         return 0;
1625     }
1626     submitted = true;
1627     return 1;
1630 function help_window(helpurl, width, height) {
1631     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1633 </script>
1634 '''%self.base
1636     def batch(self):
1637         ''' Return a batch object for results from the "current search"
1638         '''
1639         filterspec = self.filterspec
1640         sort = self.sort
1641         group = self.group
1643         # get the list of ids we're batching over
1644         klass = self.client.db.getclass(self.classname)
1645         if self.search_text:
1646             matches = self.client.db.indexer.search(
1647                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1648         else:
1649             matches = None
1650         l = klass.filter(matches, filterspec, sort, group)
1652         # return the batch object, using IDs only
1653         return Batch(self.client, l, self.pagesize, self.startwith,
1654             classname=self.classname)
1656 # extend the standard ZTUtils Batch object to remove dependency on
1657 # Acquisition and add a couple of useful methods
1658 class Batch(ZTUtils.Batch):
1659     ''' Use me to turn a list of items, or item ids of a given class, into a
1660         series of batches.
1662         ========= ========================================================
1663         Parameter  Usage
1664         ========= ========================================================
1665         sequence  a list of HTMLItems or item ids
1666         classname if sequence is a list of ids, this is the class of item
1667         size      how big to make the sequence.
1668         start     where to start (0-indexed) in the sequence.
1669         end       where to end (0-indexed) in the sequence.
1670         orphan    if the next batch would contain less items than this
1671                   value, then it is combined with this batch
1672         overlap   the number of items shared between adjacent batches
1673         ========= ========================================================
1675         Attributes: Note that the "start" attribute, unlike the
1676         argument, is a 1-based index (I know, lame).  "first" is the
1677         0-based index.  "length" is the actual number of elements in
1678         the batch.
1680         "sequence_length" is the length of the original, unbatched, sequence.
1681     '''
1682     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1683             overlap=0, classname=None):
1684         self.client = client
1685         self.last_index = self.last_item = None
1686         self.current_item = None
1687         self.classname = classname
1688         self.sequence_length = len(sequence)
1689         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1690             overlap)
1692     # overwrite so we can late-instantiate the HTMLItem instance
1693     def __getitem__(self, index):
1694         if index < 0:
1695             if index + self.end < self.first: raise IndexError, index
1696             return self._sequence[index + self.end]
1697         
1698         if index >= self.length:
1699             raise IndexError, index
1701         # move the last_item along - but only if the fetched index changes
1702         # (for some reason, index 0 is fetched twice)
1703         if index != self.last_index:
1704             self.last_item = self.current_item
1705             self.last_index = index
1707         item = self._sequence[index + self.first]
1708         if self.classname:
1709             # map the item ids to instances
1710             if self.classname == 'user':
1711                 item = HTMLUser(self.client, self.classname, item)
1712             else:
1713                 item = HTMLItem(self.client, self.classname, item)
1714         self.current_item = item
1715         return item
1717     def propchanged(self, property):
1718         ''' Detect if the property marked as being the group property
1719             changed in the last iteration fetch
1720         '''
1721         if (self.last_item is None or
1722                 self.last_item[property] != self.current_item[property]):
1723             return 1
1724         return 0
1726     # override these 'cos we don't have access to acquisition
1727     def previous(self):
1728         if self.start == 1:
1729             return None
1730         return Batch(self.client, self._sequence, self._size,
1731             self.first - self._size + self.overlap, 0, self.orphan,
1732             self.overlap)
1734     def next(self):
1735         try:
1736             self._sequence[self.end]
1737         except IndexError:
1738             return None
1739         return Batch(self.client, self._sequence, self._size,
1740             self.end - self.overlap, 0, self.orphan, self.overlap)
1742 class TemplatingUtils:
1743     ''' Utilities for templating
1744     '''
1745     def __init__(self, client):
1746         self.client = client
1747     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1748         return Batch(self.client, sequence, size, start, end, orphan,
1749             overlap)