Code

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