Code

implement built-in permission checks for rendered fields, making templating much...
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9     import cPickle as pickle
10 except ImportError:
11     import pickle
12 try:
13     import cStringIO as StringIO
14 except ImportError:
15     import StringIO
16 try:
17     import StructuredText
18 except ImportError:
19     StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 class NoTemplate(Exception):
28     pass
30 class Unauthorised(Exception):
31     def __init__(self, action, klass):
32         self.action = action
33         self.klass = klass
34     def __str__(self):
35         return 'You are not allowed to %s items of class %s'%(self.action,
36             self.klass)
38 def find_template(dir, name, extension):
39     ''' Find a template in the nominated dir
40     '''
41     # find the source
42     if extension:
43         filename = '%s.%s'%(name, extension)
44     else:
45         filename = name
47     # try old-style
48     src = os.path.join(dir, filename)
49     if os.path.exists(src):
50         return (src, filename)
52     # try with a .html extension (new-style)
53     filename = filename + '.html'
54     src = os.path.join(dir, filename)
55     if os.path.exists(src):
56         return (src, filename)
58     # no extension == no generic template is possible
59     if not extension:
60         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
62     # try for a _generic template
63     generic = '_generic.%s'%extension
64     src = os.path.join(dir, generic)
65     if os.path.exists(src):
66         return (src, generic)
68     # finally, try _generic.html
69     generic = generic + '.html'
70     src = os.path.join(dir, generic)
71     if os.path.exists(src):
72         return (src, generic)
74     raise NoTemplate, 'No template file exists for templating "%s" '\
75         'with template "%s" (neither "%s" nor "%s")'%(name, extension,
76         filename, generic)
78 class Templates:
79     templates = {}
81     def __init__(self, dir):
82         self.dir = dir
84     def precompileTemplates(self):
85         ''' Go through a directory and precompile all the templates therein
86         '''
87         for filename in os.listdir(self.dir):
88             if os.path.isdir(filename): continue
89             if '.' in filename:
90                 name, extension = filename.split('.')
91                 self.get(name, extension)
92             else:
93                 self.get(filename, None)
95     def get(self, name, extension=None):
96         ''' Interface to get a template, possibly loading a compiled template.
98             "name" and "extension" indicate the template we're after, which in
99             most cases will be "name.extension". If "extension" is None, then
100             we look for a template just called "name" with no extension.
102             If the file "name.extension" doesn't exist, we look for
103             "_generic.extension" as a fallback.
104         '''
105         # default the name to "home"
106         if name is None:
107             name = 'home'
108         elif extension is None and '.' in name:
109             # split name
110             name, extension = name.split('.')
112         # find the source
113         src, filename = find_template(self.dir, name, extension)
115         # has it changed?
116         try:
117             stime = os.stat(src)[os.path.stat.ST_MTIME]
118         except os.error, error:
119             if error.errno != errno.ENOENT:
120                 raise
122         if self.templates.has_key(src) and \
123                 stime < self.templates[src].mtime:
124             # compiled template is up to date
125             return self.templates[src]
127         # compile the template
128         self.templates[src] = pt = RoundupPageTemplate()
129         # use pt_edit so we can pass the content_type guess too
130         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
131         pt.pt_edit(open(src).read(), content_type)
132         pt.id = filename
133         pt.mtime = time.time()
134         return pt
136     def __getitem__(self, name):
137         name, extension = os.path.splitext(name)
138         if extension:
139             extension = extension[1:]
140         try:
141             return self.get(name, extension)
142         except NoTemplate, message:
143             raise KeyError, message
145 class RoundupPageTemplate(PageTemplate.PageTemplate):
146     ''' A Roundup-specific PageTemplate.
148         Interrogate the client to set up the various template variables to
149         be available:
151         *context*
152          this is one of three things:
153          1. None - we're viewing a "home" page
154          2. The current class of item being displayed. This is an HTMLClass
155             instance.
156          3. The current item from the database, if we're viewing a specific
157             item, as an HTMLItem instance.
158         *request*
159           Includes information about the current request, including:
160            - the url
161            - the current index information (``filterspec``, ``filter`` args,
162              ``properties``, etc) parsed out of the form. 
163            - methods for easy filterspec link generation
164            - *user*, the current user node as an HTMLItem instance
165            - *form*, the current CGI form information as a FieldStorage
166         *config*
167           The current tracker config.
168         *db*
169           The current database, used to access arbitrary database items.
170         *utils*
171           This is a special class that has its base in the TemplatingUtils
172           class in this file. If the tracker interfaces module defines a
173           TemplatingUtils class then it is mixed in, overriding the methods
174           in the base class.
175     '''
176     def getContext(self, client, classname, request):
177         # construct the TemplatingUtils class
178         utils = TemplatingUtils
179         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
180             class utils(client.instance.interfaces.TemplatingUtils, utils):
181                 pass
183         c = {
184              'options': {},
185              'nothing': None,
186              'request': request,
187              'db': HTMLDatabase(client),
188              'config': client.instance.config,
189              'tracker': client.instance,
190              'utils': utils(client),
191              'templates': Templates(client.instance.config.TEMPLATES),
192         }
193         # add in the item if there is one
194         if client.nodeid:
195             if classname == 'user':
196                 c['context'] = HTMLUser(client, classname, client.nodeid,
197                     anonymous=1)
198             else:
199                 c['context'] = HTMLItem(client, classname, client.nodeid,
200                     anonymous=1)
201         elif client.db.classes.has_key(classname):
202             c['context'] = HTMLClass(client, classname, anonymous=1)
203         return c
205     def render(self, client, classname, request, **options):
206         """Render this Page Template"""
208         if not self._v_cooked:
209             self._cook()
211         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
213         if self._v_errors:
214             raise PageTemplate.PTRuntimeError, \
215                 'Page Template %s has errors.'%self.id
217         # figure the context
218         classname = classname or client.classname
219         request = request or HTMLRequest(client)
220         c = self.getContext(client, classname, request)
221         c.update({'options': options})
223         # and go
224         output = StringIO.StringIO()
225         TALInterpreter(self._v_program, self.macros,
226             getEngine().getContext(c), output, tal=1, strictinsert=0)()
227         return output.getvalue()
229     def __repr__(self):
230         return '<Roundup PageTemplate %r>'%self.id
232 class HTMLDatabase:
233     ''' Return HTMLClasses for valid class fetches
234     '''
235     def __init__(self, client):
236         self._client = client
237         self._db = client.db
239         # we want config to be exposed
240         self.config = client.db.config
242     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
243         # check to see if we're actually accessing an item
244         m = desre.match(item)
245         if m:
246             self._client.db.getclass(m.group('cl'))
247             return HTMLItem(self._client, m.group('cl'), m.group('id'))
248         else:
249             self._client.db.getclass(item)
250             return HTMLClass(self._client, item)
252     def __getattr__(self, attr):
253         try:
254             return self[attr]
255         except KeyError:
256             raise AttributeError, attr
258     def classes(self):
259         l = self._client.db.classes.keys()
260         l.sort()
261         return [HTMLClass(self._client, cn) for cn in l]
263 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
264     cl = db.getclass(prop.classname)
265     l = []
266     for entry in ids:
267         if num_re.match(entry):
268             l.append(entry)
269         else:
270             try:
271                 l.append(cl.lookup(entry))
272             except KeyError:
273                 # ignore invalid keys
274                 pass
275     return l
277 class HTMLPermissions:
278     ''' Helpers that provide answers to commonly asked Permission questions.
279     '''
280     def is_edit_ok(self):
281         ''' Is the user allowed to Edit the current class?
282         '''
283         return self._db.security.hasPermission('Edit', self._client.userid,
284             self._classname)
286     def is_view_ok(self):
287         ''' Is the user allowed to View the current class?
288         '''
289         return self._db.security.hasPermission('View', self._client.userid,
290             self._classname)
292     def is_only_view_ok(self):
293         ''' Is the user only allowed to View (ie. not Edit) the current class?
294         '''
295         return self.is_view_ok() and not self.is_edit_ok()
297     def view_check(self):
298         ''' Raise the Unauthorised exception if the user's not permitted to
299             view this class.
300         '''
301         if not self.is_view_ok():
302             raise Unauthorised("view", self._classname)
304     def edit_check(self):
305         ''' Raise the Unauthorised exception if the user's not permitted to
306             edit this class.
307         '''
308         if not self.is_edit_ok():
309             raise Unauthorised("edit", self._classname)
311 def input_html4(**attrs):
312     """Generate an 'input' (html4) element with given attributes"""
313     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
315 def input_xhtml(**attrs):
316     """Generate an 'input' (xhtml) element with given attributes"""
317     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
319 class HTMLInputMixin:
320     ''' requires a _client property '''
321     def __init__(self):
322         html_version = 'html4'
323         if hasattr(self._client.instance.config, 'HTML_VERSION'):
324             html_version = self._client.instance.config.HTML_VERSION
325         if html_version == 'xhtml':
326             self.input = input_xhtml
327         else:
328             self.input = input_html4
330 class HTMLClass(HTMLInputMixin, HTMLPermissions):
331     ''' Accesses through a class (either through *class* or *db.<classname>*)
332     '''
333     def __init__(self, client, classname, anonymous=0):
334         self._client = client
335         self._db = client.db
336         self._anonymous = anonymous
338         # we want classname to be exposed, but _classname gives a
339         # consistent API for extending Class/Item
340         self._classname = self.classname = classname
341         self._klass = self._db.getclass(self.classname)
342         self._props = self._klass.getprops()
344         HTMLInputMixin.__init__(self)
346     def __repr__(self):
347         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
349     def __getitem__(self, item):
350         ''' return an HTMLProperty instance
351         '''
352        #print 'HTMLClass.getitem', (self, item)
354         # we don't exist
355         if item == 'id':
356             return None
358         # get the property
359         prop = self._props[item]
361         # look up the correct HTMLProperty class
362         form = self._client.form
363         for klass, htmlklass in propclasses:
364             if not isinstance(prop, klass):
365                 continue
366             if form.has_key(item):
367                 if isinstance(prop, hyperdb.Multilink):
368                     value = lookupIds(self._db, prop,
369                         handleListCGIValue(form[item]))
370                 elif isinstance(prop, hyperdb.Link):
371                     value = form[item].value.strip()
372                     if value:
373                         value = lookupIds(self._db, prop, [value])[0]
374                     else:
375                         value = None
376                 else:
377                     value = form[item].value.strip() or None
378             else:
379                 if isinstance(prop, hyperdb.Multilink):
380                     value = []
381                 else:
382                     value = None
383             return htmlklass(self._client, self._classname, '', prop, item,
384                 value, self._anonymous)
386         # no good
387         raise KeyError, item
389     def __getattr__(self, attr):
390         ''' convenience access '''
391         try:
392             return self[attr]
393         except KeyError:
394             raise AttributeError, attr
396     def designator(self):
397         ''' Return this class' designator (classname) '''
398         return self._classname
400     def getItem(self, itemid, num_re=re.compile('-?\d+')):
401         ''' Get an item of this class by its item id.
402         '''
403         # make sure we're looking at an itemid
404         if not num_re.match(itemid):
405             itemid = self._klass.lookup(itemid)
407         if self.classname == 'user':
408             klass = HTMLUser
409         else:
410             klass = HTMLItem
412         return klass(self._client, self.classname, itemid)
414     def properties(self, sort=1):
415         ''' Return HTMLProperty for all of this class' properties.
416         '''
417         l = []
418         for name, prop in self._props.items():
419             for klass, htmlklass in propclasses:
420                 if isinstance(prop, hyperdb.Multilink):
421                     value = []
422                 else:
423                     value = None
424                 if isinstance(prop, klass):
425                     l.append(htmlklass(self._client, self._classname, '',
426                         prop, name, value, self._anonymous))
427         if sort:
428             l.sort(lambda a,b:cmp(a._name, b._name))
429         return l
431     def list(self, sort_on=None):
432         ''' List all items in this class.
433         '''
434         if self.classname == 'user':
435             klass = HTMLUser
436         else:
437             klass = HTMLItem
439         # get the list and sort it nicely
440         l = self._klass.list()
441         sortfunc = make_sort_function(self._db, self.classname, sort_on)
442         l.sort(sortfunc)
444         l = [klass(self._client, self.classname, x) for x in l]
445         return l
447     def csv(self):
448         ''' Return the items of this class as a chunk of CSV text.
449         '''
450         if rcsv.error:
451             return rcsv.error
453         props = self.propnames()
454         s = StringIO.StringIO()
455         writer = rcsv.writer(s, rcsv.comma_separated)
456         writer.writerow(props)
457         for nodeid in self._klass.list():
458             l = []
459             for name in props:
460                 value = self._klass.get(nodeid, name)
461                 if value is None:
462                     l.append('')
463                 elif isinstance(value, type([])):
464                     l.append(':'.join(map(str, value)))
465                 else:
466                     l.append(str(self._klass.get(nodeid, name)))
467             writer.writerow(l)
468         return s.getvalue()
470     def propnames(self):
471         ''' Return the list of the names of the properties of this class.
472         '''
473         idlessprops = self._klass.getprops(protected=0).keys()
474         idlessprops.sort()
475         return ['id'] + idlessprops
477     def filter(self, request=None, filterspec={}, sort=(None,None),
478             group=(None,None)):
479         ''' Return a list of items from this class, filtered and sorted
480             by the current requested filterspec/filter/sort/group args
482             "request" takes precedence over the other three arguments.
483         '''
484         if request is not None:
485             filterspec = request.filterspec
486             sort = request.sort
487             group = request.group
488         if self.classname == 'user':
489             klass = HTMLUser
490         else:
491             klass = HTMLItem
492         l = [klass(self._client, self.classname, x)
493              for x in self._klass.filter(None, filterspec, sort, group)]
494         return l
496     def classhelp(self, properties=None, label='(list)', width='500',
497             height='400', property=''):
498         ''' Pop up a javascript window with class help
500             This generates a link to a popup window which displays the 
501             properties indicated by "properties" of the class named by
502             "classname". The "properties" should be a comma-separated list
503             (eg. 'id,name,description'). Properties defaults to all the
504             properties of a class (excluding id, creator, created and
505             activity).
507             You may optionally override the label displayed, the width and
508             height. The popup window will be resizable and scrollable.
510             If the "property" arg is given, it's passed through to the
511             javascript help_window function.
512         '''
513         if properties is None:
514             properties = self._klass.getprops(protected=0).keys()
515             properties.sort()
516             properties = ','.join(properties)
517         if property:
518             property = '&amp;property=%s'%property
519         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
520             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
521             \'%s\')">%s</a>'%(self.classname, properties, property, width,
522             height, label)
524     def submit(self, label="Submit New Entry"):
525         ''' Generate a submit button (and action hidden element)
526         '''
527         self.view_check()
528         if self.is_edit_ok():
529             return self.input(type="hidden",name="@action",value="new") + \
530                    '\n' + self.input(type="submit",name="submit",value=label)
531         return ''
533     def history(self):
534         self.view_check()
535         return 'New node - no history'
537     def renderWith(self, name, **kwargs):
538         ''' Render this class with the given template.
539         '''
540         # create a new request and override the specified args
541         req = HTMLRequest(self._client)
542         req.classname = self.classname
543         req.update(kwargs)
545         # new template, using the specified classname and request
546         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
548         # use our fabricated request
549         args = {
550             'ok_message': self._client.ok_message,
551             'error_message': self._client.error_message
552         }
553         return pt.render(self._client, self.classname, req, **args)
555 class HTMLItem(HTMLInputMixin, HTMLPermissions):
556     ''' Accesses through an *item*
557     '''
558     def __init__(self, client, classname, nodeid, anonymous=0):
559         self._client = client
560         self._db = client.db
561         self._classname = classname
562         self._nodeid = nodeid
563         self._klass = self._db.getclass(classname)
564         self._props = self._klass.getprops()
566         # do we prefix the form items with the item's identification?
567         self._anonymous = anonymous
569         HTMLInputMixin.__init__(self)
571     def __repr__(self):
572         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
573             self._nodeid)
575     def __getitem__(self, item):
576         ''' return an HTMLProperty instance
577         '''
578         #print 'HTMLItem.getitem', (self, item)
579         if item == 'id':
580             return self._nodeid
582         # get the property
583         prop = self._props[item]
585         # get the value, handling missing values
586         value = None
587         if int(self._nodeid) > 0:
588             value = self._klass.get(self._nodeid, item, None)
589         if value is None:
590             if isinstance(self._props[item], hyperdb.Multilink):
591                 value = []
593         # look up the correct HTMLProperty class
594         for klass, htmlklass in propclasses:
595             if isinstance(prop, klass):
596                 return htmlklass(self._client, self._classname,
597                     self._nodeid, prop, item, value, self._anonymous)
599         raise KeyError, item
601     def __getattr__(self, attr):
602         ''' convenience access to properties '''
603         try:
604             return self[attr]
605         except KeyError:
606             raise AttributeError, attr
608     def designator(self):
609         ''' Return this item's designator (classname + id) '''
610         return '%s%s'%(self._classname, self._nodeid)
611     
612     def submit(self, label="Submit Changes"):
613         ''' Generate a submit button (and action hidden element)
614         '''
615         return self.input(type="hidden",name="@action",value="edit") + '\n' + \
616                self.input(type="submit",name="submit",value=label)
618     def journal(self, direction='descending'):
619         ''' Return a list of HTMLJournalEntry instances.
620         '''
621         # XXX do this
622         return []
624     def history(self, direction='descending', dre=re.compile('\d+')):
625         self.view_check()
627         l = ['<table class="history">'
628              '<tr><th colspan="4" class="header">',
629              _('History'),
630              '</th></tr><tr>',
631              _('<th>Date</th>'),
632              _('<th>User</th>'),
633              _('<th>Action</th>'),
634              _('<th>Args</th>'),
635             '</tr>']
636         current = {}
637         comments = {}
638         history = self._klass.history(self._nodeid)
639         history.sort()
640         timezone = self._db.getUserTimezone()
641         if direction == 'descending':
642             history.reverse()
643             for prop_n in self._props.keys():
644                 prop = self[prop_n]
645                 if isinstance(prop, HTMLProperty):
646                     current[prop_n] = prop.plain()
647                     # make link if hrefable
648                     if (self._props.has_key(prop_n) and
649                             isinstance(self._props[prop_n], hyperdb.Link)):
650                         classname = self._props[prop_n].classname
651                         try:
652                             template = find_template(self._db.config.TEMPLATES,
653                                 classname, 'item')
654                             if template[1].startswith('_generic'):
655                                 raise NoTemplate, 'not really...'
656                         except NoTemplate:
657                             pass
658                         else:
659                             id = self._klass.get(self._nodeid, prop_n, None)
660                             current[prop_n] = '<a href="%s%s">%s</a>'%(
661                                 classname, id, current[prop_n])
662  
663         for id, evt_date, user, action, args in history:
664             date_s = str(evt_date.local(timezone)).replace("."," ")
665             arg_s = ''
666             if action == 'link' and type(args) == type(()):
667                 if len(args) == 3:
668                     linkcl, linkid, key = args
669                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
670                         linkcl, linkid, key)
671                 else:
672                     arg_s = str(args)
674             elif action == 'unlink' and type(args) == type(()):
675                 if len(args) == 3:
676                     linkcl, linkid, key = args
677                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
678                         linkcl, linkid, key)
679                 else:
680                     arg_s = str(args)
682             elif type(args) == type({}):
683                 cell = []
684                 for k in args.keys():
685                     # try to get the relevant property and treat it
686                     # specially
687                     try:
688                         prop = self._props[k]
689                     except KeyError:
690                         prop = None
691                     if prop is None:
692                         # property no longer exists
693                         comments['no_exist'] = _('''<em>The indicated property
694                             no longer exists</em>''')
695                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
696                         continue
698                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
699                             isinstance(prop, hyperdb.Link)):
700                         # figure what the link class is
701                         classname = prop.classname
702                         try:
703                             linkcl = self._db.getclass(classname)
704                         except KeyError:
705                             labelprop = None
706                             comments[classname] = _('''The linked class
707                                 %(classname)s no longer exists''')%locals()
708                         labelprop = linkcl.labelprop(1)
709                         try:
710                             template = find_template(self._db.config.TEMPLATES,
711                                 classname, 'item')
712                             if template[1].startswith('_generic'):
713                                 raise NoTemplate, 'not really...'
714                             hrefable = 1
715                         except NoTemplate:
716                             hrefable = 0
718                     if isinstance(prop, hyperdb.Multilink) and args[k]:
719                         ml = []
720                         for linkid in args[k]:
721                             if isinstance(linkid, type(())):
722                                 sublabel = linkid[0] + ' '
723                                 linkids = linkid[1]
724                             else:
725                                 sublabel = ''
726                                 linkids = [linkid]
727                             subml = []
728                             for linkid in linkids:
729                                 label = classname + linkid
730                                 # if we have a label property, try to use it
731                                 # TODO: test for node existence even when
732                                 # there's no labelprop!
733                                 try:
734                                     if labelprop is not None and \
735                                             labelprop != 'id':
736                                         label = linkcl.get(linkid, labelprop)
737                                 except IndexError:
738                                     comments['no_link'] = _('''<strike>The
739                                         linked node no longer
740                                         exists</strike>''')
741                                     subml.append('<strike>%s</strike>'%label)
742                                 else:
743                                     if hrefable:
744                                         subml.append('<a href="%s%s">%s</a>'%(
745                                             classname, linkid, label))
746                                     else:
747                                         subml.append(label)
748                             ml.append(sublabel + ', '.join(subml))
749                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
750                     elif isinstance(prop, hyperdb.Link) and args[k]:
751                         label = classname + args[k]
752                         # if we have a label property, try to use it
753                         # TODO: test for node existence even when
754                         # there's no labelprop!
755                         if labelprop is not None and labelprop != 'id':
756                             try:
757                                 label = linkcl.get(args[k], labelprop)
758                             except IndexError:
759                                 comments['no_link'] = _('''<strike>The
760                                     linked node no longer
761                                     exists</strike>''')
762                                 cell.append(' <strike>%s</strike>,\n'%label)
763                                 # "flag" this is done .... euwww
764                                 label = None
765                         if label is not None:
766                             if hrefable:
767                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
768                             else:
769                                 old = label;
770                             cell.append('%s: %s' % (k,old))
771                             if current.has_key(k):
772                                 cell[-1] += ' -> %s'%current[k]
773                                 current[k] = old
775                     elif isinstance(prop, hyperdb.Date) and args[k]:
776                         d = date.Date(args[k]).local(timezone)
777                         cell.append('%s: %s'%(k, str(d)))
778                         if current.has_key(k):
779                             cell[-1] += ' -> %s' % current[k]
780                             current[k] = str(d)
782                     elif isinstance(prop, hyperdb.Interval) and args[k]:
783                         d = date.Interval(args[k])
784                         cell.append('%s: %s'%(k, str(d)))
785                         if current.has_key(k):
786                             cell[-1] += ' -> %s'%current[k]
787                             current[k] = str(d)
789                     elif isinstance(prop, hyperdb.String) and args[k]:
790                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
791                         if current.has_key(k):
792                             cell[-1] += ' -> %s'%current[k]
793                             current[k] = cgi.escape(args[k])
795                     elif not args[k]:
796                         if current.has_key(k):
797                             cell.append('%s: %s'%(k, current[k]))
798                             current[k] = '(no value)'
799                         else:
800                             cell.append('%s: (no value)'%k)
802                     else:
803                         cell.append('%s: %s'%(k, str(args[k])))
804                         if current.has_key(k):
805                             cell[-1] += ' -> %s'%current[k]
806                             current[k] = str(args[k])
808                 arg_s = '<br />'.join(cell)
809             else:
810                 # unkown event!!
811                 comments['unknown'] = _('''<strong><em>This event is not
812                     handled by the history display!</em></strong>''')
813                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
814             date_s = date_s.replace(' ', '&nbsp;')
815             # if the user's an itemid, figure the username (older journals
816             # have the username)
817             if dre.match(user):
818                 user = self._db.user.get(user, 'username')
819             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
820                 date_s, user, action, arg_s))
821         if comments:
822             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
823         for entry in comments.values():
824             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
825         l.append('</table>')
826         return '\n'.join(l)
828     def renderQueryForm(self):
829         ''' Render this item, which is a query, as a search form.
830         '''
831         # create a new request and override the specified args
832         req = HTMLRequest(self._client)
833         req.classname = self._klass.get(self._nodeid, 'klass')
834         name = self._klass.get(self._nodeid, 'name')
835         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
836             '&@queryname=%s'%urllib.quote(name))
838         # new template, using the specified classname and request
839         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
841         # use our fabricated request
842         return pt.render(self._client, req.classname, req)
844 class HTMLUser(HTMLItem):
845     ''' Accesses through the *user* (a special case of item)
846     '''
847     def __init__(self, client, classname, nodeid, anonymous=0):
848         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
849         self._default_classname = client.classname
851         # used for security checks
852         self._security = client.db.security
854     _marker = []
855     def hasPermission(self, permission, classname=_marker):
856         ''' Determine if the user has the Permission.
858             The class being tested defaults to the template's class, but may
859             be overidden for this test by suppling an alternate classname.
860         '''
861         if classname is self._marker:
862             classname = self._default_classname
863         return self._security.hasPermission(permission, self._nodeid, classname)
865     def is_edit_ok(self):
866         ''' Is the user allowed to Edit the current class?
867             Also check whether this is the current user's info.
868         '''
869         return self._db.security.hasPermission('Edit', self._client.userid,
870             self._classname) or (self._nodeid == self._client.userid and
871             self._db.user.get(self._client.userid, 'username') != 'anonymous')
873     def is_view_ok(self):
874         ''' Is the user allowed to View the current class?
875             Also check whether this is the current user's info.
876         '''
877         return self._db.security.hasPermission('Edit', self._client.userid,
878             self._classname) or (self._nodeid == self._client.userid and
879             self._db.user.get(self._client.userid, 'username') != 'anonymous')
881 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
882     ''' String, Number, Date, Interval HTMLProperty
884         Has useful attributes:
886          _name  the name of the property
887          _value the value of the property if any
889         A wrapper object which may be stringified for the plain() behaviour.
890     '''
891     def __init__(self, client, classname, nodeid, prop, name, value,
892             anonymous=0):
893         self._client = client
894         self._db = client.db
895         self._classname = classname
896         self._nodeid = nodeid
897         self._prop = prop
898         self._value = value
899         self._anonymous = anonymous
900         self._name = name
901         if not anonymous:
902             self._formname = '%s%s@%s'%(classname, nodeid, name)
903         else:
904             self._formname = name
906         HTMLInputMixin.__init__(self)
908     def __repr__(self):
909         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
910             self._prop, self._value)
911     def __str__(self):
912         return self.plain()
913     def __cmp__(self, other):
914         if isinstance(other, HTMLProperty):
915             return cmp(self._value, other._value)
916         return cmp(self._value, other)
918 class StringHTMLProperty(HTMLProperty):
919     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
920                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
921                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
922     def _hyper_repl(self, match):
923         if match.group('url'):
924             s = match.group('url')
925             return '<a href="%s">%s</a>'%(s, s)
926         elif match.group('email'):
927             s = match.group('email')
928             return '<a href="mailto:%s">%s</a>'%(s, s)
929         else:
930             s = match.group('item')
931             s1 = match.group('class')
932             s2 = match.group('id')
933             try:
934                 # make sure s1 is a valid tracker classname
935                 self._db.getclass(s1)
936                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
937             except KeyError:
938                 return '%s%s'%(s1, s2)
940     def hyperlinked(self):
941         ''' Render a "hyperlinked" version of the text '''
942         return self.plain(hyperlink=1)
944     def plain(self, escape=0, hyperlink=0):
945         ''' Render a "plain" representation of the property
946             
947             "escape" turns on/off HTML quoting
948             "hyperlink" turns on/off in-text hyperlinking of URLs, email
949                 addresses and designators
950         '''
951         self.view_check()
953         if self._value is None:
954             return ''
955         if escape:
956             s = cgi.escape(str(self._value))
957         else:
958             s = str(self._value)
959         if hyperlink:
960             # no, we *must* escape this text
961             if not escape:
962                 s = cgi.escape(s)
963             s = self.hyper_re.sub(self._hyper_repl, s)
964         return s
966     def stext(self, escape=0):
967         ''' Render the value of the property as StructuredText.
969             This requires the StructureText module to be installed separately.
970         '''
971         self.view_check()
973         s = self.plain(escape=escape)
974         if not StructuredText:
975             return s
976         return StructuredText(s,level=1,header=0)
978     def field(self, size = 30):
979         ''' Render the property as a field in HTML.
981             If not editable, just display the value via plain().
982         '''
983         self.view_check()
985         if self._value is None:
986             value = ''
987         else:
988             value = cgi.escape(str(self._value))
990         if self.is_edit_ok():
991             value = '&quot;'.join(value.split('"'))
992             return self.input(name=self._formname,value=value,size=size)
994         return self.plain()
996     def multiline(self, escape=0, rows=5, cols=40):
997         ''' Render a multiline form edit field for the property.
999             If not editable, just display the plain() value in a <pre> tag.
1000         '''
1001         self.view_check()
1003         if self._value is None:
1004             value = ''
1005         else:
1006             value = cgi.escape(str(self._value))
1008         if self.is_edit_ok():
1009             value = '&quot;'.join(value.split('"'))
1010             return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1011                 self._formname, rows, cols, value)
1013         return '<pre>%s</pre>'%self.plain()
1015     def email(self, escape=1):
1016         ''' Render the value of the property as an obscured email address
1017         '''
1018         self.view_check()
1020         if self._value is None:
1021             value = ''
1022         else:
1023             value = str(self._value)
1024         if value.find('@') != -1:
1025             name, domain = value.split('@')
1026             domain = ' '.join(domain.split('.')[:-1])
1027             name = name.replace('.', ' ')
1028             value = '%s at %s ...'%(name, domain)
1029         else:
1030             value = value.replace('.', ' ')
1031         if escape:
1032             value = cgi.escape(value)
1033         return value
1035 class PasswordHTMLProperty(HTMLProperty):
1036     def plain(self):
1037         ''' Render a "plain" representation of the property
1038         '''
1039         self.view_check()
1041         if self._value is None:
1042             return ''
1043         return _('*encrypted*')
1045     def field(self, size = 30):
1046         ''' Render a form edit field for the property.
1048             If not editable, just display the value via plain().
1049         '''
1050         self.view_check()
1052         if self.is_edit_ok():
1053             return self.input(type="password", name=self._formname, size=size)
1055         return self.plain()
1057     def confirm(self, size = 30):
1058         ''' Render a second form edit field for the property, used for 
1059             confirmation that the user typed the password correctly. Generates
1060             a field with name "@confirm@name".
1062             If not editable, display nothing.
1063         '''
1064         self.view_check()
1066         if self.is_edit_ok():
1067             return self.input(type="password",
1068                 name="@confirm@%s"%self._formname, size=size)
1070         return ''
1072 class NumberHTMLProperty(HTMLProperty):
1073     def plain(self):
1074         ''' Render a "plain" representation of the property
1075         '''
1076         self.view_check()
1078         return str(self._value)
1080     def field(self, size = 30):
1081         ''' Render a form edit field for the property.
1083             If not editable, just display the value via plain().
1084         '''
1085         self.view_check()
1087         if self._value is None:
1088             value = ''
1089         else:
1090             value = cgi.escape(str(self._value))
1092         if self.is_edit_ok():
1093             value = '&quot;'.join(value.split('"'))
1094             return self.input(name=self._formname,value=value,size=size)
1096         return self.plain()
1098     def __int__(self):
1099         ''' Return an int of me
1100         '''
1101         return int(self._value)
1103     def __float__(self):
1104         ''' Return a float of me
1105         '''
1106         return float(self._value)
1109 class BooleanHTMLProperty(HTMLProperty):
1110     def plain(self):
1111         ''' Render a "plain" representation of the property
1112         '''
1113         self.view_check()
1115         if self._value is None:
1116             return ''
1117         return self._value and "Yes" or "No"
1119     def field(self):
1120         ''' Render a form edit field for the property
1122             If not editable, just display the value via plain().
1123         '''
1124         self.view_check()
1126         if not is_edit_ok():
1127             return self.plain()
1129         checked = self._value and "checked" or ""
1130         if self._value:
1131             s = self.input(type="radio", name=self._formname, value="yes",
1132                 checked="checked")
1133             s += 'Yes'
1134             s +=self.input(type="radio", name=self._formname, value="no")
1135             s += 'No'
1136         else:
1137             s = self.input(type="radio", name=self._formname, value="yes")
1138             s += 'Yes'
1139             s +=self.input(type="radio", name=self._formname, value="no",
1140                 checked="checked")
1141             s += 'No'
1142         return s
1144 class DateHTMLProperty(HTMLProperty):
1145     def plain(self):
1146         ''' Render a "plain" representation of the property
1147         '''
1148         self.view_check()
1150         if self._value is None:
1151             return ''
1152         return str(self._value.local(self._db.getUserTimezone()))
1154     def now(self):
1155         ''' Return the current time.
1157             This is useful for defaulting a new value. Returns a
1158             DateHTMLProperty.
1159         '''
1160         self.view_check()
1162         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1163             self._formname, date.Date('.'))
1165     def field(self, size = 30):
1166         ''' Render a form edit field for the property
1168             If not editable, just display the value via plain().
1169         '''
1170         self.view_check()
1172         if self._value is None:
1173             value = ''
1174         else:
1175             tz = self._db.getUserTimezone()
1176             value = cgi.escape(str(self._value.local(tz)))
1178         if is_edit_ok():
1179             value = '&quot;'.join(value.split('"'))
1180             return self.input(name=self._formname,value=value,size=size)
1181         
1182         return self.plain()
1184     def reldate(self, pretty=1):
1185         ''' Render the interval between the date and now.
1187             If the "pretty" flag is true, then make the display pretty.
1188         '''
1189         self.view_check()
1191         if not self._value:
1192             return ''
1194         # figure the interval
1195         interval = self._value - date.Date('.')
1196         if pretty:
1197             return interval.pretty()
1198         return str(interval)
1200     _marker = []
1201     def pretty(self, format=_marker):
1202         ''' Render the date in a pretty format (eg. month names, spaces).
1204             The format string is a standard python strftime format string.
1205             Note that if the day is zero, and appears at the start of the
1206             string, then it'll be stripped from the output. This is handy
1207             for the situatin when a date only specifies a month and a year.
1208         '''
1209         self.view_check()
1211         if format is not self._marker:
1212             return self._value.pretty(format)
1213         else:
1214             return self._value.pretty()
1216     def local(self, offset):
1217         ''' Return the date/time as a local (timezone offset) date/time.
1218         '''
1219         self.view_check()
1221         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1222             self._formname, self._value.local(offset))
1224 class IntervalHTMLProperty(HTMLProperty):
1225     def plain(self):
1226         ''' Render a "plain" representation of the property
1227         '''
1228         self.view_check()
1230         if self._value is None:
1231             return ''
1232         return str(self._value)
1234     def pretty(self):
1235         ''' Render the interval in a pretty format (eg. "yesterday")
1236         '''
1237         self.view_check()
1239         return self._value.pretty()
1241     def field(self, size = 30):
1242         ''' Render a form edit field for the property
1244             If not editable, just display the value via plain().
1245         '''
1246         self.view_check()
1248         if self._value is None:
1249             value = ''
1250         else:
1251             value = cgi.escape(str(self._value))
1253         if is_edit_ok():
1254             value = '&quot;'.join(value.split('"'))
1255             return self.input(name=self._formname,value=value,size=size)
1257         return self.plain()
1259 class LinkHTMLProperty(HTMLProperty):
1260     ''' Link HTMLProperty
1261         Include the above as well as being able to access the class
1262         information. Stringifying the object itself results in the value
1263         from the item being displayed. Accessing attributes of this object
1264         result in the appropriate entry from the class being queried for the
1265         property accessed (so item/assignedto/name would look up the user
1266         entry identified by the assignedto property on item, and then the
1267         name property of that user)
1268     '''
1269     def __init__(self, *args, **kw):
1270         HTMLProperty.__init__(self, *args, **kw)
1271         # if we're representing a form value, then the -1 from the form really
1272         # should be a None
1273         if str(self._value) == '-1':
1274             self._value = None
1276     def __getattr__(self, attr):
1277         ''' return a new HTMLItem '''
1278        #print 'Link.getattr', (self, attr, self._value)
1279         if not self._value:
1280             raise AttributeError, "Can't access missing value"
1281         if self._prop.classname == 'user':
1282             klass = HTMLUser
1283         else:
1284             klass = HTMLItem
1285         i = klass(self._client, self._prop.classname, self._value)
1286         return getattr(i, attr)
1288     def plain(self, escape=0):
1289         ''' Render a "plain" representation of the property
1290         '''
1291         self.view_check()
1293         if self._value is None:
1294             return ''
1295         linkcl = self._db.classes[self._prop.classname]
1296         k = linkcl.labelprop(1)
1297         value = str(linkcl.get(self._value, k))
1298         if escape:
1299             value = cgi.escape(value)
1300         return value
1302     def field(self, showid=0, size=None):
1303         ''' Render a form edit field for the property
1305             If not editable, just display the value via plain().
1306         '''
1307         self.view_check()
1309         if not self.is_edit_ok():
1310             return self.plain()
1312         # edit field
1313         linkcl = self._db.getclass(self._prop.classname)
1314         if self._value is None:
1315             value = ''
1316         else:
1317             k = linkcl.getkey()
1318             if k:
1319                 label = linkcl.get(self._value, k)
1320             else:
1321                 label = self._value
1322             value = cgi.escape(str(self._value))
1323             value = '&quot;'.join(value.split('"'))
1324         return '<input name="%s" value="%s" size="%s">'%(self._formname,
1325             label, size)
1327     def menu(self, size=None, height=None, showid=0, additional=[],
1328             sort_on=None, **conditions):
1329         ''' Render a form select list for this property
1331             If not editable, just display the value via plain().
1332         '''
1333         self.view_check()
1335         if not self.is_edit_ok():
1336             return self.plain()
1338         value = self._value
1340         linkcl = self._db.getclass(self._prop.classname)
1341         l = ['<select name="%s">'%self._formname]
1342         k = linkcl.labelprop(1)
1343         s = ''
1344         if value is None:
1345             s = 'selected="selected" '
1346         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1347         if linkcl.getprops().has_key('order'):  
1348             sort_on = ('+', 'order')
1349         else:  
1350             if sort_on is None:
1351                 sort_on = ('+', linkcl.labelprop())
1352             else:
1353                 sort_on = ('+', sort_on)
1354         options = linkcl.filter(None, conditions, sort_on, (None, None))
1356         # make sure we list the current value if it's retired
1357         if self._value and self._value not in options:
1358             options.insert(0, self._value)
1360         for optionid in options:
1361             # get the option value, and if it's None use an empty string
1362             option = linkcl.get(optionid, k) or ''
1364             # figure if this option is selected
1365             s = ''
1366             if value in [optionid, option]:
1367                 s = 'selected="selected" '
1369             # figure the label
1370             if showid:
1371                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1372             else:
1373                 lab = option
1375             # truncate if it's too long
1376             if size is not None and len(lab) > size:
1377                 lab = lab[:size-3] + '...'
1378             if additional:
1379                 m = []
1380                 for propname in additional:
1381                     m.append(linkcl.get(optionid, propname))
1382                 lab = lab + ' (%s)'%', '.join(map(str, m))
1384             # and generate
1385             lab = cgi.escape(lab)
1386             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1387         l.append('</select>')
1388         return '\n'.join(l)
1389 #    def checklist(self, ...)
1391 class MultilinkHTMLProperty(HTMLProperty):
1392     ''' Multilink HTMLProperty
1394         Also be iterable, returning a wrapper object like the Link case for
1395         each entry in the multilink.
1396     '''
1397     def __init__(self, *args, **kwargs):
1398         HTMLProperty.__init__(self, *args, **kwargs)
1399         if self._value:
1400             sortfun = make_sort_function(self._db, self._prop.classname)
1401             self._value.sort(sortfun)
1402     
1403     def __len__(self):
1404         ''' length of the multilink '''
1405         return len(self._value)
1407     def __getattr__(self, attr):
1408         ''' no extended attribute accesses make sense here '''
1409         raise AttributeError, attr
1411     def __getitem__(self, num):
1412         ''' iterate and return a new HTMLItem
1413         '''
1414        #print 'Multi.getitem', (self, num)
1415         value = self._value[num]
1416         if self._prop.classname == 'user':
1417             klass = HTMLUser
1418         else:
1419             klass = HTMLItem
1420         return klass(self._client, self._prop.classname, value)
1422     def __contains__(self, value):
1423         ''' Support the "in" operator. We have to make sure the passed-in
1424             value is a string first, not a *HTMLProperty.
1425         '''
1426         return str(value) in self._value
1428     def reverse(self):
1429         ''' return the list in reverse order
1430         '''
1431         l = self._value[:]
1432         l.reverse()
1433         if self._prop.classname == 'user':
1434             klass = HTMLUser
1435         else:
1436             klass = HTMLItem
1437         return [klass(self._client, self._prop.classname, value) for value in l]
1439     def plain(self, escape=0):
1440         ''' Render a "plain" representation of the property
1441         '''
1442         self.view_check()
1444         linkcl = self._db.classes[self._prop.classname]
1445         k = linkcl.labelprop(1)
1446         labels = []
1447         for v in self._value:
1448             labels.append(linkcl.get(v, k))
1449         value = ', '.join(labels)
1450         if escape:
1451             value = cgi.escape(value)
1452         return value
1454     def field(self, size=30, showid=0):
1455         ''' Render a form edit field for the property
1457             If not editable, just display the value via plain().
1458         '''
1459         self.view_check()
1461         if not self.is_edit_ok():
1462             return self.plain()
1464         linkcl = self._db.getclass(self._prop.classname)
1465         value = self._value[:]
1466         # map the id to the label property
1467         if not linkcl.getkey():
1468             showid=1
1469         if not showid:
1470             k = linkcl.labelprop(1)
1471             value = [linkcl.get(v, k) for v in value]
1472         value = cgi.escape(','.join(value))
1473         return self.input(name=self._formname,size=size,value=value)
1475     def menu(self, size=None, height=None, showid=0, additional=[],
1476             sort_on=None, **conditions):
1477         ''' Render a form select list for this property
1479             If not editable, just display the value via plain().
1480         '''
1481         self.view_check()
1483         if not self.is_edit_ok():
1484             return self.plain()
1486         value = self._value
1488         linkcl = self._db.getclass(self._prop.classname)
1489         if sort_on is None:
1490             sort_on = ('+', find_sort_key(linkcl))
1491         else:
1492             sort_on = ('+', sort_on)
1493         options = linkcl.filter(None, conditions, sort_on)
1494         height = height or min(len(options), 7)
1495         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1496         k = linkcl.labelprop(1)
1498         # make sure we list the current values if they're retired
1499         for val in value:
1500             if val not in options:
1501                 options.insert(0, val)
1503         for optionid in options:
1504             # get the option value, and if it's None use an empty string
1505             option = linkcl.get(optionid, k) or ''
1507             # figure if this option is selected
1508             s = ''
1509             if optionid in value or option in value:
1510                 s = 'selected="selected" '
1512             # figure the label
1513             if showid:
1514                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1515             else:
1516                 lab = option
1517             # truncate if it's too long
1518             if size is not None and len(lab) > size:
1519                 lab = lab[:size-3] + '...'
1520             if additional:
1521                 m = []
1522                 for propname in additional:
1523                     m.append(linkcl.get(optionid, propname))
1524                 lab = lab + ' (%s)'%', '.join(m)
1526             # and generate
1527             lab = cgi.escape(lab)
1528             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1529                 lab))
1530         l.append('</select>')
1531         return '\n'.join(l)
1533 # set the propclasses for HTMLItem
1534 propclasses = (
1535     (hyperdb.String, StringHTMLProperty),
1536     (hyperdb.Number, NumberHTMLProperty),
1537     (hyperdb.Boolean, BooleanHTMLProperty),
1538     (hyperdb.Date, DateHTMLProperty),
1539     (hyperdb.Interval, IntervalHTMLProperty),
1540     (hyperdb.Password, PasswordHTMLProperty),
1541     (hyperdb.Link, LinkHTMLProperty),
1542     (hyperdb.Multilink, MultilinkHTMLProperty),
1545 def make_sort_function(db, classname, sort_on=None):
1546     '''Make a sort function for a given class
1547     '''
1548     linkcl = db.getclass(classname)
1549     if sort_on is None:
1550         sort_on = find_sort_key(linkcl)
1551     def sortfunc(a, b):
1552         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1553     return sortfunc
1555 def find_sort_key(linkcl):
1556     if linkcl.getprops().has_key('order'):
1557         return 'order'
1558     else:
1559         return linkcl.labelprop()
1561 def handleListCGIValue(value):
1562     ''' Value is either a single item or a list of items. Each item has a
1563         .value that we're actually interested in.
1564     '''
1565     if isinstance(value, type([])):
1566         return [value.value for value in value]
1567     else:
1568         value = value.value.strip()
1569         if not value:
1570             return []
1571         return value.split(',')
1573 class ShowDict:
1574     ''' A convenience access to the :columns index parameters
1575     '''
1576     def __init__(self, columns):
1577         self.columns = {}
1578         for col in columns:
1579             self.columns[col] = 1
1580     def __getitem__(self, name):
1581         return self.columns.has_key(name)
1583 class HTMLRequest(HTMLInputMixin):
1584     ''' The *request*, holding the CGI form and environment.
1586         "form" the CGI form as a cgi.FieldStorage
1587         "env" the CGI environment variables
1588         "base" the base URL for this instance
1589         "user" a HTMLUser instance for this user
1590         "classname" the current classname (possibly None)
1591         "template" the current template (suffix, also possibly None)
1593         Index args:
1594         "columns" dictionary of the columns to display in an index page
1595         "show" a convenience access to columns - request/show/colname will
1596                be true if the columns should be displayed, false otherwise
1597         "sort" index sort column (direction, column name)
1598         "group" index grouping property (direction, column name)
1599         "filter" properties to filter the index on
1600         "filterspec" values to filter the index on
1601         "search_text" text to perform a full-text search on for an index
1603     '''
1604     def __init__(self, client):
1605         # _client is needed by HTMLInputMixin
1606         self._client = self.client = client
1608         # easier access vars
1609         self.form = client.form
1610         self.env = client.env
1611         self.base = client.base
1612         self.user = HTMLUser(client, 'user', client.userid)
1614         # store the current class name and action
1615         self.classname = client.classname
1616         self.template = client.template
1618         # the special char to use for special vars
1619         self.special_char = '@'
1621         HTMLInputMixin.__init__(self)
1623         self._post_init()
1625     def _post_init(self):
1626         ''' Set attributes based on self.form
1627         '''
1628         # extract the index display information from the form
1629         self.columns = []
1630         for name in ':columns @columns'.split():
1631             if self.form.has_key(name):
1632                 self.special_char = name[0]
1633                 self.columns = handleListCGIValue(self.form[name])
1634                 break
1635         self.show = ShowDict(self.columns)
1637         # sorting
1638         self.sort = (None, None)
1639         for name in ':sort @sort'.split():
1640             if self.form.has_key(name):
1641                 self.special_char = name[0]
1642                 sort = self.form[name].value
1643                 if sort.startswith('-'):
1644                     self.sort = ('-', sort[1:])
1645                 else:
1646                     self.sort = ('+', sort)
1647                 if self.form.has_key(self.special_char+'sortdir'):
1648                     self.sort = ('-', self.sort[1])
1650         # grouping
1651         self.group = (None, None)
1652         for name in ':group @group'.split():
1653             if self.form.has_key(name):
1654                 self.special_char = name[0]
1655                 group = self.form[name].value
1656                 if group.startswith('-'):
1657                     self.group = ('-', group[1:])
1658                 else:
1659                     self.group = ('+', group)
1660                 if self.form.has_key(self.special_char+'groupdir'):
1661                     self.group = ('-', self.group[1])
1663         # filtering
1664         self.filter = []
1665         for name in ':filter @filter'.split():
1666             if self.form.has_key(name):
1667                 self.special_char = name[0]
1668                 self.filter = handleListCGIValue(self.form[name])
1670         self.filterspec = {}
1671         db = self.client.db
1672         if self.classname is not None:
1673             props = db.getclass(self.classname).getprops()
1674             for name in self.filter:
1675                 if not self.form.has_key(name):
1676                     continue
1677                 prop = props[name]
1678                 fv = self.form[name]
1679                 if (isinstance(prop, hyperdb.Link) or
1680                         isinstance(prop, hyperdb.Multilink)):
1681                     self.filterspec[name] = lookupIds(db, prop,
1682                         handleListCGIValue(fv))
1683                 else:
1684                     if isinstance(fv, type([])):
1685                         self.filterspec[name] = [v.value for v in fv]
1686                     else:
1687                         self.filterspec[name] = fv.value
1689         # full-text search argument
1690         self.search_text = None
1691         for name in ':search_text @search_text'.split():
1692             if self.form.has_key(name):
1693                 self.special_char = name[0]
1694                 self.search_text = self.form[name].value
1696         # pagination - size and start index
1697         # figure batch args
1698         self.pagesize = 50
1699         for name in ':pagesize @pagesize'.split():
1700             if self.form.has_key(name):
1701                 self.special_char = name[0]
1702                 self.pagesize = int(self.form[name].value)
1704         self.startwith = 0
1705         for name in ':startwith @startwith'.split():
1706             if self.form.has_key(name):
1707                 self.special_char = name[0]
1708                 self.startwith = int(self.form[name].value)
1710     def updateFromURL(self, url):
1711         ''' Parse the URL for query args, and update my attributes using the
1712             values.
1713         ''' 
1714         env = {'QUERY_STRING': url}
1715         self.form = cgi.FieldStorage(environ=env)
1717         self._post_init()
1719     def update(self, kwargs):
1720         ''' Update my attributes using the keyword args
1721         '''
1722         self.__dict__.update(kwargs)
1723         if kwargs.has_key('columns'):
1724             self.show = ShowDict(self.columns)
1726     def description(self):
1727         ''' Return a description of the request - handle for the page title.
1728         '''
1729         s = [self.client.db.config.TRACKER_NAME]
1730         if self.classname:
1731             if self.client.nodeid:
1732                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1733             else:
1734                 if self.template == 'item':
1735                     s.append('- new %s'%self.classname)
1736                 elif self.template == 'index':
1737                     s.append('- %s index'%self.classname)
1738                 else:
1739                     s.append('- %s %s'%(self.classname, self.template))
1740         else:
1741             s.append('- home')
1742         return ' '.join(s)
1744     def __str__(self):
1745         d = {}
1746         d.update(self.__dict__)
1747         f = ''
1748         for k in self.form.keys():
1749             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1750         d['form'] = f
1751         e = ''
1752         for k,v in self.env.items():
1753             e += '\n     %r=%r'%(k, v)
1754         d['env'] = e
1755         return '''
1756 form: %(form)s
1757 base: %(base)r
1758 classname: %(classname)r
1759 template: %(template)r
1760 columns: %(columns)r
1761 sort: %(sort)r
1762 group: %(group)r
1763 filter: %(filter)r
1764 search_text: %(search_text)r
1765 pagesize: %(pagesize)r
1766 startwith: %(startwith)r
1767 env: %(env)s
1768 '''%d
1770     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1771             filterspec=1):
1772         ''' return the current index args as form elements '''
1773         l = []
1774         sc = self.special_char
1775         s = self.input(type="hidden",name="%s",value="%s")
1776         if columns and self.columns:
1777             l.append(s%(sc+'columns', ','.join(self.columns)))
1778         if sort and self.sort[1] is not None:
1779             if self.sort[0] == '-':
1780                 val = '-'+self.sort[1]
1781             else:
1782                 val = self.sort[1]
1783             l.append(s%(sc+'sort', val))
1784         if group and self.group[1] is not None:
1785             if self.group[0] == '-':
1786                 val = '-'+self.group[1]
1787             else:
1788                 val = self.group[1]
1789             l.append(s%(sc+'group', val))
1790         if filter and self.filter:
1791             l.append(s%(sc+'filter', ','.join(self.filter)))
1792         if filterspec:
1793             for k,v in self.filterspec.items():
1794                 if type(v) == type([]):
1795                     l.append(s%(k, ','.join(v)))
1796                 else:
1797                     l.append(s%(k, v))
1798         if self.search_text:
1799             l.append(s%(sc+'search_text', self.search_text))
1800         l.append(s%(sc+'pagesize', self.pagesize))
1801         l.append(s%(sc+'startwith', self.startwith))
1802         return '\n'.join(l)
1804     def indexargs_url(self, url, args):
1805         ''' Embed the current index args in a URL
1806         '''
1807         sc = self.special_char
1808         l = ['%s=%s'%(k,v) for k,v in args.items()]
1810         # pull out the special values (prefixed by @ or :)
1811         specials = {}
1812         for key in args.keys():
1813             if key[0] in '@:':
1814                 specials[key[1:]] = args[key]
1816         # ok, now handle the specials we received in the request
1817         if self.columns and not specials.has_key('columns'):
1818             l.append(sc+'columns=%s'%(','.join(self.columns)))
1819         if self.sort[1] is not None and not specials.has_key('sort'):
1820             if self.sort[0] == '-':
1821                 val = '-'+self.sort[1]
1822             else:
1823                 val = self.sort[1]
1824             l.append(sc+'sort=%s'%val)
1825         if self.group[1] is not None and not specials.has_key('group'):
1826             if self.group[0] == '-':
1827                 val = '-'+self.group[1]
1828             else:
1829                 val = self.group[1]
1830             l.append(sc+'group=%s'%val)
1831         if self.filter and not specials.has_key('filter'):
1832             l.append(sc+'filter=%s'%(','.join(self.filter)))
1833         if self.search_text and not specials.has_key('search_text'):
1834             l.append(sc+'search_text=%s'%self.search_text)
1835         if not specials.has_key('pagesize'):
1836             l.append(sc+'pagesize=%s'%self.pagesize)
1837         if not specials.has_key('startwith'):
1838             l.append(sc+'startwith=%s'%self.startwith)
1840         # finally, the remainder of the filter args in the request
1841         for k,v in self.filterspec.items():
1842             if not args.has_key(k):
1843                 if type(v) == type([]):
1844                     l.append('%s=%s'%(k, ','.join(v)))
1845                 else:
1846                     l.append('%s=%s'%(k, v))
1847         return '%s?%s'%(url, '&'.join(l))
1848     indexargs_href = indexargs_url
1850     def base_javascript(self):
1851         return '''
1852 <script type="text/javascript">
1853 submitted = false;
1854 function submit_once() {
1855     if (submitted) {
1856         alert("Your request is being processed.\\nPlease be patient.");
1857         event.returnValue = 0;    // work-around for IE
1858         return 0;
1859     }
1860     submitted = true;
1861     return 1;
1864 function help_window(helpurl, width, height) {
1865     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1867 </script>
1868 '''%self.base
1870     def batch(self):
1871         ''' Return a batch object for results from the "current search"
1872         '''
1873         filterspec = self.filterspec
1874         sort = self.sort
1875         group = self.group
1877         # get the list of ids we're batching over
1878         klass = self.client.db.getclass(self.classname)
1879         if self.search_text:
1880             matches = self.client.db.indexer.search(
1881                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1882         else:
1883             matches = None
1884         l = klass.filter(matches, filterspec, sort, group)
1886         # return the batch object, using IDs only
1887         return Batch(self.client, l, self.pagesize, self.startwith,
1888             classname=self.classname)
1890 # extend the standard ZTUtils Batch object to remove dependency on
1891 # Acquisition and add a couple of useful methods
1892 class Batch(ZTUtils.Batch):
1893     ''' Use me to turn a list of items, or item ids of a given class, into a
1894         series of batches.
1896         ========= ========================================================
1897         Parameter  Usage
1898         ========= ========================================================
1899         sequence  a list of HTMLItems or item ids
1900         classname if sequence is a list of ids, this is the class of item
1901         size      how big to make the sequence.
1902         start     where to start (0-indexed) in the sequence.
1903         end       where to end (0-indexed) in the sequence.
1904         orphan    if the next batch would contain less items than this
1905                   value, then it is combined with this batch
1906         overlap   the number of items shared between adjacent batches
1907         ========= ========================================================
1909         Attributes: Note that the "start" attribute, unlike the
1910         argument, is a 1-based index (I know, lame).  "first" is the
1911         0-based index.  "length" is the actual number of elements in
1912         the batch.
1914         "sequence_length" is the length of the original, unbatched, sequence.
1915     '''
1916     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1917             overlap=0, classname=None):
1918         self.client = client
1919         self.last_index = self.last_item = None
1920         self.current_item = None
1921         self.classname = classname
1922         self.sequence_length = len(sequence)
1923         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1924             overlap)
1926     # overwrite so we can late-instantiate the HTMLItem instance
1927     def __getitem__(self, index):
1928         if index < 0:
1929             if index + self.end < self.first: raise IndexError, index
1930             return self._sequence[index + self.end]
1931         
1932         if index >= self.length:
1933             raise IndexError, index
1935         # move the last_item along - but only if the fetched index changes
1936         # (for some reason, index 0 is fetched twice)
1937         if index != self.last_index:
1938             self.last_item = self.current_item
1939             self.last_index = index
1941         item = self._sequence[index + self.first]
1942         if self.classname:
1943             # map the item ids to instances
1944             if self.classname == 'user':
1945                 item = HTMLUser(self.client, self.classname, item)
1946             else:
1947                 item = HTMLItem(self.client, self.classname, item)
1948         self.current_item = item
1949         return item
1951     def propchanged(self, property):
1952         ''' Detect if the property marked as being the group property
1953             changed in the last iteration fetch
1954         '''
1955         if (self.last_item is None or
1956                 self.last_item[property] != self.current_item[property]):
1957             return 1
1958         return 0
1960     # override these 'cos we don't have access to acquisition
1961     def previous(self):
1962         if self.start == 1:
1963             return None
1964         return Batch(self.client, self._sequence, self._size,
1965             self.first - self._size + self.overlap, 0, self.orphan,
1966             self.overlap)
1968     def next(self):
1969         try:
1970             self._sequence[self.end]
1971         except IndexError:
1972             return None
1973         return Batch(self.client, self._sequence, self._size,
1974             self.end - self.overlap, 0, self.orphan, self.overlap)
1976 class TemplatingUtils:
1977     ''' Utilities for templating
1978     '''
1979     def __init__(self, client):
1980         self.client = client
1981     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1982         return Batch(self.client, sequence, size, start, end, orphan,
1983             overlap)