Code

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