Code

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