Code

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