Code

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