Code

fix StringHTMLProperty hyperlinking
[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         if self.classname == 'user':
395             klass = HTMLUser
396         else:
397             klass = HTMLItem
398         l = [klass(self._client, self.classname, x)
399              for x in self._klass.filter(None, filterspec, sort, group)]
400         return l
402     def classhelp(self, properties=None, label='list', width='500',
403             height='400'):
404         ''' Pop up a javascript window with class help
406             This generates a link to a popup window which displays the 
407             properties indicated by "properties" of the class named by
408             "classname". The "properties" should be a comma-separated list
409             (eg. 'id,name,description'). Properties defaults to all the
410             properties of a class (excluding id, creator, created and
411             activity).
413             You may optionally override the label displayed, the width and
414             height. The popup window will be resizable and scrollable.
415         '''
416         if properties is None:
417             properties = self._klass.getprops(protected=0).keys()
418             properties.sort()
419             properties = ','.join(properties)
420         return '<a href="javascript:help_window(\'%s?:template=help&' \
421             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
422             self.classname, properties, width, height, label)
424     def submit(self, label="Submit New Entry"):
425         ''' Generate a submit button (and action hidden element)
426         '''
427         return '  <input type="hidden" name=":action" value="new">\n'\
428         '  <input type="submit" name="submit" value="%s">'%label
430     def history(self):
431         return 'New node - no history'
433     def renderWith(self, name, **kwargs):
434         ''' Render this class with the given template.
435         '''
436         # create a new request and override the specified args
437         req = HTMLRequest(self._client)
438         req.classname = self.classname
439         req.update(kwargs)
441         # new template, using the specified classname and request
442         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
444         # use our fabricated request
445         return pt.render(self._client, self.classname, req)
447 class HTMLItem(HTMLPermissions):
448     ''' Accesses through an *item*
449     '''
450     def __init__(self, client, classname, nodeid):
451         self._client = client
452         self._db = client.db
453         self._classname = classname
454         self._nodeid = nodeid
455         self._klass = self._db.getclass(classname)
456         self._props = self._klass.getprops()
458     def __repr__(self):
459         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
460             self._nodeid)
462     def __getitem__(self, item):
463         ''' return an HTMLProperty instance
464         '''
465         #print 'HTMLItem.getitem', (self, item)
466         if item == 'id':
467             return self._nodeid
469         # get the property
470         prop = self._props[item]
472         # get the value, handling missing values
473         value = self._klass.get(self._nodeid, item, None)
474         if value is None:
475             if isinstance(self._props[item], hyperdb.Multilink):
476                 value = []
478         # look up the correct HTMLProperty class
479         for klass, htmlklass in propclasses:
480             if isinstance(prop, klass):
481                 return htmlklass(self._client, self._nodeid, prop, item, value)
483         raise KeyError, item
485     def __getattr__(self, attr):
486         ''' convenience access to properties '''
487         try:
488             return self[attr]
489         except KeyError:
490             raise AttributeError, attr
491     
492     def submit(self, label="Submit Changes"):
493         ''' Generate a submit button (and action hidden element)
494         '''
495         return '  <input type="hidden" name=":action" value="edit">\n'\
496         '  <input type="submit" name="submit" value="%s">'%label
498     def journal(self, direction='descending'):
499         ''' Return a list of HTMLJournalEntry instances.
500         '''
501         # XXX do this
502         return []
504     def history(self, direction='descending', dre=re.compile('\d+')):
505         l = ['<table class="history">'
506              '<tr><th colspan="4" class="header">',
507              _('History'),
508              '</th></tr><tr>',
509              _('<th>Date</th>'),
510              _('<th>User</th>'),
511              _('<th>Action</th>'),
512              _('<th>Args</th>'),
513             '</tr>']
514         current = {}
515         comments = {}
516         history = self._klass.history(self._nodeid)
517         history.sort()
518         if direction == 'descending':
519             history.reverse()
520             for prop_n in self._props.keys():
521                 prop = self[prop_n]
522                 if isinstance(prop, HTMLProperty):
523                     current[prop_n] = prop.plain()
524                     # make link if hrefable
525                     if (self._props.has_key(prop_n) and
526                             isinstance(self._props[prop_n], hyperdb.Link)):
527                         classname = self._props[prop_n].classname
528                         if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
529                             current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
530                                 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
531  
532         for id, evt_date, user, action, args in history:
533             date_s = str(evt_date).replace("."," ")
534             arg_s = ''
535             if action == 'link' and type(args) == type(()):
536                 if len(args) == 3:
537                     linkcl, linkid, key = args
538                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
539                         linkcl, linkid, key)
540                 else:
541                     arg_s = str(args)
543             elif action == 'unlink' and type(args) == type(()):
544                 if len(args) == 3:
545                     linkcl, linkid, key = args
546                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
547                         linkcl, linkid, key)
548                 else:
549                     arg_s = str(args)
551             elif type(args) == type({}):
552                 cell = []
553                 for k in args.keys():
554                     # try to get the relevant property and treat it
555                     # specially
556                     try:
557                         prop = self._props[k]
558                     except KeyError:
559                         prop = None
560                     if prop is not None:
561                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
562                                 isinstance(prop, hyperdb.Link)):
563                             # figure what the link class is
564                             classname = prop.classname
565                             try:
566                                 linkcl = self._db.getclass(classname)
567                             except KeyError:
568                                 labelprop = None
569                                 comments[classname] = _('''The linked class
570                                     %(classname)s no longer exists''')%locals()
571                             labelprop = linkcl.labelprop(1)
572                             hrefable = os.path.exists(
573                                 os.path.join(self._db.config.TEMPLATES,
574                                 classname+'.item'))
576                         if isinstance(prop, hyperdb.Multilink) and \
577                                 len(args[k]) > 0:
578                             ml = []
579                             for linkid in args[k]:
580                                 if isinstance(linkid, type(())):
581                                     sublabel = linkid[0] + ' '
582                                     linkids = linkid[1]
583                                 else:
584                                     sublabel = ''
585                                     linkids = [linkid]
586                                 subml = []
587                                 for linkid in linkids:
588                                     label = classname + linkid
589                                     # if we have a label property, try to use it
590                                     # TODO: test for node existence even when
591                                     # there's no labelprop!
592                                     try:
593                                         if labelprop is not None and \
594                                                 labelprop != 'id':
595                                             label = linkcl.get(linkid, labelprop)
596                                     except IndexError:
597                                         comments['no_link'] = _('''<strike>The
598                                             linked node no longer
599                                             exists</strike>''')
600                                         subml.append('<strike>%s</strike>'%label)
601                                     else:
602                                         if hrefable:
603                                             subml.append('<a href="%s%s">%s</a>'%(
604                                                 classname, linkid, label))
605                                         else:
606                                             subml.append(label)
607                                 ml.append(sublabel + ', '.join(subml))
608                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
609                         elif isinstance(prop, hyperdb.Link) and args[k]:
610                             label = classname + args[k]
611                             # if we have a label property, try to use it
612                             # TODO: test for node existence even when
613                             # there's no labelprop!
614                             if labelprop is not None and labelprop != 'id':
615                                 try:
616                                     label = linkcl.get(args[k], labelprop)
617                                 except IndexError:
618                                     comments['no_link'] = _('''<strike>The
619                                         linked node no longer
620                                         exists</strike>''')
621                                     cell.append(' <strike>%s</strike>,\n'%label)
622                                     # "flag" this is done .... euwww
623                                     label = None
624                             if label is not None:
625                                 if hrefable:
626                                     old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
627                                 else:
628                                     old = label;
629                                 cell.append('%s: %s' % (k,old))
630                                 if current.has_key(k):
631                                     cell[-1] += ' -> %s'%current[k]
632                                     current[k] = old
634                         elif isinstance(prop, hyperdb.Date) and args[k]:
635                             d = date.Date(args[k])
636                             cell.append('%s: %s'%(k, str(d)))
637                             if current.has_key(k):
638                                 cell[-1] += ' -> %s'%current[k]
639                                 current[k] = str(d)
641                         elif isinstance(prop, hyperdb.Interval) and args[k]:
642                             d = date.Interval(args[k])
643                             cell.append('%s: %s'%(k, str(d)))
644                             if current.has_key(k):
645                                 cell[-1] += ' -> %s'%current[k]
646                                 current[k] = str(d)
648                         elif isinstance(prop, hyperdb.String) and args[k]:
649                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
650                             if current.has_key(k):
651                                 cell[-1] += ' -> %s'%current[k]
652                                 current[k] = cgi.escape(args[k])
654                         elif not args[k]:
655                             if current.has_key(k):
656                                 cell.append('%s: %s'%(k, current[k]))
657                                 current[k] = '(no value)'
658                             else:
659                                 cell.append('%s: (no value)'%k)
661                         else:
662                             cell.append('%s: %s'%(k, str(args[k])))
663                             if current.has_key(k):
664                                 cell[-1] += ' -> %s'%current[k]
665                                 current[k] = str(args[k])
666                     else:
667                         # property no longer exists
668                         comments['no_exist'] = _('''<em>The indicated property
669                             no longer exists</em>''')
670                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
671                 arg_s = '<br />'.join(cell)
672             else:
673                 # unkown event!!
674                 comments['unknown'] = _('''<strong><em>This event is not
675                     handled by the history display!</em></strong>''')
676                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
677             date_s = date_s.replace(' ', '&nbsp;')
678             # if the user's an itemid, figure the username (older journals
679             # have the username)
680             if dre.match(user):
681                 user = self._db.user.get(user, 'username')
682             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
683                 date_s, user, action, arg_s))
684         if comments:
685             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
686         for entry in comments.values():
687             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
688         l.append('</table>')
689         return '\n'.join(l)
691     def renderQueryForm(self):
692         ''' Render this item, which is a query, as a search form.
693         '''
694         # create a new request and override the specified args
695         req = HTMLRequest(self._client)
696         req.classname = self._klass.get(self._nodeid, 'klass')
697         name = self._klass.get(self._nodeid, 'name')
698         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
699             '&:queryname=%s'%urllib.quote(name))
701         # new template, using the specified classname and request
702         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
704         # use our fabricated request
705         return pt.render(self._client, req.classname, req)
707 class HTMLUser(HTMLItem):
708     ''' Accesses through the *user* (a special case of item)
709     '''
710     def __init__(self, client, classname, nodeid):
711         HTMLItem.__init__(self, client, 'user', nodeid)
712         self._default_classname = client.classname
714         # used for security checks
715         self._security = client.db.security
717     _marker = []
718     def hasPermission(self, role, classname=_marker):
719         ''' Determine if the user has the Role.
721             The class being tested defaults to the template's class, but may
722             be overidden for this test by suppling an alternate classname.
723         '''
724         if classname is self._marker:
725             classname = self._default_classname
726         return self._security.hasPermission(role, self._nodeid, classname)
728     def is_edit_ok(self):
729         ''' Is the user allowed to Edit the current class?
730             Also check whether this is the current user's info.
731         '''
732         return self._db.security.hasPermission('Edit', self._client.userid,
733             self._classname) or self._nodeid == self._client.userid
735     def is_view_ok(self):
736         ''' Is the user allowed to View 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 class HTMLProperty:
743     ''' String, Number, Date, Interval HTMLProperty
745         Has useful attributes:
747          _name  the name of the property
748          _value the value of the property if any
750         A wrapper object which may be stringified for the plain() behaviour.
751     '''
752     def __init__(self, client, nodeid, prop, name, value):
753         self._client = client
754         self._db = client.db
755         self._nodeid = nodeid
756         self._prop = prop
757         self._name = name
758         self._value = value
759     def __repr__(self):
760         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
761     def __str__(self):
762         return self.plain()
763     def __cmp__(self, other):
764         if isinstance(other, HTMLProperty):
765             return cmp(self._value, other._value)
766         return cmp(self._value, other)
768 class StringHTMLProperty(HTMLProperty):
769     url_re = re.compile(r'\w{3,6}://\S+')
770     email_re = re.compile(r'[\w\.]+@[\w\.\-]+')
771     designator_re = re.compile(r'([a-z_]+)(\d+)')
772     def _url_repl(self, match):
773         s = match.group(0)
774         return '<a href="%s">%s</a>'%(s, s)
775     def _email_repl(self, match):
776         s = match.group(0)
777         return '<a href="mailto:%s">%s</a>'%(s, s)
778     def _designator_repl(self, match):
779         s = match.group(0)
780         s1 = match.group(1)
781         s2 = match.group(2)
782         try:
783             # make sure s1 is a valid tracker classname
784             self._db.getclass(s1)
785             return '<a href="%s">%s %s</a>'%(s, s1, s2)
786         except KeyError:
787             return '%s%s'%(s1, s2)
789     def plain(self, escape=0, hyperlink=0):
790         ''' Render a "plain" representation of the property
791             
792             "escape" turns on/off HTML quoting
793             "hyperlink" turns on/off in-text hyperlinking of URLs, email
794                 addresses and designators
795         '''
796         if self._value is None:
797             return ''
798         if escape:
799             s = cgi.escape(str(self._value))
800         else:
801             s = str(self._value)
802         if hyperlink:
803             if not escape:
804                 s = cgi.escape(s)
805             s = self.url_re.sub(self._url_repl, s)
806             s = self.email_re.sub(self._email_repl, s)
807             s = self.designator_re.sub(self._designator_repl, s)
808         return s
810     def stext(self, escape=0):
811         ''' Render the value of the property as StructuredText.
813             This requires the StructureText module to be installed separately.
814         '''
815         s = self.plain(escape=escape)
816         if not StructuredText:
817             return s
818         return StructuredText(s,level=1,header=0)
820     def field(self, size = 30):
821         ''' Render a form edit field for the property
822         '''
823         if self._value is None:
824             value = ''
825         else:
826             value = cgi.escape(str(self._value))
827             value = '&quot;'.join(value.split('"'))
828         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
830     def multiline(self, escape=0, rows=5, cols=40):
831         ''' Render a multiline form edit field for the property
832         '''
833         if self._value is None:
834             value = ''
835         else:
836             value = cgi.escape(str(self._value))
837             value = '&quot;'.join(value.split('"'))
838         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
839             self._name, rows, cols, value)
841     def email(self, escape=1):
842         ''' Render the value of the property as an obscured email address
843         '''
844         if self._value is None: value = ''
845         else: value = str(self._value)
846         if value.find('@') != -1:
847             name, domain = value.split('@')
848             domain = ' '.join(domain.split('.')[:-1])
849             name = name.replace('.', ' ')
850             value = '%s at %s ...'%(name, domain)
851         else:
852             value = value.replace('.', ' ')
853         if escape:
854             value = cgi.escape(value)
855         return value
857 class PasswordHTMLProperty(HTMLProperty):
858     def plain(self):
859         ''' Render a "plain" representation of the property
860         '''
861         if self._value is None:
862             return ''
863         return _('*encrypted*')
865     def field(self, size = 30):
866         ''' Render a form edit field for the property.
867         '''
868         return '<input type="password" name="%s" size="%s">'%(self._name, size)
870     def confirm(self, size = 30):
871         ''' Render a second form edit field for the property, used for 
872             confirmation that the user typed the password correctly. Generates
873             a field with name "name:confirm".
874         '''
875         return '<input type="password" name="%s:confirm" size="%s">'%(
876             self._name, size)
878 class NumberHTMLProperty(HTMLProperty):
879     def plain(self):
880         ''' Render a "plain" representation of the property
881         '''
882         return str(self._value)
884     def field(self, size = 30):
885         ''' Render a form edit field for the property
886         '''
887         if self._value is None:
888             value = ''
889         else:
890             value = cgi.escape(str(self._value))
891             value = '&quot;'.join(value.split('"'))
892         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
894 class BooleanHTMLProperty(HTMLProperty):
895     def plain(self):
896         ''' Render a "plain" representation of the property
897         '''
898         if self._value is None:
899             return ''
900         return self._value and "Yes" or "No"
902     def field(self):
903         ''' Render a form edit field for the property
904         '''
905         checked = self._value and "checked" or ""
906         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
907             checked)
908         if checked:
909             checked = ""
910         else:
911             checked = "checked"
912         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
913             checked)
914         return s
916 class DateHTMLProperty(HTMLProperty):
917     def plain(self):
918         ''' Render a "plain" representation of the property
919         '''
920         if self._value is None:
921             return ''
922         return str(self._value)
924     def field(self, size = 30):
925         ''' Render a form edit field for the property
926         '''
927         if self._value is None:
928             value = ''
929         else:
930             value = cgi.escape(str(self._value))
931             value = '&quot;'.join(value.split('"'))
932         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
934     def reldate(self, pretty=1):
935         ''' Render the interval between the date and now.
937             If the "pretty" flag is true, then make the display pretty.
938         '''
939         if not self._value:
940             return ''
942         # figure the interval
943         interval = date.Date('.') - self._value
944         if pretty:
945             return interval.pretty()
946         return str(interval)
948     def pretty(self, format='%d %B %Y'):
949         ''' Render the date in a pretty format (eg. month names, spaces).
951             The format string is a standard python strftime format string.
952             Note that if the day is zero, and appears at the start of the
953             string, then it'll be stripped from the output. This is handy
954             for the situatin when a date only specifies a month and a year.
955         '''
956         return self._value.pretty()
958     def local(self, offset):
959         ''' Return the date/time as a local (timezone offset) date/time.
960         '''
961         return DateHTMLProperty(self._client, self._nodeid, self._prop,
962             self._name, self._value.local())
964 class IntervalHTMLProperty(HTMLProperty):
965     def plain(self):
966         ''' Render a "plain" representation of the property
967         '''
968         if self._value is None:
969             return ''
970         return str(self._value)
972     def pretty(self):
973         ''' Render the interval in a pretty format (eg. "yesterday")
974         '''
975         return self._value.pretty()
977     def field(self, size = 30):
978         ''' Render a form edit field for the property
979         '''
980         if self._value is None:
981             value = ''
982         else:
983             value = cgi.escape(str(self._value))
984             value = '&quot;'.join(value.split('"'))
985         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
987 class LinkHTMLProperty(HTMLProperty):
988     ''' Link HTMLProperty
989         Include the above as well as being able to access the class
990         information. Stringifying the object itself results in the value
991         from the item being displayed. Accessing attributes of this object
992         result in the appropriate entry from the class being queried for the
993         property accessed (so item/assignedto/name would look up the user
994         entry identified by the assignedto property on item, and then the
995         name property of that user)
996     '''
997     def __init__(self, *args):
998         HTMLProperty.__init__(self, *args)
999         # if we're representing a form value, then the -1 from the form really
1000         # should be a None
1001         if str(self._value) == '-1':
1002             self._value = None
1004     def __getattr__(self, attr):
1005         ''' return a new HTMLItem '''
1006        #print 'Link.getattr', (self, attr, self._value)
1007         if not self._value:
1008             raise AttributeError, "Can't access missing value"
1009         if self._prop.classname == 'user':
1010             klass = HTMLUser
1011         else:
1012             klass = HTMLItem
1013         i = klass(self._client, self._prop.classname, self._value)
1014         return getattr(i, attr)
1016     def plain(self, escape=0):
1017         ''' Render a "plain" representation of the property
1018         '''
1019         if self._value is None:
1020             return ''
1021         linkcl = self._db.classes[self._prop.classname]
1022         k = linkcl.labelprop(1)
1023         value = str(linkcl.get(self._value, k))
1024         if escape:
1025             value = cgi.escape(value)
1026         return value
1028     def field(self, showid=0, size=None):
1029         ''' Render a form edit field for the property
1030         '''
1031         linkcl = self._db.getclass(self._prop.classname)
1032         if linkcl.getprops().has_key('order'):  
1033             sort_on = 'order'  
1034         else:  
1035             sort_on = linkcl.labelprop()  
1036         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1037         # TODO: make this a field display, not a menu one!
1038         l = ['<select name="%s">'%self._name]
1039         k = linkcl.labelprop(1)
1040         if self._value is None:
1041             s = 'selected '
1042         else:
1043             s = ''
1044         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1046         # make sure we list the current value if it's retired
1047         if self._value and self._value not in options:
1048             options.insert(0, self._value)
1050         for optionid in options:
1051             # get the option value, and if it's None use an empty string
1052             option = linkcl.get(optionid, k) or ''
1054             # figure if this option is selected
1055             s = ''
1056             if optionid == self._value:
1057                 s = 'selected '
1059             # figure the label
1060             if showid:
1061                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1062             else:
1063                 lab = option
1065             # truncate if it's too long
1066             if size is not None and len(lab) > size:
1067                 lab = lab[:size-3] + '...'
1069             # and generate
1070             lab = cgi.escape(lab)
1071             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1072         l.append('</select>')
1073         return '\n'.join(l)
1075     def menu(self, size=None, height=None, showid=0, additional=[],
1076             **conditions):
1077         ''' Render a form select list for this property
1078         '''
1079         value = self._value
1081         # sort function
1082         sortfunc = make_sort_function(self._db, self._prop.classname)
1084         linkcl = self._db.getclass(self._prop.classname)
1085         l = ['<select name="%s">'%self._name]
1086         k = linkcl.labelprop(1)
1087         s = ''
1088         if value is None:
1089             s = 'selected '
1090         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1091         if linkcl.getprops().has_key('order'):  
1092             sort_on = ('+', 'order')
1093         else:  
1094             sort_on = ('+', linkcl.labelprop())
1095         options = linkcl.filter(None, conditions, sort_on, (None, None))
1097         # make sure we list the current value if it's retired
1098         if self._value and self._value not in options:
1099             options.insert(0, self._value)
1101         for optionid in options:
1102             # get the option value, and if it's None use an empty string
1103             option = linkcl.get(optionid, k) or ''
1105             # figure if this option is selected
1106             s = ''
1107             if value in [optionid, option]:
1108                 s = 'selected '
1110             # figure the label
1111             if showid:
1112                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1113             else:
1114                 lab = option
1116             # truncate if it's too long
1117             if size is not None and len(lab) > size:
1118                 lab = lab[:size-3] + '...'
1119             if additional:
1120                 m = []
1121                 for propname in additional:
1122                     m.append(linkcl.get(optionid, propname))
1123                 lab = lab + ' (%s)'%', '.join(map(str, m))
1125             # and generate
1126             lab = cgi.escape(lab)
1127             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1128         l.append('</select>')
1129         return '\n'.join(l)
1130 #    def checklist(self, ...)
1132 class MultilinkHTMLProperty(HTMLProperty):
1133     ''' Multilink HTMLProperty
1135         Also be iterable, returning a wrapper object like the Link case for
1136         each entry in the multilink.
1137     '''
1138     def __len__(self):
1139         ''' length of the multilink '''
1140         return len(self._value)
1142     def __getattr__(self, attr):
1143         ''' no extended attribute accesses make sense here '''
1144         raise AttributeError, attr
1146     def __getitem__(self, num):
1147         ''' iterate and return a new HTMLItem
1148         '''
1149        #print 'Multi.getitem', (self, num)
1150         value = self._value[num]
1151         if self._prop.classname == 'user':
1152             klass = HTMLUser
1153         else:
1154             klass = HTMLItem
1155         return klass(self._client, self._prop.classname, value)
1157     def __contains__(self, value):
1158         ''' Support the "in" operator. We have to make sure the passed-in
1159             value is a string first, not a *HTMLProperty.
1160         '''
1161         return str(value) in self._value
1163     def reverse(self):
1164         ''' return the list in reverse order
1165         '''
1166         l = self._value[:]
1167         l.reverse()
1168         if self._prop.classname == 'user':
1169             klass = HTMLUser
1170         else:
1171             klass = HTMLItem
1172         return [klass(self._client, self._prop.classname, value) for value in l]
1174     def plain(self, escape=0):
1175         ''' Render a "plain" representation of the property
1176         '''
1177         linkcl = self._db.classes[self._prop.classname]
1178         k = linkcl.labelprop(1)
1179         labels = []
1180         for v in self._value:
1181             labels.append(linkcl.get(v, k))
1182         value = ', '.join(labels)
1183         if escape:
1184             value = cgi.escape(value)
1185         return value
1187     def field(self, size=30, showid=0):
1188         ''' Render a form edit field for the property
1189         '''
1190         sortfunc = make_sort_function(self._db, self._prop.classname)
1191         linkcl = self._db.getclass(self._prop.classname)
1192         value = self._value[:]
1193         if value:
1194             value.sort(sortfunc)
1195         # map the id to the label property
1196         if not linkcl.getkey():
1197             showid=1
1198         if not showid:
1199             k = linkcl.labelprop(1)
1200             value = [linkcl.get(v, k) for v in value]
1201         value = cgi.escape(','.join(value))
1202         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1204     def menu(self, size=None, height=None, showid=0, additional=[],
1205             **conditions):
1206         ''' Render a form select list for this property
1207         '''
1208         value = self._value
1210         # sort function
1211         sortfunc = make_sort_function(self._db, self._prop.classname)
1213         linkcl = self._db.getclass(self._prop.classname)
1214         if linkcl.getprops().has_key('order'):  
1215             sort_on = ('+', 'order')
1216         else:  
1217             sort_on = ('+', linkcl.labelprop())
1218         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1219         height = height or min(len(options), 7)
1220         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1221         k = linkcl.labelprop(1)
1223         # make sure we list the current values if they're retired
1224         for val in value:
1225             if val not in options:
1226                 options.insert(0, val)
1228         for optionid in options:
1229             # get the option value, and if it's None use an empty string
1230             option = linkcl.get(optionid, k) or ''
1232             # figure if this option is selected
1233             s = ''
1234             if optionid in value or option in value:
1235                 s = 'selected '
1237             # figure the label
1238             if showid:
1239                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1240             else:
1241                 lab = option
1242             # truncate if it's too long
1243             if size is not None and len(lab) > size:
1244                 lab = lab[:size-3] + '...'
1245             if additional:
1246                 m = []
1247                 for propname in additional:
1248                     m.append(linkcl.get(optionid, propname))
1249                 lab = lab + ' (%s)'%', '.join(m)
1251             # and generate
1252             lab = cgi.escape(lab)
1253             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1254                 lab))
1255         l.append('</select>')
1256         return '\n'.join(l)
1258 # set the propclasses for HTMLItem
1259 propclasses = (
1260     (hyperdb.String, StringHTMLProperty),
1261     (hyperdb.Number, NumberHTMLProperty),
1262     (hyperdb.Boolean, BooleanHTMLProperty),
1263     (hyperdb.Date, DateHTMLProperty),
1264     (hyperdb.Interval, IntervalHTMLProperty),
1265     (hyperdb.Password, PasswordHTMLProperty),
1266     (hyperdb.Link, LinkHTMLProperty),
1267     (hyperdb.Multilink, MultilinkHTMLProperty),
1270 def make_sort_function(db, classname):
1271     '''Make a sort function for a given class
1272     '''
1273     linkcl = db.getclass(classname)
1274     if linkcl.getprops().has_key('order'):
1275         sort_on = 'order'
1276     else:
1277         sort_on = linkcl.labelprop()
1278     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1279         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1280     return sortfunc
1282 def handleListCGIValue(value):
1283     ''' Value is either a single item or a list of items. Each item has a
1284         .value that we're actually interested in.
1285     '''
1286     if isinstance(value, type([])):
1287         return [value.value for value in value]
1288     else:
1289         value = value.value.strip()
1290         if not value:
1291             return []
1292         return value.split(',')
1294 class ShowDict:
1295     ''' A convenience access to the :columns index parameters
1296     '''
1297     def __init__(self, columns):
1298         self.columns = {}
1299         for col in columns:
1300             self.columns[col] = 1
1301     def __getitem__(self, name):
1302         return self.columns.has_key(name)
1304 class HTMLRequest:
1305     ''' The *request*, holding the CGI form and environment.
1307         "form" the CGI form as a cgi.FieldStorage
1308         "env" the CGI environment variables
1309         "base" the base URL for this instance
1310         "user" a HTMLUser instance for this user
1311         "classname" the current classname (possibly None)
1312         "template" the current template (suffix, also possibly None)
1314         Index args:
1315         "columns" dictionary of the columns to display in an index page
1316         "show" a convenience access to columns - request/show/colname will
1317                be true if the columns should be displayed, false otherwise
1318         "sort" index sort column (direction, column name)
1319         "group" index grouping property (direction, column name)
1320         "filter" properties to filter the index on
1321         "filterspec" values to filter the index on
1322         "search_text" text to perform a full-text search on for an index
1324     '''
1325     def __init__(self, client):
1326         self.client = client
1328         # easier access vars
1329         self.form = client.form
1330         self.env = client.env
1331         self.base = client.base
1332         self.user = HTMLUser(client, 'user', client.userid)
1334         # store the current class name and action
1335         self.classname = client.classname
1336         self.template = client.template
1338         self._post_init()
1340     def _post_init(self):
1341         ''' Set attributes based on self.form
1342         '''
1343         # extract the index display information from the form
1344         self.columns = []
1345         if self.form.has_key(':columns'):
1346             self.columns = handleListCGIValue(self.form[':columns'])
1347         self.show = ShowDict(self.columns)
1349         # sorting
1350         self.sort = (None, None)
1351         if self.form.has_key(':sort'):
1352             sort = self.form[':sort'].value
1353             if sort.startswith('-'):
1354                 self.sort = ('-', sort[1:])
1355             else:
1356                 self.sort = ('+', sort)
1357         if self.form.has_key(':sortdir'):
1358             self.sort = ('-', self.sort[1])
1360         # grouping
1361         self.group = (None, None)
1362         if self.form.has_key(':group'):
1363             group = self.form[':group'].value
1364             if group.startswith('-'):
1365                 self.group = ('-', group[1:])
1366             else:
1367                 self.group = ('+', group)
1368         if self.form.has_key(':groupdir'):
1369             self.group = ('-', self.group[1])
1371         # filtering
1372         self.filter = []
1373         if self.form.has_key(':filter'):
1374             self.filter = handleListCGIValue(self.form[':filter'])
1375         self.filterspec = {}
1376         db = self.client.db
1377         if self.classname is not None:
1378             props = db.getclass(self.classname).getprops()
1379             for name in self.filter:
1380                 if self.form.has_key(name):
1381                     prop = props[name]
1382                     fv = self.form[name]
1383                     if (isinstance(prop, hyperdb.Link) or
1384                             isinstance(prop, hyperdb.Multilink)):
1385                         self.filterspec[name] = lookupIds(db, prop,
1386                             handleListCGIValue(fv))
1387                     else:
1388                         self.filterspec[name] = fv.value
1390         # full-text search argument
1391         self.search_text = None
1392         if self.form.has_key(':search_text'):
1393             self.search_text = self.form[':search_text'].value
1395         # pagination - size and start index
1396         # figure batch args
1397         if self.form.has_key(':pagesize'):
1398             self.pagesize = int(self.form[':pagesize'].value)
1399         else:
1400             self.pagesize = 50
1401         if self.form.has_key(':startwith'):
1402             self.startwith = int(self.form[':startwith'].value)
1403         else:
1404             self.startwith = 0
1406     def updateFromURL(self, url):
1407         ''' Parse the URL for query args, and update my attributes using the
1408             values.
1409         ''' 
1410         self.form = {}
1411         for name, value in cgi.parse_qsl(url):
1412             if self.form.has_key(name):
1413                 if isinstance(self.form[name], type([])):
1414                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1415                 else:
1416                     self.form[name] = [self.form[name],
1417                         cgi.MiniFieldStorage(name, value)]
1418             else:
1419                 self.form[name] = cgi.MiniFieldStorage(name, value)
1420         self._post_init()
1422     def update(self, kwargs):
1423         ''' Update my attributes using the keyword args
1424         '''
1425         self.__dict__.update(kwargs)
1426         if kwargs.has_key('columns'):
1427             self.show = ShowDict(self.columns)
1429     def description(self):
1430         ''' Return a description of the request - handle for the page title.
1431         '''
1432         s = [self.client.db.config.TRACKER_NAME]
1433         if self.classname:
1434             if self.client.nodeid:
1435                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1436             else:
1437                 if self.template == 'item':
1438                     s.append('- new %s'%self.classname)
1439                 elif self.template == 'index':
1440                     s.append('- %s index'%self.classname)
1441                 else:
1442                     s.append('- %s %s'%(self.classname, self.template))
1443         else:
1444             s.append('- home')
1445         return ' '.join(s)
1447     def __str__(self):
1448         d = {}
1449         d.update(self.__dict__)
1450         f = ''
1451         for k in self.form.keys():
1452             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1453         d['form'] = f
1454         e = ''
1455         for k,v in self.env.items():
1456             e += '\n     %r=%r'%(k, v)
1457         d['env'] = e
1458         return '''
1459 form: %(form)s
1460 base: %(base)r
1461 classname: %(classname)r
1462 template: %(template)r
1463 columns: %(columns)r
1464 sort: %(sort)r
1465 group: %(group)r
1466 filter: %(filter)r
1467 search_text: %(search_text)r
1468 pagesize: %(pagesize)r
1469 startwith: %(startwith)r
1470 env: %(env)s
1471 '''%d
1473     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1474             filterspec=1):
1475         ''' return the current index args as form elements '''
1476         l = []
1477         s = '<input type="hidden" name="%s" value="%s">'
1478         if columns and self.columns:
1479             l.append(s%(':columns', ','.join(self.columns)))
1480         if sort and self.sort[1] is not None:
1481             if self.sort[0] == '-':
1482                 val = '-'+self.sort[1]
1483             else:
1484                 val = self.sort[1]
1485             l.append(s%(':sort', val))
1486         if group and self.group[1] is not None:
1487             if self.group[0] == '-':
1488                 val = '-'+self.group[1]
1489             else:
1490                 val = self.group[1]
1491             l.append(s%(':group', val))
1492         if filter and self.filter:
1493             l.append(s%(':filter', ','.join(self.filter)))
1494         if filterspec:
1495             for k,v in self.filterspec.items():
1496                 if type(v) == type([]):
1497                     l.append(s%(k, ','.join(v)))
1498                 else:
1499                     l.append(s%(k, v))
1500         if self.search_text:
1501             l.append(s%(':search_text', self.search_text))
1502         l.append(s%(':pagesize', self.pagesize))
1503         l.append(s%(':startwith', self.startwith))
1504         return '\n'.join(l)
1506     def indexargs_url(self, url, args):
1507         ''' embed the current index args in a URL '''
1508         l = ['%s=%s'%(k,v) for k,v in args.items()]
1509         if self.columns and not args.has_key(':columns'):
1510             l.append(':columns=%s'%(','.join(self.columns)))
1511         if self.sort[1] is not None and not args.has_key(':sort'):
1512             if self.sort[0] == '-':
1513                 val = '-'+self.sort[1]
1514             else:
1515                 val = self.sort[1]
1516             l.append(':sort=%s'%val)
1517         if self.group[1] is not None and not args.has_key(':group'):
1518             if self.group[0] == '-':
1519                 val = '-'+self.group[1]
1520             else:
1521                 val = self.group[1]
1522             l.append(':group=%s'%val)
1523         if self.filter and not args.has_key(':columns'):
1524             l.append(':filter=%s'%(','.join(self.filter)))
1525         for k,v in self.filterspec.items():
1526             if not args.has_key(k):
1527                 if type(v) == type([]):
1528                     l.append('%s=%s'%(k, ','.join(v)))
1529                 else:
1530                     l.append('%s=%s'%(k, v))
1531         if self.search_text and not args.has_key(':search_text'):
1532             l.append(':search_text=%s'%self.search_text)
1533         if not args.has_key(':pagesize'):
1534             l.append(':pagesize=%s'%self.pagesize)
1535         if not args.has_key(':startwith'):
1536             l.append(':startwith=%s'%self.startwith)
1537         return '%s?%s'%(url, '&'.join(l))
1538     indexargs_href = indexargs_url
1540     def base_javascript(self):
1541         return '''
1542 <script language="javascript">
1543 submitted = false;
1544 function submit_once() {
1545     if (submitted) {
1546         alert("Your request is being processed.\\nPlease be patient.");
1547         return 0;
1548     }
1549     submitted = true;
1550     return 1;
1553 function help_window(helpurl, width, height) {
1554     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1556 </script>
1557 '''%self.base
1559     def batch(self):
1560         ''' Return a batch object for results from the "current search"
1561         '''
1562         filterspec = self.filterspec
1563         sort = self.sort
1564         group = self.group
1566         # get the list of ids we're batching over
1567         klass = self.client.db.getclass(self.classname)
1568         if self.search_text:
1569             matches = self.client.db.indexer.search(
1570                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1571         else:
1572             matches = None
1573         l = klass.filter(matches, filterspec, sort, group)
1575         # return the batch object, using IDs only
1576         return Batch(self.client, l, self.pagesize, self.startwith,
1577             classname=self.classname)
1579 # extend the standard ZTUtils Batch object to remove dependency on
1580 # Acquisition and add a couple of useful methods
1581 class Batch(ZTUtils.Batch):
1582     ''' Use me to turn a list of items, or item ids of a given class, into a
1583         series of batches.
1585         ========= ========================================================
1586         Parameter  Usage
1587         ========= ========================================================
1588         sequence  a list of HTMLItems or item ids
1589         classname if sequence is a list of ids, this is the class of item
1590         size      how big to make the sequence.
1591         start     where to start (0-indexed) in the sequence.
1592         end       where to end (0-indexed) in the sequence.
1593         orphan    if the next batch would contain less items than this
1594                   value, then it is combined with this batch
1595         overlap   the number of items shared between adjacent batches
1596         ========= ========================================================
1598         Attributes: Note that the "start" attribute, unlike the
1599         argument, is a 1-based index (I know, lame).  "first" is the
1600         0-based index.  "length" is the actual number of elements in
1601         the batch.
1603         "sequence_length" is the length of the original, unbatched, sequence.
1604     '''
1605     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1606             overlap=0, classname=None):
1607         self.client = client
1608         self.last_index = self.last_item = None
1609         self.current_item = None
1610         self.classname = classname
1611         self.sequence_length = len(sequence)
1612         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1613             overlap)
1615     # overwrite so we can late-instantiate the HTMLItem instance
1616     def __getitem__(self, index):
1617         if index < 0:
1618             if index + self.end < self.first: raise IndexError, index
1619             return self._sequence[index + self.end]
1620         
1621         if index >= self.length:
1622             raise IndexError, index
1624         # move the last_item along - but only if the fetched index changes
1625         # (for some reason, index 0 is fetched twice)
1626         if index != self.last_index:
1627             self.last_item = self.current_item
1628             self.last_index = index
1630         item = self._sequence[index + self.first]
1631         if self.classname:
1632             # map the item ids to instances
1633             if self.classname == 'user':
1634                 item = HTMLUser(self.client, self.classname, item)
1635             else:
1636                 item = HTMLItem(self.client, self.classname, item)
1637         self.current_item = item
1638         return item
1640     def propchanged(self, property):
1641         ''' Detect if the property marked as being the group property
1642             changed in the last iteration fetch
1643         '''
1644         if (self.last_item is None or
1645                 self.last_item[property] != self.current_item[property]):
1646             return 1
1647         return 0
1649     # override these 'cos we don't have access to acquisition
1650     def previous(self):
1651         if self.start == 1:
1652             return None
1653         return Batch(self.client, self._sequence, self._size,
1654             self.first - self._size + self.overlap, 0, self.orphan,
1655             self.overlap)
1657     def next(self):
1658         try:
1659             self._sequence[self.end]
1660         except IndexError:
1661             return None
1662         return Batch(self.client, self._sequence, self._size,
1663             self.end - self.overlap, 0, self.orphan, self.overlap)
1665 class TemplatingUtils:
1666     ''' Utilities for templating
1667     '''
1668     def __init__(self, client):
1669         self.client = client
1670     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1671         return Batch(self.client, sequence, size, start, end, orphan,
1672             overlap)