Code

documentation cleanup
[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             c['context'] = HTMLClass(client, classname, anonymous=1)
209         return c
211     def render(self, client, classname, request, **options):
212         """Render this Page Template"""
214         if not self._v_cooked:
215             self._cook()
217         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
219         if self._v_errors:
220             raise PageTemplate.PTRuntimeError, \
221                 'Page Template %s has errors.'%self.id
223         # figure the context
224         classname = classname or client.classname
225         request = request or HTMLRequest(client)
226         c = self.getContext(client, classname, request)
227         c.update({'options': options})
229         # and go
230         output = StringIO.StringIO()
231         TALInterpreter(self._v_program, self.macros,
232             getEngine().getContext(c), output, tal=1, strictinsert=0)()
233         return output.getvalue()
235     def __repr__(self):
236         return '<Roundup PageTemplate %r>'%self.id
238 class HTMLDatabase:
239     ''' Return HTMLClasses for valid class fetches
240     '''
241     def __init__(self, client):
242         self._client = client
243         self._db = client.db
245         # we want config to be exposed
246         self.config = client.db.config
248     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
249         # check to see if we're actually accessing an item
250         m = desre.match(item)
251         if m:
252             self._client.db.getclass(m.group('cl'))
253             return HTMLItem(self._client, m.group('cl'), m.group('id'))
254         else:
255             self._client.db.getclass(item)
256             return HTMLClass(self._client, item)
258     def __getattr__(self, attr):
259         try:
260             return self[attr]
261         except KeyError:
262             raise AttributeError, attr
264     def classes(self):
265         l = self._client.db.classes.keys()
266         l.sort()
267         return [HTMLClass(self._client, cn) for cn in l]
269 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
270     cl = db.getclass(prop.classname)
271     l = []
272     for entry in ids:
273         if num_re.match(entry):
274             l.append(entry)
275         else:
276             try:
277                 l.append(cl.lookup(entry))
278             except KeyError:
279                 # ignore invalid keys
280                 pass
281     return l
283 class HTMLPermissions:
284     ''' Helpers that provide answers to commonly asked Permission questions.
285     '''
286     def is_edit_ok(self):
287         ''' Is the user allowed to Edit the current class?
288         '''
289         return self._db.security.hasPermission('Edit', self._client.userid,
290             self._classname)
292     def is_view_ok(self):
293         ''' Is the user allowed to View the current class?
294         '''
295         return self._db.security.hasPermission('View', self._client.userid,
296             self._classname)
298     def is_only_view_ok(self):
299         ''' Is the user only allowed to View (ie. not Edit) the current class?
300         '''
301         return self.is_view_ok() and not self.is_edit_ok()
303     def view_check(self):
304         ''' Raise the Unauthorised exception if the user's not permitted to
305             view this class.
306         '''
307         if not self.is_view_ok():
308             raise Unauthorised("view", self._classname)
310     def edit_check(self):
311         ''' Raise the Unauthorised exception if the user's not permitted to
312             edit this class.
313         '''
314         if not self.is_edit_ok():
315             raise Unauthorised("edit", self._classname)
317 def input_html4(**attrs):
318     """Generate an 'input' (html4) element with given attributes"""
319     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
321 def input_xhtml(**attrs):
322     """Generate an 'input' (xhtml) element with given attributes"""
323     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
325 class HTMLInputMixin:
326     ''' requires a _client property '''
327     def __init__(self):
328         html_version = 'html4'
329         if hasattr(self._client.instance.config, 'HTML_VERSION'):
330             html_version = self._client.instance.config.HTML_VERSION
331         if html_version == 'xhtml':
332             self.input = input_xhtml
333         else:
334             self.input = input_html4
336 class HTMLClass(HTMLInputMixin, HTMLPermissions):
337     ''' Accesses through a class (either through *class* or *db.<classname>*)
338     '''
339     def __init__(self, client, classname, anonymous=0):
340         self._client = client
341         self._db = client.db
342         self._anonymous = anonymous
344         # we want classname to be exposed, but _classname gives a
345         # consistent API for extending Class/Item
346         self._classname = self.classname = classname
347         self._klass = self._db.getclass(self.classname)
348         self._props = self._klass.getprops()
350         HTMLInputMixin.__init__(self)
352     def __repr__(self):
353         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
355     def __getitem__(self, item):
356         ''' return an HTMLProperty instance
357         '''
358        #print 'HTMLClass.getitem', (self, item)
360         # we don't exist
361         if item == 'id':
362             return None
364         # get the property
365         prop = self._props[item]
367         # look up the correct HTMLProperty class
368         form = self._client.form
369         for klass, htmlklass in propclasses:
370             if not isinstance(prop, klass):
371                 continue
372             if form.has_key(item):
373                 if isinstance(prop, hyperdb.Multilink):
374                     value = lookupIds(self._db, prop,
375                         handleListCGIValue(form[item]))
376                 elif isinstance(prop, hyperdb.Link):
377                     value = form[item].value.strip()
378                     if value:
379                         value = lookupIds(self._db, prop, [value])[0]
380                     else:
381                         value = None
382                 else:
383                     value = form[item].value.strip() or None
384             else:
385                 if isinstance(prop, hyperdb.Multilink):
386                     value = []
387                 else:
388                     value = None
389             return htmlklass(self._client, self._classname, '', prop, item,
390                 value, self._anonymous)
392         # no good
393         raise KeyError, item
395     def __getattr__(self, attr):
396         ''' convenience access '''
397         try:
398             return self[attr]
399         except KeyError:
400             raise AttributeError, attr
402     def designator(self):
403         ''' Return this class' designator (classname) '''
404         return self._classname
406     def getItem(self, itemid, num_re=re.compile('-?\d+')):
407         ''' Get an item of this class by its item id.
408         '''
409         # make sure we're looking at an itemid
410         if not num_re.match(itemid):
411             itemid = self._klass.lookup(itemid)
413         if self.classname == 'user':
414             klass = HTMLUser
415         else:
416             klass = HTMLItem
418         return klass(self._client, self.classname, itemid)
420     def properties(self, sort=1):
421         ''' Return HTMLProperty for all of this class' properties.
422         '''
423         l = []
424         for name, prop in self._props.items():
425             for klass, htmlklass in propclasses:
426                 if isinstance(prop, hyperdb.Multilink):
427                     value = []
428                 else:
429                     value = None
430                 if isinstance(prop, klass):
431                     l.append(htmlklass(self._client, self._classname, '',
432                         prop, name, value, self._anonymous))
433         if sort:
434             l.sort(lambda a,b:cmp(a._name, b._name))
435         return l
437     def list(self, sort_on=None):
438         ''' List all items in this class.
439         '''
440         if self.classname == 'user':
441             klass = HTMLUser
442         else:
443             klass = HTMLItem
445         # get the list and sort it nicely
446         l = self._klass.list()
447         sortfunc = make_sort_function(self._db, self.classname, sort_on)
448         l.sort(sortfunc)
450         l = [klass(self._client, self.classname, x) for x in l]
451         return l
453     def csv(self):
454         ''' Return the items of this class as a chunk of CSV text.
455         '''
456         if rcsv.error:
457             return rcsv.error
459         props = self.propnames()
460         s = StringIO.StringIO()
461         writer = rcsv.writer(s, rcsv.comma_separated)
462         writer.writerow(props)
463         for nodeid in self._klass.list():
464             l = []
465             for name in props:
466                 value = self._klass.get(nodeid, name)
467                 if value is None:
468                     l.append('')
469                 elif isinstance(value, type([])):
470                     l.append(':'.join(map(str, value)))
471                 else:
472                     l.append(str(self._klass.get(nodeid, name)))
473             writer.writerow(l)
474         return s.getvalue()
476     def propnames(self):
477         ''' Return the list of the names of the properties of this class.
478         '''
479         idlessprops = self._klass.getprops(protected=0).keys()
480         idlessprops.sort()
481         return ['id'] + idlessprops
483     def filter(self, request=None, filterspec={}, sort=(None,None),
484             group=(None,None)):
485         ''' Return a list of items from this class, filtered and sorted
486             by the current requested filterspec/filter/sort/group args
488             "request" takes precedence over the other three arguments.
489         '''
490         if request is not None:
491             filterspec = request.filterspec
492             sort = request.sort
493             group = request.group
494         if self.classname == 'user':
495             klass = HTMLUser
496         else:
497             klass = HTMLItem
498         l = [klass(self._client, self.classname, x)
499              for x in self._klass.filter(None, filterspec, sort, group)]
500         return l
502     def classhelp(self, properties=None, label='(list)', width='500',
503             height='400', property=''):
504         ''' Pop up a javascript window with class help
506             This generates a link to a popup window which displays the 
507             properties indicated by "properties" of the class named by
508             "classname". The "properties" should be a comma-separated list
509             (eg. 'id,name,description'). Properties defaults to all the
510             properties of a class (excluding id, creator, created and
511             activity).
513             You may optionally override the label displayed, the width and
514             height. The popup window will be resizable and scrollable.
516             If the "property" arg is given, it's passed through to the
517             javascript help_window function.
518         '''
519         if properties is None:
520             properties = self._klass.getprops(protected=0).keys()
521             properties.sort()
522             properties = ','.join(properties)
523         if property:
524             property = '&amp;property=%s'%property
525         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
526             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
527             \'%s\')">%s</a>'%(self.classname, properties, property, width,
528             height, label)
530     def submit(self, label="Submit New Entry"):
531         ''' Generate a submit button (and action hidden element)
532         '''
533         self.view_check()
534         if self.is_edit_ok():
535             return self.input(type="hidden",name="@action",value="new") + \
536                    '\n' + self.input(type="submit",name="submit",value=label)
537         return ''
539     def history(self):
540         self.view_check()
541         return 'New node - no history'
543     def renderWith(self, name, **kwargs):
544         ''' Render this class with the given template.
545         '''
546         # create a new request and override the specified args
547         req = HTMLRequest(self._client)
548         req.classname = self.classname
549         req.update(kwargs)
551         # new template, using the specified classname and request
552         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
554         # use our fabricated request
555         args = {
556             'ok_message': self._client.ok_message,
557             'error_message': self._client.error_message
558         }
559         return pt.render(self._client, self.classname, req, **args)
561 class HTMLItem(HTMLInputMixin, HTMLPermissions):
562     ''' Accesses through an *item*
563     '''
564     def __init__(self, client, classname, nodeid, anonymous=0):
565         self._client = client
566         self._db = client.db
567         self._classname = classname
568         self._nodeid = nodeid
569         self._klass = self._db.getclass(classname)
570         self._props = self._klass.getprops()
572         # do we prefix the form items with the item's identification?
573         self._anonymous = anonymous
575         HTMLInputMixin.__init__(self)
577     def __repr__(self):
578         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
579             self._nodeid)
581     def __getitem__(self, item):
582         ''' return an HTMLProperty instance
583         '''
584         #print 'HTMLItem.getitem', (self, item)
585         if item == 'id':
586             return self._nodeid
588         # get the property
589         prop = self._props[item]
591         # get the value, handling missing values
592         value = None
593         if int(self._nodeid) > 0:
594             value = self._klass.get(self._nodeid, item, None)
595         if value is None:
596             if isinstance(self._props[item], hyperdb.Multilink):
597                 value = []
599         # look up the correct HTMLProperty class
600         for klass, htmlklass in propclasses:
601             if isinstance(prop, klass):
602                 return htmlklass(self._client, self._classname,
603                     self._nodeid, prop, item, value, self._anonymous)
605         raise KeyError, item
607     def __getattr__(self, attr):
608         ''' convenience access to properties '''
609         try:
610             return self[attr]
611         except KeyError:
612             raise AttributeError, attr
614     def designator(self):
615         ''' Return this item's designator (classname + id) '''
616         return '%s%s'%(self._classname, self._nodeid)
617     
618     def submit(self, label="Submit Changes"):
619         ''' Generate a submit button (and action hidden element)
620         '''
621         return self.input(type="hidden",name="@action",value="edit") + '\n' + \
622                self.input(type="submit",name="submit",value=label)
624     def journal(self, direction='descending'):
625         ''' Return a list of HTMLJournalEntry instances.
626         '''
627         # XXX do this
628         return []
630     def history(self, direction='descending', dre=re.compile('\d+')):
631         self.view_check()
633         l = ['<table class="history">'
634              '<tr><th colspan="4" class="header">',
635              _('History'),
636              '</th></tr><tr>',
637              _('<th>Date</th>'),
638              _('<th>User</th>'),
639              _('<th>Action</th>'),
640              _('<th>Args</th>'),
641             '</tr>']
642         current = {}
643         comments = {}
644         history = self._klass.history(self._nodeid)
645         history.sort()
646         timezone = self._db.getUserTimezone()
647         if direction == 'descending':
648             history.reverse()
649             for prop_n in self._props.keys():
650                 prop = self[prop_n]
651                 if isinstance(prop, HTMLProperty):
652                     current[prop_n] = prop.plain()
653                     # make link if hrefable
654                     if (self._props.has_key(prop_n) and
655                             isinstance(self._props[prop_n], hyperdb.Link)):
656                         classname = self._props[prop_n].classname
657                         try:
658                             template = find_template(self._db.config.TEMPLATES,
659                                 classname, 'item')
660                             if template[1].startswith('_generic'):
661                                 raise NoTemplate, 'not really...'
662                         except NoTemplate:
663                             pass
664                         else:
665                             id = self._klass.get(self._nodeid, prop_n, None)
666                             current[prop_n] = '<a href="%s%s">%s</a>'%(
667                                 classname, id, current[prop_n])
668  
669         for id, evt_date, user, action, args in history:
670             date_s = str(evt_date.local(timezone)).replace("."," ")
671             arg_s = ''
672             if action == 'link' and type(args) == type(()):
673                 if len(args) == 3:
674                     linkcl, linkid, key = args
675                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
676                         linkcl, linkid, key)
677                 else:
678                     arg_s = str(args)
680             elif action == 'unlink' and type(args) == type(()):
681                 if len(args) == 3:
682                     linkcl, linkid, key = args
683                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
684                         linkcl, linkid, key)
685                 else:
686                     arg_s = str(args)
688             elif type(args) == type({}):
689                 cell = []
690                 for k in args.keys():
691                     # try to get the relevant property and treat it
692                     # specially
693                     try:
694                         prop = self._props[k]
695                     except KeyError:
696                         prop = None
697                     if prop is None:
698                         # property no longer exists
699                         comments['no_exist'] = _('''<em>The indicated property
700                             no longer exists</em>''')
701                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
702                         continue
704                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
705                             isinstance(prop, hyperdb.Link)):
706                         # figure what the link class is
707                         classname = prop.classname
708                         try:
709                             linkcl = self._db.getclass(classname)
710                         except KeyError:
711                             labelprop = None
712                             comments[classname] = _('''The linked class
713                                 %(classname)s no longer exists''')%locals()
714                         labelprop = linkcl.labelprop(1)
715                         try:
716                             template = find_template(self._db.config.TEMPLATES,
717                                 classname, 'item')
718                             if template[1].startswith('_generic'):
719                                 raise NoTemplate, 'not really...'
720                             hrefable = 1
721                         except NoTemplate:
722                             hrefable = 0
724                     if isinstance(prop, hyperdb.Multilink) and args[k]:
725                         ml = []
726                         for linkid in args[k]:
727                             if isinstance(linkid, type(())):
728                                 sublabel = linkid[0] + ' '
729                                 linkids = linkid[1]
730                             else:
731                                 sublabel = ''
732                                 linkids = [linkid]
733                             subml = []
734                             for linkid in linkids:
735                                 label = classname + linkid
736                                 # if we have a label property, try to use it
737                                 # TODO: test for node existence even when
738                                 # there's no labelprop!
739                                 try:
740                                     if labelprop is not None and \
741                                             labelprop != 'id':
742                                         label = linkcl.get(linkid, labelprop)
743                                 except IndexError:
744                                     comments['no_link'] = _('''<strike>The
745                                         linked node no longer
746                                         exists</strike>''')
747                                     subml.append('<strike>%s</strike>'%label)
748                                 else:
749                                     if hrefable:
750                                         subml.append('<a href="%s%s">%s</a>'%(
751                                             classname, linkid, label))
752                                     else:
753                                         subml.append(label)
754                             ml.append(sublabel + ', '.join(subml))
755                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
756                     elif isinstance(prop, hyperdb.Link) and args[k]:
757                         label = classname + args[k]
758                         # if we have a label property, try to use it
759                         # TODO: test for node existence even when
760                         # there's no labelprop!
761                         if labelprop is not None and labelprop != 'id':
762                             try:
763                                 label = linkcl.get(args[k], labelprop)
764                             except IndexError:
765                                 comments['no_link'] = _('''<strike>The
766                                     linked node no longer
767                                     exists</strike>''')
768                                 cell.append(' <strike>%s</strike>,\n'%label)
769                                 # "flag" this is done .... euwww
770                                 label = None
771                         if label is not None:
772                             if hrefable:
773                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
774                             else:
775                                 old = label;
776                             cell.append('%s: %s' % (k,old))
777                             if current.has_key(k):
778                                 cell[-1] += ' -> %s'%current[k]
779                                 current[k] = old
781                     elif isinstance(prop, hyperdb.Date) and args[k]:
782                         d = date.Date(args[k]).local(timezone)
783                         cell.append('%s: %s'%(k, str(d)))
784                         if current.has_key(k):
785                             cell[-1] += ' -> %s' % current[k]
786                             current[k] = str(d)
788                     elif isinstance(prop, hyperdb.Interval) and args[k]:
789                         d = date.Interval(args[k])
790                         cell.append('%s: %s'%(k, str(d)))
791                         if current.has_key(k):
792                             cell[-1] += ' -> %s'%current[k]
793                             current[k] = str(d)
795                     elif isinstance(prop, hyperdb.String) and args[k]:
796                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
797                         if current.has_key(k):
798                             cell[-1] += ' -> %s'%current[k]
799                             current[k] = cgi.escape(args[k])
801                     elif not args[k]:
802                         if current.has_key(k):
803                             cell.append('%s: %s'%(k, current[k]))
804                             current[k] = '(no value)'
805                         else:
806                             cell.append('%s: (no value)'%k)
808                     else:
809                         cell.append('%s: %s'%(k, str(args[k])))
810                         if current.has_key(k):
811                             cell[-1] += ' -> %s'%current[k]
812                             current[k] = str(args[k])
814                 arg_s = '<br />'.join(cell)
815             else:
816                 # unkown event!!
817                 comments['unknown'] = _('''<strong><em>This event is not
818                     handled by the history display!</em></strong>''')
819                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
820             date_s = date_s.replace(' ', '&nbsp;')
821             # if the user's an itemid, figure the username (older journals
822             # have the username)
823             if dre.match(user):
824                 user = self._db.user.get(user, 'username')
825             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
826                 date_s, user, action, arg_s))
827         if comments:
828             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
829         for entry in comments.values():
830             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
831         l.append('</table>')
832         return '\n'.join(l)
834     def renderQueryForm(self):
835         ''' Render this item, which is a query, as a search form.
836         '''
837         # create a new request and override the specified args
838         req = HTMLRequest(self._client)
839         req.classname = self._klass.get(self._nodeid, 'klass')
840         name = self._klass.get(self._nodeid, 'name')
841         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
842             '&@queryname=%s'%urllib.quote(name))
844         # new template, using the specified classname and request
845         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
847         # use our fabricated request
848         return pt.render(self._client, req.classname, req)
850 class HTMLUser(HTMLItem):
851     ''' Accesses through the *user* (a special case of item)
852     '''
853     def __init__(self, client, classname, nodeid, anonymous=0):
854         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
855         self._default_classname = client.classname
857         # used for security checks
858         self._security = client.db.security
860     _marker = []
861     def hasPermission(self, permission, classname=_marker):
862         ''' Determine if the user has the Permission.
864             The class being tested defaults to the template's class, but may
865             be overidden for this test by suppling an alternate classname.
866         '''
867         if classname is self._marker:
868             classname = self._default_classname
869         return self._security.hasPermission(permission, self._nodeid, classname)
871     def is_edit_ok(self):
872         ''' Is the user allowed to Edit the current class?
873             Also check whether this is the current user's info.
874         '''
875         return self._db.security.hasPermission('Edit', self._client.userid,
876             self._classname) or (self._nodeid == self._client.userid and
877             self._db.user.get(self._client.userid, 'username') != 'anonymous')
879     def is_view_ok(self):
880         ''' Is the user allowed to View the current class?
881             Also check whether this is the current user's info.
882         '''
883         return self._db.security.hasPermission('View', self._client.userid,
884             self._classname) or (self._nodeid == self._client.userid and
885             self._db.user.get(self._client.userid, 'username') != 'anonymous')
887 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
888     ''' String, Number, Date, Interval HTMLProperty
890         Has useful attributes:
892          _name  the name of the property
893          _value the value of the property if any
895         A wrapper object which may be stringified for the plain() behaviour.
896     '''
897     def __init__(self, client, classname, nodeid, prop, name, value,
898             anonymous=0):
899         self._client = client
900         self._db = client.db
901         self._classname = classname
902         self._nodeid = nodeid
903         self._prop = prop
904         self._value = value
905         self._anonymous = anonymous
906         self._name = name
907         if not anonymous:
908             self._formname = '%s%s@%s'%(classname, nodeid, name)
909         else:
910             self._formname = name
912         HTMLInputMixin.__init__(self)
914     def __repr__(self):
915         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
916             self._prop, self._value)
917     def __str__(self):
918         return self.plain()
919     def __cmp__(self, other):
920         if isinstance(other, HTMLProperty):
921             return cmp(self._value, other._value)
922         return cmp(self._value, other)
924     def is_edit_ok(self):
925         ''' Is the user allowed to Edit the current class?
926         '''
927         thing = HTMLDatabase(self._client)[self._classname]
928         if self._nodeid:
929             # this is a special-case for the User class where permission's
930             # on a per-item basis :(
931             thing = thing.getItem(self._nodeid)
932         return thing.is_edit_ok()
934     def is_view_ok(self):
935         ''' Is the user allowed to View the current class?
936         '''
937         thing = HTMLDatabase(self._client)[self._classname]
938         if self._nodeid:
939             # this is a special-case for the User class where permission's
940             # on a per-item basis :(
941             thing = thing.getItem(self._nodeid)
942         return thing.is_view_ok()
944 class StringHTMLProperty(HTMLProperty):
945     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
946                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
947                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
948     def _hyper_repl(self, match):
949         if match.group('url'):
950             s = match.group('url')
951             return '<a href="%s">%s</a>'%(s, s)
952         elif match.group('email'):
953             s = match.group('email')
954             return '<a href="mailto:%s">%s</a>'%(s, s)
955         else:
956             s = match.group('item')
957             s1 = match.group('class')
958             s2 = match.group('id')
959             try:
960                 # make sure s1 is a valid tracker classname
961                 self._db.getclass(s1)
962                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
963             except KeyError:
964                 return '%s%s'%(s1, s2)
966     def hyperlinked(self):
967         ''' Render a "hyperlinked" version of the text '''
968         return self.plain(hyperlink=1)
970     def plain(self, escape=0, hyperlink=0):
971         '''Render a "plain" representation of the property
972             
973         - "escape" turns on/off HTML quoting
974         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
975           addresses and designators
976         '''
977         self.view_check()
979         if self._value is None:
980             return ''
981         if escape:
982             s = cgi.escape(str(self._value))
983         else:
984             s = str(self._value)
985         if hyperlink:
986             # no, we *must* escape this text
987             if not escape:
988                 s = cgi.escape(s)
989             s = self.hyper_re.sub(self._hyper_repl, s)
990         return s
992     def stext(self, escape=0):
993         ''' Render the value of the property as StructuredText.
995             This requires the StructureText module to be installed separately.
996         '''
997         self.view_check()
999         s = self.plain(escape=escape)
1000         if not StructuredText:
1001             return s
1002         return StructuredText(s,level=1,header=0)
1004     def field(self, size = 30):
1005         ''' Render the property as a field in HTML.
1007             If not editable, just display the value via plain().
1008         '''
1009         self.view_check()
1011         if self._value is None:
1012             value = ''
1013         else:
1014             value = cgi.escape(str(self._value))
1016         if self.is_edit_ok():
1017             value = '&quot;'.join(value.split('"'))
1018             return self.input(name=self._formname,value=value,size=size)
1020         return self.plain()
1022     def multiline(self, escape=0, rows=5, cols=40):
1023         ''' Render a multiline form edit field for the property.
1025             If not editable, just display the plain() value in a <pre> tag.
1026         '''
1027         self.view_check()
1029         if self._value is None:
1030             value = ''
1031         else:
1032             value = cgi.escape(str(self._value))
1034         if self.is_edit_ok():
1035             value = '&quot;'.join(value.split('"'))
1036             return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1037                 self._formname, rows, cols, value)
1039         return '<pre>%s</pre>'%self.plain()
1041     def email(self, escape=1):
1042         ''' Render the value of the property as an obscured email address
1043         '''
1044         self.view_check()
1046         if self._value is None:
1047             value = ''
1048         else:
1049             value = str(self._value)
1050         if value.find('@') != -1:
1051             name, domain = value.split('@')
1052             domain = ' '.join(domain.split('.')[:-1])
1053             name = name.replace('.', ' ')
1054             value = '%s at %s ...'%(name, domain)
1055         else:
1056             value = value.replace('.', ' ')
1057         if escape:
1058             value = cgi.escape(value)
1059         return value
1061 class PasswordHTMLProperty(HTMLProperty):
1062     def plain(self):
1063         ''' Render a "plain" representation of the property
1064         '''
1065         self.view_check()
1067         if self._value is None:
1068             return ''
1069         return _('*encrypted*')
1071     def field(self, size = 30):
1072         ''' Render a form edit field for the property.
1074             If not editable, just display the value via plain().
1075         '''
1076         self.view_check()
1078         if self.is_edit_ok():
1079             return self.input(type="password", name=self._formname, size=size)
1081         return self.plain()
1083     def confirm(self, size = 30):
1084         ''' Render a second form edit field for the property, used for 
1085             confirmation that the user typed the password correctly. Generates
1086             a field with name "@confirm@name".
1088             If not editable, display nothing.
1089         '''
1090         self.view_check()
1092         if self.is_edit_ok():
1093             return self.input(type="password",
1094                 name="@confirm@%s"%self._formname, size=size)
1096         return ''
1098 class NumberHTMLProperty(HTMLProperty):
1099     def plain(self):
1100         ''' Render a "plain" representation of the property
1101         '''
1102         self.view_check()
1104         return str(self._value)
1106     def field(self, size = 30):
1107         ''' Render a form edit field for the property.
1109             If not editable, just display the value via plain().
1110         '''
1111         self.view_check()
1113         if self._value is None:
1114             value = ''
1115         else:
1116             value = cgi.escape(str(self._value))
1118         if self.is_edit_ok():
1119             value = '&quot;'.join(value.split('"'))
1120             return self.input(name=self._formname,value=value,size=size)
1122         return self.plain()
1124     def __int__(self):
1125         ''' Return an int of me
1126         '''
1127         return int(self._value)
1129     def __float__(self):
1130         ''' Return a float of me
1131         '''
1132         return float(self._value)
1135 class BooleanHTMLProperty(HTMLProperty):
1136     def plain(self):
1137         ''' Render a "plain" representation of the property
1138         '''
1139         self.view_check()
1141         if self._value is None:
1142             return ''
1143         return self._value and "Yes" or "No"
1145     def field(self):
1146         ''' Render a form edit field for the property
1148             If not editable, just display the value via plain().
1149         '''
1150         self.view_check()
1152         if not is_edit_ok():
1153             return self.plain()
1155         checked = self._value and "checked" or ""
1156         if self._value:
1157             s = self.input(type="radio", name=self._formname, value="yes",
1158                 checked="checked")
1159             s += 'Yes'
1160             s +=self.input(type="radio", name=self._formname, value="no")
1161             s += 'No'
1162         else:
1163             s = self.input(type="radio", name=self._formname, value="yes")
1164             s += 'Yes'
1165             s +=self.input(type="radio", name=self._formname, value="no",
1166                 checked="checked")
1167             s += 'No'
1168         return s
1170 class DateHTMLProperty(HTMLProperty):
1171     def plain(self):
1172         ''' Render a "plain" representation of the property
1173         '''
1174         self.view_check()
1176         if self._value is None:
1177             return ''
1178         return str(self._value.local(self._db.getUserTimezone()))
1180     def now(self):
1181         ''' Return the current time.
1183             This is useful for defaulting a new value. Returns a
1184             DateHTMLProperty.
1185         '''
1186         self.view_check()
1188         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1189             self._formname, date.Date('.'))
1191     def field(self, size = 30):
1192         ''' Render a form edit field for the property
1194             If not editable, just display the value via plain().
1195         '''
1196         self.view_check()
1198         if self._value is None:
1199             value = ''
1200         else:
1201             tz = self._db.getUserTimezone()
1202             value = cgi.escape(str(self._value.local(tz)))
1204         if is_edit_ok():
1205             value = '&quot;'.join(value.split('"'))
1206             return self.input(name=self._formname,value=value,size=size)
1207         
1208         return self.plain()
1210     def reldate(self, pretty=1):
1211         ''' Render the interval between the date and now.
1213             If the "pretty" flag is true, then make the display pretty.
1214         '''
1215         self.view_check()
1217         if not self._value:
1218             return ''
1220         # figure the interval
1221         interval = self._value - date.Date('.')
1222         if pretty:
1223             return interval.pretty()
1224         return str(interval)
1226     _marker = []
1227     def pretty(self, format=_marker):
1228         ''' Render the date in a pretty format (eg. month names, spaces).
1230             The format string is a standard python strftime format string.
1231             Note that if the day is zero, and appears at the start of the
1232             string, then it'll be stripped from the output. This is handy
1233             for the situatin when a date only specifies a month and a year.
1234         '''
1235         self.view_check()
1237         if format is not self._marker:
1238             return self._value.pretty(format)
1239         else:
1240             return self._value.pretty()
1242     def local(self, offset):
1243         ''' Return the date/time as a local (timezone offset) date/time.
1244         '''
1245         self.view_check()
1247         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1248             self._formname, self._value.local(offset))
1250 class IntervalHTMLProperty(HTMLProperty):
1251     def plain(self):
1252         ''' Render a "plain" representation of the property
1253         '''
1254         self.view_check()
1256         if self._value is None:
1257             return ''
1258         return str(self._value)
1260     def pretty(self):
1261         ''' Render the interval in a pretty format (eg. "yesterday")
1262         '''
1263         self.view_check()
1265         return self._value.pretty()
1267     def field(self, size = 30):
1268         ''' Render a form edit field for the property
1270             If not editable, just display the value via plain().
1271         '''
1272         self.view_check()
1274         if self._value is None:
1275             value = ''
1276         else:
1277             value = cgi.escape(str(self._value))
1279         if is_edit_ok():
1280             value = '&quot;'.join(value.split('"'))
1281             return self.input(name=self._formname,value=value,size=size)
1283         return self.plain()
1285 class LinkHTMLProperty(HTMLProperty):
1286     ''' Link HTMLProperty
1287         Include the above as well as being able to access the class
1288         information. Stringifying the object itself results in the value
1289         from the item being displayed. Accessing attributes of this object
1290         result in the appropriate entry from the class being queried for the
1291         property accessed (so item/assignedto/name would look up the user
1292         entry identified by the assignedto property on item, and then the
1293         name property of that user)
1294     '''
1295     def __init__(self, *args, **kw):
1296         HTMLProperty.__init__(self, *args, **kw)
1297         # if we're representing a form value, then the -1 from the form really
1298         # should be a None
1299         if str(self._value) == '-1':
1300             self._value = None
1302     def __getattr__(self, attr):
1303         ''' return a new HTMLItem '''
1304        #print 'Link.getattr', (self, attr, self._value)
1305         if not self._value:
1306             raise AttributeError, "Can't access missing value"
1307         if self._prop.classname == 'user':
1308             klass = HTMLUser
1309         else:
1310             klass = HTMLItem
1311         i = klass(self._client, self._prop.classname, self._value)
1312         return getattr(i, attr)
1314     def plain(self, escape=0):
1315         ''' Render a "plain" representation of the property
1316         '''
1317         self.view_check()
1319         if self._value is None:
1320             return ''
1321         linkcl = self._db.classes[self._prop.classname]
1322         k = linkcl.labelprop(1)
1323         value = str(linkcl.get(self._value, k))
1324         if escape:
1325             value = cgi.escape(value)
1326         return value
1328     def field(self, showid=0, size=None):
1329         ''' Render a form edit field for the property
1331             If not editable, just display the value via plain().
1332         '''
1333         self.view_check()
1335         if not self.is_edit_ok():
1336             return self.plain()
1338         # edit field
1339         linkcl = self._db.getclass(self._prop.classname)
1340         if self._value is None:
1341             value = ''
1342         else:
1343             k = linkcl.getkey()
1344             if k:
1345                 label = linkcl.get(self._value, k)
1346             else:
1347                 label = self._value
1348             value = cgi.escape(str(self._value))
1349             value = '&quot;'.join(value.split('"'))
1350         return '<input name="%s" value="%s" size="%s">'%(self._formname,
1351             label, size)
1353     def menu(self, size=None, height=None, showid=0, additional=[],
1354             sort_on=None, **conditions):
1355         ''' Render a form select list for this property
1357             If not editable, just display the value via plain().
1358         '''
1359         self.view_check()
1361         if not self.is_edit_ok():
1362             return self.plain()
1364         value = self._value
1366         linkcl = self._db.getclass(self._prop.classname)
1367         l = ['<select name="%s">'%self._formname]
1368         k = linkcl.labelprop(1)
1369         s = ''
1370         if value is None:
1371             s = 'selected="selected" '
1372         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1373         if linkcl.getprops().has_key('order'):  
1374             sort_on = ('+', 'order')
1375         else:  
1376             if sort_on is None:
1377                 sort_on = ('+', linkcl.labelprop())
1378             else:
1379                 sort_on = ('+', sort_on)
1380         options = linkcl.filter(None, conditions, sort_on, (None, None))
1382         # make sure we list the current value if it's retired
1383         if self._value and self._value not in options:
1384             options.insert(0, self._value)
1386         for optionid in options:
1387             # get the option value, and if it's None use an empty string
1388             option = linkcl.get(optionid, k) or ''
1390             # figure if this option is selected
1391             s = ''
1392             if value in [optionid, option]:
1393                 s = 'selected="selected" '
1395             # figure the label
1396             if showid:
1397                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1398             else:
1399                 lab = option
1401             # truncate if it's too long
1402             if size is not None and len(lab) > size:
1403                 lab = lab[:size-3] + '...'
1404             if additional:
1405                 m = []
1406                 for propname in additional:
1407                     m.append(linkcl.get(optionid, propname))
1408                 lab = lab + ' (%s)'%', '.join(map(str, m))
1410             # and generate
1411             lab = cgi.escape(lab)
1412             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1413         l.append('</select>')
1414         return '\n'.join(l)
1415 #    def checklist(self, ...)
1417 class MultilinkHTMLProperty(HTMLProperty):
1418     ''' Multilink HTMLProperty
1420         Also be iterable, returning a wrapper object like the Link case for
1421         each entry in the multilink.
1422     '''
1423     def __init__(self, *args, **kwargs):
1424         HTMLProperty.__init__(self, *args, **kwargs)
1425         if self._value:
1426             sortfun = make_sort_function(self._db, self._prop.classname)
1427             self._value.sort(sortfun)
1428     
1429     def __len__(self):
1430         ''' length of the multilink '''
1431         return len(self._value)
1433     def __getattr__(self, attr):
1434         ''' no extended attribute accesses make sense here '''
1435         raise AttributeError, attr
1437     def __getitem__(self, num):
1438         ''' iterate and return a new HTMLItem
1439         '''
1440        #print 'Multi.getitem', (self, num)
1441         value = self._value[num]
1442         if self._prop.classname == 'user':
1443             klass = HTMLUser
1444         else:
1445             klass = HTMLItem
1446         return klass(self._client, self._prop.classname, value)
1448     def __contains__(self, value):
1449         ''' Support the "in" operator. We have to make sure the passed-in
1450             value is a string first, not a HTMLProperty.
1451         '''
1452         return str(value) in self._value
1454     def reverse(self):
1455         ''' return the list in reverse order
1456         '''
1457         l = self._value[:]
1458         l.reverse()
1459         if self._prop.classname == 'user':
1460             klass = HTMLUser
1461         else:
1462             klass = HTMLItem
1463         return [klass(self._client, self._prop.classname, value) for value in l]
1465     def plain(self, escape=0):
1466         ''' Render a "plain" representation of the property
1467         '''
1468         self.view_check()
1470         linkcl = self._db.classes[self._prop.classname]
1471         k = linkcl.labelprop(1)
1472         labels = []
1473         for v in self._value:
1474             labels.append(linkcl.get(v, k))
1475         value = ', '.join(labels)
1476         if escape:
1477             value = cgi.escape(value)
1478         return value
1480     def field(self, size=30, showid=0):
1481         ''' Render a form edit field for the property
1483             If not editable, just display the value via plain().
1484         '''
1485         self.view_check()
1487         if not self.is_edit_ok():
1488             return self.plain()
1490         linkcl = self._db.getclass(self._prop.classname)
1491         value = self._value[:]
1492         # map the id to the label property
1493         if not linkcl.getkey():
1494             showid=1
1495         if not showid:
1496             k = linkcl.labelprop(1)
1497             value = [linkcl.get(v, k) for v in value]
1498         value = cgi.escape(','.join(value))
1499         return self.input(name=self._formname,size=size,value=value)
1501     def menu(self, size=None, height=None, showid=0, additional=[],
1502             sort_on=None, **conditions):
1503         ''' Render a form select list for this property
1505             If not editable, just display the value via plain().
1506         '''
1507         self.view_check()
1509         if not self.is_edit_ok():
1510             return self.plain()
1512         value = self._value
1514         linkcl = self._db.getclass(self._prop.classname)
1515         if sort_on is None:
1516             sort_on = ('+', find_sort_key(linkcl))
1517         else:
1518             sort_on = ('+', sort_on)
1519         options = linkcl.filter(None, conditions, sort_on)
1520         height = height or min(len(options), 7)
1521         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1522         k = linkcl.labelprop(1)
1524         # make sure we list the current values if they're retired
1525         for val in value:
1526             if val not in options:
1527                 options.insert(0, val)
1529         for optionid in options:
1530             # get the option value, and if it's None use an empty string
1531             option = linkcl.get(optionid, k) or ''
1533             # figure if this option is selected
1534             s = ''
1535             if optionid in value or option in value:
1536                 s = 'selected="selected" '
1538             # figure the label
1539             if showid:
1540                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1541             else:
1542                 lab = option
1543             # truncate if it's too long
1544             if size is not None and len(lab) > size:
1545                 lab = lab[:size-3] + '...'
1546             if additional:
1547                 m = []
1548                 for propname in additional:
1549                     m.append(linkcl.get(optionid, propname))
1550                 lab = lab + ' (%s)'%', '.join(m)
1552             # and generate
1553             lab = cgi.escape(lab)
1554             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1555                 lab))
1556         l.append('</select>')
1557         return '\n'.join(l)
1559 # set the propclasses for HTMLItem
1560 propclasses = (
1561     (hyperdb.String, StringHTMLProperty),
1562     (hyperdb.Number, NumberHTMLProperty),
1563     (hyperdb.Boolean, BooleanHTMLProperty),
1564     (hyperdb.Date, DateHTMLProperty),
1565     (hyperdb.Interval, IntervalHTMLProperty),
1566     (hyperdb.Password, PasswordHTMLProperty),
1567     (hyperdb.Link, LinkHTMLProperty),
1568     (hyperdb.Multilink, MultilinkHTMLProperty),
1571 def make_sort_function(db, classname, sort_on=None):
1572     '''Make a sort function for a given class
1573     '''
1574     linkcl = db.getclass(classname)
1575     if sort_on is None:
1576         sort_on = find_sort_key(linkcl)
1577     def sortfunc(a, b):
1578         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1579     return sortfunc
1581 def find_sort_key(linkcl):
1582     if linkcl.getprops().has_key('order'):
1583         return 'order'
1584     else:
1585         return linkcl.labelprop()
1587 def handleListCGIValue(value):
1588     ''' Value is either a single item or a list of items. Each item has a
1589         .value that we're actually interested in.
1590     '''
1591     if isinstance(value, type([])):
1592         return [value.value for value in value]
1593     else:
1594         value = value.value.strip()
1595         if not value:
1596             return []
1597         return value.split(',')
1599 class ShowDict:
1600     ''' A convenience access to the :columns index parameters
1601     '''
1602     def __init__(self, columns):
1603         self.columns = {}
1604         for col in columns:
1605             self.columns[col] = 1
1606     def __getitem__(self, name):
1607         return self.columns.has_key(name)
1609 class HTMLRequest(HTMLInputMixin):
1610     '''The *request*, holding the CGI form and environment.
1612     - "form" the CGI form as a cgi.FieldStorage
1613     - "env" the CGI environment variables
1614     - "base" the base URL for this instance
1615     - "user" a HTMLUser instance for this user
1616     - "classname" the current classname (possibly None)
1617     - "template" the current template (suffix, also possibly None)
1619     Index args:
1621     - "columns" dictionary of the columns to display in an index page
1622     - "show" a convenience access to columns - request/show/colname will
1623       be true if the columns should be displayed, false otherwise
1624     - "sort" index sort column (direction, column name)
1625     - "group" index grouping property (direction, column name)
1626     - "filter" properties to filter the index on
1627     - "filterspec" values to filter the index on
1628     - "search_text" text to perform a full-text search on for an index
1629     '''
1630     def __init__(self, client):
1631         # _client is needed by HTMLInputMixin
1632         self._client = self.client = client
1634         # easier access vars
1635         self.form = client.form
1636         self.env = client.env
1637         self.base = client.base
1638         self.user = HTMLUser(client, 'user', client.userid)
1640         # store the current class name and action
1641         self.classname = client.classname
1642         self.template = client.template
1644         # the special char to use for special vars
1645         self.special_char = '@'
1647         HTMLInputMixin.__init__(self)
1649         self._post_init()
1651     def _post_init(self):
1652         ''' Set attributes based on self.form
1653         '''
1654         # extract the index display information from the form
1655         self.columns = []
1656         for name in ':columns @columns'.split():
1657             if self.form.has_key(name):
1658                 self.special_char = name[0]
1659                 self.columns = handleListCGIValue(self.form[name])
1660                 break
1661         self.show = ShowDict(self.columns)
1663         # sorting
1664         self.sort = (None, None)
1665         for name in ':sort @sort'.split():
1666             if self.form.has_key(name):
1667                 self.special_char = name[0]
1668                 sort = self.form[name].value
1669                 if sort.startswith('-'):
1670                     self.sort = ('-', sort[1:])
1671                 else:
1672                     self.sort = ('+', sort)
1673                 if self.form.has_key(self.special_char+'sortdir'):
1674                     self.sort = ('-', self.sort[1])
1676         # grouping
1677         self.group = (None, None)
1678         for name in ':group @group'.split():
1679             if self.form.has_key(name):
1680                 self.special_char = name[0]
1681                 group = self.form[name].value
1682                 if group.startswith('-'):
1683                     self.group = ('-', group[1:])
1684                 else:
1685                     self.group = ('+', group)
1686                 if self.form.has_key(self.special_char+'groupdir'):
1687                     self.group = ('-', self.group[1])
1689         # filtering
1690         self.filter = []
1691         for name in ':filter @filter'.split():
1692             if self.form.has_key(name):
1693                 self.special_char = name[0]
1694                 self.filter = handleListCGIValue(self.form[name])
1696         self.filterspec = {}
1697         db = self.client.db
1698         if self.classname is not None:
1699             props = db.getclass(self.classname).getprops()
1700             for name in self.filter:
1701                 if not self.form.has_key(name):
1702                     continue
1703                 prop = props[name]
1704                 fv = self.form[name]
1705                 if (isinstance(prop, hyperdb.Link) or
1706                         isinstance(prop, hyperdb.Multilink)):
1707                     self.filterspec[name] = lookupIds(db, prop,
1708                         handleListCGIValue(fv))
1709                 else:
1710                     if isinstance(fv, type([])):
1711                         self.filterspec[name] = [v.value for v in fv]
1712                     else:
1713                         self.filterspec[name] = fv.value
1715         # full-text search argument
1716         self.search_text = None
1717         for name in ':search_text @search_text'.split():
1718             if self.form.has_key(name):
1719                 self.special_char = name[0]
1720                 self.search_text = self.form[name].value
1722         # pagination - size and start index
1723         # figure batch args
1724         self.pagesize = 50
1725         for name in ':pagesize @pagesize'.split():
1726             if self.form.has_key(name):
1727                 self.special_char = name[0]
1728                 self.pagesize = int(self.form[name].value)
1730         self.startwith = 0
1731         for name in ':startwith @startwith'.split():
1732             if self.form.has_key(name):
1733                 self.special_char = name[0]
1734                 self.startwith = int(self.form[name].value)
1736     def updateFromURL(self, url):
1737         ''' Parse the URL for query args, and update my attributes using the
1738             values.
1739         ''' 
1740         env = {'QUERY_STRING': url}
1741         self.form = cgi.FieldStorage(environ=env)
1743         self._post_init()
1745     def update(self, kwargs):
1746         ''' Update my attributes using the keyword args
1747         '''
1748         self.__dict__.update(kwargs)
1749         if kwargs.has_key('columns'):
1750             self.show = ShowDict(self.columns)
1752     def description(self):
1753         ''' Return a description of the request - handle for the page title.
1754         '''
1755         s = [self.client.db.config.TRACKER_NAME]
1756         if self.classname:
1757             if self.client.nodeid:
1758                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1759             else:
1760                 if self.template == 'item':
1761                     s.append('- new %s'%self.classname)
1762                 elif self.template == 'index':
1763                     s.append('- %s index'%self.classname)
1764                 else:
1765                     s.append('- %s %s'%(self.classname, self.template))
1766         else:
1767             s.append('- home')
1768         return ' '.join(s)
1770     def __str__(self):
1771         d = {}
1772         d.update(self.__dict__)
1773         f = ''
1774         for k in self.form.keys():
1775             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1776         d['form'] = f
1777         e = ''
1778         for k,v in self.env.items():
1779             e += '\n     %r=%r'%(k, v)
1780         d['env'] = e
1781         return '''
1782 form: %(form)s
1783 base: %(base)r
1784 classname: %(classname)r
1785 template: %(template)r
1786 columns: %(columns)r
1787 sort: %(sort)r
1788 group: %(group)r
1789 filter: %(filter)r
1790 search_text: %(search_text)r
1791 pagesize: %(pagesize)r
1792 startwith: %(startwith)r
1793 env: %(env)s
1794 '''%d
1796     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1797             filterspec=1):
1798         ''' return the current index args as form elements '''
1799         l = []
1800         sc = self.special_char
1801         s = self.input(type="hidden",name="%s",value="%s")
1802         if columns and self.columns:
1803             l.append(s%(sc+'columns', ','.join(self.columns)))
1804         if sort and self.sort[1] is not None:
1805             if self.sort[0] == '-':
1806                 val = '-'+self.sort[1]
1807             else:
1808                 val = self.sort[1]
1809             l.append(s%(sc+'sort', val))
1810         if group and self.group[1] is not None:
1811             if self.group[0] == '-':
1812                 val = '-'+self.group[1]
1813             else:
1814                 val = self.group[1]
1815             l.append(s%(sc+'group', val))
1816         if filter and self.filter:
1817             l.append(s%(sc+'filter', ','.join(self.filter)))
1818         if filterspec:
1819             for k,v in self.filterspec.items():
1820                 if type(v) == type([]):
1821                     l.append(s%(k, ','.join(v)))
1822                 else:
1823                     l.append(s%(k, v))
1824         if self.search_text:
1825             l.append(s%(sc+'search_text', self.search_text))
1826         l.append(s%(sc+'pagesize', self.pagesize))
1827         l.append(s%(sc+'startwith', self.startwith))
1828         return '\n'.join(l)
1830     def indexargs_url(self, url, args):
1831         ''' Embed the current index args in a URL
1832         '''
1833         sc = self.special_char
1834         l = ['%s=%s'%(k,v) for k,v in args.items()]
1836         # pull out the special values (prefixed by @ or :)
1837         specials = {}
1838         for key in args.keys():
1839             if key[0] in '@:':
1840                 specials[key[1:]] = args[key]
1842         # ok, now handle the specials we received in the request
1843         if self.columns and not specials.has_key('columns'):
1844             l.append(sc+'columns=%s'%(','.join(self.columns)))
1845         if self.sort[1] is not None and not specials.has_key('sort'):
1846             if self.sort[0] == '-':
1847                 val = '-'+self.sort[1]
1848             else:
1849                 val = self.sort[1]
1850             l.append(sc+'sort=%s'%val)
1851         if self.group[1] is not None and not specials.has_key('group'):
1852             if self.group[0] == '-':
1853                 val = '-'+self.group[1]
1854             else:
1855                 val = self.group[1]
1856             l.append(sc+'group=%s'%val)
1857         if self.filter and not specials.has_key('filter'):
1858             l.append(sc+'filter=%s'%(','.join(self.filter)))
1859         if self.search_text and not specials.has_key('search_text'):
1860             l.append(sc+'search_text=%s'%self.search_text)
1861         if not specials.has_key('pagesize'):
1862             l.append(sc+'pagesize=%s'%self.pagesize)
1863         if not specials.has_key('startwith'):
1864             l.append(sc+'startwith=%s'%self.startwith)
1866         # finally, the remainder of the filter args in the request
1867         for k,v in self.filterspec.items():
1868             if not args.has_key(k):
1869                 if type(v) == type([]):
1870                     l.append('%s=%s'%(k, ','.join(v)))
1871                 else:
1872                     l.append('%s=%s'%(k, v))
1873         return '%s?%s'%(url, '&'.join(l))
1874     indexargs_href = indexargs_url
1876     def base_javascript(self):
1877         return '''
1878 <script type="text/javascript">
1879 submitted = false;
1880 function submit_once() {
1881     if (submitted) {
1882         alert("Your request is being processed.\\nPlease be patient.");
1883         event.returnValue = 0;    // work-around for IE
1884         return 0;
1885     }
1886     submitted = true;
1887     return 1;
1890 function help_window(helpurl, width, height) {
1891     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1893 </script>
1894 '''%self.base
1896     def batch(self):
1897         ''' Return a batch object for results from the "current search"
1898         '''
1899         filterspec = self.filterspec
1900         sort = self.sort
1901         group = self.group
1903         # get the list of ids we're batching over
1904         klass = self.client.db.getclass(self.classname)
1905         if self.search_text:
1906             matches = self.client.db.indexer.search(
1907                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1908         else:
1909             matches = None
1910         l = klass.filter(matches, filterspec, sort, group)
1912         # return the batch object, using IDs only
1913         return Batch(self.client, l, self.pagesize, self.startwith,
1914             classname=self.classname)
1916 # extend the standard ZTUtils Batch object to remove dependency on
1917 # Acquisition and add a couple of useful methods
1918 class Batch(ZTUtils.Batch):
1919     ''' Use me to turn a list of items, or item ids of a given class, into a
1920         series of batches.
1922         ========= ========================================================
1923         Parameter  Usage
1924         ========= ========================================================
1925         sequence  a list of HTMLItems or item ids
1926         classname if sequence is a list of ids, this is the class of item
1927         size      how big to make the sequence.
1928         start     where to start (0-indexed) in the sequence.
1929         end       where to end (0-indexed) in the sequence.
1930         orphan    if the next batch would contain less items than this
1931                   value, then it is combined with this batch
1932         overlap   the number of items shared between adjacent batches
1933         ========= ========================================================
1935         Attributes: Note that the "start" attribute, unlike the
1936         argument, is a 1-based index (I know, lame).  "first" is the
1937         0-based index.  "length" is the actual number of elements in
1938         the batch.
1940         "sequence_length" is the length of the original, unbatched, sequence.
1941     '''
1942     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1943             overlap=0, classname=None):
1944         self.client = client
1945         self.last_index = self.last_item = None
1946         self.current_item = None
1947         self.classname = classname
1948         self.sequence_length = len(sequence)
1949         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1950             overlap)
1952     # overwrite so we can late-instantiate the HTMLItem instance
1953     def __getitem__(self, index):
1954         if index < 0:
1955             if index + self.end < self.first: raise IndexError, index
1956             return self._sequence[index + self.end]
1957         
1958         if index >= self.length:
1959             raise IndexError, index
1961         # move the last_item along - but only if the fetched index changes
1962         # (for some reason, index 0 is fetched twice)
1963         if index != self.last_index:
1964             self.last_item = self.current_item
1965             self.last_index = index
1967         item = self._sequence[index + self.first]
1968         if self.classname:
1969             # map the item ids to instances
1970             if self.classname == 'user':
1971                 item = HTMLUser(self.client, self.classname, item)
1972             else:
1973                 item = HTMLItem(self.client, self.classname, item)
1974         self.current_item = item
1975         return item
1977     def propchanged(self, property):
1978         ''' Detect if the property marked as being the group property
1979             changed in the last iteration fetch
1980         '''
1981         if (self.last_item is None or
1982                 self.last_item[property] != self.current_item[property]):
1983             return 1
1984         return 0
1986     # override these 'cos we don't have access to acquisition
1987     def previous(self):
1988         if self.start == 1:
1989             return None
1990         return Batch(self.client, self._sequence, self._size,
1991             self.first - self._size + self.overlap, 0, self.orphan,
1992             self.overlap)
1994     def next(self):
1995         try:
1996             self._sequence[self.end]
1997         except IndexError:
1998             return None
1999         return Batch(self.client, self._sequence, self._size,
2000             self.end - self.overlap, 0, self.orphan, self.overlap)
2002 class TemplatingUtils:
2003     ''' Utilities for templating
2004     '''
2005     def __init__(self, client):
2006         self.client = client
2007     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2008         return Batch(self.client, sequence, size, start, end, orphan,
2009             overlap)