Code

handle Boolean values in history HTML display
[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 = stime
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              'template': self,
199         }
200         # add in the item if there is one
201         if client.nodeid:
202             if classname == 'user':
203                 c['context'] = HTMLUser(client, classname, client.nodeid,
204                     anonymous=1)
205             else:
206                 c['context'] = HTMLItem(client, classname, client.nodeid,
207                     anonymous=1)
208         elif client.db.classes.has_key(classname):
209             if classname == 'user':
210                 c['context'] = HTMLUserClass(client, classname, anonymous=1)
211             else:
212                 c['context'] = HTMLClass(client, classname, anonymous=1)
213         return c
215     def render(self, client, classname, request, **options):
216         """Render this Page Template"""
218         if not self._v_cooked:
219             self._cook()
221         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
223         if self._v_errors:
224             raise PageTemplate.PTRuntimeError, \
225                 'Page Template %s has errors.'%self.id
227         # figure the context
228         classname = classname or client.classname
229         request = request or HTMLRequest(client)
230         c = self.getContext(client, classname, request)
231         c.update({'options': options})
233         # and go
234         output = StringIO.StringIO()
235         TALInterpreter(self._v_program, self.macros,
236             getEngine().getContext(c), output, tal=1, strictinsert=0)()
237         return output.getvalue()
239     def __repr__(self):
240         return '<Roundup PageTemplate %r>'%self.id
242 class HTMLDatabase:
243     ''' Return HTMLClasses for valid class fetches
244     '''
245     def __init__(self, client):
246         self._client = client
247         self._db = client.db
249         # we want config to be exposed
250         self.config = client.db.config
252     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
253         # check to see if we're actually accessing an item
254         m = desre.match(item)
255         if m:
256             cl = m.group('cl')
257             self._client.db.getclass(cl)
258             if cl == 'user':
259                 klass = HTMLUser
260             else:
261                 klass = HTMLItem
262             return klass(self._client, cl, m.group('id'))
263         else:
264             self._client.db.getclass(item)
265             if item == 'user':
266                 return HTMLUserClass(self._client, item)
267             return HTMLClass(self._client, item)
269     def __getattr__(self, attr):
270         try:
271             return self[attr]
272         except KeyError:
273             raise AttributeError, attr
275     def classes(self):
276         l = self._client.db.classes.keys()
277         l.sort()
278         m = []
279         for item in l:
280             if item == 'user':
281                 m.append(HTMLUserClass(self._client, item))
282             m.append(HTMLClass(self._client, item))
283         return m
285 def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('-?\d+')):
286     ''' "fail_ok" should be specified if we wish to pass through bad values
287         (most likely form values that we wish to represent back to the user)
288     '''
289     cl = db.getclass(prop.classname)
290     l = []
291     for entry in ids:
292         if num_re.match(entry):
293             l.append(entry)
294         else:
295             try:
296                 l.append(cl.lookup(entry))
297             except (TypeError, KeyError):
298                 if fail_ok:
299                     # pass through the bad value
300                     l.append(entry)
301     return l
303 def lookupKeys(linkcl, key, ids, num_re=re.compile('-?\d+')):
304     ''' Look up the "key" values for "ids" list - though some may already
305     be key values, not ids.
306     '''
307     l = []
308     for entry in ids:
309         if num_re.match(entry):
310             l.append(linkcl.get(entry, key))
311         else:
312             l.append(entry)
313     return l
315 class HTMLPermissions:
316     ''' Helpers that provide answers to commonly asked Permission questions.
317     '''
318     def is_edit_ok(self):
319         ''' Is the user allowed to Edit the current class?
320         '''
321         return self._db.security.hasPermission('Edit', self._client.userid,
322             self._classname)
324     def is_view_ok(self):
325         ''' Is the user allowed to View the current class?
326         '''
327         return self._db.security.hasPermission('View', self._client.userid,
328             self._classname)
330     def is_only_view_ok(self):
331         ''' Is the user only allowed to View (ie. not Edit) the current class?
332         '''
333         return self.is_view_ok() and not self.is_edit_ok()
335     def view_check(self):
336         ''' Raise the Unauthorised exception if the user's not permitted to
337             view this class.
338         '''
339         if not self.is_view_ok():
340             raise Unauthorised("view", self._classname)
342     def edit_check(self):
343         ''' Raise the Unauthorised exception if the user's not permitted to
344             edit this class.
345         '''
346         if not self.is_edit_ok():
347             raise Unauthorised("edit", self._classname)
349 def input_html4(**attrs):
350     """Generate an 'input' (html4) element with given attributes"""
351     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
353 def input_xhtml(**attrs):
354     """Generate an 'input' (xhtml) element with given attributes"""
355     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
357 class HTMLInputMixin:
358     ''' requires a _client property '''
359     def __init__(self):
360         html_version = 'html4'
361         if hasattr(self._client.instance.config, 'HTML_VERSION'):
362             html_version = self._client.instance.config.HTML_VERSION
363         if html_version == 'xhtml':
364             self.input = input_xhtml
365         else:
366             self.input = input_html4
368 class HTMLClass(HTMLInputMixin, HTMLPermissions):
369     ''' Accesses through a class (either through *class* or *db.<classname>*)
370     '''
371     def __init__(self, client, classname, anonymous=0):
372         self._client = client
373         self._db = client.db
374         self._anonymous = anonymous
376         # we want classname to be exposed, but _classname gives a
377         # consistent API for extending Class/Item
378         self._classname = self.classname = classname
379         self._klass = self._db.getclass(self.classname)
380         self._props = self._klass.getprops()
382         HTMLInputMixin.__init__(self)
384     def __repr__(self):
385         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
387     def __getitem__(self, item):
388         ''' return an HTMLProperty instance
389         '''
390        #print 'HTMLClass.getitem', (self, item)
392         # we don't exist
393         if item == 'id':
394             return None
396         # get the property
397         try:
398             prop = self._props[item]
399         except KeyError:
400             raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
402         # look up the correct HTMLProperty class
403         form = self._client.form
404         for klass, htmlklass in propclasses:
405             if not isinstance(prop, klass):
406                 continue
407             if form.has_key(item):
408                 if isinstance(prop, hyperdb.Multilink):
409                     value = lookupIds(self._db, prop,
410                         handleListCGIValue(form[item]), fail_ok=1)
411                 elif isinstance(prop, hyperdb.Link):
412                     value = form[item].value.strip()
413                     if value:
414                         value = lookupIds(self._db, prop, [value],
415                             fail_ok=1)[0]
416                     else:
417                         value = None
418                 else:
419                     value = form[item].value.strip() or None
420             else:
421                 if isinstance(prop, hyperdb.Multilink):
422                     value = []
423                 else:
424                     value = None
425             return htmlklass(self._client, self._classname, '', prop, item,
426                 value, self._anonymous)
428         # no good
429         raise KeyError, item
431     def __getattr__(self, attr):
432         ''' convenience access '''
433         try:
434             return self[attr]
435         except KeyError:
436             raise AttributeError, attr
438     def designator(self):
439         ''' Return this class' designator (classname) '''
440         return self._classname
442     def getItem(self, itemid, num_re=re.compile('-?\d+')):
443         ''' Get an item of this class by its item id.
444         '''
445         # make sure we're looking at an itemid
446         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
447             itemid = self._klass.lookup(itemid)
449         if self.classname == 'user':
450             klass = HTMLUser
451         else:
452             klass = HTMLItem
454         return klass(self._client, self.classname, itemid)
456     def properties(self, sort=1):
457         ''' Return HTMLProperty for all of this class' properties.
458         '''
459         l = []
460         for name, prop in self._props.items():
461             for klass, htmlklass in propclasses:
462                 if isinstance(prop, hyperdb.Multilink):
463                     value = []
464                 else:
465                     value = None
466                 if isinstance(prop, klass):
467                     l.append(htmlklass(self._client, self._classname, '',
468                         prop, name, value, self._anonymous))
469         if sort:
470             l.sort(lambda a,b:cmp(a._name, b._name))
471         return l
473     def list(self, sort_on=None):
474         ''' List all items in this class.
475         '''
476         if self.classname == 'user':
477             klass = HTMLUser
478         else:
479             klass = HTMLItem
481         # get the list and sort it nicely
482         l = self._klass.list()
483         sortfunc = make_sort_function(self._db, self.classname, sort_on)
484         l.sort(sortfunc)
486         l = [klass(self._client, self.classname, x) for x in l]
487         return l
489     def csv(self):
490         ''' Return the items of this class as a chunk of CSV text.
491         '''
492         if rcsv.error:
493             return rcsv.error
495         props = self.propnames()
496         s = StringIO.StringIO()
497         writer = rcsv.writer(s, rcsv.comma_separated)
498         writer.writerow(props)
499         for nodeid in self._klass.list():
500             l = []
501             for name in props:
502                 value = self._klass.get(nodeid, name)
503                 if value is None:
504                     l.append('')
505                 elif isinstance(value, type([])):
506                     l.append(':'.join(map(str, value)))
507                 else:
508                     l.append(str(self._klass.get(nodeid, name)))
509             writer.writerow(l)
510         return s.getvalue()
512     def propnames(self):
513         ''' Return the list of the names of the properties of this class.
514         '''
515         idlessprops = self._klass.getprops(protected=0).keys()
516         idlessprops.sort()
517         return ['id'] + idlessprops
519     def filter(self, request=None, filterspec={}, sort=(None,None),
520             group=(None,None)):
521         ''' Return a list of items from this class, filtered and sorted
522             by the current requested filterspec/filter/sort/group args
524             "request" takes precedence over the other three arguments.
525         '''
526         if request is not None:
527             filterspec = request.filterspec
528             sort = request.sort
529             group = request.group
530         if self.classname == 'user':
531             klass = HTMLUser
532         else:
533             klass = HTMLItem
534         l = [klass(self._client, self.classname, x)
535              for x in self._klass.filter(None, filterspec, sort, group)]
536         return l
538     def classhelp(self, properties=None, label='(list)', width='500',
539             height='400', property=''):
540         ''' Pop up a javascript window with class help
542             This generates a link to a popup window which displays the 
543             properties indicated by "properties" of the class named by
544             "classname". The "properties" should be a comma-separated list
545             (eg. 'id,name,description'). Properties defaults to all the
546             properties of a class (excluding id, creator, created and
547             activity).
549             You may optionally override the label displayed, the width and
550             height. The popup window will be resizable and scrollable.
552             If the "property" arg is given, it's passed through to the
553             javascript help_window function.
554         '''
555         if properties is None:
556             properties = self._klass.getprops(protected=0).keys()
557             properties.sort()
558             properties = ','.join(properties)
559         if property:
560             property = '&amp;property=%s'%property
561         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
562             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
563             \'%s\')">%s</a>'%(self.classname, properties, property, width,
564             height, label)
566     def submit(self, label="Submit New Entry"):
567         ''' Generate a submit button (and action hidden element)
568         '''
569         self.view_check()
570         if self.is_edit_ok():
571             return self.input(type="hidden",name="@action",value="new") + \
572                    '\n' + self.input(type="submit",name="submit",value=label)
573         return ''
575     def history(self):
576         self.view_check()
577         return 'New node - no history'
579     def renderWith(self, name, **kwargs):
580         ''' Render this class with the given template.
581         '''
582         # create a new request and override the specified args
583         req = HTMLRequest(self._client)
584         req.classname = self.classname
585         req.update(kwargs)
587         # new template, using the specified classname and request
588         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
590         # use our fabricated request
591         args = {
592             'ok_message': self._client.ok_message,
593             'error_message': self._client.error_message
594         }
595         return pt.render(self._client, self.classname, req, **args)
597 class HTMLItem(HTMLInputMixin, HTMLPermissions):
598     ''' Accesses through an *item*
599     '''
600     def __init__(self, client, classname, nodeid, anonymous=0):
601         self._client = client
602         self._db = client.db
603         self._classname = classname
604         self._nodeid = nodeid
605         self._klass = self._db.getclass(classname)
606         self._props = self._klass.getprops()
608         # do we prefix the form items with the item's identification?
609         self._anonymous = anonymous
611         HTMLInputMixin.__init__(self)
613     def __repr__(self):
614         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
615             self._nodeid)
617     def __getitem__(self, item):
618         ''' return an HTMLProperty instance
619         '''
620         #print 'HTMLItem.getitem', (self, item)
621         if item == 'id':
622             return self._nodeid
624         # get the property
625         prop = self._props[item]
627         # get the value, handling missing values
628         value = None
629         if int(self._nodeid) > 0:
630             value = self._klass.get(self._nodeid, item, None)
631         if value is None:
632             if isinstance(self._props[item], hyperdb.Multilink):
633                 value = []
635         # look up the correct HTMLProperty class
636         for klass, htmlklass in propclasses:
637             if isinstance(prop, klass):
638                 return htmlklass(self._client, self._classname,
639                     self._nodeid, prop, item, value, self._anonymous)
641         raise KeyError, item
643     def __getattr__(self, attr):
644         ''' convenience access to properties '''
645         try:
646             return self[attr]
647         except KeyError:
648             raise AttributeError, attr
650     def designator(self):
651         """Return this item's designator (classname + id)."""
652         return '%s%s'%(self._classname, self._nodeid)
654     def is_retired(self):
655         """Is this item retired?"""
656         return self._klass.is_retired(self._nodeid)
657     
658     def submit(self, label="Submit Changes"):
659         """Generate a submit button.
661         Also sneak in the lastactivity and action hidden elements.
662         """
663         return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
664                self.input(type="hidden", name="@action", value="edit") + '\n' + \
665                self.input(type="submit", name="submit", value=label)
667     def journal(self, direction='descending'):
668         ''' Return a list of HTMLJournalEntry instances.
669         '''
670         # XXX do this
671         return []
673     def history(self, direction='descending', dre=re.compile('\d+')):
674         self.view_check()
676         l = ['<table class="history">'
677              '<tr><th colspan="4" class="header">',
678              _('History'),
679              '</th></tr><tr>',
680              _('<th>Date</th>'),
681              _('<th>User</th>'),
682              _('<th>Action</th>'),
683              _('<th>Args</th>'),
684             '</tr>']
685         current = {}
686         comments = {}
687         history = self._klass.history(self._nodeid)
688         history.sort()
689         timezone = self._db.getUserTimezone()
690         if direction == 'descending':
691             history.reverse()
692             # pre-load the history with the current state
693             for prop_n in self._props.keys():
694                 prop = self[prop_n]
695                 if not isinstance(prop, HTMLProperty):
696                     continue
697                 current[prop_n] = prop.plain()
698                 # make link if hrefable
699                 if (self._props.has_key(prop_n) and
700                         isinstance(self._props[prop_n], hyperdb.Link)):
701                     classname = self._props[prop_n].classname
702                     try:
703                         template = find_template(self._db.config.TEMPLATES,
704                             classname, 'item')
705                         if template[1].startswith('_generic'):
706                             raise NoTemplate, 'not really...'
707                     except NoTemplate:
708                         pass
709                     else:
710                         id = self._klass.get(self._nodeid, prop_n, None)
711                         current[prop_n] = '<a href="%s%s">%s</a>'%(
712                             classname, id, current[prop_n])
713  
714         for id, evt_date, user, action, args in history:
715             date_s = str(evt_date.local(timezone)).replace("."," ")
716             arg_s = ''
717             if action == 'link' and type(args) == type(()):
718                 if len(args) == 3:
719                     linkcl, linkid, key = args
720                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
721                         linkcl, linkid, key)
722                 else:
723                     arg_s = str(args)
725             elif action == 'unlink' and type(args) == type(()):
726                 if len(args) == 3:
727                     linkcl, linkid, key = args
728                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
729                         linkcl, linkid, key)
730                 else:
731                     arg_s = str(args)
733             elif type(args) == type({}):
734                 cell = []
735                 for k in args.keys():
736                     # try to get the relevant property and treat it
737                     # specially
738                     try:
739                         prop = self._props[k]
740                     except KeyError:
741                         prop = None
742                     if prop is None:
743                         # property no longer exists
744                         comments['no_exist'] = _('''<em>The indicated property
745                             no longer exists</em>''')
746                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
747                         continue
749                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
750                             isinstance(prop, hyperdb.Link)):
751                         # figure what the link class is
752                         classname = prop.classname
753                         try:
754                             linkcl = self._db.getclass(classname)
755                         except KeyError:
756                             labelprop = None
757                             comments[classname] = _('''The linked class
758                                 %(classname)s no longer exists''')%locals()
759                         labelprop = linkcl.labelprop(1)
760                         try:
761                             template = find_template(self._db.config.TEMPLATES,
762                                 classname, 'item')
763                             if template[1].startswith('_generic'):
764                                 raise NoTemplate, 'not really...'
765                             hrefable = 1
766                         except NoTemplate:
767                             hrefable = 0
769                     if isinstance(prop, hyperdb.Multilink) and args[k]:
770                         ml = []
771                         for linkid in args[k]:
772                             if isinstance(linkid, type(())):
773                                 sublabel = linkid[0] + ' '
774                                 linkids = linkid[1]
775                             else:
776                                 sublabel = ''
777                                 linkids = [linkid]
778                             subml = []
779                             for linkid in linkids:
780                                 label = classname + linkid
781                                 # if we have a label property, try to use it
782                                 # TODO: test for node existence even when
783                                 # there's no labelprop!
784                                 try:
785                                     if labelprop is not None and \
786                                             labelprop != 'id':
787                                         label = linkcl.get(linkid, labelprop)
788                                 except IndexError:
789                                     comments['no_link'] = _('''<strike>The
790                                         linked node no longer
791                                         exists</strike>''')
792                                     subml.append('<strike>%s</strike>'%label)
793                                 else:
794                                     if hrefable:
795                                         subml.append('<a href="%s%s">%s</a>'%(
796                                             classname, linkid, label))
797                                     else:
798                                         subml.append(label)
799                             ml.append(sublabel + ', '.join(subml))
800                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
801                     elif isinstance(prop, hyperdb.Link) and args[k]:
802                         label = classname + args[k]
803                         # if we have a label property, try to use it
804                         # TODO: test for node existence even when
805                         # there's no labelprop!
806                         if labelprop is not None and labelprop != 'id':
807                             try:
808                                 label = linkcl.get(args[k], labelprop)
809                             except IndexError:
810                                 comments['no_link'] = _('''<strike>The
811                                     linked node no longer
812                                     exists</strike>''')
813                                 cell.append(' <strike>%s</strike>,\n'%label)
814                                 # "flag" this is done .... euwww
815                                 label = None
816                         if label is not None:
817                             if hrefable:
818                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
819                             else:
820                                 old = label;
821                             cell.append('%s: %s' % (k,old))
822                             if current.has_key(k):
823                                 cell[-1] += ' -> %s'%current[k]
824                                 current[k] = old
826                     elif isinstance(prop, hyperdb.Date) and args[k]:
827                         d = date.Date(args[k]).local(timezone)
828                         cell.append('%s: %s'%(k, str(d)))
829                         if current.has_key(k):
830                             cell[-1] += ' -> %s' % current[k]
831                             current[k] = str(d)
833                     elif isinstance(prop, hyperdb.Interval) and args[k]:
834                         val = str(date.Interval(args[k]))
835                         cell.append('%s: %s'%(k, val))
836                         if current.has_key(k):
837                             cell[-1] += ' -> %s'%current[k]
838                             current[k] = val
840                     elif isinstance(prop, hyperdb.String) and args[k]:
841                         val = cgi.escape(args[k])
842                         cell.append('%s: %s'%(k, val))
843                         if current.has_key(k):
844                             cell[-1] += ' -> %s'%current[k]
845                             current[k] = val
847                     elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
848                         val = args[k] and 'Yes' or 'No'
849                         cell.append('%s: %s'%(k, val))
850                         if current.has_key(k):
851                             cell[-1] += ' -> %s'%current[k]
852                             current[k] = val
854                     elif not args[k]:
855                         if current.has_key(k):
856                             cell.append('%s: %s'%(k, current[k]))
857                             current[k] = '(no value)'
858                         else:
859                             cell.append('%s: (no value)'%k)
861                     else:
862                         cell.append('%s: %s'%(k, str(args[k])))
863                         if current.has_key(k):
864                             cell[-1] += ' -> %s'%current[k]
865                             current[k] = str(args[k])
867                 arg_s = '<br />'.join(cell)
868             else:
869                 # unkown event!!
870                 comments['unknown'] = _('''<strong><em>This event is not
871                     handled by the history display!</em></strong>''')
872                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
873             date_s = date_s.replace(' ', '&nbsp;')
874             # if the user's an itemid, figure the username (older journals
875             # have the username)
876             if dre.match(user):
877                 user = self._db.user.get(user, 'username')
878             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
879                 date_s, user, action, arg_s))
880         if comments:
881             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
882         for entry in comments.values():
883             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
884         l.append('</table>')
885         return '\n'.join(l)
887     def renderQueryForm(self):
888         ''' Render this item, which is a query, as a search form.
889         '''
890         # create a new request and override the specified args
891         req = HTMLRequest(self._client)
892         req.classname = self._klass.get(self._nodeid, 'klass')
893         name = self._klass.get(self._nodeid, 'name')
894         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
895             '&@queryname=%s'%urllib.quote(name))
897         # new template, using the specified classname and request
898         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
900         # use our fabricated request
901         return pt.render(self._client, req.classname, req)
903 class HTMLUserPermission:
905     def is_edit_ok(self):
906         ''' Is the user allowed to Edit the current class?
907             Also check whether this is the current user's info.
908         '''
909         return self._user_perm_check('Edit')
911     def is_view_ok(self):
912         ''' Is the user allowed to View the current class?
913             Also check whether this is the current user's info.
914         '''
915         return self._user_perm_check('View')
917     def _user_perm_check(self, type):
918         # some users may view / edit all users
919         s = self._db.security
920         userid = self._client.userid
921         if s.hasPermission(type, userid, self._classname):
922             return 1
924         # users may view their own info
925         is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
926         if getattr(self, '_nodeid', None) == userid and not is_anonymous:
927             return 1
929         # may anonymous users register?
930         if (is_anonymous and s.hasPermission('Web Registration', userid,
931                 self._classname)):
932             return 1
934         # nope, no access here
935         return 0
937 class HTMLUserClass(HTMLUserPermission, HTMLClass):
938     pass
940 class HTMLUser(HTMLUserPermission, HTMLItem):
941     ''' Accesses through the *user* (a special case of item)
942     '''
943     def __init__(self, client, classname, nodeid, anonymous=0):
944         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
945         self._default_classname = client.classname
947         # used for security checks
948         self._security = client.db.security
950     _marker = []
951     def hasPermission(self, permission, classname=_marker):
952         ''' Determine if the user has the Permission.
954             The class being tested defaults to the template's class, but may
955             be overidden for this test by suppling an alternate classname.
956         '''
957         if classname is self._marker:
958             classname = self._default_classname
959         return self._security.hasPermission(permission, self._nodeid, classname)
961 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
962     ''' String, Number, Date, Interval HTMLProperty
964         Has useful attributes:
966          _name  the name of the property
967          _value the value of the property if any
969         A wrapper object which may be stringified for the plain() behaviour.
970     '''
971     def __init__(self, client, classname, nodeid, prop, name, value,
972             anonymous=0):
973         self._client = client
974         self._db = client.db
975         self._classname = classname
976         self._nodeid = nodeid
977         self._prop = prop
978         self._value = value
979         self._anonymous = anonymous
980         self._name = name
981         if not anonymous:
982             self._formname = '%s%s@%s'%(classname, nodeid, name)
983         else:
984             self._formname = name
986         HTMLInputMixin.__init__(self)
988     def __repr__(self):
989         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
990             self._prop, self._value)
991     def __str__(self):
992         return self.plain()
993     def __cmp__(self, other):
994         if isinstance(other, HTMLProperty):
995             return cmp(self._value, other._value)
996         return cmp(self._value, other)
998     def is_edit_ok(self):
999         ''' Is the user allowed to Edit the current class?
1000         '''
1001         thing = HTMLDatabase(self._client)[self._classname]
1002         if self._nodeid:
1003             # this is a special-case for the User class where permission's
1004             # on a per-item basis :(
1005             thing = thing.getItem(self._nodeid)
1006         return thing.is_edit_ok()
1008     def is_view_ok(self):
1009         ''' Is the user allowed to View the current class?
1010         '''
1011         thing = HTMLDatabase(self._client)[self._classname]
1012         if self._nodeid:
1013             # this is a special-case for the User class where permission's
1014             # on a per-item basis :(
1015             thing = thing.getItem(self._nodeid)
1016         return thing.is_view_ok()
1018 class StringHTMLProperty(HTMLProperty):
1019     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
1020                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
1021                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
1022     def _hyper_repl(self, match):
1023         if match.group('url'):
1024             s = match.group('url')
1025             return '<a href="%s">%s</a>'%(s, s)
1026         elif match.group('email'):
1027             s = match.group('email')
1028             return '<a href="mailto:%s">%s</a>'%(s, s)
1029         else:
1030             s = match.group('item')
1031             s1 = match.group('class')
1032             s2 = match.group('id')
1033             try:
1034                 # make sure s1 is a valid tracker classname
1035                 cl = self._db.getclass(s1)
1036                 if not cl.hasnode(s2):
1037                     raise KeyError, 'oops'
1038                 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1039             except KeyError:
1040                 return '%s%s'%(s1, s2)
1042     def hyperlinked(self):
1043         ''' Render a "hyperlinked" version of the text '''
1044         return self.plain(hyperlink=1)
1046     def plain(self, escape=0, hyperlink=0):
1047         '''Render a "plain" representation of the property
1048             
1049         - "escape" turns on/off HTML quoting
1050         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1051           addresses and designators
1052         '''
1053         self.view_check()
1055         if self._value is None:
1056             return ''
1057         if escape:
1058             s = cgi.escape(str(self._value))
1059         else:
1060             s = str(self._value)
1061         if hyperlink:
1062             # no, we *must* escape this text
1063             if not escape:
1064                 s = cgi.escape(s)
1065             s = self.hyper_re.sub(self._hyper_repl, s)
1066         return s
1068     def stext(self, escape=0):
1069         ''' Render the value of the property as StructuredText.
1071             This requires the StructureText module to be installed separately.
1072         '''
1073         self.view_check()
1075         s = self.plain(escape=escape)
1076         if not StructuredText:
1077             return s
1078         return StructuredText(s,level=1,header=0)
1080     def field(self, size = 30):
1081         ''' Render the property as a field in HTML.
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 multiline(self, escape=0, rows=5, cols=40):
1099         ''' Render a multiline form edit field for the property.
1101             If not editable, just display the plain() value in a <pre> tag.
1102         '''
1103         self.view_check()
1105         if self._value is None:
1106             value = ''
1107         else:
1108             value = cgi.escape(str(self._value))
1110         if self.is_edit_ok():
1111             value = '&quot;'.join(value.split('"'))
1112             return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1113                 self._formname, rows, cols, value)
1115         return '<pre>%s</pre>'%self.plain()
1117     def email(self, escape=1):
1118         ''' Render the value of the property as an obscured email address
1119         '''
1120         self.view_check()
1122         if self._value is None:
1123             value = ''
1124         else:
1125             value = str(self._value)
1126         if value.find('@') != -1:
1127             name, domain = value.split('@')
1128             domain = ' '.join(domain.split('.')[:-1])
1129             name = name.replace('.', ' ')
1130             value = '%s at %s ...'%(name, domain)
1131         else:
1132             value = value.replace('.', ' ')
1133         if escape:
1134             value = cgi.escape(value)
1135         return value
1137 class PasswordHTMLProperty(HTMLProperty):
1138     def plain(self):
1139         ''' Render a "plain" representation of the property
1140         '''
1141         self.view_check()
1143         if self._value is None:
1144             return ''
1145         return _('*encrypted*')
1147     def field(self, size = 30):
1148         ''' Render a form edit field for the property.
1150             If not editable, just display the value via plain().
1151         '''
1152         self.view_check()
1154         if self.is_edit_ok():
1155             return self.input(type="password", name=self._formname, size=size)
1157         return self.plain()
1159     def confirm(self, size = 30):
1160         ''' Render a second form edit field for the property, used for 
1161             confirmation that the user typed the password correctly. Generates
1162             a field with name "@confirm@name".
1164             If not editable, display nothing.
1165         '''
1166         self.view_check()
1168         if self.is_edit_ok():
1169             return self.input(type="password",
1170                 name="@confirm@%s"%self._formname, size=size)
1172         return ''
1174 class NumberHTMLProperty(HTMLProperty):
1175     def plain(self):
1176         ''' Render a "plain" representation of the property
1177         '''
1178         self.view_check()
1180         return str(self._value)
1182     def field(self, size = 30):
1183         ''' Render a form edit field for the property.
1185             If not editable, just display the value via plain().
1186         '''
1187         self.view_check()
1189         if self._value is None:
1190             value = ''
1191         else:
1192             value = cgi.escape(str(self._value))
1194         if self.is_edit_ok():
1195             value = '&quot;'.join(value.split('"'))
1196             return self.input(name=self._formname,value=value,size=size)
1198         return self.plain()
1200     def __int__(self):
1201         ''' Return an int of me
1202         '''
1203         return int(self._value)
1205     def __float__(self):
1206         ''' Return a float of me
1207         '''
1208         return float(self._value)
1211 class BooleanHTMLProperty(HTMLProperty):
1212     def plain(self):
1213         ''' Render a "plain" representation of the property
1214         '''
1215         self.view_check()
1217         if self._value is None:
1218             return ''
1219         return self._value and "Yes" or "No"
1221     def field(self):
1222         ''' Render a form edit field for the property
1224             If not editable, just display the value via plain().
1225         '''
1226         self.view_check()
1228         if not self.is_edit_ok():
1229             return self.plain()
1231         checked = self._value and "checked" or ""
1232         if self._value:
1233             s = self.input(type="radio", name=self._formname, value="yes",
1234                 checked="checked")
1235             s += 'Yes'
1236             s +=self.input(type="radio", name=self._formname, value="no")
1237             s += 'No'
1238         else:
1239             s = self.input(type="radio", name=self._formname, value="yes")
1240             s += 'Yes'
1241             s +=self.input(type="radio", name=self._formname, value="no",
1242                 checked="checked")
1243             s += 'No'
1244         return s
1246 class DateHTMLProperty(HTMLProperty):
1247     def plain(self):
1248         ''' Render a "plain" representation of the property
1249         '''
1250         self.view_check()
1252         if self._value is None:
1253             return ''
1254         return str(self._value.local(self._db.getUserTimezone()))
1256     def now(self):
1257         ''' Return the current time.
1259             This is useful for defaulting a new value. Returns a
1260             DateHTMLProperty.
1261         '''
1262         self.view_check()
1264         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1265             self._prop, self._formname, date.Date('.'))
1267     def field(self, size = 30):
1268         ''' Render a form edit field for the property
1270             If not editable, just display the value via plain().
1271         '''
1272         self.view_check()
1274         if self._value is None:
1275             value = ''
1276         else:
1277             tz = self._db.getUserTimezone()
1278             value = cgi.escape(str(self._value.local(tz)))
1280         if self.is_edit_ok():
1281             value = '&quot;'.join(value.split('"'))
1282             return self.input(name=self._formname,value=value,size=size)
1283         
1284         return self.plain()
1286     def reldate(self, pretty=1):
1287         ''' Render the interval between the date and now.
1289             If the "pretty" flag is true, then make the display pretty.
1290         '''
1291         self.view_check()
1293         if not self._value:
1294             return ''
1296         # figure the interval
1297         interval = self._value - date.Date('.')
1298         if pretty:
1299             return interval.pretty()
1300         return str(interval)
1302     _marker = []
1303     def pretty(self, format=_marker):
1304         ''' Render the date in a pretty format (eg. month names, spaces).
1306             The format string is a standard python strftime format string.
1307             Note that if the day is zero, and appears at the start of the
1308             string, then it'll be stripped from the output. This is handy
1309             for the situatin when a date only specifies a month and a year.
1310         '''
1311         self.view_check()
1313         if format is not self._marker:
1314             return self._value.pretty(format)
1315         else:
1316             return self._value.pretty()
1318     def local(self, offset):
1319         ''' Return the date/time as a local (timezone offset) date/time.
1320         '''
1321         self.view_check()
1323         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1324             self._prop, self._formname, self._value.local(offset))
1326 class IntervalHTMLProperty(HTMLProperty):
1327     def plain(self):
1328         ''' Render a "plain" representation of the property
1329         '''
1330         self.view_check()
1332         if self._value is None:
1333             return ''
1334         return str(self._value)
1336     def pretty(self):
1337         ''' Render the interval in a pretty format (eg. "yesterday")
1338         '''
1339         self.view_check()
1341         return self._value.pretty()
1343     def field(self, size = 30):
1344         ''' Render a form edit field for the property
1346             If not editable, just display the value via plain().
1347         '''
1348         self.view_check()
1350         if self._value is None:
1351             value = ''
1352         else:
1353             value = cgi.escape(str(self._value))
1355         if is_edit_ok():
1356             value = '&quot;'.join(value.split('"'))
1357             return self.input(name=self._formname,value=value,size=size)
1359         return self.plain()
1361 class LinkHTMLProperty(HTMLProperty):
1362     ''' Link HTMLProperty
1363         Include the above as well as being able to access the class
1364         information. Stringifying the object itself results in the value
1365         from the item being displayed. Accessing attributes of this object
1366         result in the appropriate entry from the class being queried for the
1367         property accessed (so item/assignedto/name would look up the user
1368         entry identified by the assignedto property on item, and then the
1369         name property of that user)
1370     '''
1371     def __init__(self, *args, **kw):
1372         HTMLProperty.__init__(self, *args, **kw)
1373         # if we're representing a form value, then the -1 from the form really
1374         # should be a None
1375         if str(self._value) == '-1':
1376             self._value = None
1378     def __getattr__(self, attr):
1379         ''' return a new HTMLItem '''
1380        #print 'Link.getattr', (self, attr, self._value)
1381         if not self._value:
1382             raise AttributeError, "Can't access missing value"
1383         if self._prop.classname == 'user':
1384             klass = HTMLUser
1385         else:
1386             klass = HTMLItem
1387         i = klass(self._client, self._prop.classname, self._value)
1388         return getattr(i, attr)
1390     def plain(self, escape=0):
1391         ''' Render a "plain" representation of the property
1392         '''
1393         self.view_check()
1395         if self._value is None:
1396             return ''
1397         linkcl = self._db.classes[self._prop.classname]
1398         k = linkcl.labelprop(1)
1399         value = str(linkcl.get(self._value, k))
1400         if escape:
1401             value = cgi.escape(value)
1402         return value
1404     def field(self, showid=0, size=None):
1405         ''' Render a form edit field for the property
1407             If not editable, just display the value via plain().
1408         '''
1409         self.view_check()
1411         if not self.is_edit_ok():
1412             return self.plain()
1414         # edit field
1415         linkcl = self._db.getclass(self._prop.classname)
1416         if self._value is None:
1417             value = ''
1418         else:
1419             k = linkcl.getkey()
1420             if k:
1421                 value = linkcl.get(self._value, k)
1422             else:
1423                 value = self._value
1424             value = cgi.escape(str(value))
1425             value = '&quot;'.join(value.split('"'))
1426         return '<input name="%s" value="%s" size="%s">'%(self._formname,
1427             value, size)
1429     def menu(self, size=None, height=None, showid=0, additional=[],
1430             sort_on=None, **conditions):
1431         ''' Render a form select list for this property
1433             If not editable, just display the value via plain().
1434         '''
1435         self.view_check()
1437         if not self.is_edit_ok():
1438             return self.plain()
1440         value = self._value
1442         linkcl = self._db.getclass(self._prop.classname)
1443         l = ['<select name="%s">'%self._formname]
1444         k = linkcl.labelprop(1)
1445         s = ''
1446         if value is None:
1447             s = 'selected="selected" '
1448         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1449         if linkcl.getprops().has_key('order'):  
1450             sort_on = ('+', 'order')
1451         else:  
1452             if sort_on is None:
1453                 sort_on = ('+', linkcl.labelprop())
1454             else:
1455                 sort_on = ('+', sort_on)
1456         options = linkcl.filter(None, conditions, sort_on, (None, None))
1458         # make sure we list the current value if it's retired
1459         if self._value and self._value not in options:
1460             options.insert(0, self._value)
1462         for optionid in options:
1463             # get the option value, and if it's None use an empty string
1464             option = linkcl.get(optionid, k) or ''
1466             # figure if this option is selected
1467             s = ''
1468             if value in [optionid, option]:
1469                 s = 'selected="selected" '
1471             # figure the label
1472             if showid:
1473                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1474             else:
1475                 lab = option
1477             # truncate if it's too long
1478             if size is not None and len(lab) > size:
1479                 lab = lab[:size-3] + '...'
1480             if additional:
1481                 m = []
1482                 for propname in additional:
1483                     m.append(linkcl.get(optionid, propname))
1484                 lab = lab + ' (%s)'%', '.join(map(str, m))
1486             # and generate
1487             lab = cgi.escape(lab)
1488             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1489         l.append('</select>')
1490         return '\n'.join(l)
1491 #    def checklist(self, ...)
1493 class MultilinkHTMLProperty(HTMLProperty):
1494     ''' Multilink HTMLProperty
1496         Also be iterable, returning a wrapper object like the Link case for
1497         each entry in the multilink.
1498     '''
1499     def __init__(self, *args, **kwargs):
1500         HTMLProperty.__init__(self, *args, **kwargs)
1501         if self._value:
1502             sortfun = make_sort_function(self._db, self._prop.classname)
1503             self._value.sort(sortfun)
1504     
1505     def __len__(self):
1506         ''' length of the multilink '''
1507         return len(self._value)
1509     def __getattr__(self, attr):
1510         ''' no extended attribute accesses make sense here '''
1511         raise AttributeError, attr
1513     def __getitem__(self, num):
1514         ''' iterate and return a new HTMLItem
1515         '''
1516        #print 'Multi.getitem', (self, num)
1517         value = self._value[num]
1518         if self._prop.classname == 'user':
1519             klass = HTMLUser
1520         else:
1521             klass = HTMLItem
1522         return klass(self._client, self._prop.classname, value)
1524     def __contains__(self, value):
1525         ''' Support the "in" operator. We have to make sure the passed-in
1526             value is a string first, not a HTMLProperty.
1527         '''
1528         return str(value) in self._value
1530     def reverse(self):
1531         ''' return the list in reverse order
1532         '''
1533         l = self._value[:]
1534         l.reverse()
1535         if self._prop.classname == 'user':
1536             klass = HTMLUser
1537         else:
1538             klass = HTMLItem
1539         return [klass(self._client, self._prop.classname, value) for value in l]
1541     def plain(self, escape=0):
1542         ''' Render a "plain" representation of the property
1543         '''
1544         self.view_check()
1546         linkcl = self._db.classes[self._prop.classname]
1547         k = linkcl.labelprop(1)
1548         labels = []
1549         for v in self._value:
1550             labels.append(linkcl.get(v, k))
1551         value = ', '.join(labels)
1552         if escape:
1553             value = cgi.escape(value)
1554         return value
1556     def field(self, size=30, showid=0):
1557         ''' Render a form edit field for the property
1559             If not editable, just display the value via plain().
1560         '''
1561         self.view_check()
1563         if not self.is_edit_ok():
1564             return self.plain()
1566         linkcl = self._db.getclass(self._prop.classname)
1567         value = self._value[:]
1568         # map the id to the label property
1569         if not linkcl.getkey():
1570             showid=1
1571         if not showid:
1572             k = linkcl.labelprop(1)
1573             value = lookupKeys(linkcl, k, value)
1574         value = cgi.escape(','.join(value))
1575         return self.input(name=self._formname,size=size,value=value)
1577     def menu(self, size=None, height=None, showid=0, additional=[],
1578             sort_on=None, **conditions):
1579         ''' Render a form select list for this property
1581             If not editable, just display the value via plain().
1582         '''
1583         self.view_check()
1585         if not self.is_edit_ok():
1586             return self.plain()
1588         value = self._value
1590         linkcl = self._db.getclass(self._prop.classname)
1591         if sort_on is None:
1592             sort_on = ('+', find_sort_key(linkcl))
1593         else:
1594             sort_on = ('+', sort_on)
1595         options = linkcl.filter(None, conditions, sort_on)
1596         height = height or min(len(options), 7)
1597         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1598         k = linkcl.labelprop(1)
1600         # make sure we list the current values if they're retired
1601         for val in value:
1602             if val not in options:
1603                 options.insert(0, val)
1605         for optionid in options:
1606             # get the option value, and if it's None use an empty string
1607             option = linkcl.get(optionid, k) or ''
1609             # figure if this option is selected
1610             s = ''
1611             if optionid in value or option in value:
1612                 s = 'selected="selected" '
1614             # figure the label
1615             if showid:
1616                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1617             else:
1618                 lab = option
1619             # truncate if it's too long
1620             if size is not None and len(lab) > size:
1621                 lab = lab[:size-3] + '...'
1622             if additional:
1623                 m = []
1624                 for propname in additional:
1625                     m.append(linkcl.get(optionid, propname))
1626                 lab = lab + ' (%s)'%', '.join(m)
1628             # and generate
1629             lab = cgi.escape(lab)
1630             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1631                 lab))
1632         l.append('</select>')
1633         return '\n'.join(l)
1635 # set the propclasses for HTMLItem
1636 propclasses = (
1637     (hyperdb.String, StringHTMLProperty),
1638     (hyperdb.Number, NumberHTMLProperty),
1639     (hyperdb.Boolean, BooleanHTMLProperty),
1640     (hyperdb.Date, DateHTMLProperty),
1641     (hyperdb.Interval, IntervalHTMLProperty),
1642     (hyperdb.Password, PasswordHTMLProperty),
1643     (hyperdb.Link, LinkHTMLProperty),
1644     (hyperdb.Multilink, MultilinkHTMLProperty),
1647 def make_sort_function(db, classname, sort_on=None):
1648     '''Make a sort function for a given class
1649     '''
1650     linkcl = db.getclass(classname)
1651     if sort_on is None:
1652         sort_on = find_sort_key(linkcl)
1653     def sortfunc(a, b):
1654         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1655     return sortfunc
1657 def find_sort_key(linkcl):
1658     if linkcl.getprops().has_key('order'):
1659         return 'order'
1660     else:
1661         return linkcl.labelprop()
1663 def handleListCGIValue(value):
1664     ''' Value is either a single item or a list of items. Each item has a
1665         .value that we're actually interested in.
1666     '''
1667     if isinstance(value, type([])):
1668         return [value.value for value in value]
1669     else:
1670         value = value.value.strip()
1671         if not value:
1672             return []
1673         return value.split(',')
1675 class ShowDict:
1676     ''' A convenience access to the :columns index parameters
1677     '''
1678     def __init__(self, columns):
1679         self.columns = {}
1680         for col in columns:
1681             self.columns[col] = 1
1682     def __getitem__(self, name):
1683         return self.columns.has_key(name)
1685 class HTMLRequest(HTMLInputMixin):
1686     '''The *request*, holding the CGI form and environment.
1688     - "form" the CGI form as a cgi.FieldStorage
1689     - "env" the CGI environment variables
1690     - "base" the base URL for this instance
1691     - "user" a HTMLUser instance for this user
1692     - "classname" the current classname (possibly None)
1693     - "template" the current template (suffix, also possibly None)
1695     Index args:
1697     - "columns" dictionary of the columns to display in an index page
1698     - "show" a convenience access to columns - request/show/colname will
1699       be true if the columns should be displayed, false otherwise
1700     - "sort" index sort column (direction, column name)
1701     - "group" index grouping property (direction, column name)
1702     - "filter" properties to filter the index on
1703     - "filterspec" values to filter the index on
1704     - "search_text" text to perform a full-text search on for an index
1705     '''
1706     def __init__(self, client):
1707         # _client is needed by HTMLInputMixin
1708         self._client = self.client = client
1710         # easier access vars
1711         self.form = client.form
1712         self.env = client.env
1713         self.base = client.base
1714         self.user = HTMLUser(client, 'user', client.userid)
1716         # store the current class name and action
1717         self.classname = client.classname
1718         self.template = client.template
1720         # the special char to use for special vars
1721         self.special_char = '@'
1723         HTMLInputMixin.__init__(self)
1725         self._post_init()
1727     def _post_init(self):
1728         ''' Set attributes based on self.form
1729         '''
1730         # extract the index display information from the form
1731         self.columns = []
1732         for name in ':columns @columns'.split():
1733             if self.form.has_key(name):
1734                 self.special_char = name[0]
1735                 self.columns = handleListCGIValue(self.form[name])
1736                 break
1737         self.show = ShowDict(self.columns)
1739         # sorting
1740         self.sort = (None, None)
1741         for name in ':sort @sort'.split():
1742             if self.form.has_key(name):
1743                 self.special_char = name[0]
1744                 sort = self.form[name].value
1745                 if sort.startswith('-'):
1746                     self.sort = ('-', sort[1:])
1747                 else:
1748                     self.sort = ('+', sort)
1749                 if self.form.has_key(self.special_char+'sortdir'):
1750                     self.sort = ('-', self.sort[1])
1752         # grouping
1753         self.group = (None, None)
1754         for name in ':group @group'.split():
1755             if self.form.has_key(name):
1756                 self.special_char = name[0]
1757                 group = self.form[name].value
1758                 if group.startswith('-'):
1759                     self.group = ('-', group[1:])
1760                 else:
1761                     self.group = ('+', group)
1762                 if self.form.has_key(self.special_char+'groupdir'):
1763                     self.group = ('-', self.group[1])
1765         # filtering
1766         self.filter = []
1767         for name in ':filter @filter'.split():
1768             if self.form.has_key(name):
1769                 self.special_char = name[0]
1770                 self.filter = handleListCGIValue(self.form[name])
1772         self.filterspec = {}
1773         db = self.client.db
1774         if self.classname is not None:
1775             props = db.getclass(self.classname).getprops()
1776             for name in self.filter:
1777                 if not self.form.has_key(name):
1778                     continue
1779                 prop = props[name]
1780                 fv = self.form[name]
1781                 if (isinstance(prop, hyperdb.Link) or
1782                         isinstance(prop, hyperdb.Multilink)):
1783                     self.filterspec[name] = lookupIds(db, prop,
1784                         handleListCGIValue(fv))
1785                 else:
1786                     if isinstance(fv, type([])):
1787                         self.filterspec[name] = [v.value for v in fv]
1788                     else:
1789                         self.filterspec[name] = fv.value
1791         # full-text search argument
1792         self.search_text = None
1793         for name in ':search_text @search_text'.split():
1794             if self.form.has_key(name):
1795                 self.special_char = name[0]
1796                 self.search_text = self.form[name].value
1798         # pagination - size and start index
1799         # figure batch args
1800         self.pagesize = 50
1801         for name in ':pagesize @pagesize'.split():
1802             if self.form.has_key(name):
1803                 self.special_char = name[0]
1804                 self.pagesize = int(self.form[name].value)
1806         self.startwith = 0
1807         for name in ':startwith @startwith'.split():
1808             if self.form.has_key(name):
1809                 self.special_char = name[0]
1810                 self.startwith = int(self.form[name].value)
1812     def updateFromURL(self, url):
1813         ''' Parse the URL for query args, and update my attributes using the
1814             values.
1815         ''' 
1816         env = {'QUERY_STRING': url}
1817         self.form = cgi.FieldStorage(environ=env)
1819         self._post_init()
1821     def update(self, kwargs):
1822         ''' Update my attributes using the keyword args
1823         '''
1824         self.__dict__.update(kwargs)
1825         if kwargs.has_key('columns'):
1826             self.show = ShowDict(self.columns)
1828     def description(self):
1829         ''' Return a description of the request - handle for the page title.
1830         '''
1831         s = [self.client.db.config.TRACKER_NAME]
1832         if self.classname:
1833             if self.client.nodeid:
1834                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1835             else:
1836                 if self.template == 'item':
1837                     s.append('- new %s'%self.classname)
1838                 elif self.template == 'index':
1839                     s.append('- %s index'%self.classname)
1840                 else:
1841                     s.append('- %s %s'%(self.classname, self.template))
1842         else:
1843             s.append('- home')
1844         return ' '.join(s)
1846     def __str__(self):
1847         d = {}
1848         d.update(self.__dict__)
1849         f = ''
1850         for k in self.form.keys():
1851             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1852         d['form'] = f
1853         e = ''
1854         for k,v in self.env.items():
1855             e += '\n     %r=%r'%(k, v)
1856         d['env'] = e
1857         return '''
1858 form: %(form)s
1859 base: %(base)r
1860 classname: %(classname)r
1861 template: %(template)r
1862 columns: %(columns)r
1863 sort: %(sort)r
1864 group: %(group)r
1865 filter: %(filter)r
1866 search_text: %(search_text)r
1867 pagesize: %(pagesize)r
1868 startwith: %(startwith)r
1869 env: %(env)s
1870 '''%d
1872     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1873             filterspec=1):
1874         ''' return the current index args as form elements '''
1875         l = []
1876         sc = self.special_char
1877         s = self.input(type="hidden",name="%s",value="%s")
1878         if columns and self.columns:
1879             l.append(s%(sc+'columns', ','.join(self.columns)))
1880         if sort and self.sort[1] is not None:
1881             if self.sort[0] == '-':
1882                 val = '-'+self.sort[1]
1883             else:
1884                 val = self.sort[1]
1885             l.append(s%(sc+'sort', val))
1886         if group and self.group[1] is not None:
1887             if self.group[0] == '-':
1888                 val = '-'+self.group[1]
1889             else:
1890                 val = self.group[1]
1891             l.append(s%(sc+'group', val))
1892         if filter and self.filter:
1893             l.append(s%(sc+'filter', ','.join(self.filter)))
1894         if filterspec:
1895             for k,v in self.filterspec.items():
1896                 if type(v) == type([]):
1897                     l.append(s%(k, ','.join(v)))
1898                 else:
1899                     l.append(s%(k, v))
1900         if self.search_text:
1901             l.append(s%(sc+'search_text', self.search_text))
1902         l.append(s%(sc+'pagesize', self.pagesize))
1903         l.append(s%(sc+'startwith', self.startwith))
1904         return '\n'.join(l)
1906     def indexargs_url(self, url, args):
1907         ''' Embed the current index args in a URL
1908         '''
1909         sc = self.special_char
1910         l = ['%s=%s'%(k,v) for k,v in args.items()]
1912         # pull out the special values (prefixed by @ or :)
1913         specials = {}
1914         for key in args.keys():
1915             if key[0] in '@:':
1916                 specials[key[1:]] = args[key]
1918         # ok, now handle the specials we received in the request
1919         if self.columns and not specials.has_key('columns'):
1920             l.append(sc+'columns=%s'%(','.join(self.columns)))
1921         if self.sort[1] is not None and not specials.has_key('sort'):
1922             if self.sort[0] == '-':
1923                 val = '-'+self.sort[1]
1924             else:
1925                 val = self.sort[1]
1926             l.append(sc+'sort=%s'%val)
1927         if self.group[1] is not None and not specials.has_key('group'):
1928             if self.group[0] == '-':
1929                 val = '-'+self.group[1]
1930             else:
1931                 val = self.group[1]
1932             l.append(sc+'group=%s'%val)
1933         if self.filter and not specials.has_key('filter'):
1934             l.append(sc+'filter=%s'%(','.join(self.filter)))
1935         if self.search_text and not specials.has_key('search_text'):
1936             l.append(sc+'search_text=%s'%self.search_text)
1937         if not specials.has_key('pagesize'):
1938             l.append(sc+'pagesize=%s'%self.pagesize)
1939         if not specials.has_key('startwith'):
1940             l.append(sc+'startwith=%s'%self.startwith)
1942         # finally, the remainder of the filter args in the request
1943         for k,v in self.filterspec.items():
1944             if not args.has_key(k):
1945                 if type(v) == type([]):
1946                     l.append('%s=%s'%(k, ','.join(v)))
1947                 else:
1948                     l.append('%s=%s'%(k, v))
1949         return '%s?%s'%(url, '&'.join(l))
1950     indexargs_href = indexargs_url
1952     def base_javascript(self):
1953         return '''
1954 <script type="text/javascript">
1955 submitted = false;
1956 function submit_once() {
1957     if (submitted) {
1958         alert("Your request is being processed.\\nPlease be patient.");
1959         event.returnValue = 0;    // work-around for IE
1960         return 0;
1961     }
1962     submitted = true;
1963     return 1;
1966 function help_window(helpurl, width, height) {
1967     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1969 </script>
1970 '''%self.base
1972     def batch(self):
1973         ''' Return a batch object for results from the "current search"
1974         '''
1975         filterspec = self.filterspec
1976         sort = self.sort
1977         group = self.group
1979         # get the list of ids we're batching over
1980         klass = self.client.db.getclass(self.classname)
1981         if self.search_text:
1982             matches = self.client.db.indexer.search(
1983                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1984         else:
1985             matches = None
1986         l = klass.filter(matches, filterspec, sort, group)
1988         # return the batch object, using IDs only
1989         return Batch(self.client, l, self.pagesize, self.startwith,
1990             classname=self.classname)
1992 # extend the standard ZTUtils Batch object to remove dependency on
1993 # Acquisition and add a couple of useful methods
1994 class Batch(ZTUtils.Batch):
1995     ''' Use me to turn a list of items, or item ids of a given class, into a
1996         series of batches.
1998         ========= ========================================================
1999         Parameter  Usage
2000         ========= ========================================================
2001         sequence  a list of HTMLItems or item ids
2002         classname if sequence is a list of ids, this is the class of item
2003         size      how big to make the sequence.
2004         start     where to start (0-indexed) in the sequence.
2005         end       where to end (0-indexed) in the sequence.
2006         orphan    if the next batch would contain less items than this
2007                   value, then it is combined with this batch
2008         overlap   the number of items shared between adjacent batches
2009         ========= ========================================================
2011         Attributes: Note that the "start" attribute, unlike the
2012         argument, is a 1-based index (I know, lame).  "first" is the
2013         0-based index.  "length" is the actual number of elements in
2014         the batch.
2016         "sequence_length" is the length of the original, unbatched, sequence.
2017     '''
2018     def __init__(self, client, sequence, size, start, end=0, orphan=0,
2019             overlap=0, classname=None):
2020         self.client = client
2021         self.last_index = self.last_item = None
2022         self.current_item = None
2023         self.classname = classname
2024         self.sequence_length = len(sequence)
2025         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2026             overlap)
2028     # overwrite so we can late-instantiate the HTMLItem instance
2029     def __getitem__(self, index):
2030         if index < 0:
2031             if index + self.end < self.first: raise IndexError, index
2032             return self._sequence[index + self.end]
2033         
2034         if index >= self.length:
2035             raise IndexError, index
2037         # move the last_item along - but only if the fetched index changes
2038         # (for some reason, index 0 is fetched twice)
2039         if index != self.last_index:
2040             self.last_item = self.current_item
2041             self.last_index = index
2043         item = self._sequence[index + self.first]
2044         if self.classname:
2045             # map the item ids to instances
2046             if self.classname == 'user':
2047                 item = HTMLUser(self.client, self.classname, item)
2048             else:
2049                 item = HTMLItem(self.client, self.classname, item)
2050         self.current_item = item
2051         return item
2053     def propchanged(self, property):
2054         ''' Detect if the property marked as being the group property
2055             changed in the last iteration fetch
2056         '''
2057         if (self.last_item is None or
2058                 self.last_item[property] != self.current_item[property]):
2059             return 1
2060         return 0
2062     # override these 'cos we don't have access to acquisition
2063     def previous(self):
2064         if self.start == 1:
2065             return None
2066         return Batch(self.client, self._sequence, self._size,
2067             self.first - self._size + self.overlap, 0, self.orphan,
2068             self.overlap)
2070     def next(self):
2071         try:
2072             self._sequence[self.end]
2073         except IndexError:
2074             return None
2075         return Batch(self.client, self._sequence, self._size,
2076             self.end - self.overlap, 0, self.orphan, self.overlap)
2078 class TemplatingUtils:
2079     ''' Utilities for templating
2080     '''
2081     def __init__(self, client):
2082         self.client = client
2083     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2084         return Batch(self.client, sequence, size, start, end, orphan,
2085             overlap)