Code

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