Code

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