Code

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