Code

don't insert spaces into designators, it just confuses users (sf bug 898087)
[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 isinstance(itemid, type(1)) and 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.
621         Also sneak in the lastactivity and action hidden elements.
622         """
623         return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
624                self.input(type="hidden", name="@action", value="edit") + '\n' + \
625                self.input(type="submit", name="submit", value=label)
627     def journal(self, direction='descending'):
628         ''' Return a list of HTMLJournalEntry instances.
629         '''
630         # XXX do this
631         return []
633     def history(self, direction='descending', dre=re.compile('\d+')):
634         self.view_check()
636         l = ['<table class="history">'
637              '<tr><th colspan="4" class="header">',
638              _('History'),
639              '</th></tr><tr>',
640              _('<th>Date</th>'),
641              _('<th>User</th>'),
642              _('<th>Action</th>'),
643              _('<th>Args</th>'),
644             '</tr>']
645         current = {}
646         comments = {}
647         history = self._klass.history(self._nodeid)
648         history.sort()
649         timezone = self._db.getUserTimezone()
650         if direction == 'descending':
651             history.reverse()
652             for prop_n in self._props.keys():
653                 prop = self[prop_n]
654                 if isinstance(prop, HTMLProperty):
655                     current[prop_n] = prop.plain()
656                     # make link if hrefable
657                     if (self._props.has_key(prop_n) and
658                             isinstance(self._props[prop_n], hyperdb.Link)):
659                         classname = self._props[prop_n].classname
660                         try:
661                             template = find_template(self._db.config.TEMPLATES,
662                                 classname, 'item')
663                             if template[1].startswith('_generic'):
664                                 raise NoTemplate, 'not really...'
665                         except NoTemplate:
666                             pass
667                         else:
668                             id = self._klass.get(self._nodeid, prop_n, None)
669                             current[prop_n] = '<a href="%s%s">%s</a>'%(
670                                 classname, id, current[prop_n])
671  
672         for id, evt_date, user, action, args in history:
673             date_s = str(evt_date.local(timezone)).replace("."," ")
674             arg_s = ''
675             if action == 'link' and type(args) == type(()):
676                 if len(args) == 3:
677                     linkcl, linkid, key = args
678                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
679                         linkcl, linkid, key)
680                 else:
681                     arg_s = str(args)
683             elif action == 'unlink' and type(args) == type(()):
684                 if len(args) == 3:
685                     linkcl, linkid, key = args
686                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
687                         linkcl, linkid, key)
688                 else:
689                     arg_s = str(args)
691             elif type(args) == type({}):
692                 cell = []
693                 for k in args.keys():
694                     # try to get the relevant property and treat it
695                     # specially
696                     try:
697                         prop = self._props[k]
698                     except KeyError:
699                         prop = None
700                     if prop is None:
701                         # property no longer exists
702                         comments['no_exist'] = _('''<em>The indicated property
703                             no longer exists</em>''')
704                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
705                         continue
707                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
708                             isinstance(prop, hyperdb.Link)):
709                         # figure what the link class is
710                         classname = prop.classname
711                         try:
712                             linkcl = self._db.getclass(classname)
713                         except KeyError:
714                             labelprop = None
715                             comments[classname] = _('''The linked class
716                                 %(classname)s no longer exists''')%locals()
717                         labelprop = linkcl.labelprop(1)
718                         try:
719                             template = find_template(self._db.config.TEMPLATES,
720                                 classname, 'item')
721                             if template[1].startswith('_generic'):
722                                 raise NoTemplate, 'not really...'
723                             hrefable = 1
724                         except NoTemplate:
725                             hrefable = 0
727                     if isinstance(prop, hyperdb.Multilink) and args[k]:
728                         ml = []
729                         for linkid in args[k]:
730                             if isinstance(linkid, type(())):
731                                 sublabel = linkid[0] + ' '
732                                 linkids = linkid[1]
733                             else:
734                                 sublabel = ''
735                                 linkids = [linkid]
736                             subml = []
737                             for linkid in linkids:
738                                 label = classname + linkid
739                                 # if we have a label property, try to use it
740                                 # TODO: test for node existence even when
741                                 # there's no labelprop!
742                                 try:
743                                     if labelprop is not None and \
744                                             labelprop != 'id':
745                                         label = linkcl.get(linkid, labelprop)
746                                 except IndexError:
747                                     comments['no_link'] = _('''<strike>The
748                                         linked node no longer
749                                         exists</strike>''')
750                                     subml.append('<strike>%s</strike>'%label)
751                                 else:
752                                     if hrefable:
753                                         subml.append('<a href="%s%s">%s</a>'%(
754                                             classname, linkid, label))
755                                     else:
756                                         subml.append(label)
757                             ml.append(sublabel + ', '.join(subml))
758                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
759                     elif isinstance(prop, hyperdb.Link) and args[k]:
760                         label = classname + args[k]
761                         # if we have a label property, try to use it
762                         # TODO: test for node existence even when
763                         # there's no labelprop!
764                         if labelprop is not None and labelprop != 'id':
765                             try:
766                                 label = linkcl.get(args[k], labelprop)
767                             except IndexError:
768                                 comments['no_link'] = _('''<strike>The
769                                     linked node no longer
770                                     exists</strike>''')
771                                 cell.append(' <strike>%s</strike>,\n'%label)
772                                 # "flag" this is done .... euwww
773                                 label = None
774                         if label is not None:
775                             if hrefable:
776                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
777                             else:
778                                 old = label;
779                             cell.append('%s: %s' % (k,old))
780                             if current.has_key(k):
781                                 cell[-1] += ' -> %s'%current[k]
782                                 current[k] = old
784                     elif isinstance(prop, hyperdb.Date) and args[k]:
785                         d = date.Date(args[k]).local(timezone)
786                         cell.append('%s: %s'%(k, str(d)))
787                         if current.has_key(k):
788                             cell[-1] += ' -> %s' % current[k]
789                             current[k] = str(d)
791                     elif isinstance(prop, hyperdb.Interval) and args[k]:
792                         d = date.Interval(args[k])
793                         cell.append('%s: %s'%(k, str(d)))
794                         if current.has_key(k):
795                             cell[-1] += ' -> %s'%current[k]
796                             current[k] = str(d)
798                     elif isinstance(prop, hyperdb.String) and args[k]:
799                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
800                         if current.has_key(k):
801                             cell[-1] += ' -> %s'%current[k]
802                             current[k] = cgi.escape(args[k])
804                     elif not args[k]:
805                         if current.has_key(k):
806                             cell.append('%s: %s'%(k, current[k]))
807                             current[k] = '(no value)'
808                         else:
809                             cell.append('%s: (no value)'%k)
811                     else:
812                         cell.append('%s: %s'%(k, str(args[k])))
813                         if current.has_key(k):
814                             cell[-1] += ' -> %s'%current[k]
815                             current[k] = str(args[k])
817                 arg_s = '<br />'.join(cell)
818             else:
819                 # unkown event!!
820                 comments['unknown'] = _('''<strong><em>This event is not
821                     handled by the history display!</em></strong>''')
822                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
823             date_s = date_s.replace(' ', '&nbsp;')
824             # if the user's an itemid, figure the username (older journals
825             # have the username)
826             if dre.match(user):
827                 user = self._db.user.get(user, 'username')
828             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
829                 date_s, user, action, arg_s))
830         if comments:
831             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
832         for entry in comments.values():
833             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
834         l.append('</table>')
835         return '\n'.join(l)
837     def renderQueryForm(self):
838         ''' Render this item, which is a query, as a search form.
839         '''
840         # create a new request and override the specified args
841         req = HTMLRequest(self._client)
842         req.classname = self._klass.get(self._nodeid, 'klass')
843         name = self._klass.get(self._nodeid, 'name')
844         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
845             '&@queryname=%s'%urllib.quote(name))
847         # new template, using the specified classname and request
848         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
850         # use our fabricated request
851         return pt.render(self._client, req.classname, req)
853 class HTMLUser(HTMLItem):
854     ''' Accesses through the *user* (a special case of item)
855     '''
856     def __init__(self, client, classname, nodeid, anonymous=0):
857         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
858         self._default_classname = client.classname
860         # used for security checks
861         self._security = client.db.security
863     _marker = []
864     def hasPermission(self, permission, classname=_marker):
865         ''' Determine if the user has the Permission.
867             The class being tested defaults to the template's class, but may
868             be overidden for this test by suppling an alternate classname.
869         '''
870         if classname is self._marker:
871             classname = self._default_classname
872         return self._security.hasPermission(permission, self._nodeid, classname)
874     def is_edit_ok(self):
875         ''' Is the user allowed to Edit the current class?
876             Also check whether this is the current user's info.
877         '''
878         return self._db.security.hasPermission('Edit', self._client.userid,
879             self._classname) or (self._nodeid == self._client.userid and
880             self._db.user.get(self._client.userid, 'username') != 'anonymous')
882     def is_view_ok(self):
883         ''' Is the user allowed to View the current class?
884             Also check whether this is the current user's info.
885         '''
886         return self._db.security.hasPermission('View', self._client.userid,
887             self._classname) or (self._nodeid == self._client.userid and
888             self._db.user.get(self._client.userid, 'username') != 'anonymous')
890 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
891     ''' String, Number, Date, Interval HTMLProperty
893         Has useful attributes:
895          _name  the name of the property
896          _value the value of the property if any
898         A wrapper object which may be stringified for the plain() behaviour.
899     '''
900     def __init__(self, client, classname, nodeid, prop, name, value,
901             anonymous=0):
902         self._client = client
903         self._db = client.db
904         self._classname = classname
905         self._nodeid = nodeid
906         self._prop = prop
907         self._value = value
908         self._anonymous = anonymous
909         self._name = name
910         if not anonymous:
911             self._formname = '%s%s@%s'%(classname, nodeid, name)
912         else:
913             self._formname = name
915         HTMLInputMixin.__init__(self)
917     def __repr__(self):
918         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
919             self._prop, self._value)
920     def __str__(self):
921         return self.plain()
922     def __cmp__(self, other):
923         if isinstance(other, HTMLProperty):
924             return cmp(self._value, other._value)
925         return cmp(self._value, other)
927     def is_edit_ok(self):
928         ''' Is the user allowed to Edit the current class?
929         '''
930         thing = HTMLDatabase(self._client)[self._classname]
931         if self._nodeid:
932             # this is a special-case for the User class where permission's
933             # on a per-item basis :(
934             thing = thing.getItem(self._nodeid)
935         return thing.is_edit_ok()
937     def is_view_ok(self):
938         ''' Is the user allowed to View the current class?
939         '''
940         thing = HTMLDatabase(self._client)[self._classname]
941         if self._nodeid:
942             # this is a special-case for the User class where permission's
943             # on a per-item basis :(
944             thing = thing.getItem(self._nodeid)
945         return thing.is_view_ok()
947 class StringHTMLProperty(HTMLProperty):
948     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
949                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
950                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
951     def _hyper_repl(self, match):
952         if match.group('url'):
953             s = match.group('url')
954             return '<a href="%s">%s</a>'%(s, s)
955         elif match.group('email'):
956             s = match.group('email')
957             return '<a href="mailto:%s">%s</a>'%(s, s)
958         else:
959             s = match.group('item')
960             s1 = match.group('class')
961             s2 = match.group('id')
962             try:
963                 # make sure s1 is a valid tracker classname
964                 cl = self._db.getclass(s1)
965                 if not cl.hasnode(s2):
966                     raise KeyError, 'oops'
967                 return '<a href="%s">%s%s</a>'%(s, s1, s2)
968             except KeyError:
969                 return '%s%s'%(s1, s2)
971     def hyperlinked(self):
972         ''' Render a "hyperlinked" version of the text '''
973         return self.plain(hyperlink=1)
975     def plain(self, escape=0, hyperlink=0):
976         '''Render a "plain" representation of the property
977             
978         - "escape" turns on/off HTML quoting
979         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
980           addresses and designators
981         '''
982         self.view_check()
984         if self._value is None:
985             return ''
986         if escape:
987             s = cgi.escape(str(self._value))
988         else:
989             s = str(self._value)
990         if hyperlink:
991             # no, we *must* escape this text
992             if not escape:
993                 s = cgi.escape(s)
994             s = self.hyper_re.sub(self._hyper_repl, s)
995         return s
997     def stext(self, escape=0):
998         ''' Render the value of the property as StructuredText.
1000             This requires the StructureText module to be installed separately.
1001         '''
1002         self.view_check()
1004         s = self.plain(escape=escape)
1005         if not StructuredText:
1006             return s
1007         return StructuredText(s,level=1,header=0)
1009     def field(self, size = 30):
1010         ''' Render the property as a field in HTML.
1012             If not editable, just display the value via plain().
1013         '''
1014         self.view_check()
1016         if self._value is None:
1017             value = ''
1018         else:
1019             value = cgi.escape(str(self._value))
1021         if self.is_edit_ok():
1022             value = '&quot;'.join(value.split('"'))
1023             return self.input(name=self._formname,value=value,size=size)
1025         return self.plain()
1027     def multiline(self, escape=0, rows=5, cols=40):
1028         ''' Render a multiline form edit field for the property.
1030             If not editable, just display the plain() value in a <pre> tag.
1031         '''
1032         self.view_check()
1034         if self._value is None:
1035             value = ''
1036         else:
1037             value = cgi.escape(str(self._value))
1039         if self.is_edit_ok():
1040             value = '&quot;'.join(value.split('"'))
1041             return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1042                 self._formname, rows, cols, value)
1044         return '<pre>%s</pre>'%self.plain()
1046     def email(self, escape=1):
1047         ''' Render the value of the property as an obscured email address
1048         '''
1049         self.view_check()
1051         if self._value is None:
1052             value = ''
1053         else:
1054             value = str(self._value)
1055         if value.find('@') != -1:
1056             name, domain = value.split('@')
1057             domain = ' '.join(domain.split('.')[:-1])
1058             name = name.replace('.', ' ')
1059             value = '%s at %s ...'%(name, domain)
1060         else:
1061             value = value.replace('.', ' ')
1062         if escape:
1063             value = cgi.escape(value)
1064         return value
1066 class PasswordHTMLProperty(HTMLProperty):
1067     def plain(self):
1068         ''' Render a "plain" representation of the property
1069         '''
1070         self.view_check()
1072         if self._value is None:
1073             return ''
1074         return _('*encrypted*')
1076     def field(self, size = 30):
1077         ''' Render a form edit field for the property.
1079             If not editable, just display the value via plain().
1080         '''
1081         self.view_check()
1083         if self.is_edit_ok():
1084             return self.input(type="password", name=self._formname, size=size)
1086         return self.plain()
1088     def confirm(self, size = 30):
1089         ''' Render a second form edit field for the property, used for 
1090             confirmation that the user typed the password correctly. Generates
1091             a field with name "@confirm@name".
1093             If not editable, display nothing.
1094         '''
1095         self.view_check()
1097         if self.is_edit_ok():
1098             return self.input(type="password",
1099                 name="@confirm@%s"%self._formname, size=size)
1101         return ''
1103 class NumberHTMLProperty(HTMLProperty):
1104     def plain(self):
1105         ''' Render a "plain" representation of the property
1106         '''
1107         self.view_check()
1109         return str(self._value)
1111     def field(self, size = 30):
1112         ''' Render a form edit field for the property.
1114             If not editable, just display the value via plain().
1115         '''
1116         self.view_check()
1118         if self._value is None:
1119             value = ''
1120         else:
1121             value = cgi.escape(str(self._value))
1123         if self.is_edit_ok():
1124             value = '&quot;'.join(value.split('"'))
1125             return self.input(name=self._formname,value=value,size=size)
1127         return self.plain()
1129     def __int__(self):
1130         ''' Return an int of me
1131         '''
1132         return int(self._value)
1134     def __float__(self):
1135         ''' Return a float of me
1136         '''
1137         return float(self._value)
1140 class BooleanHTMLProperty(HTMLProperty):
1141     def plain(self):
1142         ''' Render a "plain" representation of the property
1143         '''
1144         self.view_check()
1146         if self._value is None:
1147             return ''
1148         return self._value and "Yes" or "No"
1150     def field(self):
1151         ''' Render a form edit field for the property
1153             If not editable, just display the value via plain().
1154         '''
1155         self.view_check()
1157         if not is_edit_ok():
1158             return self.plain()
1160         checked = self._value and "checked" or ""
1161         if self._value:
1162             s = self.input(type="radio", name=self._formname, value="yes",
1163                 checked="checked")
1164             s += 'Yes'
1165             s +=self.input(type="radio", name=self._formname, value="no")
1166             s += 'No'
1167         else:
1168             s = self.input(type="radio", name=self._formname, value="yes")
1169             s += 'Yes'
1170             s +=self.input(type="radio", name=self._formname, value="no",
1171                 checked="checked")
1172             s += 'No'
1173         return s
1175 class DateHTMLProperty(HTMLProperty):
1176     def plain(self):
1177         ''' Render a "plain" representation of the property
1178         '''
1179         self.view_check()
1181         if self._value is None:
1182             return ''
1183         return str(self._value.local(self._db.getUserTimezone()))
1185     def now(self):
1186         ''' Return the current time.
1188             This is useful for defaulting a new value. Returns a
1189             DateHTMLProperty.
1190         '''
1191         self.view_check()
1193         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1194             self._formname, date.Date('.'))
1196     def field(self, size = 30):
1197         ''' Render a form edit field for the property
1199             If not editable, just display the value via plain().
1200         '''
1201         self.view_check()
1203         if self._value is None:
1204             value = ''
1205         else:
1206             tz = self._db.getUserTimezone()
1207             value = cgi.escape(str(self._value.local(tz)))
1209         if is_edit_ok():
1210             value = '&quot;'.join(value.split('"'))
1211             return self.input(name=self._formname,value=value,size=size)
1212         
1213         return self.plain()
1215     def reldate(self, pretty=1):
1216         ''' Render the interval between the date and now.
1218             If the "pretty" flag is true, then make the display pretty.
1219         '''
1220         self.view_check()
1222         if not self._value:
1223             return ''
1225         # figure the interval
1226         interval = self._value - date.Date('.')
1227         if pretty:
1228             return interval.pretty()
1229         return str(interval)
1231     _marker = []
1232     def pretty(self, format=_marker):
1233         ''' Render the date in a pretty format (eg. month names, spaces).
1235             The format string is a standard python strftime format string.
1236             Note that if the day is zero, and appears at the start of the
1237             string, then it'll be stripped from the output. This is handy
1238             for the situatin when a date only specifies a month and a year.
1239         '''
1240         self.view_check()
1242         if format is not self._marker:
1243             return self._value.pretty(format)
1244         else:
1245             return self._value.pretty()
1247     def local(self, offset):
1248         ''' Return the date/time as a local (timezone offset) date/time.
1249         '''
1250         self.view_check()
1252         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1253             self._formname, self._value.local(offset))
1255 class IntervalHTMLProperty(HTMLProperty):
1256     def plain(self):
1257         ''' Render a "plain" representation of the property
1258         '''
1259         self.view_check()
1261         if self._value is None:
1262             return ''
1263         return str(self._value)
1265     def pretty(self):
1266         ''' Render the interval in a pretty format (eg. "yesterday")
1267         '''
1268         self.view_check()
1270         return self._value.pretty()
1272     def field(self, size = 30):
1273         ''' Render a form edit field for the property
1275             If not editable, just display the value via plain().
1276         '''
1277         self.view_check()
1279         if self._value is None:
1280             value = ''
1281         else:
1282             value = cgi.escape(str(self._value))
1284         if is_edit_ok():
1285             value = '&quot;'.join(value.split('"'))
1286             return self.input(name=self._formname,value=value,size=size)
1288         return self.plain()
1290 class LinkHTMLProperty(HTMLProperty):
1291     ''' Link HTMLProperty
1292         Include the above as well as being able to access the class
1293         information. Stringifying the object itself results in the value
1294         from the item being displayed. Accessing attributes of this object
1295         result in the appropriate entry from the class being queried for the
1296         property accessed (so item/assignedto/name would look up the user
1297         entry identified by the assignedto property on item, and then the
1298         name property of that user)
1299     '''
1300     def __init__(self, *args, **kw):
1301         HTMLProperty.__init__(self, *args, **kw)
1302         # if we're representing a form value, then the -1 from the form really
1303         # should be a None
1304         if str(self._value) == '-1':
1305             self._value = None
1307     def __getattr__(self, attr):
1308         ''' return a new HTMLItem '''
1309        #print 'Link.getattr', (self, attr, self._value)
1310         if not self._value:
1311             raise AttributeError, "Can't access missing value"
1312         if self._prop.classname == 'user':
1313             klass = HTMLUser
1314         else:
1315             klass = HTMLItem
1316         i = klass(self._client, self._prop.classname, self._value)
1317         return getattr(i, attr)
1319     def plain(self, escape=0):
1320         ''' Render a "plain" representation of the property
1321         '''
1322         self.view_check()
1324         if self._value is None:
1325             return ''
1326         linkcl = self._db.classes[self._prop.classname]
1327         k = linkcl.labelprop(1)
1328         value = str(linkcl.get(self._value, k))
1329         if escape:
1330             value = cgi.escape(value)
1331         return value
1333     def field(self, showid=0, size=None):
1334         ''' Render a form edit field for the property
1336             If not editable, just display the value via plain().
1337         '''
1338         self.view_check()
1340         if not self.is_edit_ok():
1341             return self.plain()
1343         # edit field
1344         linkcl = self._db.getclass(self._prop.classname)
1345         if self._value is None:
1346             value = ''
1347         else:
1348             k = linkcl.getkey()
1349             if k:
1350                 label = linkcl.get(self._value, k)
1351             else:
1352                 label = self._value
1353             value = cgi.escape(str(self._value))
1354             value = '&quot;'.join(value.split('"'))
1355         return '<input name="%s" value="%s" size="%s">'%(self._formname,
1356             label, size)
1358     def menu(self, size=None, height=None, showid=0, additional=[],
1359             sort_on=None, **conditions):
1360         ''' Render a form select list for this property
1362             If not editable, just display the value via plain().
1363         '''
1364         self.view_check()
1366         if not self.is_edit_ok():
1367             return self.plain()
1369         value = self._value
1371         linkcl = self._db.getclass(self._prop.classname)
1372         l = ['<select name="%s">'%self._formname]
1373         k = linkcl.labelprop(1)
1374         s = ''
1375         if value is None:
1376             s = 'selected="selected" '
1377         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1378         if linkcl.getprops().has_key('order'):  
1379             sort_on = ('+', 'order')
1380         else:  
1381             if sort_on is None:
1382                 sort_on = ('+', linkcl.labelprop())
1383             else:
1384                 sort_on = ('+', sort_on)
1385         options = linkcl.filter(None, conditions, sort_on, (None, None))
1387         # make sure we list the current value if it's retired
1388         if self._value and self._value not in options:
1389             options.insert(0, self._value)
1391         for optionid in options:
1392             # get the option value, and if it's None use an empty string
1393             option = linkcl.get(optionid, k) or ''
1395             # figure if this option is selected
1396             s = ''
1397             if value in [optionid, option]:
1398                 s = 'selected="selected" '
1400             # figure the label
1401             if showid:
1402                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1403             else:
1404                 lab = option
1406             # truncate if it's too long
1407             if size is not None and len(lab) > size:
1408                 lab = lab[:size-3] + '...'
1409             if additional:
1410                 m = []
1411                 for propname in additional:
1412                     m.append(linkcl.get(optionid, propname))
1413                 lab = lab + ' (%s)'%', '.join(map(str, m))
1415             # and generate
1416             lab = cgi.escape(lab)
1417             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1418         l.append('</select>')
1419         return '\n'.join(l)
1420 #    def checklist(self, ...)
1422 class MultilinkHTMLProperty(HTMLProperty):
1423     ''' Multilink HTMLProperty
1425         Also be iterable, returning a wrapper object like the Link case for
1426         each entry in the multilink.
1427     '''
1428     def __init__(self, *args, **kwargs):
1429         HTMLProperty.__init__(self, *args, **kwargs)
1430         if self._value:
1431             sortfun = make_sort_function(self._db, self._prop.classname)
1432             self._value.sort(sortfun)
1433     
1434     def __len__(self):
1435         ''' length of the multilink '''
1436         return len(self._value)
1438     def __getattr__(self, attr):
1439         ''' no extended attribute accesses make sense here '''
1440         raise AttributeError, attr
1442     def __getitem__(self, num):
1443         ''' iterate and return a new HTMLItem
1444         '''
1445        #print 'Multi.getitem', (self, num)
1446         value = self._value[num]
1447         if self._prop.classname == 'user':
1448             klass = HTMLUser
1449         else:
1450             klass = HTMLItem
1451         return klass(self._client, self._prop.classname, value)
1453     def __contains__(self, value):
1454         ''' Support the "in" operator. We have to make sure the passed-in
1455             value is a string first, not a HTMLProperty.
1456         '''
1457         return str(value) in self._value
1459     def reverse(self):
1460         ''' return the list in reverse order
1461         '''
1462         l = self._value[:]
1463         l.reverse()
1464         if self._prop.classname == 'user':
1465             klass = HTMLUser
1466         else:
1467             klass = HTMLItem
1468         return [klass(self._client, self._prop.classname, value) for value in l]
1470     def plain(self, escape=0):
1471         ''' Render a "plain" representation of the property
1472         '''
1473         self.view_check()
1475         linkcl = self._db.classes[self._prop.classname]
1476         k = linkcl.labelprop(1)
1477         labels = []
1478         for v in self._value:
1479             labels.append(linkcl.get(v, k))
1480         value = ', '.join(labels)
1481         if escape:
1482             value = cgi.escape(value)
1483         return value
1485     def field(self, size=30, showid=0):
1486         ''' Render a form edit field for the property
1488             If not editable, just display the value via plain().
1489         '''
1490         self.view_check()
1492         if not self.is_edit_ok():
1493             return self.plain()
1495         linkcl = self._db.getclass(self._prop.classname)
1496         value = self._value[:]
1497         # map the id to the label property
1498         if not linkcl.getkey():
1499             showid=1
1500         if not showid:
1501             k = linkcl.labelprop(1)
1502             value = [linkcl.get(v, k) for v in value]
1503         value = cgi.escape(','.join(value))
1504         return self.input(name=self._formname,size=size,value=value)
1506     def menu(self, size=None, height=None, showid=0, additional=[],
1507             sort_on=None, **conditions):
1508         ''' Render a form select list for this property
1510             If not editable, just display the value via plain().
1511         '''
1512         self.view_check()
1514         if not self.is_edit_ok():
1515             return self.plain()
1517         value = self._value
1519         linkcl = self._db.getclass(self._prop.classname)
1520         if sort_on is None:
1521             sort_on = ('+', find_sort_key(linkcl))
1522         else:
1523             sort_on = ('+', sort_on)
1524         options = linkcl.filter(None, conditions, sort_on)
1525         height = height or min(len(options), 7)
1526         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1527         k = linkcl.labelprop(1)
1529         # make sure we list the current values if they're retired
1530         for val in value:
1531             if val not in options:
1532                 options.insert(0, val)
1534         for optionid in options:
1535             # get the option value, and if it's None use an empty string
1536             option = linkcl.get(optionid, k) or ''
1538             # figure if this option is selected
1539             s = ''
1540             if optionid in value or option in value:
1541                 s = 'selected="selected" '
1543             # figure the label
1544             if showid:
1545                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1546             else:
1547                 lab = option
1548             # truncate if it's too long
1549             if size is not None and len(lab) > size:
1550                 lab = lab[:size-3] + '...'
1551             if additional:
1552                 m = []
1553                 for propname in additional:
1554                     m.append(linkcl.get(optionid, propname))
1555                 lab = lab + ' (%s)'%', '.join(m)
1557             # and generate
1558             lab = cgi.escape(lab)
1559             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1560                 lab))
1561         l.append('</select>')
1562         return '\n'.join(l)
1564 # set the propclasses for HTMLItem
1565 propclasses = (
1566     (hyperdb.String, StringHTMLProperty),
1567     (hyperdb.Number, NumberHTMLProperty),
1568     (hyperdb.Boolean, BooleanHTMLProperty),
1569     (hyperdb.Date, DateHTMLProperty),
1570     (hyperdb.Interval, IntervalHTMLProperty),
1571     (hyperdb.Password, PasswordHTMLProperty),
1572     (hyperdb.Link, LinkHTMLProperty),
1573     (hyperdb.Multilink, MultilinkHTMLProperty),
1576 def make_sort_function(db, classname, sort_on=None):
1577     '''Make a sort function for a given class
1578     '''
1579     linkcl = db.getclass(classname)
1580     if sort_on is None:
1581         sort_on = find_sort_key(linkcl)
1582     def sortfunc(a, b):
1583         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1584     return sortfunc
1586 def find_sort_key(linkcl):
1587     if linkcl.getprops().has_key('order'):
1588         return 'order'
1589     else:
1590         return linkcl.labelprop()
1592 def handleListCGIValue(value):
1593     ''' Value is either a single item or a list of items. Each item has a
1594         .value that we're actually interested in.
1595     '''
1596     if isinstance(value, type([])):
1597         return [value.value for value in value]
1598     else:
1599         value = value.value.strip()
1600         if not value:
1601             return []
1602         return value.split(',')
1604 class ShowDict:
1605     ''' A convenience access to the :columns index parameters
1606     '''
1607     def __init__(self, columns):
1608         self.columns = {}
1609         for col in columns:
1610             self.columns[col] = 1
1611     def __getitem__(self, name):
1612         return self.columns.has_key(name)
1614 class HTMLRequest(HTMLInputMixin):
1615     '''The *request*, holding the CGI form and environment.
1617     - "form" the CGI form as a cgi.FieldStorage
1618     - "env" the CGI environment variables
1619     - "base" the base URL for this instance
1620     - "user" a HTMLUser instance for this user
1621     - "classname" the current classname (possibly None)
1622     - "template" the current template (suffix, also possibly None)
1624     Index args:
1626     - "columns" dictionary of the columns to display in an index page
1627     - "show" a convenience access to columns - request/show/colname will
1628       be true if the columns should be displayed, false otherwise
1629     - "sort" index sort column (direction, column name)
1630     - "group" index grouping property (direction, column name)
1631     - "filter" properties to filter the index on
1632     - "filterspec" values to filter the index on
1633     - "search_text" text to perform a full-text search on for an index
1634     '''
1635     def __init__(self, client):
1636         # _client is needed by HTMLInputMixin
1637         self._client = self.client = client
1639         # easier access vars
1640         self.form = client.form
1641         self.env = client.env
1642         self.base = client.base
1643         self.user = HTMLUser(client, 'user', client.userid)
1645         # store the current class name and action
1646         self.classname = client.classname
1647         self.template = client.template
1649         # the special char to use for special vars
1650         self.special_char = '@'
1652         HTMLInputMixin.__init__(self)
1654         self._post_init()
1656     def _post_init(self):
1657         ''' Set attributes based on self.form
1658         '''
1659         # extract the index display information from the form
1660         self.columns = []
1661         for name in ':columns @columns'.split():
1662             if self.form.has_key(name):
1663                 self.special_char = name[0]
1664                 self.columns = handleListCGIValue(self.form[name])
1665                 break
1666         self.show = ShowDict(self.columns)
1668         # sorting
1669         self.sort = (None, None)
1670         for name in ':sort @sort'.split():
1671             if self.form.has_key(name):
1672                 self.special_char = name[0]
1673                 sort = self.form[name].value
1674                 if sort.startswith('-'):
1675                     self.sort = ('-', sort[1:])
1676                 else:
1677                     self.sort = ('+', sort)
1678                 if self.form.has_key(self.special_char+'sortdir'):
1679                     self.sort = ('-', self.sort[1])
1681         # grouping
1682         self.group = (None, None)
1683         for name in ':group @group'.split():
1684             if self.form.has_key(name):
1685                 self.special_char = name[0]
1686                 group = self.form[name].value
1687                 if group.startswith('-'):
1688                     self.group = ('-', group[1:])
1689                 else:
1690                     self.group = ('+', group)
1691                 if self.form.has_key(self.special_char+'groupdir'):
1692                     self.group = ('-', self.group[1])
1694         # filtering
1695         self.filter = []
1696         for name in ':filter @filter'.split():
1697             if self.form.has_key(name):
1698                 self.special_char = name[0]
1699                 self.filter = handleListCGIValue(self.form[name])
1701         self.filterspec = {}
1702         db = self.client.db
1703         if self.classname is not None:
1704             props = db.getclass(self.classname).getprops()
1705             for name in self.filter:
1706                 if not self.form.has_key(name):
1707                     continue
1708                 prop = props[name]
1709                 fv = self.form[name]
1710                 if (isinstance(prop, hyperdb.Link) or
1711                         isinstance(prop, hyperdb.Multilink)):
1712                     self.filterspec[name] = lookupIds(db, prop,
1713                         handleListCGIValue(fv))
1714                 else:
1715                     if isinstance(fv, type([])):
1716                         self.filterspec[name] = [v.value for v in fv]
1717                     else:
1718                         self.filterspec[name] = fv.value
1720         # full-text search argument
1721         self.search_text = None
1722         for name in ':search_text @search_text'.split():
1723             if self.form.has_key(name):
1724                 self.special_char = name[0]
1725                 self.search_text = self.form[name].value
1727         # pagination - size and start index
1728         # figure batch args
1729         self.pagesize = 50
1730         for name in ':pagesize @pagesize'.split():
1731             if self.form.has_key(name):
1732                 self.special_char = name[0]
1733                 self.pagesize = int(self.form[name].value)
1735         self.startwith = 0
1736         for name in ':startwith @startwith'.split():
1737             if self.form.has_key(name):
1738                 self.special_char = name[0]
1739                 self.startwith = int(self.form[name].value)
1741     def updateFromURL(self, url):
1742         ''' Parse the URL for query args, and update my attributes using the
1743             values.
1744         ''' 
1745         env = {'QUERY_STRING': url}
1746         self.form = cgi.FieldStorage(environ=env)
1748         self._post_init()
1750     def update(self, kwargs):
1751         ''' Update my attributes using the keyword args
1752         '''
1753         self.__dict__.update(kwargs)
1754         if kwargs.has_key('columns'):
1755             self.show = ShowDict(self.columns)
1757     def description(self):
1758         ''' Return a description of the request - handle for the page title.
1759         '''
1760         s = [self.client.db.config.TRACKER_NAME]
1761         if self.classname:
1762             if self.client.nodeid:
1763                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1764             else:
1765                 if self.template == 'item':
1766                     s.append('- new %s'%self.classname)
1767                 elif self.template == 'index':
1768                     s.append('- %s index'%self.classname)
1769                 else:
1770                     s.append('- %s %s'%(self.classname, self.template))
1771         else:
1772             s.append('- home')
1773         return ' '.join(s)
1775     def __str__(self):
1776         d = {}
1777         d.update(self.__dict__)
1778         f = ''
1779         for k in self.form.keys():
1780             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1781         d['form'] = f
1782         e = ''
1783         for k,v in self.env.items():
1784             e += '\n     %r=%r'%(k, v)
1785         d['env'] = e
1786         return '''
1787 form: %(form)s
1788 base: %(base)r
1789 classname: %(classname)r
1790 template: %(template)r
1791 columns: %(columns)r
1792 sort: %(sort)r
1793 group: %(group)r
1794 filter: %(filter)r
1795 search_text: %(search_text)r
1796 pagesize: %(pagesize)r
1797 startwith: %(startwith)r
1798 env: %(env)s
1799 '''%d
1801     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1802             filterspec=1):
1803         ''' return the current index args as form elements '''
1804         l = []
1805         sc = self.special_char
1806         s = self.input(type="hidden",name="%s",value="%s")
1807         if columns and self.columns:
1808             l.append(s%(sc+'columns', ','.join(self.columns)))
1809         if sort and self.sort[1] is not None:
1810             if self.sort[0] == '-':
1811                 val = '-'+self.sort[1]
1812             else:
1813                 val = self.sort[1]
1814             l.append(s%(sc+'sort', val))
1815         if group and self.group[1] is not None:
1816             if self.group[0] == '-':
1817                 val = '-'+self.group[1]
1818             else:
1819                 val = self.group[1]
1820             l.append(s%(sc+'group', val))
1821         if filter and self.filter:
1822             l.append(s%(sc+'filter', ','.join(self.filter)))
1823         if filterspec:
1824             for k,v in self.filterspec.items():
1825                 if type(v) == type([]):
1826                     l.append(s%(k, ','.join(v)))
1827                 else:
1828                     l.append(s%(k, v))
1829         if self.search_text:
1830             l.append(s%(sc+'search_text', self.search_text))
1831         l.append(s%(sc+'pagesize', self.pagesize))
1832         l.append(s%(sc+'startwith', self.startwith))
1833         return '\n'.join(l)
1835     def indexargs_url(self, url, args):
1836         ''' Embed the current index args in a URL
1837         '''
1838         sc = self.special_char
1839         l = ['%s=%s'%(k,v) for k,v in args.items()]
1841         # pull out the special values (prefixed by @ or :)
1842         specials = {}
1843         for key in args.keys():
1844             if key[0] in '@:':
1845                 specials[key[1:]] = args[key]
1847         # ok, now handle the specials we received in the request
1848         if self.columns and not specials.has_key('columns'):
1849             l.append(sc+'columns=%s'%(','.join(self.columns)))
1850         if self.sort[1] is not None and not specials.has_key('sort'):
1851             if self.sort[0] == '-':
1852                 val = '-'+self.sort[1]
1853             else:
1854                 val = self.sort[1]
1855             l.append(sc+'sort=%s'%val)
1856         if self.group[1] is not None and not specials.has_key('group'):
1857             if self.group[0] == '-':
1858                 val = '-'+self.group[1]
1859             else:
1860                 val = self.group[1]
1861             l.append(sc+'group=%s'%val)
1862         if self.filter and not specials.has_key('filter'):
1863             l.append(sc+'filter=%s'%(','.join(self.filter)))
1864         if self.search_text and not specials.has_key('search_text'):
1865             l.append(sc+'search_text=%s'%self.search_text)
1866         if not specials.has_key('pagesize'):
1867             l.append(sc+'pagesize=%s'%self.pagesize)
1868         if not specials.has_key('startwith'):
1869             l.append(sc+'startwith=%s'%self.startwith)
1871         # finally, the remainder of the filter args in the request
1872         for k,v in self.filterspec.items():
1873             if not args.has_key(k):
1874                 if type(v) == type([]):
1875                     l.append('%s=%s'%(k, ','.join(v)))
1876                 else:
1877                     l.append('%s=%s'%(k, v))
1878         return '%s?%s'%(url, '&'.join(l))
1879     indexargs_href = indexargs_url
1881     def base_javascript(self):
1882         return '''
1883 <script type="text/javascript">
1884 submitted = false;
1885 function submit_once() {
1886     if (submitted) {
1887         alert("Your request is being processed.\\nPlease be patient.");
1888         event.returnValue = 0;    // work-around for IE
1889         return 0;
1890     }
1891     submitted = true;
1892     return 1;
1895 function help_window(helpurl, width, height) {
1896     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1898 </script>
1899 '''%self.base
1901     def batch(self):
1902         ''' Return a batch object for results from the "current search"
1903         '''
1904         filterspec = self.filterspec
1905         sort = self.sort
1906         group = self.group
1908         # get the list of ids we're batching over
1909         klass = self.client.db.getclass(self.classname)
1910         if self.search_text:
1911             matches = self.client.db.indexer.search(
1912                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1913         else:
1914             matches = None
1915         l = klass.filter(matches, filterspec, sort, group)
1917         # return the batch object, using IDs only
1918         return Batch(self.client, l, self.pagesize, self.startwith,
1919             classname=self.classname)
1921 # extend the standard ZTUtils Batch object to remove dependency on
1922 # Acquisition and add a couple of useful methods
1923 class Batch(ZTUtils.Batch):
1924     ''' Use me to turn a list of items, or item ids of a given class, into a
1925         series of batches.
1927         ========= ========================================================
1928         Parameter  Usage
1929         ========= ========================================================
1930         sequence  a list of HTMLItems or item ids
1931         classname if sequence is a list of ids, this is the class of item
1932         size      how big to make the sequence.
1933         start     where to start (0-indexed) in the sequence.
1934         end       where to end (0-indexed) in the sequence.
1935         orphan    if the next batch would contain less items than this
1936                   value, then it is combined with this batch
1937         overlap   the number of items shared between adjacent batches
1938         ========= ========================================================
1940         Attributes: Note that the "start" attribute, unlike the
1941         argument, is a 1-based index (I know, lame).  "first" is the
1942         0-based index.  "length" is the actual number of elements in
1943         the batch.
1945         "sequence_length" is the length of the original, unbatched, sequence.
1946     '''
1947     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1948             overlap=0, classname=None):
1949         self.client = client
1950         self.last_index = self.last_item = None
1951         self.current_item = None
1952         self.classname = classname
1953         self.sequence_length = len(sequence)
1954         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1955             overlap)
1957     # overwrite so we can late-instantiate the HTMLItem instance
1958     def __getitem__(self, index):
1959         if index < 0:
1960             if index + self.end < self.first: raise IndexError, index
1961             return self._sequence[index + self.end]
1962         
1963         if index >= self.length:
1964             raise IndexError, index
1966         # move the last_item along - but only if the fetched index changes
1967         # (for some reason, index 0 is fetched twice)
1968         if index != self.last_index:
1969             self.last_item = self.current_item
1970             self.last_index = index
1972         item = self._sequence[index + self.first]
1973         if self.classname:
1974             # map the item ids to instances
1975             if self.classname == 'user':
1976                 item = HTMLUser(self.client, self.classname, item)
1977             else:
1978                 item = HTMLItem(self.client, self.classname, item)
1979         self.current_item = item
1980         return item
1982     def propchanged(self, property):
1983         ''' Detect if the property marked as being the group property
1984             changed in the last iteration fetch
1985         '''
1986         if (self.last_item is None or
1987                 self.last_item[property] != self.current_item[property]):
1988             return 1
1989         return 0
1991     # override these 'cos we don't have access to acquisition
1992     def previous(self):
1993         if self.start == 1:
1994             return None
1995         return Batch(self.client, self._sequence, self._size,
1996             self.first - self._size + self.overlap, 0, self.orphan,
1997             self.overlap)
1999     def next(self):
2000         try:
2001             self._sequence[self.end]
2002         except IndexError:
2003             return None
2004         return Batch(self.client, self._sequence, self._size,
2005             self.end - self.overlap, 0, self.orphan, self.overlap)
2007 class TemplatingUtils:
2008     ''' Utilities for templating
2009     '''
2010     def __init__(self, client):
2011         self.client = client
2012     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2013         return Batch(self.client, sequence, size, start, end, orphan,
2014             overlap)