Code

allow negative items, helping construct forms
[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 = '&amp;property=%s'%property
471         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
472             '@startwith=0&amp;@template=help&amp;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 and
811             self._db.user.get(self._client.userid, 'username') != 'anonymous')
813     def is_view_ok(self):
814         ''' Is the user allowed to View the current class?
815             Also check whether this is the current user's info.
816         '''
817         return self._db.security.hasPermission('Edit', self._client.userid,
818             self._classname) or (self._nodeid == self._client.userid and
819             self._db.user.get(self._client.userid, 'username') != 'anonymous')
821 class HTMLProperty:
822     ''' String, Number, Date, Interval HTMLProperty
824         Has useful attributes:
826          _name  the name of the property
827          _value the value of the property if any
829         A wrapper object which may be stringified for the plain() behaviour.
830     '''
831     def __init__(self, client, classname, nodeid, prop, name, value,
832             anonymous=0):
833         self._client = client
834         self._db = client.db
835         self._classname = classname
836         self._nodeid = nodeid
837         self._prop = prop
838         self._value = value
839         self._anonymous = anonymous
840         self._name = name
841         if not anonymous:
842             self._formname = '%s%s@%s'%(classname, nodeid, name)
843         else:
844             self._formname = name
845     def __repr__(self):
846         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
847             self._prop, self._value)
848     def __str__(self):
849         return self.plain()
850     def __cmp__(self, other):
851         if isinstance(other, HTMLProperty):
852             return cmp(self._value, other._value)
853         return cmp(self._value, other)
855 class StringHTMLProperty(HTMLProperty):
856     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
857                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
858                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
859     def _hyper_repl(self, match):
860         if match.group('url'):
861             s = match.group('url')
862             return '<a href="%s">%s</a>'%(s, s)
863         elif match.group('email'):
864             s = match.group('email')
865             return '<a href="mailto:%s">%s</a>'%(s, s)
866         else:
867             s = match.group('item')
868             s1 = match.group('class')
869             s2 = match.group('id')
870             try:
871                 # make sure s1 is a valid tracker classname
872                 self._db.getclass(s1)
873                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
874             except KeyError:
875                 return '%s%s'%(s1, s2)
877     def hyperlinked(self):
878         ''' Render a "hyperlinked" version of the text '''
879         return self.plain(hyperlink=1)
881     def plain(self, escape=0, hyperlink=0):
882         ''' Render a "plain" representation of the property
883             
884             "escape" turns on/off HTML quoting
885             "hyperlink" turns on/off in-text hyperlinking of URLs, email
886                 addresses and designators
887         '''
888         if self._value is None:
889             return ''
890         if escape:
891             s = cgi.escape(str(self._value))
892         else:
893             s = str(self._value)
894         if hyperlink:
895             # no, we *must* escape this text
896             if not escape:
897                 s = cgi.escape(s)
898             s = self.hyper_re.sub(self._hyper_repl, s)
899         return s
901     def stext(self, escape=0):
902         ''' Render the value of the property as StructuredText.
904             This requires the StructureText module to be installed separately.
905         '''
906         s = self.plain(escape=escape)
907         if not StructuredText:
908             return s
909         return StructuredText(s,level=1,header=0)
911     def field(self, size = 30):
912         ''' Render a form edit field for the property
913         '''
914         if self._value is None:
915             value = ''
916         else:
917             value = cgi.escape(str(self._value))
918             value = '&quot;'.join(value.split('"'))
919         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
921     def multiline(self, escape=0, rows=5, cols=40):
922         ''' Render a multiline form edit field for the property
923         '''
924         if self._value is None:
925             value = ''
926         else:
927             value = cgi.escape(str(self._value))
928             value = '&quot;'.join(value.split('"'))
929         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
930             self._formname, rows, cols, value)
932     def email(self, escape=1):
933         ''' Render the value of the property as an obscured email address
934         '''
935         if self._value is None: value = ''
936         else: value = str(self._value)
937         if value.find('@') != -1:
938             name, domain = value.split('@')
939             domain = ' '.join(domain.split('.')[:-1])
940             name = name.replace('.', ' ')
941             value = '%s at %s ...'%(name, domain)
942         else:
943             value = value.replace('.', ' ')
944         if escape:
945             value = cgi.escape(value)
946         return value
948 class PasswordHTMLProperty(HTMLProperty):
949     def plain(self):
950         ''' Render a "plain" representation of the property
951         '''
952         if self._value is None:
953             return ''
954         return _('*encrypted*')
956     def field(self, size = 30):
957         ''' Render a form edit field for the property.
958         '''
959         return '<input type="password" name="%s" size="%s">'%(self._formname, size)
961     def confirm(self, size = 30):
962         ''' Render a second form edit field for the property, used for 
963             confirmation that the user typed the password correctly. Generates
964             a field with name "@confirm@name".
965         '''
966         return '<input type="password" name="@confirm@%s" size="%s">'%(
967             self._formname, size)
969 class NumberHTMLProperty(HTMLProperty):
970     def plain(self):
971         ''' Render a "plain" representation of the property
972         '''
973         return str(self._value)
975     def field(self, size = 30):
976         ''' Render a form edit field for the property
977         '''
978         if self._value is None:
979             value = ''
980         else:
981             value = cgi.escape(str(self._value))
982             value = '&quot;'.join(value.split('"'))
983         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
985     def __int__(self):
986         ''' Return an int of me
987         '''
988         return int(self._value)
990     def __float__(self):
991         ''' Return a float of me
992         '''
993         return float(self._value)
996 class BooleanHTMLProperty(HTMLProperty):
997     def plain(self):
998         ''' Render a "plain" representation of the property
999         '''
1000         if self._value is None:
1001             return ''
1002         return self._value and "Yes" or "No"
1004     def field(self):
1005         ''' Render a form edit field for the property
1006         '''
1007         checked = self._value and "checked" or ""
1008         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
1009             checked)
1010         if checked:
1011             checked = ""
1012         else:
1013             checked = "checked"
1014         s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
1015             checked)
1016         return s
1018 class DateHTMLProperty(HTMLProperty):
1019     def plain(self):
1020         ''' Render a "plain" representation of the property
1021         '''
1022         if self._value is None:
1023             return ''
1024         return str(self._value.local(self._db.getUserTimezone()))
1026     def now(self):
1027         ''' Return the current time.
1029             This is useful for defaulting a new value. Returns a
1030             DateHTMLProperty.
1031         '''
1032         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1033             self._formname, date.Date('.'))
1035     def field(self, size = 30):
1036         ''' Render a form edit field for the property
1037         '''
1038         if self._value is None:
1039             value = ''
1040         else:
1041             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1042             value = '&quot;'.join(value.split('"'))
1043         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1045     def reldate(self, pretty=1):
1046         ''' Render the interval between the date and now.
1048             If the "pretty" flag is true, then make the display pretty.
1049         '''
1050         if not self._value:
1051             return ''
1053         # figure the interval
1054         interval = date.Date('.') - self._value
1055         if pretty:
1056             return interval.pretty()
1057         return str(interval)
1059     _marker = []
1060     def pretty(self, format=_marker):
1061         ''' Render the date in a pretty format (eg. month names, spaces).
1063             The format string is a standard python strftime format string.
1064             Note that if the day is zero, and appears at the start of the
1065             string, then it'll be stripped from the output. This is handy
1066             for the situatin when a date only specifies a month and a year.
1067         '''
1068         if format is not self._marker:
1069             return self._value.pretty(format)
1070         else:
1071             return self._value.pretty()
1073     def local(self, offset):
1074         ''' Return the date/time as a local (timezone offset) date/time.
1075         '''
1076         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1077             self._formname, self._value.local(offset))
1079 class IntervalHTMLProperty(HTMLProperty):
1080     def plain(self):
1081         ''' Render a "plain" representation of the property
1082         '''
1083         if self._value is None:
1084             return ''
1085         return str(self._value)
1087     def pretty(self):
1088         ''' Render the interval in a pretty format (eg. "yesterday")
1089         '''
1090         return self._value.pretty()
1092     def field(self, size = 30):
1093         ''' Render a form edit field for the property
1094         '''
1095         if self._value is None:
1096             value = ''
1097         else:
1098             value = cgi.escape(str(self._value))
1099             value = '&quot;'.join(value.split('"'))
1100         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1102 class LinkHTMLProperty(HTMLProperty):
1103     ''' Link HTMLProperty
1104         Include the above as well as being able to access the class
1105         information. Stringifying the object itself results in the value
1106         from the item being displayed. Accessing attributes of this object
1107         result in the appropriate entry from the class being queried for the
1108         property accessed (so item/assignedto/name would look up the user
1109         entry identified by the assignedto property on item, and then the
1110         name property of that user)
1111     '''
1112     def __init__(self, *args, **kw):
1113         HTMLProperty.__init__(self, *args, **kw)
1114         # if we're representing a form value, then the -1 from the form really
1115         # should be a None
1116         if str(self._value) == '-1':
1117             self._value = None
1119     def __getattr__(self, attr):
1120         ''' return a new HTMLItem '''
1121        #print 'Link.getattr', (self, attr, self._value)
1122         if not self._value:
1123             raise AttributeError, "Can't access missing value"
1124         if self._prop.classname == 'user':
1125             klass = HTMLUser
1126         else:
1127             klass = HTMLItem
1128         i = klass(self._client, self._prop.classname, self._value)
1129         return getattr(i, attr)
1131     def plain(self, escape=0):
1132         ''' Render a "plain" representation of the property
1133         '''
1134         if self._value is None:
1135             return ''
1136         linkcl = self._db.classes[self._prop.classname]
1137         k = linkcl.labelprop(1)
1138         value = str(linkcl.get(self._value, k))
1139         if escape:
1140             value = cgi.escape(value)
1141         return value
1143     def field(self, showid=0, size=None):
1144         ''' Render a form edit field for the property
1145         '''
1146         linkcl = self._db.getclass(self._prop.classname)
1147         if linkcl.getprops().has_key('order'):  
1148             sort_on = 'order'  
1149         else:  
1150             sort_on = linkcl.labelprop()  
1151         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1152         # TODO: make this a field display, not a menu one!
1153         l = ['<select name="%s">'%self._formname]
1154         k = linkcl.labelprop(1)
1155         if self._value is None:
1156             s = 'selected '
1157         else:
1158             s = ''
1159         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1161         # make sure we list the current value if it's retired
1162         if self._value and self._value not in options:
1163             options.insert(0, self._value)
1165         for optionid in options:
1166             # get the option value, and if it's None use an empty string
1167             option = linkcl.get(optionid, k) or ''
1169             # figure if this option is selected
1170             s = ''
1171             if optionid == self._value:
1172                 s = 'selected '
1174             # figure the label
1175             if showid:
1176                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1177             else:
1178                 lab = option
1180             # truncate if it's too long
1181             if size is not None and len(lab) > size:
1182                 lab = lab[:size-3] + '...'
1184             # and generate
1185             lab = cgi.escape(lab)
1186             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1187         l.append('</select>')
1188         return '\n'.join(l)
1190     def menu(self, size=None, height=None, showid=0, additional=[],
1191             **conditions):
1192         ''' Render a form select list for this property
1193         '''
1194         value = self._value
1196         # sort function
1197         sortfunc = make_sort_function(self._db, self._prop.classname)
1199         linkcl = self._db.getclass(self._prop.classname)
1200         l = ['<select name="%s">'%self._formname]
1201         k = linkcl.labelprop(1)
1202         s = ''
1203         if value is None:
1204             s = 'selected '
1205         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1206         if linkcl.getprops().has_key('order'):  
1207             sort_on = ('+', 'order')
1208         else:  
1209             sort_on = ('+', linkcl.labelprop())
1210         options = linkcl.filter(None, conditions, sort_on, (None, None))
1212         # make sure we list the current value if it's retired
1213         if self._value and self._value not in options:
1214             options.insert(0, self._value)
1216         for optionid in options:
1217             # get the option value, and if it's None use an empty string
1218             option = linkcl.get(optionid, k) or ''
1220             # figure if this option is selected
1221             s = ''
1222             if value in [optionid, option]:
1223                 s = 'selected '
1225             # figure the label
1226             if showid:
1227                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1228             else:
1229                 lab = option
1231             # truncate if it's too long
1232             if size is not None and len(lab) > size:
1233                 lab = lab[:size-3] + '...'
1234             if additional:
1235                 m = []
1236                 for propname in additional:
1237                     m.append(linkcl.get(optionid, propname))
1238                 lab = lab + ' (%s)'%', '.join(map(str, m))
1240             # and generate
1241             lab = cgi.escape(lab)
1242             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1243         l.append('</select>')
1244         return '\n'.join(l)
1245 #    def checklist(self, ...)
1247 class MultilinkHTMLProperty(HTMLProperty):
1248     ''' Multilink HTMLProperty
1250         Also be iterable, returning a wrapper object like the Link case for
1251         each entry in the multilink.
1252     '''
1253     def __len__(self):
1254         ''' length of the multilink '''
1255         return len(self._value)
1257     def __getattr__(self, attr):
1258         ''' no extended attribute accesses make sense here '''
1259         raise AttributeError, attr
1261     def __getitem__(self, num):
1262         ''' iterate and return a new HTMLItem
1263         '''
1264        #print 'Multi.getitem', (self, num)
1265         value = self._value[num]
1266         if self._prop.classname == 'user':
1267             klass = HTMLUser
1268         else:
1269             klass = HTMLItem
1270         return klass(self._client, self._prop.classname, value)
1272     def __contains__(self, value):
1273         ''' Support the "in" operator. We have to make sure the passed-in
1274             value is a string first, not a *HTMLProperty.
1275         '''
1276         return str(value) in self._value
1278     def reverse(self):
1279         ''' return the list in reverse order
1280         '''
1281         l = self._value[:]
1282         l.reverse()
1283         if self._prop.classname == 'user':
1284             klass = HTMLUser
1285         else:
1286             klass = HTMLItem
1287         return [klass(self._client, self._prop.classname, value) for value in l]
1289     def plain(self, escape=0):
1290         ''' Render a "plain" representation of the property
1291         '''
1292         linkcl = self._db.classes[self._prop.classname]
1293         k = linkcl.labelprop(1)
1294         labels = []
1295         for v in self._value:
1296             labels.append(linkcl.get(v, k))
1297         value = ', '.join(labels)
1298         if escape:
1299             value = cgi.escape(value)
1300         return value
1302     def field(self, size=30, showid=0):
1303         ''' Render a form edit field for the property
1304         '''
1305         sortfunc = make_sort_function(self._db, self._prop.classname)
1306         linkcl = self._db.getclass(self._prop.classname)
1307         value = self._value[:]
1308         if value:
1309             value.sort(sortfunc)
1310         # map the id to the label property
1311         if not linkcl.getkey():
1312             showid=1
1313         if not showid:
1314             k = linkcl.labelprop(1)
1315             value = [linkcl.get(v, k) for v in value]
1316         value = cgi.escape(','.join(value))
1317         return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1319     def menu(self, size=None, height=None, showid=0, additional=[],
1320             **conditions):
1321         ''' Render a form select list for this property
1322         '''
1323         value = self._value
1325         # sort function
1326         sortfunc = make_sort_function(self._db, self._prop.classname)
1328         linkcl = self._db.getclass(self._prop.classname)
1329         if linkcl.getprops().has_key('order'):  
1330             sort_on = ('+', 'order')
1331         else:  
1332             sort_on = ('+', linkcl.labelprop())
1333         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1334         height = height or min(len(options), 7)
1335         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1336         k = linkcl.labelprop(1)
1338         # make sure we list the current values if they're retired
1339         for val in value:
1340             if val not in options:
1341                 options.insert(0, val)
1343         for optionid in options:
1344             # get the option value, and if it's None use an empty string
1345             option = linkcl.get(optionid, k) or ''
1347             # figure if this option is selected
1348             s = ''
1349             if optionid in value or option in value:
1350                 s = 'selected '
1352             # figure the label
1353             if showid:
1354                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1355             else:
1356                 lab = option
1357             # truncate if it's too long
1358             if size is not None and len(lab) > size:
1359                 lab = lab[:size-3] + '...'
1360             if additional:
1361                 m = []
1362                 for propname in additional:
1363                     m.append(linkcl.get(optionid, propname))
1364                 lab = lab + ' (%s)'%', '.join(m)
1366             # and generate
1367             lab = cgi.escape(lab)
1368             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1369                 lab))
1370         l.append('</select>')
1371         return '\n'.join(l)
1373 # set the propclasses for HTMLItem
1374 propclasses = (
1375     (hyperdb.String, StringHTMLProperty),
1376     (hyperdb.Number, NumberHTMLProperty),
1377     (hyperdb.Boolean, BooleanHTMLProperty),
1378     (hyperdb.Date, DateHTMLProperty),
1379     (hyperdb.Interval, IntervalHTMLProperty),
1380     (hyperdb.Password, PasswordHTMLProperty),
1381     (hyperdb.Link, LinkHTMLProperty),
1382     (hyperdb.Multilink, MultilinkHTMLProperty),
1385 def make_sort_function(db, classname):
1386     '''Make a sort function for a given class
1387     '''
1388     linkcl = db.getclass(classname)
1389     if linkcl.getprops().has_key('order'):
1390         sort_on = 'order'
1391     else:
1392         sort_on = linkcl.labelprop()
1393     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1394         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1395     return sortfunc
1397 def handleListCGIValue(value):
1398     ''' Value is either a single item or a list of items. Each item has a
1399         .value that we're actually interested in.
1400     '''
1401     if isinstance(value, type([])):
1402         return [value.value for value in value]
1403     else:
1404         value = value.value.strip()
1405         if not value:
1406             return []
1407         return value.split(',')
1409 class ShowDict:
1410     ''' A convenience access to the :columns index parameters
1411     '''
1412     def __init__(self, columns):
1413         self.columns = {}
1414         for col in columns:
1415             self.columns[col] = 1
1416     def __getitem__(self, name):
1417         return self.columns.has_key(name)
1419 class HTMLRequest:
1420     ''' The *request*, holding the CGI form and environment.
1422         "form" the CGI form as a cgi.FieldStorage
1423         "env" the CGI environment variables
1424         "base" the base URL for this instance
1425         "user" a HTMLUser instance for this user
1426         "classname" the current classname (possibly None)
1427         "template" the current template (suffix, also possibly None)
1429         Index args:
1430         "columns" dictionary of the columns to display in an index page
1431         "show" a convenience access to columns - request/show/colname will
1432                be true if the columns should be displayed, false otherwise
1433         "sort" index sort column (direction, column name)
1434         "group" index grouping property (direction, column name)
1435         "filter" properties to filter the index on
1436         "filterspec" values to filter the index on
1437         "search_text" text to perform a full-text search on for an index
1439     '''
1440     def __init__(self, client):
1441         self.client = client
1443         # easier access vars
1444         self.form = client.form
1445         self.env = client.env
1446         self.base = client.base
1447         self.user = HTMLUser(client, 'user', client.userid)
1449         # store the current class name and action
1450         self.classname = client.classname
1451         self.template = client.template
1453         # the special char to use for special vars
1454         self.special_char = '@'
1456         self._post_init()
1458     def _post_init(self):
1459         ''' Set attributes based on self.form
1460         '''
1461         # extract the index display information from the form
1462         self.columns = []
1463         for name in ':columns @columns'.split():
1464             if self.form.has_key(name):
1465                 self.special_char = name[0]
1466                 self.columns = handleListCGIValue(self.form[name])
1467                 break
1468         self.show = ShowDict(self.columns)
1470         # sorting
1471         self.sort = (None, None)
1472         for name in ':sort @sort'.split():
1473             if self.form.has_key(name):
1474                 self.special_char = name[0]
1475                 sort = self.form[name].value
1476                 if sort.startswith('-'):
1477                     self.sort = ('-', sort[1:])
1478                 else:
1479                     self.sort = ('+', sort)
1480                 if self.form.has_key(self.special_char+'sortdir'):
1481                     self.sort = ('-', self.sort[1])
1483         # grouping
1484         self.group = (None, None)
1485         for name in ':group @group'.split():
1486             if self.form.has_key(name):
1487                 self.special_char = name[0]
1488                 group = self.form[name].value
1489                 if group.startswith('-'):
1490                     self.group = ('-', group[1:])
1491                 else:
1492                     self.group = ('+', group)
1493                 if self.form.has_key(self.special_char+'groupdir'):
1494                     self.group = ('-', self.group[1])
1496         # filtering
1497         self.filter = []
1498         for name in ':filter @filter'.split():
1499             if self.form.has_key(name):
1500                 self.special_char = name[0]
1501                 self.filter = handleListCGIValue(self.form[name])
1503         self.filterspec = {}
1504         db = self.client.db
1505         if self.classname is not None:
1506             props = db.getclass(self.classname).getprops()
1507             for name in self.filter:
1508                 if not self.form.has_key(name):
1509                     continue
1510                 prop = props[name]
1511                 fv = self.form[name]
1512                 if (isinstance(prop, hyperdb.Link) or
1513                         isinstance(prop, hyperdb.Multilink)):
1514                     self.filterspec[name] = lookupIds(db, prop,
1515                         handleListCGIValue(fv))
1516                 else:
1517                     if isinstance(fv, type([])):
1518                         self.filterspec[name] = [v.value for v in fv]
1519                     else:
1520                         self.filterspec[name] = fv.value
1522         # full-text search argument
1523         self.search_text = None
1524         for name in ':search_text @search_text'.split():
1525             if self.form.has_key(name):
1526                 self.special_char = name[0]
1527                 self.search_text = self.form[name].value
1529         # pagination - size and start index
1530         # figure batch args
1531         self.pagesize = 50
1532         for name in ':pagesize @pagesize'.split():
1533             if self.form.has_key(name):
1534                 self.special_char = name[0]
1535                 self.pagesize = int(self.form[name].value)
1537         self.startwith = 0
1538         for name in ':startwith @startwith'.split():
1539             if self.form.has_key(name):
1540                 self.special_char = name[0]
1541                 self.startwith = int(self.form[name].value)
1543     def updateFromURL(self, url):
1544         ''' Parse the URL for query args, and update my attributes using the
1545             values.
1546         ''' 
1547         env = {'QUERY_STRING': url}
1548         self.form = cgi.FieldStorage(environ=env)
1550         self._post_init()
1552     def update(self, kwargs):
1553         ''' Update my attributes using the keyword args
1554         '''
1555         self.__dict__.update(kwargs)
1556         if kwargs.has_key('columns'):
1557             self.show = ShowDict(self.columns)
1559     def description(self):
1560         ''' Return a description of the request - handle for the page title.
1561         '''
1562         s = [self.client.db.config.TRACKER_NAME]
1563         if self.classname:
1564             if self.client.nodeid:
1565                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1566             else:
1567                 if self.template == 'item':
1568                     s.append('- new %s'%self.classname)
1569                 elif self.template == 'index':
1570                     s.append('- %s index'%self.classname)
1571                 else:
1572                     s.append('- %s %s'%(self.classname, self.template))
1573         else:
1574             s.append('- home')
1575         return ' '.join(s)
1577     def __str__(self):
1578         d = {}
1579         d.update(self.__dict__)
1580         f = ''
1581         for k in self.form.keys():
1582             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1583         d['form'] = f
1584         e = ''
1585         for k,v in self.env.items():
1586             e += '\n     %r=%r'%(k, v)
1587         d['env'] = e
1588         return '''
1589 form: %(form)s
1590 base: %(base)r
1591 classname: %(classname)r
1592 template: %(template)r
1593 columns: %(columns)r
1594 sort: %(sort)r
1595 group: %(group)r
1596 filter: %(filter)r
1597 search_text: %(search_text)r
1598 pagesize: %(pagesize)r
1599 startwith: %(startwith)r
1600 env: %(env)s
1601 '''%d
1603     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1604             filterspec=1):
1605         ''' return the current index args as form elements '''
1606         l = []
1607         sc = self.special_char
1608         s = '<input type="hidden" name="%s" value="%s">'
1609         if columns and self.columns:
1610             l.append(s%(sc+'columns', ','.join(self.columns)))
1611         if sort and self.sort[1] is not None:
1612             if self.sort[0] == '-':
1613                 val = '-'+self.sort[1]
1614             else:
1615                 val = self.sort[1]
1616             l.append(s%(sc+'sort', val))
1617         if group and self.group[1] is not None:
1618             if self.group[0] == '-':
1619                 val = '-'+self.group[1]
1620             else:
1621                 val = self.group[1]
1622             l.append(s%(sc+'group', val))
1623         if filter and self.filter:
1624             l.append(s%(sc+'filter', ','.join(self.filter)))
1625         if filterspec:
1626             for k,v in self.filterspec.items():
1627                 if type(v) == type([]):
1628                     l.append(s%(k, ','.join(v)))
1629                 else:
1630                     l.append(s%(k, v))
1631         if self.search_text:
1632             l.append(s%(sc+'search_text', self.search_text))
1633         l.append(s%(sc+'pagesize', self.pagesize))
1634         l.append(s%(sc+'startwith', self.startwith))
1635         return '\n'.join(l)
1637     def indexargs_url(self, url, args):
1638         ''' Embed the current index args in a URL
1639         '''
1640         sc = self.special_char
1641         l = ['%s=%s'%(k,v) for k,v in args.items()]
1643         # pull out the special values (prefixed by @ or :)
1644         specials = {}
1645         for key in args.keys():
1646             if key[0] in '@:':
1647                 specials[key[1:]] = args[key]
1649         # ok, now handle the specials we received in the request
1650         if self.columns and not specials.has_key('columns'):
1651             l.append(sc+'columns=%s'%(','.join(self.columns)))
1652         if self.sort[1] is not None and not specials.has_key('sort'):
1653             if self.sort[0] == '-':
1654                 val = '-'+self.sort[1]
1655             else:
1656                 val = self.sort[1]
1657             l.append(sc+'sort=%s'%val)
1658         if self.group[1] is not None and not specials.has_key('group'):
1659             if self.group[0] == '-':
1660                 val = '-'+self.group[1]
1661             else:
1662                 val = self.group[1]
1663             l.append(sc+'group=%s'%val)
1664         if self.filter and not specials.has_key('filter'):
1665             l.append(sc+'filter=%s'%(','.join(self.filter)))
1666         if self.search_text and not specials.has_key('search_text'):
1667             l.append(sc+'search_text=%s'%self.search_text)
1668         if not specials.has_key('pagesize'):
1669             l.append(sc+'pagesize=%s'%self.pagesize)
1670         if not specials.has_key('startwith'):
1671             l.append(sc+'startwith=%s'%self.startwith)
1673         # finally, the remainder of the filter args in the request
1674         for k,v in self.filterspec.items():
1675             if not args.has_key(k):
1676                 if type(v) == type([]):
1677                     l.append('%s=%s'%(k, ','.join(v)))
1678                 else:
1679                     l.append('%s=%s'%(k, v))
1680         return '%s?%s'%(url, '&'.join(l))
1681     indexargs_href = indexargs_url
1683     def base_javascript(self):
1684         return '''
1685 <script type="text/javascript">
1686 submitted = false;
1687 function submit_once() {
1688     if (submitted) {
1689         alert("Your request is being processed.\\nPlease be patient.");
1690         return 0;
1691     }
1692     submitted = true;
1693     return 1;
1696 function help_window(helpurl, width, height) {
1697     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1699 </script>
1700 '''%self.base
1702     def batch(self):
1703         ''' Return a batch object for results from the "current search"
1704         '''
1705         filterspec = self.filterspec
1706         sort = self.sort
1707         group = self.group
1709         # get the list of ids we're batching over
1710         klass = self.client.db.getclass(self.classname)
1711         if self.search_text:
1712             matches = self.client.db.indexer.search(
1713                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1714         else:
1715             matches = None
1716         l = klass.filter(matches, filterspec, sort, group)
1718         # return the batch object, using IDs only
1719         return Batch(self.client, l, self.pagesize, self.startwith,
1720             classname=self.classname)
1722 # extend the standard ZTUtils Batch object to remove dependency on
1723 # Acquisition and add a couple of useful methods
1724 class Batch(ZTUtils.Batch):
1725     ''' Use me to turn a list of items, or item ids of a given class, into a
1726         series of batches.
1728         ========= ========================================================
1729         Parameter  Usage
1730         ========= ========================================================
1731         sequence  a list of HTMLItems or item ids
1732         classname if sequence is a list of ids, this is the class of item
1733         size      how big to make the sequence.
1734         start     where to start (0-indexed) in the sequence.
1735         end       where to end (0-indexed) in the sequence.
1736         orphan    if the next batch would contain less items than this
1737                   value, then it is combined with this batch
1738         overlap   the number of items shared between adjacent batches
1739         ========= ========================================================
1741         Attributes: Note that the "start" attribute, unlike the
1742         argument, is a 1-based index (I know, lame).  "first" is the
1743         0-based index.  "length" is the actual number of elements in
1744         the batch.
1746         "sequence_length" is the length of the original, unbatched, sequence.
1747     '''
1748     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1749             overlap=0, classname=None):
1750         self.client = client
1751         self.last_index = self.last_item = None
1752         self.current_item = None
1753         self.classname = classname
1754         self.sequence_length = len(sequence)
1755         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1756             overlap)
1758     # overwrite so we can late-instantiate the HTMLItem instance
1759     def __getitem__(self, index):
1760         if index < 0:
1761             if index + self.end < self.first: raise IndexError, index
1762             return self._sequence[index + self.end]
1763         
1764         if index >= self.length:
1765             raise IndexError, index
1767         # move the last_item along - but only if the fetched index changes
1768         # (for some reason, index 0 is fetched twice)
1769         if index != self.last_index:
1770             self.last_item = self.current_item
1771             self.last_index = index
1773         item = self._sequence[index + self.first]
1774         if self.classname:
1775             # map the item ids to instances
1776             if self.classname == 'user':
1777                 item = HTMLUser(self.client, self.classname, item)
1778             else:
1779                 item = HTMLItem(self.client, self.classname, item)
1780         self.current_item = item
1781         return item
1783     def propchanged(self, property):
1784         ''' Detect if the property marked as being the group property
1785             changed in the last iteration fetch
1786         '''
1787         if (self.last_item is None or
1788                 self.last_item[property] != self.current_item[property]):
1789             return 1
1790         return 0
1792     # override these 'cos we don't have access to acquisition
1793     def previous(self):
1794         if self.start == 1:
1795             return None
1796         return Batch(self.client, self._sequence, self._size,
1797             self.first - self._size + self.overlap, 0, self.orphan,
1798             self.overlap)
1800     def next(self):
1801         try:
1802             self._sequence[self.end]
1803         except IndexError:
1804             return None
1805         return Batch(self.client, self._sequence, self._size,
1806             self.end - self.overlap, 0, self.orphan, self.overlap)
1808 class TemplatingUtils:
1809     ''' Utilities for templating
1810     '''
1811     def __init__(self, client):
1812         self.client = client
1813     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1814         return Batch(self.client, sequence, size, start, end, orphan,
1815             overlap)