Code

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