Code

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