Code

give access to the input() method all over the place
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9     import cPickle as pickle
10 except ImportError:
11     import pickle
12 try:
13     import cStringIO as StringIO
14 except ImportError:
15     import StringIO
16 try:
17     import StructuredText
18 except ImportError:
19     StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 class NoTemplate(Exception):
28     pass
30 def find_template(dir, name, extension):
31     ''' Find a template in the nominated dir
32     '''
33     # find the source
34     if extension:
35         filename = '%s.%s'%(name, extension)
36     else:
37         filename = name
39     # try old-style
40     src = os.path.join(dir, filename)
41     if os.path.exists(src):
42         return (src, filename)
44     # try with a .html extension (new-style)
45     filename = filename + '.html'
46     src = os.path.join(dir, filename)
47     if os.path.exists(src):
48         return (src, filename)
50     # no extension == no generic template is possible
51     if not extension:
52         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
54     # try for a _generic template
55     generic = '_generic.%s'%extension
56     src = os.path.join(dir, generic)
57     if os.path.exists(src):
58         return (src, generic)
60     # finally, try _generic.html
61     generic = generic + '.html'
62     src = os.path.join(dir, generic)
63     if os.path.exists(src):
64         return (src, generic)
66     raise NoTemplate, 'No template file exists for templating "%s" '\
67         'with template "%s" (neither "%s" nor "%s")'%(name, extension,
68         filename, generic)
70 class Templates:
71     templates = {}
73     def __init__(self, dir):
74         self.dir = dir
76     def precompileTemplates(self):
77         ''' Go through a directory and precompile all the templates therein
78         '''
79         for filename in os.listdir(self.dir):
80             if os.path.isdir(filename): continue
81             if '.' in filename:
82                 name, extension = filename.split('.')
83                 self.get(name, extension)
84             else:
85                 self.get(filename, None)
87     def get(self, name, extension=None):
88         ''' Interface to get a template, possibly loading a compiled template.
90             "name" and "extension" indicate the template we're after, which in
91             most cases will be "name.extension". If "extension" is None, then
92             we look for a template just called "name" with no extension.
94             If the file "name.extension" doesn't exist, we look for
95             "_generic.extension" as a fallback.
96         '''
97         # default the name to "home"
98         if name is None:
99             name = 'home'
100         elif extension is None and '.' in name:
101             # split name
102             name, extension = name.split('.')
104         # find the source
105         src, filename = find_template(self.dir, name, extension)
107         # has it changed?
108         try:
109             stime = os.stat(src)[os.path.stat.ST_MTIME]
110         except os.error, error:
111             if error.errno != errno.ENOENT:
112                 raise
114         if self.templates.has_key(src) and \
115                 stime < self.templates[src].mtime:
116             # compiled template is up to date
117             return self.templates[src]
119         # compile the template
120         self.templates[src] = pt = RoundupPageTemplate()
121         # use pt_edit so we can pass the content_type guess too
122         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
123         pt.pt_edit(open(src).read(), content_type)
124         pt.id = filename
125         pt.mtime = time.time()
126         return pt
128     def __getitem__(self, name):
129         name, extension = os.path.splitext(name)
130         if extension:
131             extension = extension[1:]
132         try:
133             return self.get(name, extension)
134         except NoTemplate, message:
135             raise KeyError, message
137 class RoundupPageTemplate(PageTemplate.PageTemplate):
138     ''' A Roundup-specific PageTemplate.
140         Interrogate the client to set up the various template variables to
141         be available:
143         *context*
144          this is one of three things:
145          1. None - we're viewing a "home" page
146          2. The current class of item being displayed. This is an HTMLClass
147             instance.
148          3. The current item from the database, if we're viewing a specific
149             item, as an HTMLItem instance.
150         *request*
151           Includes information about the current request, including:
152            - the url
153            - the current index information (``filterspec``, ``filter`` args,
154              ``properties``, etc) parsed out of the form. 
155            - methods for easy filterspec link generation
156            - *user*, the current user node as an HTMLItem instance
157            - *form*, the current CGI form information as a FieldStorage
158         *config*
159           The current tracker config.
160         *db*
161           The current database, used to access arbitrary database items.
162         *utils*
163           This is a special class that has its base in the TemplatingUtils
164           class in this file. If the tracker interfaces module defines a
165           TemplatingUtils class then it is mixed in, overriding the methods
166           in the base class.
167     '''
168     def getContext(self, client, classname, request):
169         # construct the TemplatingUtils class
170         utils = TemplatingUtils
171         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
172             class utils(client.instance.interfaces.TemplatingUtils, utils):
173                 pass
175         c = {
176              'options': {},
177              'nothing': None,
178              'request': request,
179              'db': HTMLDatabase(client),
180              'config': client.instance.config,
181              'tracker': client.instance,
182              'utils': utils(client),
183              'templates': Templates(client.instance.config.TEMPLATES),
184         }
185         # add in the item if there is one
186         if client.nodeid:
187             if classname == 'user':
188                 c['context'] = HTMLUser(client, classname, client.nodeid,
189                     anonymous=1)
190             else:
191                 c['context'] = HTMLItem(client, classname, client.nodeid,
192                     anonymous=1)
193         elif client.db.classes.has_key(classname):
194             c['context'] = HTMLClass(client, classname, anonymous=1)
195         return c
197     def render(self, client, classname, request, **options):
198         """Render this Page Template"""
200         if not self._v_cooked:
201             self._cook()
203         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
205         if self._v_errors:
206             raise PageTemplate.PTRuntimeError, \
207                 'Page Template %s has errors.'%self.id
209         # figure the context
210         classname = classname or client.classname
211         request = request or HTMLRequest(client)
212         c = self.getContext(client, classname, request)
213         c.update({'options': options})
215         # and go
216         output = StringIO.StringIO()
217         TALInterpreter(self._v_program, self.macros,
218             getEngine().getContext(c), output, tal=1, strictinsert=0)()
219         return output.getvalue()
221 class HTMLDatabase:
222     ''' Return HTMLClasses for valid class fetches
223     '''
224     def __init__(self, client):
225         self._client = client
226         self._db = client.db
228         # we want config to be exposed
229         self.config = client.db.config
231     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
232         # check to see if we're actually accessing an item
233         m = desre.match(item)
234         if m:
235             self._client.db.getclass(m.group('cl'))
236             return HTMLItem(self._client, m.group('cl'), m.group('id'))
237         else:
238             self._client.db.getclass(item)
239             return HTMLClass(self._client, item)
241     def __getattr__(self, attr):
242         try:
243             return self[attr]
244         except KeyError:
245             raise AttributeError, attr
247     def classes(self):
248         l = self._client.db.classes.keys()
249         l.sort()
250         return [HTMLClass(self._client, cn) for cn in l]
252 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
253     cl = db.getclass(prop.classname)
254     l = []
255     for entry in ids:
256         if num_re.match(entry):
257             l.append(entry)
258         else:
259             try:
260                 l.append(cl.lookup(entry))
261             except KeyError:
262                 # ignore invalid keys
263                 pass
264     return l
266 class HTMLPermissions:
267     ''' Helpers that provide answers to commonly asked Permission questions.
268     '''
269     def is_edit_ok(self):
270         ''' Is the user allowed to Edit the current class?
271         '''
272         return self._db.security.hasPermission('Edit', self._client.userid,
273             self._classname)
274     def is_view_ok(self):
275         ''' Is the user allowed to View the current class?
276         '''
277         return self._db.security.hasPermission('View', self._client.userid,
278             self._classname)
279     def is_only_view_ok(self):
280         ''' Is the user only allowed to View (ie. not Edit) the current class?
281         '''
282         return self.is_view_ok() and not self.is_edit_ok()
284 def input_html4(**attrs):
285     """Generate an 'input' (html4) element with given attributes"""
286     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
288 def input_xhtml(**attrs):
289     """Generate an 'input' (xhtml) element with given attributes"""
290     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
292 class HTMLInputMixin:
293     ''' requires a _client property '''
294     def __init__(self):
295         html_version = 'html4'
296         if hasattr(self._client.instance.config, 'HTML_VERSION'):
297             html_version = self._client.instance.config.HTML_VERSION
298         if html_version == 'xhtml':
299             self.input = input_xhtml
300         else:
301             self.input = input_html4
303 class HTMLClass(HTMLInputMixin, HTMLPermissions):
304     ''' Accesses through a class (either through *class* or *db.<classname>*)
305     '''
306     def __init__(self, client, classname, anonymous=0):
307         self._client = client
308         self._db = client.db
309         self._anonymous = anonymous
311         # we want classname to be exposed, but _classname gives a
312         # consistent API for extending Class/Item
313         self._classname = self.classname = classname
314         self._klass = self._db.getclass(self.classname)
315         self._props = self._klass.getprops()
317         HTMLInputMixin.__init__(self)
319     def __repr__(self):
320         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
322     def __getitem__(self, item):
323         ''' return an HTMLProperty instance
324         '''
325        #print 'HTMLClass.getitem', (self, item)
327         # we don't exist
328         if item == 'id':
329             return None
331         # get the property
332         prop = self._props[item]
334         # look up the correct HTMLProperty class
335         form = self._client.form
336         for klass, htmlklass in propclasses:
337             if not isinstance(prop, klass):
338                 continue
339             if form.has_key(item):
340                 if isinstance(prop, hyperdb.Multilink):
341                     value = lookupIds(self._db, prop,
342                         handleListCGIValue(form[item]))
343                 elif isinstance(prop, hyperdb.Link):
344                     value = form[item].value.strip()
345                     if value:
346                         value = lookupIds(self._db, prop, [value])[0]
347                     else:
348                         value = None
349                 else:
350                     value = form[item].value.strip() or None
351             else:
352                 if isinstance(prop, hyperdb.Multilink):
353                     value = []
354                 else:
355                     value = None
356             return htmlklass(self._client, self._classname, '', prop, item,
357                 value, self._anonymous)
359         # no good
360         raise KeyError, item
362     def __getattr__(self, attr):
363         ''' convenience access '''
364         try:
365             return self[attr]
366         except KeyError:
367             raise AttributeError, attr
369     def designator(self):
370         ''' Return this class' designator (classname) '''
371         return self._classname
373     def getItem(self, itemid, num_re=re.compile('-?\d+')):
374         ''' Get an item of this class by its item id.
375         '''
376         # make sure we're looking at an itemid
377         if not num_re.match(itemid):
378             itemid = self._klass.lookup(itemid)
380         if self.classname == 'user':
381             klass = HTMLUser
382         else:
383             klass = HTMLItem
385         return klass(self._client, self.classname, itemid)
387     def properties(self, sort=1):
388         ''' Return HTMLProperty for all of this class' properties.
389         '''
390         l = []
391         for name, prop in self._props.items():
392             for klass, htmlklass in propclasses:
393                 if isinstance(prop, hyperdb.Multilink):
394                     value = []
395                 else:
396                     value = None
397                 if isinstance(prop, klass):
398                     l.append(htmlklass(self._client, self._classname, '',
399                         prop, name, value, self._anonymous))
400         if sort:
401             l.sort(lambda a,b:cmp(a._name, b._name))
402         return l
404     def list(self):
405         ''' List all items in this class.
406         '''
407         if self.classname == 'user':
408             klass = HTMLUser
409         else:
410             klass = HTMLItem
412         # get the list and sort it nicely
413         l = self._klass.list()
414         sortfunc = make_sort_function(self._db, self.classname)
415         l.sort(sortfunc)
417         l = [klass(self._client, self.classname, x) for x in l]
418         return l
420     def csv(self):
421         ''' Return the items of this class as a chunk of CSV text.
422         '''
423         if rcsv.error:
424             return rcsv.error
426         props = self.propnames()
427         s = StringIO.StringIO()
428         writer = rcsv.writer(s, rcsv.comma_separated)
429         writer.writerow(props)
430         for nodeid in self._klass.list():
431             l = []
432             for name in props:
433                 value = self._klass.get(nodeid, name)
434                 if value is None:
435                     l.append('')
436                 elif isinstance(value, type([])):
437                     l.append(':'.join(map(str, value)))
438                 else:
439                     l.append(str(self._klass.get(nodeid, name)))
440             writer.writerow(l)
441         return s.getvalue()
443     def propnames(self):
444         ''' Return the list of the names of the properties of this class.
445         '''
446         idlessprops = self._klass.getprops(protected=0).keys()
447         idlessprops.sort()
448         return ['id'] + idlessprops
450     def filter(self, request=None):
451         ''' Return a list of items from this class, filtered and sorted
452             by the current requested filterspec/filter/sort/group args
453         '''
454         # XXX allow direct specification of the filterspec etc.
455         if request is not None:
456             filterspec = request.filterspec
457             sort = request.sort
458             group = request.group
459         else:
460             filterspec = {}
461             sort = (None,None)
462             group = (None,None)
463         if self.classname == 'user':
464             klass = HTMLUser
465         else:
466             klass = HTMLItem
467         l = [klass(self._client, self.classname, x)
468              for x in self._klass.filter(None, filterspec, sort, group)]
469         return l
471     def classhelp(self, properties=None, label='(list)', width='500',
472             height='400', property=''):
473         ''' Pop up a javascript window with class help
475             This generates a link to a popup window which displays the 
476             properties indicated by "properties" of the class named by
477             "classname". The "properties" should be a comma-separated list
478             (eg. 'id,name,description'). Properties defaults to all the
479             properties of a class (excluding id, creator, created and
480             activity).
482             You may optionally override the label displayed, the width and
483             height. The popup window will be resizable and scrollable.
485             If the "property" arg is given, it's passed through to the
486             javascript help_window function.
487         '''
488         if properties is None:
489             properties = self._klass.getprops(protected=0).keys()
490             properties.sort()
491             properties = ','.join(properties)
492         if property:
493             property = '&amp;property=%s'%property
494         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
495             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
496             \'%s\')">%s</a>'%(self.classname, properties, property, width,
497             height, label)
499     def submit(self, label="Submit New Entry"):
500         ''' Generate a submit button (and action hidden element)
501         '''
502         return self.input(type="hidden",name="@action",value="new") + '\n' + \
503                self.input(type="submit",name="submit",value=label)
505     def history(self):
506         return 'New node - no history'
508     def renderWith(self, name, **kwargs):
509         ''' Render this class with the given template.
510         '''
511         # create a new request and override the specified args
512         req = HTMLRequest(self._client)
513         req.classname = self.classname
514         req.update(kwargs)
516         # new template, using the specified classname and request
517         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
519         # use our fabricated request
520         return pt.render(self._client, self.classname, req)
522 class HTMLItem(HTMLInputMixin, HTMLPermissions):
523     ''' Accesses through an *item*
524     '''
525     def __init__(self, client, classname, nodeid, anonymous=0):
526         self._client = client
527         self._db = client.db
528         self._classname = classname
529         self._nodeid = nodeid
530         self._klass = self._db.getclass(classname)
531         self._props = self._klass.getprops()
533         # do we prefix the form items with the item's identification?
534         self._anonymous = anonymous
536         HTMLInputMixin.__init__(self)
538     def __repr__(self):
539         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
540             self._nodeid)
542     def __getitem__(self, item):
543         ''' return an HTMLProperty instance
544         '''
545         #print 'HTMLItem.getitem', (self, item)
546         if item == 'id':
547             return self._nodeid
549         # get the property
550         prop = self._props[item]
552         # get the value, handling missing values
553         value = None
554         if int(self._nodeid) > 0:
555             value = self._klass.get(self._nodeid, item, None)
556         if value is None:
557             if isinstance(self._props[item], hyperdb.Multilink):
558                 value = []
560         # look up the correct HTMLProperty class
561         for klass, htmlklass in propclasses:
562             if isinstance(prop, klass):
563                 return htmlklass(self._client, self._classname,
564                     self._nodeid, prop, item, value, self._anonymous)
566         raise KeyError, item
568     def __getattr__(self, attr):
569         ''' convenience access to properties '''
570         try:
571             return self[attr]
572         except KeyError:
573             raise AttributeError, attr
575     def designator(self):
576         ''' Return this item's designator (classname + id) '''
577         return '%s%s'%(self._classname, self._nodeid)
578     
579     def submit(self, label="Submit Changes"):
580         ''' Generate a submit button (and action hidden element)
581         '''
582         return self.input(type="hidden",name="@action",value="edit") + '\n' + \
583                self.input(type="submit",name="submit",value=label)
585     def journal(self, direction='descending'):
586         ''' Return a list of HTMLJournalEntry instances.
587         '''
588         # XXX do this
589         return []
591     def history(self, direction='descending', dre=re.compile('\d+')):
592         l = ['<table class="history">'
593              '<tr><th colspan="4" class="header">',
594              _('History'),
595              '</th></tr><tr>',
596              _('<th>Date</th>'),
597              _('<th>User</th>'),
598              _('<th>Action</th>'),
599              _('<th>Args</th>'),
600             '</tr>']
601         current = {}
602         comments = {}
603         history = self._klass.history(self._nodeid)
604         history.sort()
605         timezone = self._db.getUserTimezone()
606         if direction == 'descending':
607             history.reverse()
608             for prop_n in self._props.keys():
609                 prop = self[prop_n]
610                 if isinstance(prop, HTMLProperty):
611                     current[prop_n] = prop.plain()
612                     # make link if hrefable
613                     if (self._props.has_key(prop_n) and
614                             isinstance(self._props[prop_n], hyperdb.Link)):
615                         classname = self._props[prop_n].classname
616                         try:
617                             template = find_template(self._db.config.TEMPLATES,
618                                 classname, 'item')
619                             if template[1].startswith('_generic'):
620                                 raise NoTemplate, 'not really...'
621                         except NoTemplate:
622                             pass
623                         else:
624                             id = self._klass.get(self._nodeid, prop_n, None)
625                             current[prop_n] = '<a href="%s%s">%s</a>'%(
626                                 classname, id, current[prop_n])
627  
628         for id, evt_date, user, action, args in history:
629             date_s = str(evt_date.local(timezone)).replace("."," ")
630             arg_s = ''
631             if action == 'link' and type(args) == type(()):
632                 if len(args) == 3:
633                     linkcl, linkid, key = args
634                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
635                         linkcl, linkid, key)
636                 else:
637                     arg_s = str(args)
639             elif action == 'unlink' and type(args) == type(()):
640                 if len(args) == 3:
641                     linkcl, linkid, key = args
642                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
643                         linkcl, linkid, key)
644                 else:
645                     arg_s = str(args)
647             elif type(args) == type({}):
648                 cell = []
649                 for k in args.keys():
650                     # try to get the relevant property and treat it
651                     # specially
652                     try:
653                         prop = self._props[k]
654                     except KeyError:
655                         prop = None
656                     if prop is None:
657                         # property no longer exists
658                         comments['no_exist'] = _('''<em>The indicated property
659                             no longer exists</em>''')
660                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
661                         continue
663                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
664                             isinstance(prop, hyperdb.Link)):
665                         # figure what the link class is
666                         classname = prop.classname
667                         try:
668                             linkcl = self._db.getclass(classname)
669                         except KeyError:
670                             labelprop = None
671                             comments[classname] = _('''The linked class
672                                 %(classname)s no longer exists''')%locals()
673                         labelprop = linkcl.labelprop(1)
674                         try:
675                             template = find_template(self._db.config.TEMPLATES,
676                                 classname, 'item')
677                             if template[1].startswith('_generic'):
678                                 raise NoTemplate, 'not really...'
679                             hrefable = 1
680                         except NoTemplate:
681                             hrefable = 0
683                     if isinstance(prop, hyperdb.Multilink) and args[k]:
684                         ml = []
685                         for linkid in args[k]:
686                             if isinstance(linkid, type(())):
687                                 sublabel = linkid[0] + ' '
688                                 linkids = linkid[1]
689                             else:
690                                 sublabel = ''
691                                 linkids = [linkid]
692                             subml = []
693                             for linkid in linkids:
694                                 label = classname + linkid
695                                 # if we have a label property, try to use it
696                                 # TODO: test for node existence even when
697                                 # there's no labelprop!
698                                 try:
699                                     if labelprop is not None and \
700                                             labelprop != 'id':
701                                         label = linkcl.get(linkid, labelprop)
702                                 except IndexError:
703                                     comments['no_link'] = _('''<strike>The
704                                         linked node no longer
705                                         exists</strike>''')
706                                     subml.append('<strike>%s</strike>'%label)
707                                 else:
708                                     if hrefable:
709                                         subml.append('<a href="%s%s">%s</a>'%(
710                                             classname, linkid, label))
711                                     else:
712                                         subml.append(label)
713                             ml.append(sublabel + ', '.join(subml))
714                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
715                     elif isinstance(prop, hyperdb.Link) and args[k]:
716                         label = classname + args[k]
717                         # if we have a label property, try to use it
718                         # TODO: test for node existence even when
719                         # there's no labelprop!
720                         if labelprop is not None and labelprop != 'id':
721                             try:
722                                 label = linkcl.get(args[k], labelprop)
723                             except IndexError:
724                                 comments['no_link'] = _('''<strike>The
725                                     linked node no longer
726                                     exists</strike>''')
727                                 cell.append(' <strike>%s</strike>,\n'%label)
728                                 # "flag" this is done .... euwww
729                                 label = None
730                         if label is not None:
731                             if hrefable:
732                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
733                             else:
734                                 old = label;
735                             cell.append('%s: %s' % (k,old))
736                             if current.has_key(k):
737                                 cell[-1] += ' -> %s'%current[k]
738                                 current[k] = old
740                     elif isinstance(prop, hyperdb.Date) and args[k]:
741                         d = date.Date(args[k]).local(timezone)
742                         cell.append('%s: %s'%(k, str(d)))
743                         if current.has_key(k):
744                             cell[-1] += ' -> %s' % current[k]
745                             current[k] = str(d)
747                     elif isinstance(prop, hyperdb.Interval) and args[k]:
748                         d = date.Interval(args[k])
749                         cell.append('%s: %s'%(k, str(d)))
750                         if current.has_key(k):
751                             cell[-1] += ' -> %s'%current[k]
752                             current[k] = str(d)
754                     elif isinstance(prop, hyperdb.String) and args[k]:
755                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
756                         if current.has_key(k):
757                             cell[-1] += ' -> %s'%current[k]
758                             current[k] = cgi.escape(args[k])
760                     elif not args[k]:
761                         if current.has_key(k):
762                             cell.append('%s: %s'%(k, current[k]))
763                             current[k] = '(no value)'
764                         else:
765                             cell.append('%s: (no value)'%k)
767                     else:
768                         cell.append('%s: %s'%(k, str(args[k])))
769                         if current.has_key(k):
770                             cell[-1] += ' -> %s'%current[k]
771                             current[k] = str(args[k])
773                 arg_s = '<br />'.join(cell)
774             else:
775                 # unkown event!!
776                 comments['unknown'] = _('''<strong><em>This event is not
777                     handled by the history display!</em></strong>''')
778                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
779             date_s = date_s.replace(' ', '&nbsp;')
780             # if the user's an itemid, figure the username (older journals
781             # have the username)
782             if dre.match(user):
783                 user = self._db.user.get(user, 'username')
784             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
785                 date_s, user, action, arg_s))
786         if comments:
787             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
788         for entry in comments.values():
789             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
790         l.append('</table>')
791         return '\n'.join(l)
793     def renderQueryForm(self):
794         ''' Render this item, which is a query, as a search form.
795         '''
796         # create a new request and override the specified args
797         req = HTMLRequest(self._client)
798         req.classname = self._klass.get(self._nodeid, 'klass')
799         name = self._klass.get(self._nodeid, 'name')
800         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
801             '&@queryname=%s'%urllib.quote(name))
803         # new template, using the specified classname and request
804         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
806         # use our fabricated request
807         return pt.render(self._client, req.classname, req)
809 class HTMLUser(HTMLItem):
810     ''' Accesses through the *user* (a special case of item)
811     '''
812     def __init__(self, client, classname, nodeid, anonymous=0):
813         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
814         self._default_classname = client.classname
816         # used for security checks
817         self._security = client.db.security
819     _marker = []
820     def hasPermission(self, permission, classname=_marker):
821         ''' Determine if the user has the Permission.
823             The class being tested defaults to the template's class, but may
824             be overidden for this test by suppling an alternate classname.
825         '''
826         if classname is self._marker:
827             classname = self._default_classname
828         return self._security.hasPermission(permission, self._nodeid, classname)
830     def is_edit_ok(self):
831         ''' Is the user allowed to Edit the current class?
832             Also check whether this is the current user's info.
833         '''
834         return self._db.security.hasPermission('Edit', self._client.userid,
835             self._classname) or (self._nodeid == self._client.userid and
836             self._db.user.get(self._client.userid, 'username') != 'anonymous')
838     def is_view_ok(self):
839         ''' Is the user allowed to View the current class?
840             Also check whether this is the current user's info.
841         '''
842         return self._db.security.hasPermission('Edit', self._client.userid,
843             self._classname) or (self._nodeid == self._client.userid and
844             self._db.user.get(self._client.userid, 'username') != 'anonymous')
846 class HTMLProperty(HTMLInputMixin):
847     ''' String, Number, Date, Interval HTMLProperty
849         Has useful attributes:
851          _name  the name of the property
852          _value the value of the property if any
854         A wrapper object which may be stringified for the plain() behaviour.
855     '''
856     def __init__(self, client, classname, nodeid, prop, name, value,
857             anonymous=0):
858         self._client = client
859         self._db = client.db
860         self._classname = classname
861         self._nodeid = nodeid
862         self._prop = prop
863         self._value = value
864         self._anonymous = anonymous
865         self._name = name
866         if not anonymous:
867             self._formname = '%s%s@%s'%(classname, nodeid, name)
868         else:
869             self._formname = name
871         HTMLInputMixin.__init__(self)
872         
873     def __repr__(self):
874         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
875             self._prop, self._value)
876     def __str__(self):
877         return self.plain()
878     def __cmp__(self, other):
879         if isinstance(other, HTMLProperty):
880             return cmp(self._value, other._value)
881         return cmp(self._value, other)
883 class StringHTMLProperty(HTMLProperty):
884     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
885                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
886                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
887     def _hyper_repl(self, match):
888         if match.group('url'):
889             s = match.group('url')
890             return '<a href="%s">%s</a>'%(s, s)
891         elif match.group('email'):
892             s = match.group('email')
893             return '<a href="mailto:%s">%s</a>'%(s, s)
894         else:
895             s = match.group('item')
896             s1 = match.group('class')
897             s2 = match.group('id')
898             try:
899                 # make sure s1 is a valid tracker classname
900                 self._db.getclass(s1)
901                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
902             except KeyError:
903                 return '%s%s'%(s1, s2)
905     def hyperlinked(self):
906         ''' Render a "hyperlinked" version of the text '''
907         return self.plain(hyperlink=1)
909     def plain(self, escape=0, hyperlink=0):
910         ''' Render a "plain" representation of the property
911             
912             "escape" turns on/off HTML quoting
913             "hyperlink" turns on/off in-text hyperlinking of URLs, email
914                 addresses and designators
915         '''
916         if self._value is None:
917             return ''
918         if escape:
919             s = cgi.escape(str(self._value))
920         else:
921             s = str(self._value)
922         if hyperlink:
923             # no, we *must* escape this text
924             if not escape:
925                 s = cgi.escape(s)
926             s = self.hyper_re.sub(self._hyper_repl, s)
927         return s
929     def stext(self, escape=0):
930         ''' Render the value of the property as StructuredText.
932             This requires the StructureText module to be installed separately.
933         '''
934         s = self.plain(escape=escape)
935         if not StructuredText:
936             return s
937         return StructuredText(s,level=1,header=0)
939     def field(self, size = 30):
940         ''' Render a form edit field for the property
941         '''
942         if self._value is None:
943             value = ''
944         else:
945             value = cgi.escape(str(self._value))
946             value = '&quot;'.join(value.split('"'))
947         return self.input(name=self._formname,value=value,size=size)
949     def multiline(self, escape=0, rows=5, cols=40):
950         ''' Render a multiline form edit field for the property
951         '''
952         if self._value is None:
953             value = ''
954         else:
955             value = cgi.escape(str(self._value))
956             value = '&quot;'.join(value.split('"'))
957         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
958             self._formname, rows, cols, value)
960     def email(self, escape=1):
961         ''' Render the value of the property as an obscured email address
962         '''
963         if self._value is None: value = ''
964         else: value = str(self._value)
965         if value.find('@') != -1:
966             name, domain = value.split('@')
967             domain = ' '.join(domain.split('.')[:-1])
968             name = name.replace('.', ' ')
969             value = '%s at %s ...'%(name, domain)
970         else:
971             value = value.replace('.', ' ')
972         if escape:
973             value = cgi.escape(value)
974         return value
976 class PasswordHTMLProperty(HTMLProperty):
977     def plain(self):
978         ''' Render a "plain" representation of the property
979         '''
980         if self._value is None:
981             return ''
982         return _('*encrypted*')
984     def field(self, size = 30):
985         ''' Render a form edit field for the property.
986         '''
987         return self.input(type="password", name=self._formname, size=size)
989     def confirm(self, size = 30):
990         ''' Render a second form edit field for the property, used for 
991             confirmation that the user typed the password correctly. Generates
992             a field with name "@confirm@name".
993         '''
994         return self.input(type="password", name="@confirm@%s"%self._formname,
995             size=size)
997 class NumberHTMLProperty(HTMLProperty):
998     def plain(self):
999         ''' Render a "plain" representation of the property
1000         '''
1001         return str(self._value)
1003     def field(self, size = 30):
1004         ''' Render a form edit field for the property
1005         '''
1006         if self._value is None:
1007             value = ''
1008         else:
1009             value = cgi.escape(str(self._value))
1010             value = '&quot;'.join(value.split('"'))
1011         return self.input(name=self._formname,value=value,size=size)
1013     def __int__(self):
1014         ''' Return an int of me
1015         '''
1016         return int(self._value)
1018     def __float__(self):
1019         ''' Return a float of me
1020         '''
1021         return float(self._value)
1024 class BooleanHTMLProperty(HTMLProperty):
1025     def plain(self):
1026         ''' Render a "plain" representation of the property
1027         '''
1028         if self._value is None:
1029             return ''
1030         return self._value and "Yes" or "No"
1032     def field(self):
1033         ''' Render a form edit field for the property
1034         '''
1035         checked = self._value and "checked" or ""
1036         if self._value:
1037             s = self.input(type="radio",name=self._formname,value="yes",checked="checked")
1038             s += 'Yes'
1039             s +=self.input(type="radio",name=self._formname,value="no")
1040             s += 'No'
1041         else:
1042             s = self.input(type="radio",name=self._formname,value="yes")
1043             s += 'Yes'
1044             s +=self.input(type="radio",name=self._formname,value="no",checked="checked")
1045             s += 'No'
1046         return s
1048 class DateHTMLProperty(HTMLProperty):
1049     def plain(self):
1050         ''' Render a "plain" representation of the property
1051         '''
1052         if self._value is None:
1053             return ''
1054         return str(self._value.local(self._db.getUserTimezone()))
1056     def now(self):
1057         ''' Return the current time.
1059             This is useful for defaulting a new value. Returns a
1060             DateHTMLProperty.
1061         '''
1062         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1063             self._formname, date.Date('.'))
1065     def field(self, size = 30):
1066         ''' Render a form edit field for the property
1067         '''
1068         if self._value is None:
1069             value = ''
1070         else:
1071             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1072             value = '&quot;'.join(value.split('"'))
1073         return self.input(name=self._formname,value=value,size=size)
1075     def reldate(self, pretty=1):
1076         ''' Render the interval between the date and now.
1078             If the "pretty" flag is true, then make the display pretty.
1079         '''
1080         if not self._value:
1081             return ''
1083         # figure the interval
1084         interval = self._value - date.Date('.')
1085         if pretty:
1086             return interval.pretty()
1087         return str(interval)
1089     _marker = []
1090     def pretty(self, format=_marker):
1091         ''' Render the date in a pretty format (eg. month names, spaces).
1093             The format string is a standard python strftime format string.
1094             Note that if the day is zero, and appears at the start of the
1095             string, then it'll be stripped from the output. This is handy
1096             for the situatin when a date only specifies a month and a year.
1097         '''
1098         if format is not self._marker:
1099             return self._value.pretty(format)
1100         else:
1101             return self._value.pretty()
1103     def local(self, offset):
1104         ''' Return the date/time as a local (timezone offset) date/time.
1105         '''
1106         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1107             self._formname, self._value.local(offset))
1109 class IntervalHTMLProperty(HTMLProperty):
1110     def plain(self):
1111         ''' Render a "plain" representation of the property
1112         '''
1113         if self._value is None:
1114             return ''
1115         return str(self._value)
1117     def pretty(self):
1118         ''' Render the interval in a pretty format (eg. "yesterday")
1119         '''
1120         return self._value.pretty()
1122     def field(self, size = 30):
1123         ''' Render a form edit field for the property
1124         '''
1125         if self._value is None:
1126             value = ''
1127         else:
1128             value = cgi.escape(str(self._value))
1129             value = '&quot;'.join(value.split('"'))
1130         return self.input(name=self._formname,value=value,size=size)
1132 class LinkHTMLProperty(HTMLProperty):
1133     ''' Link HTMLProperty
1134         Include the above as well as being able to access the class
1135         information. Stringifying the object itself results in the value
1136         from the item being displayed. Accessing attributes of this object
1137         result in the appropriate entry from the class being queried for the
1138         property accessed (so item/assignedto/name would look up the user
1139         entry identified by the assignedto property on item, and then the
1140         name property of that user)
1141     '''
1142     def __init__(self, *args, **kw):
1143         HTMLProperty.__init__(self, *args, **kw)
1144         # if we're representing a form value, then the -1 from the form really
1145         # should be a None
1146         if str(self._value) == '-1':
1147             self._value = None
1149     def __getattr__(self, attr):
1150         ''' return a new HTMLItem '''
1151        #print 'Link.getattr', (self, attr, self._value)
1152         if not self._value:
1153             raise AttributeError, "Can't access missing value"
1154         if self._prop.classname == 'user':
1155             klass = HTMLUser
1156         else:
1157             klass = HTMLItem
1158         i = klass(self._client, self._prop.classname, self._value)
1159         return getattr(i, attr)
1161     def plain(self, escape=0):
1162         ''' Render a "plain" representation of the property
1163         '''
1164         if self._value is None:
1165             return ''
1166         linkcl = self._db.classes[self._prop.classname]
1167         k = linkcl.labelprop(1)
1168         value = str(linkcl.get(self._value, k))
1169         if escape:
1170             value = cgi.escape(value)
1171         return value
1173     def field(self, showid=0, size=None):
1174         ''' Render a form edit field for the property
1175         '''
1176         linkcl = self._db.getclass(self._prop.classname)
1177         if linkcl.getprops().has_key('order'):  
1178             sort_on = 'order'  
1179         else:  
1180             sort_on = linkcl.labelprop()  
1181         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1182         # TODO: make this a field display, not a menu one!
1183         l = ['<select name="%s">'%self._formname]
1184         k = linkcl.labelprop(1)
1185         if self._value is None:
1186             s = 'selected="selected" '
1187         else:
1188             s = ''
1189         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1191         # make sure we list the current value if it's retired
1192         if self._value and self._value not in options:
1193             options.insert(0, self._value)
1195         for optionid in options:
1196             # get the option value, and if it's None use an empty string
1197             option = linkcl.get(optionid, k) or ''
1199             # figure if this option is selected
1200             s = ''
1201             if optionid == self._value:
1202                 s = 'selected="selected" '
1204             # figure the label
1205             if showid:
1206                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1207             else:
1208                 lab = option
1210             # truncate if it's too long
1211             if size is not None and len(lab) > size:
1212                 lab = lab[:size-3] + '...'
1214             # and generate
1215             lab = cgi.escape(lab)
1216             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1217         l.append('</select>')
1218         return '\n'.join(l)
1220     def menu(self, size=None, height=None, showid=0, additional=[],
1221             **conditions):
1222         ''' Render a form select list for this property
1223         '''
1224         value = self._value
1226         linkcl = self._db.getclass(self._prop.classname)
1227         l = ['<select name="%s">'%self._formname]
1228         k = linkcl.labelprop(1)
1229         s = ''
1230         if value is None:
1231             s = 'selected="selected" '
1232         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1233         if linkcl.getprops().has_key('order'):  
1234             sort_on = ('+', 'order')
1235         else:  
1236             sort_on = ('+', linkcl.labelprop())
1237         options = linkcl.filter(None, conditions, sort_on, (None, None))
1239         # make sure we list the current value if it's retired
1240         if self._value and self._value not in options:
1241             options.insert(0, self._value)
1243         for optionid in options:
1244             # get the option value, and if it's None use an empty string
1245             option = linkcl.get(optionid, k) or ''
1247             # figure if this option is selected
1248             s = ''
1249             if value in [optionid, option]:
1250                 s = 'selected="selected" '
1252             # figure the label
1253             if showid:
1254                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1255             else:
1256                 lab = option
1258             # truncate if it's too long
1259             if size is not None and len(lab) > size:
1260                 lab = lab[:size-3] + '...'
1261             if additional:
1262                 m = []
1263                 for propname in additional:
1264                     m.append(linkcl.get(optionid, propname))
1265                 lab = lab + ' (%s)'%', '.join(map(str, m))
1267             # and generate
1268             lab = cgi.escape(lab)
1269             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1270         l.append('</select>')
1271         return '\n'.join(l)
1272 #    def checklist(self, ...)
1274 class MultilinkHTMLProperty(HTMLProperty):
1275     ''' Multilink HTMLProperty
1277         Also be iterable, returning a wrapper object like the Link case for
1278         each entry in the multilink.
1279     '''
1280     def __init__(self, *args, **kwargs):
1281         HTMLProperty.__init__(self, *args, **kwargs)
1282         if self._value:
1283             self._value.sort(make_sort_function(self._db, self._prop.classname))
1284     
1285     def __len__(self):
1286         ''' length of the multilink '''
1287         return len(self._value)
1289     def __getattr__(self, attr):
1290         ''' no extended attribute accesses make sense here '''
1291         raise AttributeError, attr
1293     def __getitem__(self, num):
1294         ''' iterate and return a new HTMLItem
1295         '''
1296        #print 'Multi.getitem', (self, num)
1297         value = self._value[num]
1298         if self._prop.classname == 'user':
1299             klass = HTMLUser
1300         else:
1301             klass = HTMLItem
1302         return klass(self._client, self._prop.classname, value)
1304     def __contains__(self, value):
1305         ''' Support the "in" operator. We have to make sure the passed-in
1306             value is a string first, not a *HTMLProperty.
1307         '''
1308         return str(value) in self._value
1310     def reverse(self):
1311         ''' return the list in reverse order
1312         '''
1313         l = self._value[:]
1314         l.reverse()
1315         if self._prop.classname == 'user':
1316             klass = HTMLUser
1317         else:
1318             klass = HTMLItem
1319         return [klass(self._client, self._prop.classname, value) for value in l]
1321     def plain(self, escape=0):
1322         ''' Render a "plain" representation of the property
1323         '''
1324         linkcl = self._db.classes[self._prop.classname]
1325         k = linkcl.labelprop(1)
1326         labels = []
1327         for v in self._value:
1328             labels.append(linkcl.get(v, k))
1329         value = ', '.join(labels)
1330         if escape:
1331             value = cgi.escape(value)
1332         return value
1334     def field(self, size=30, showid=0):
1335         ''' Render a form edit field for the property
1336         '''
1337         linkcl = self._db.getclass(self._prop.classname)
1338         value = self._value[:]
1339         # map the id to the label property
1340         if not linkcl.getkey():
1341             showid=1
1342         if not showid:
1343             k = linkcl.labelprop(1)
1344             value = [linkcl.get(v, k) for v in value]
1345         value = cgi.escape(','.join(value))
1346         return self.input(name=self._formname,size=size,value=value)
1348     def menu(self, size=None, height=None, showid=0, additional=[],
1349             **conditions):
1350         ''' Render a form select list for this property
1351         '''
1352         value = self._value
1354         linkcl = self._db.getclass(self._prop.classname)
1355         sort_on = ('+', find_sort_key(linkcl))
1356         options = linkcl.filter(None, conditions, sort_on)
1357         height = height or min(len(options), 7)
1358         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1359         k = linkcl.labelprop(1)
1361         # make sure we list the current values if they're retired
1362         for val in value:
1363             if val not in options:
1364                 options.insert(0, val)
1366         for optionid in options:
1367             # get the option value, and if it's None use an empty string
1368             option = linkcl.get(optionid, k) or ''
1370             # figure if this option is selected
1371             s = ''
1372             if optionid in value or option in value:
1373                 s = 'selected="selected" '
1375             # figure the label
1376             if showid:
1377                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1378             else:
1379                 lab = option
1380             # truncate if it's too long
1381             if size is not None and len(lab) > size:
1382                 lab = lab[:size-3] + '...'
1383             if additional:
1384                 m = []
1385                 for propname in additional:
1386                     m.append(linkcl.get(optionid, propname))
1387                 lab = lab + ' (%s)'%', '.join(m)
1389             # and generate
1390             lab = cgi.escape(lab)
1391             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1392                 lab))
1393         l.append('</select>')
1394         return '\n'.join(l)
1396 # set the propclasses for HTMLItem
1397 propclasses = (
1398     (hyperdb.String, StringHTMLProperty),
1399     (hyperdb.Number, NumberHTMLProperty),
1400     (hyperdb.Boolean, BooleanHTMLProperty),
1401     (hyperdb.Date, DateHTMLProperty),
1402     (hyperdb.Interval, IntervalHTMLProperty),
1403     (hyperdb.Password, PasswordHTMLProperty),
1404     (hyperdb.Link, LinkHTMLProperty),
1405     (hyperdb.Multilink, MultilinkHTMLProperty),
1408 def make_sort_function(db, classname):
1409     '''Make a sort function for a given class
1410     '''
1411     linkcl = db.getclass(classname)
1412     sort_on = find_sort_key(linkcl)
1413     def sortfunc(a, b):
1414         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1415     return sortfunc
1417 def find_sort_key(linkcl):
1418     if linkcl.getprops().has_key('order'):
1419         return 'order'
1420     else:
1421         return linkcl.labelprop()
1423 def handleListCGIValue(value):
1424     ''' Value is either a single item or a list of items. Each item has a
1425         .value that we're actually interested in.
1426     '''
1427     if isinstance(value, type([])):
1428         return [value.value for value in value]
1429     else:
1430         value = value.value.strip()
1431         if not value:
1432             return []
1433         return value.split(',')
1435 class ShowDict:
1436     ''' A convenience access to the :columns index parameters
1437     '''
1438     def __init__(self, columns):
1439         self.columns = {}
1440         for col in columns:
1441             self.columns[col] = 1
1442     def __getitem__(self, name):
1443         return self.columns.has_key(name)
1445 class HTMLRequest(HTMLInputMixin):
1446     ''' The *request*, holding the CGI form and environment.
1448         "form" the CGI form as a cgi.FieldStorage
1449         "env" the CGI environment variables
1450         "base" the base URL for this instance
1451         "user" a HTMLUser instance for this user
1452         "classname" the current classname (possibly None)
1453         "template" the current template (suffix, also possibly None)
1455         Index args:
1456         "columns" dictionary of the columns to display in an index page
1457         "show" a convenience access to columns - request/show/colname will
1458                be true if the columns should be displayed, false otherwise
1459         "sort" index sort column (direction, column name)
1460         "group" index grouping property (direction, column name)
1461         "filter" properties to filter the index on
1462         "filterspec" values to filter the index on
1463         "search_text" text to perform a full-text search on for an index
1465     '''
1466     def __init__(self, client):
1467         # _client is needed by HTMLInputMixin
1468         self._client = self.client = client
1470         # easier access vars
1471         self.form = client.form
1472         self.env = client.env
1473         self.base = client.base
1474         self.user = HTMLUser(client, 'user', client.userid)
1476         # store the current class name and action
1477         self.classname = client.classname
1478         self.template = client.template
1480         # the special char to use for special vars
1481         self.special_char = '@'
1483         HTMLInputMixin.__init__(self)
1485         self._post_init()
1487     def _post_init(self):
1488         ''' Set attributes based on self.form
1489         '''
1490         # extract the index display information from the form
1491         self.columns = []
1492         for name in ':columns @columns'.split():
1493             if self.form.has_key(name):
1494                 self.special_char = name[0]
1495                 self.columns = handleListCGIValue(self.form[name])
1496                 break
1497         self.show = ShowDict(self.columns)
1499         # sorting
1500         self.sort = (None, None)
1501         for name in ':sort @sort'.split():
1502             if self.form.has_key(name):
1503                 self.special_char = name[0]
1504                 sort = self.form[name].value
1505                 if sort.startswith('-'):
1506                     self.sort = ('-', sort[1:])
1507                 else:
1508                     self.sort = ('+', sort)
1509                 if self.form.has_key(self.special_char+'sortdir'):
1510                     self.sort = ('-', self.sort[1])
1512         # grouping
1513         self.group = (None, None)
1514         for name in ':group @group'.split():
1515             if self.form.has_key(name):
1516                 self.special_char = name[0]
1517                 group = self.form[name].value
1518                 if group.startswith('-'):
1519                     self.group = ('-', group[1:])
1520                 else:
1521                     self.group = ('+', group)
1522                 if self.form.has_key(self.special_char+'groupdir'):
1523                     self.group = ('-', self.group[1])
1525         # filtering
1526         self.filter = []
1527         for name in ':filter @filter'.split():
1528             if self.form.has_key(name):
1529                 self.special_char = name[0]
1530                 self.filter = handleListCGIValue(self.form[name])
1532         self.filterspec = {}
1533         db = self.client.db
1534         if self.classname is not None:
1535             props = db.getclass(self.classname).getprops()
1536             for name in self.filter:
1537                 if not self.form.has_key(name):
1538                     continue
1539                 prop = props[name]
1540                 fv = self.form[name]
1541                 if (isinstance(prop, hyperdb.Link) or
1542                         isinstance(prop, hyperdb.Multilink)):
1543                     self.filterspec[name] = lookupIds(db, prop,
1544                         handleListCGIValue(fv))
1545                 else:
1546                     if isinstance(fv, type([])):
1547                         self.filterspec[name] = [v.value for v in fv]
1548                     else:
1549                         self.filterspec[name] = fv.value
1551         # full-text search argument
1552         self.search_text = None
1553         for name in ':search_text @search_text'.split():
1554             if self.form.has_key(name):
1555                 self.special_char = name[0]
1556                 self.search_text = self.form[name].value
1558         # pagination - size and start index
1559         # figure batch args
1560         self.pagesize = 50
1561         for name in ':pagesize @pagesize'.split():
1562             if self.form.has_key(name):
1563                 self.special_char = name[0]
1564                 self.pagesize = int(self.form[name].value)
1566         self.startwith = 0
1567         for name in ':startwith @startwith'.split():
1568             if self.form.has_key(name):
1569                 self.special_char = name[0]
1570                 self.startwith = int(self.form[name].value)
1572     def updateFromURL(self, url):
1573         ''' Parse the URL for query args, and update my attributes using the
1574             values.
1575         ''' 
1576         env = {'QUERY_STRING': url}
1577         self.form = cgi.FieldStorage(environ=env)
1579         self._post_init()
1581     def update(self, kwargs):
1582         ''' Update my attributes using the keyword args
1583         '''
1584         self.__dict__.update(kwargs)
1585         if kwargs.has_key('columns'):
1586             self.show = ShowDict(self.columns)
1588     def description(self):
1589         ''' Return a description of the request - handle for the page title.
1590         '''
1591         s = [self.client.db.config.TRACKER_NAME]
1592         if self.classname:
1593             if self.client.nodeid:
1594                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1595             else:
1596                 if self.template == 'item':
1597                     s.append('- new %s'%self.classname)
1598                 elif self.template == 'index':
1599                     s.append('- %s index'%self.classname)
1600                 else:
1601                     s.append('- %s %s'%(self.classname, self.template))
1602         else:
1603             s.append('- home')
1604         return ' '.join(s)
1606     def __str__(self):
1607         d = {}
1608         d.update(self.__dict__)
1609         f = ''
1610         for k in self.form.keys():
1611             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1612         d['form'] = f
1613         e = ''
1614         for k,v in self.env.items():
1615             e += '\n     %r=%r'%(k, v)
1616         d['env'] = e
1617         return '''
1618 form: %(form)s
1619 base: %(base)r
1620 classname: %(classname)r
1621 template: %(template)r
1622 columns: %(columns)r
1623 sort: %(sort)r
1624 group: %(group)r
1625 filter: %(filter)r
1626 search_text: %(search_text)r
1627 pagesize: %(pagesize)r
1628 startwith: %(startwith)r
1629 env: %(env)s
1630 '''%d
1632     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1633             filterspec=1):
1634         ''' return the current index args as form elements '''
1635         l = []
1636         sc = self.special_char
1637         s = self.input(type="hidden",name="%s",value="%s")
1638         if columns and self.columns:
1639             l.append(s%(sc+'columns', ','.join(self.columns)))
1640         if sort and self.sort[1] is not None:
1641             if self.sort[0] == '-':
1642                 val = '-'+self.sort[1]
1643             else:
1644                 val = self.sort[1]
1645             l.append(s%(sc+'sort', val))
1646         if group and self.group[1] is not None:
1647             if self.group[0] == '-':
1648                 val = '-'+self.group[1]
1649             else:
1650                 val = self.group[1]
1651             l.append(s%(sc+'group', val))
1652         if filter and self.filter:
1653             l.append(s%(sc+'filter', ','.join(self.filter)))
1654         if filterspec:
1655             for k,v in self.filterspec.items():
1656                 if type(v) == type([]):
1657                     l.append(s%(k, ','.join(v)))
1658                 else:
1659                     l.append(s%(k, v))
1660         if self.search_text:
1661             l.append(s%(sc+'search_text', self.search_text))
1662         l.append(s%(sc+'pagesize', self.pagesize))
1663         l.append(s%(sc+'startwith', self.startwith))
1664         return '\n'.join(l)
1666     def indexargs_url(self, url, args):
1667         ''' Embed the current index args in a URL
1668         '''
1669         sc = self.special_char
1670         l = ['%s=%s'%(k,v) for k,v in args.items()]
1672         # pull out the special values (prefixed by @ or :)
1673         specials = {}
1674         for key in args.keys():
1675             if key[0] in '@:':
1676                 specials[key[1:]] = args[key]
1678         # ok, now handle the specials we received in the request
1679         if self.columns and not specials.has_key('columns'):
1680             l.append(sc+'columns=%s'%(','.join(self.columns)))
1681         if self.sort[1] is not None and not specials.has_key('sort'):
1682             if self.sort[0] == '-':
1683                 val = '-'+self.sort[1]
1684             else:
1685                 val = self.sort[1]
1686             l.append(sc+'sort=%s'%val)
1687         if self.group[1] is not None and not specials.has_key('group'):
1688             if self.group[0] == '-':
1689                 val = '-'+self.group[1]
1690             else:
1691                 val = self.group[1]
1692             l.append(sc+'group=%s'%val)
1693         if self.filter and not specials.has_key('filter'):
1694             l.append(sc+'filter=%s'%(','.join(self.filter)))
1695         if self.search_text and not specials.has_key('search_text'):
1696             l.append(sc+'search_text=%s'%self.search_text)
1697         if not specials.has_key('pagesize'):
1698             l.append(sc+'pagesize=%s'%self.pagesize)
1699         if not specials.has_key('startwith'):
1700             l.append(sc+'startwith=%s'%self.startwith)
1702         # finally, the remainder of the filter args in the request
1703         for k,v in self.filterspec.items():
1704             if not args.has_key(k):
1705                 if type(v) == type([]):
1706                     l.append('%s=%s'%(k, ','.join(v)))
1707                 else:
1708                     l.append('%s=%s'%(k, v))
1709         return '%s?%s'%(url, '&'.join(l))
1710     indexargs_href = indexargs_url
1712     def base_javascript(self):
1713         return '''
1714 <script type="text/javascript">
1715 submitted = false;
1716 function submit_once() {
1717     if (submitted) {
1718         alert("Your request is being processed.\\nPlease be patient.");
1719         return 0;
1720     }
1721     submitted = true;
1722     return 1;
1725 function help_window(helpurl, width, height) {
1726     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1728 </script>
1729 '''%self.base
1731     def batch(self):
1732         ''' Return a batch object for results from the "current search"
1733         '''
1734         filterspec = self.filterspec
1735         sort = self.sort
1736         group = self.group
1738         # get the list of ids we're batching over
1739         klass = self.client.db.getclass(self.classname)
1740         if self.search_text:
1741             matches = self.client.db.indexer.search(
1742                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1743         else:
1744             matches = None
1745         l = klass.filter(matches, filterspec, sort, group)
1747         # return the batch object, using IDs only
1748         return Batch(self.client, l, self.pagesize, self.startwith,
1749             classname=self.classname)
1751 # extend the standard ZTUtils Batch object to remove dependency on
1752 # Acquisition and add a couple of useful methods
1753 class Batch(ZTUtils.Batch):
1754     ''' Use me to turn a list of items, or item ids of a given class, into a
1755         series of batches.
1757         ========= ========================================================
1758         Parameter  Usage
1759         ========= ========================================================
1760         sequence  a list of HTMLItems or item ids
1761         classname if sequence is a list of ids, this is the class of item
1762         size      how big to make the sequence.
1763         start     where to start (0-indexed) in the sequence.
1764         end       where to end (0-indexed) in the sequence.
1765         orphan    if the next batch would contain less items than this
1766                   value, then it is combined with this batch
1767         overlap   the number of items shared between adjacent batches
1768         ========= ========================================================
1770         Attributes: Note that the "start" attribute, unlike the
1771         argument, is a 1-based index (I know, lame).  "first" is the
1772         0-based index.  "length" is the actual number of elements in
1773         the batch.
1775         "sequence_length" is the length of the original, unbatched, sequence.
1776     '''
1777     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1778             overlap=0, classname=None):
1779         self.client = client
1780         self.last_index = self.last_item = None
1781         self.current_item = None
1782         self.classname = classname
1783         self.sequence_length = len(sequence)
1784         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1785             overlap)
1787     # overwrite so we can late-instantiate the HTMLItem instance
1788     def __getitem__(self, index):
1789         if index < 0:
1790             if index + self.end < self.first: raise IndexError, index
1791             return self._sequence[index + self.end]
1792         
1793         if index >= self.length:
1794             raise IndexError, index
1796         # move the last_item along - but only if the fetched index changes
1797         # (for some reason, index 0 is fetched twice)
1798         if index != self.last_index:
1799             self.last_item = self.current_item
1800             self.last_index = index
1802         item = self._sequence[index + self.first]
1803         if self.classname:
1804             # map the item ids to instances
1805             if self.classname == 'user':
1806                 item = HTMLUser(self.client, self.classname, item)
1807             else:
1808                 item = HTMLItem(self.client, self.classname, item)
1809         self.current_item = item
1810         return item
1812     def propchanged(self, property):
1813         ''' Detect if the property marked as being the group property
1814             changed in the last iteration fetch
1815         '''
1816         if (self.last_item is None or
1817                 self.last_item[property] != self.current_item[property]):
1818             return 1
1819         return 0
1821     # override these 'cos we don't have access to acquisition
1822     def previous(self):
1823         if self.start == 1:
1824             return None
1825         return Batch(self.client, self._sequence, self._size,
1826             self.first - self._size + self.overlap, 0, self.orphan,
1827             self.overlap)
1829     def next(self):
1830         try:
1831             self._sequence[self.end]
1832         except IndexError:
1833             return None
1834         return Batch(self.client, self._sequence, self._size,
1835             self.end - self.overlap, 0, self.orphan, self.overlap)
1837 class TemplatingUtils:
1838     ''' Utilities for templating
1839     '''
1840     def __init__(self, client):
1841         self.client = client
1842     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1843         return Batch(self.client, sequence, size, start, end, orphan,
1844             overlap)