Code

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