Code

If no hours info is provided to Date constructors it defaults to local midnight,...
[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
202         # we want config to be exposed
203         self.config = client.db.config
205     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
206         # check to see if we're actually accessing an item
207         m = desre.match(item)
208         if m:
209             self._client.db.getclass(m.group('cl'))
210             return HTMLItem(self._client, m.group('cl'), m.group('id'))
211         else:
212             self._client.db.getclass(item)
213             return HTMLClass(self._client, item)
215     def __getattr__(self, attr):
216         try:
217             return self[attr]
218         except KeyError:
219             raise AttributeError, attr
221     def classes(self):
222         l = self._client.db.classes.keys()
223         l.sort()
224         return [HTMLClass(self._client, cn) for cn in l]
226 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
227     cl = db.getclass(prop.classname)
228     l = []
229     for entry in ids:
230         if num_re.match(entry):
231             l.append(entry)
232         else:
233             try:
234                 l.append(cl.lookup(entry))
235             except KeyError:
236                 # ignore invalid keys
237                 pass
238     return l
240 class HTMLPermissions:
241     ''' Helpers that provide answers to commonly asked Permission questions.
242     '''
243     def is_edit_ok(self):
244         ''' Is the user allowed to Edit the current class?
245         '''
246         return self._db.security.hasPermission('Edit', self._client.userid,
247             self._classname)
248     def is_view_ok(self):
249         ''' Is the user allowed to View the current class?
250         '''
251         return self._db.security.hasPermission('View', self._client.userid,
252             self._classname)
253     def is_only_view_ok(self):
254         ''' Is the user only allowed to View (ie. not Edit) the current class?
255         '''
256         return self.is_view_ok() and not self.is_edit_ok()
258 class HTMLClass(HTMLPermissions):
259     ''' Accesses through a class (either through *class* or *db.<classname>*)
260     '''
261     def __init__(self, client, classname, anonymous=0):
262         self._client = client
263         self._db = client.db
264         self._anonymous = anonymous
266         # we want classname to be exposed, but _classname gives a
267         # consistent API for extending Class/Item
268         self._classname = self.classname = classname
269         self._klass = self._db.getclass(self.classname)
270         self._props = self._klass.getprops()
272     def __repr__(self):
273         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
275     def __getitem__(self, item):
276         ''' return an HTMLProperty instance
277         '''
278        #print 'HTMLClass.getitem', (self, item)
280         # we don't exist
281         if item == 'id':
282             return None
284         # get the property
285         prop = self._props[item]
287         # look up the correct HTMLProperty class
288         form = self._client.form
289         for klass, htmlklass in propclasses:
290             if not isinstance(prop, klass):
291                 continue
292             if form.has_key(item):
293                 if isinstance(prop, hyperdb.Multilink):
294                     value = lookupIds(self._db, prop,
295                         handleListCGIValue(form[item]))
296                 elif isinstance(prop, hyperdb.Link):
297                     value = form[item].value.strip()
298                     if value:
299                         value = lookupIds(self._db, prop, [value])[0]
300                     else:
301                         value = None
302                 else:
303                     value = form[item].value.strip() or None
304             else:
305                 if isinstance(prop, hyperdb.Multilink):
306                     value = []
307                 else:
308                     value = None
309             return htmlklass(self._client, self._classname, '', prop, item,
310                 value, self._anonymous)
312         # no good
313         raise KeyError, item
315     def __getattr__(self, attr):
316         ''' convenience access '''
317         try:
318             return self[attr]
319         except KeyError:
320             raise AttributeError, attr
322     def getItem(self, itemid, num_re=re.compile('\d+')):
323         ''' Get an item of this class by its item id.
324         '''
325         # make sure we're looking at an itemid
326         if not num_re.match(itemid):
327             itemid = self._klass.lookup(itemid)
329         if self.classname == 'user':
330             klass = HTMLUser
331         else:
332             klass = HTMLItem
334         return klass(self._client, self.classname, itemid)
336     def properties(self):
337         ''' Return HTMLProperty for all of this class' properties.
338         '''
339         l = []
340         for name, prop in self._props.items():
341             for klass, htmlklass in propclasses:
342                 if isinstance(prop, hyperdb.Multilink):
343                     value = []
344                 else:
345                     value = None
346                 if isinstance(prop, klass):
347                     l.append(htmlklass(self._client, self._classname, '',
348                         prop, name, value, self._anonymous))
349         return l
351     def list(self):
352         ''' List all items in this class.
353         '''
354         if self.classname == 'user':
355             klass = HTMLUser
356         else:
357             klass = HTMLItem
359         # get the list and sort it nicely
360         l = self._klass.list()
361         sortfunc = make_sort_function(self._db, self.classname)
362         l.sort(sortfunc)
364         l = [klass(self._client, self.classname, x) for x in l]
365         return l
367     def csv(self):
368         ''' Return the items of this class as a chunk of CSV text.
369         '''
370         # get the CSV module
371         try:
372             import csv
373         except ImportError:
374             return 'Sorry, you need the csv module to use this function.\n'\
375                 'Get it from: http://www.object-craft.com.au/projects/csv/'
377         props = self.propnames()
378         p = csv.parser()
379         s = StringIO.StringIO()
380         s.write(p.join(props) + '\n')
381         for nodeid in self._klass.list():
382             l = []
383             for name in props:
384                 value = self._klass.get(nodeid, name)
385                 if value is None:
386                     l.append('')
387                 elif isinstance(value, type([])):
388                     l.append(':'.join(map(str, value)))
389                 else:
390                     l.append(str(self._klass.get(nodeid, name)))
391             s.write(p.join(l) + '\n')
392         return s.getvalue()
394     def propnames(self):
395         ''' Return the list of the names of the properties of this class.
396         '''
397         idlessprops = self._klass.getprops(protected=0).keys()
398         idlessprops.sort()
399         return ['id'] + idlessprops
401     def filter(self, request=None):
402         ''' Return a list of items from this class, filtered and sorted
403             by the current requested filterspec/filter/sort/group args
404         '''
405         if request is not None:
406             filterspec = request.filterspec
407             sort = request.sort
408             group = request.group
409         else:
410             filterspec = {}
411             sort = (None,None)
412             group = (None,None)
413         if self.classname == 'user':
414             klass = HTMLUser
415         else:
416             klass = HTMLItem
417         l = [klass(self._client, self.classname, x)
418              for x in self._klass.filter(None, filterspec, sort, group)]
419         return l
421     def classhelp(self, properties=None, label='list', width='500',
422             height='400'):
423         ''' Pop up a javascript window with class help
425             This generates a link to a popup window which displays the 
426             properties indicated by "properties" of the class named by
427             "classname". The "properties" should be a comma-separated list
428             (eg. 'id,name,description'). Properties defaults to all the
429             properties of a class (excluding id, creator, created and
430             activity).
432             You may optionally override the label displayed, the width and
433             height. The popup window will be resizable and scrollable.
434         '''
435         if properties is None:
436             properties = self._klass.getprops(protected=0).keys()
437             properties.sort()
438             properties = ','.join(properties)
439         return '<a href="javascript:help_window(\'%s?:template=help&' \
440             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
441             self.classname, properties, width, height, label)
443     def submit(self, label="Submit New Entry"):
444         ''' Generate a submit button (and action hidden element)
445         '''
446         return '  <input type="hidden" name=":action" value="new">\n'\
447         '  <input type="submit" name="submit" value="%s">'%label
449     def history(self):
450         return 'New node - no history'
452     def renderWith(self, name, **kwargs):
453         ''' Render this class with the given template.
454         '''
455         # create a new request and override the specified args
456         req = HTMLRequest(self._client)
457         req.classname = self.classname
458         req.update(kwargs)
460         # new template, using the specified classname and request
461         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
463         # use our fabricated request
464         return pt.render(self._client, self.classname, req)
466 class HTMLItem(HTMLPermissions):
467     ''' Accesses through an *item*
468     '''
469     def __init__(self, client, classname, nodeid, anonymous=0):
470         self._client = client
471         self._db = client.db
472         self._classname = classname
473         self._nodeid = nodeid
474         self._klass = self._db.getclass(classname)
475         self._props = self._klass.getprops()
477         # do we prefix the form items with the item's identification?
478         self._anonymous = anonymous
480     def __repr__(self):
481         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
482             self._nodeid)
484     def __getitem__(self, item):
485         ''' return an HTMLProperty instance
486         '''
487         #print 'HTMLItem.getitem', (self, item)
488         if item == 'id':
489             return self._nodeid
491         # get the property
492         prop = self._props[item]
494         # get the value, handling missing values
495         value = None
496         if int(self._nodeid) > 0:
497             value = self._klass.get(self._nodeid, item, None)
498         if value is None:
499             if isinstance(self._props[item], hyperdb.Multilink):
500                 value = []
502         # look up the correct HTMLProperty class
503         for klass, htmlklass in propclasses:
504             if isinstance(prop, klass):
505                 return htmlklass(self._client, self._classname,
506                     self._nodeid, prop, item, value, self._anonymous)
508         raise KeyError, item
510     def __getattr__(self, attr):
511         ''' convenience access to properties '''
512         try:
513             return self[attr]
514         except KeyError:
515             raise AttributeError, attr
516     
517     def submit(self, label="Submit Changes"):
518         ''' Generate a submit button (and action hidden element)
519         '''
520         return '  <input type="hidden" name=":action" value="edit">\n'\
521         '  <input type="submit" name="submit" value="%s">'%label
523     def journal(self, direction='descending'):
524         ''' Return a list of HTMLJournalEntry instances.
525         '''
526         # XXX do this
527         return []
529     def history(self, direction='descending', dre=re.compile('\d+')):
530         l = ['<table class="history">'
531              '<tr><th colspan="4" class="header">',
532              _('History'),
533              '</th></tr><tr>',
534              _('<th>Date</th>'),
535              _('<th>User</th>'),
536              _('<th>Action</th>'),
537              _('<th>Args</th>'),
538             '</tr>']
539         current = {}
540         comments = {}
541         history = self._klass.history(self._nodeid)
542         history.sort()
543         timezone = self._db.getUserTimezone()
544         if direction == 'descending':
545             history.reverse()
546             for prop_n in self._props.keys():
547                 prop = self[prop_n]
548                 if isinstance(prop, HTMLProperty):
549                     current[prop_n] = prop.plain()
550                     # make link if hrefable
551                     if (self._props.has_key(prop_n) and
552                             isinstance(self._props[prop_n], hyperdb.Link)):
553                         classname = self._props[prop_n].classname
554                         if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
555                             current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
556                                 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
557  
558         for id, evt_date, user, action, args in history:
559             date_s = str(evt_date.local(timezone)).replace("."," ")
560             arg_s = ''
561             if action == 'link' and type(args) == type(()):
562                 if len(args) == 3:
563                     linkcl, linkid, key = args
564                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
565                         linkcl, linkid, key)
566                 else:
567                     arg_s = str(args)
569             elif action == 'unlink' and type(args) == type(()):
570                 if len(args) == 3:
571                     linkcl, linkid, key = args
572                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
573                         linkcl, linkid, key)
574                 else:
575                     arg_s = str(args)
577             elif type(args) == type({}):
578                 cell = []
579                 for k in args.keys():
580                     # try to get the relevant property and treat it
581                     # specially
582                     try:
583                         prop = self._props[k]
584                     except KeyError:
585                         prop = None
586                     if prop is not None:
587                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
588                                 isinstance(prop, hyperdb.Link)):
589                             # figure what the link class is
590                             classname = prop.classname
591                             try:
592                                 linkcl = self._db.getclass(classname)
593                             except KeyError:
594                                 labelprop = None
595                                 comments[classname] = _('''The linked class
596                                     %(classname)s no longer exists''')%locals()
597                             labelprop = linkcl.labelprop(1)
598                             hrefable = os.path.exists(
599                                 os.path.join(self._db.config.TEMPLATES,
600                                 classname+'.item'))
602                         if isinstance(prop, hyperdb.Multilink) and \
603                                 len(args[k]) > 0:
604                             ml = []
605                             for linkid in args[k]:
606                                 if isinstance(linkid, type(())):
607                                     sublabel = linkid[0] + ' '
608                                     linkids = linkid[1]
609                                 else:
610                                     sublabel = ''
611                                     linkids = [linkid]
612                                 subml = []
613                                 for linkid in linkids:
614                                     label = classname + linkid
615                                     # if we have a label property, try to use it
616                                     # TODO: test for node existence even when
617                                     # there's no labelprop!
618                                     try:
619                                         if labelprop is not None and \
620                                                 labelprop != 'id':
621                                             label = linkcl.get(linkid, labelprop)
622                                     except IndexError:
623                                         comments['no_link'] = _('''<strike>The
624                                             linked node no longer
625                                             exists</strike>''')
626                                         subml.append('<strike>%s</strike>'%label)
627                                     else:
628                                         if hrefable:
629                                             subml.append('<a href="%s%s">%s</a>'%(
630                                                 classname, linkid, label))
631                                         else:
632                                             subml.append(label)
633                                 ml.append(sublabel + ', '.join(subml))
634                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
635                         elif isinstance(prop, hyperdb.Link) and args[k]:
636                             label = classname + args[k]
637                             # if we have a label property, try to use it
638                             # TODO: test for node existence even when
639                             # there's no labelprop!
640                             if labelprop is not None and labelprop != 'id':
641                                 try:
642                                     label = linkcl.get(args[k], labelprop)
643                                 except IndexError:
644                                     comments['no_link'] = _('''<strike>The
645                                         linked node no longer
646                                         exists</strike>''')
647                                     cell.append(' <strike>%s</strike>,\n'%label)
648                                     # "flag" this is done .... euwww
649                                     label = None
650                             if label is not None:
651                                 if hrefable:
652                                     old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
653                                 else:
654                                     old = label;
655                                 cell.append('%s: %s' % (k,old))
656                                 if current.has_key(k):
657                                     cell[-1] += ' -> %s'%current[k]
658                                     current[k] = old
660                         elif isinstance(prop, hyperdb.Date) and args[k]:
661                             d = date.Date(args[k]).local(timezone)
662                             cell.append('%s: %s'%(k, str(d)))
663                             if current.has_key(k):
664                                 cell[-1] += ' -> %s' % current[k]
665                                 current[k] = str(d)
667                         elif isinstance(prop, hyperdb.Interval) and args[k]:
668                             d = date.Interval(args[k])
669                             cell.append('%s: %s'%(k, str(d)))
670                             if current.has_key(k):
671                                 cell[-1] += ' -> %s'%current[k]
672                                 current[k] = str(d)
674                         elif isinstance(prop, hyperdb.String) and args[k]:
675                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
676                             if current.has_key(k):
677                                 cell[-1] += ' -> %s'%current[k]
678                                 current[k] = cgi.escape(args[k])
680                         elif not args[k]:
681                             if current.has_key(k):
682                                 cell.append('%s: %s'%(k, current[k]))
683                                 current[k] = '(no value)'
684                             else:
685                                 cell.append('%s: (no value)'%k)
687                         else:
688                             cell.append('%s: %s'%(k, str(args[k])))
689                             if current.has_key(k):
690                                 cell[-1] += ' -> %s'%current[k]
691                                 current[k] = str(args[k])
692                     else:
693                         # property no longer exists
694                         comments['no_exist'] = _('''<em>The indicated property
695                             no longer exists</em>''')
696                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
697                 arg_s = '<br />'.join(cell)
698             else:
699                 # unkown event!!
700                 comments['unknown'] = _('''<strong><em>This event is not
701                     handled by the history display!</em></strong>''')
702                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
703             date_s = date_s.replace(' ', '&nbsp;')
704             # if the user's an itemid, figure the username (older journals
705             # have the username)
706             if dre.match(user):
707                 user = self._db.user.get(user, 'username')
708             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
709                 date_s, user, action, arg_s))
710         if comments:
711             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
712         for entry in comments.values():
713             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
714         l.append('</table>')
715         return '\n'.join(l)
717     def renderQueryForm(self):
718         ''' Render this item, which is a query, as a search form.
719         '''
720         # create a new request and override the specified args
721         req = HTMLRequest(self._client)
722         req.classname = self._klass.get(self._nodeid, 'klass')
723         name = self._klass.get(self._nodeid, 'name')
724         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
725             '&:queryname=%s'%urllib.quote(name))
727         # new template, using the specified classname and request
728         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
730         # use our fabricated request
731         return pt.render(self._client, req.classname, req)
733 class HTMLUser(HTMLItem):
734     ''' Accesses through the *user* (a special case of item)
735     '''
736     def __init__(self, client, classname, nodeid, anonymous=0):
737         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
738         self._default_classname = client.classname
740         # used for security checks
741         self._security = client.db.security
743     _marker = []
744     def hasPermission(self, permission, classname=_marker):
745         ''' Determine if the user has the Permission.
747             The class being tested defaults to the template's class, but may
748             be overidden for this test by suppling an alternate classname.
749         '''
750         if classname is self._marker:
751             classname = self._default_classname
752         return self._security.hasPermission(permission, self._nodeid, classname)
754     def is_edit_ok(self):
755         ''' Is the user allowed to Edit the current class?
756             Also check whether this is the current user's info.
757         '''
758         return self._db.security.hasPermission('Edit', self._client.userid,
759             self._classname) or self._nodeid == self._client.userid
761     def is_view_ok(self):
762         ''' Is the user allowed to View the current class?
763             Also check whether this is the current user's info.
764         '''
765         return self._db.security.hasPermission('Edit', self._client.userid,
766             self._classname) or self._nodeid == self._client.userid
768 class HTMLProperty:
769     ''' String, Number, Date, Interval HTMLProperty
771         Has useful attributes:
773          _name  the name of the property
774          _value the value of the property if any
776         A wrapper object which may be stringified for the plain() behaviour.
777     '''
778     def __init__(self, client, classname, nodeid, prop, name, value,
779             anonymous=0):
780         self._client = client
781         self._db = client.db
782         self._classname = classname
783         self._nodeid = nodeid
784         self._prop = prop
785         self._value = value
786         self._anonymous = anonymous
787         if not anonymous:
788             self._name = '%s%s@%s'%(classname, nodeid, name)
789         else:
790             self._name = name
791     def __repr__(self):
792         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name,
793             self._prop, self._value)
794     def __str__(self):
795         return self.plain()
796     def __cmp__(self, other):
797         if isinstance(other, HTMLProperty):
798             return cmp(self._value, other._value)
799         return cmp(self._value, other)
801 class StringHTMLProperty(HTMLProperty):
802     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
803                           r'(?P<email>[\w\.]+@[\w\.\-]+)|'
804                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
805     def _hyper_repl(self, match):
806         if match.group('url'):
807             s = match.group('url')
808             return '<a href="%s">%s</a>'%(s, s)
809         elif match.group('email'):
810             s = match.group('email')
811             return '<a href="mailto:%s">%s</a>'%(s, s)
812         else:
813             s = match.group('item')
814             s1 = match.group('class')
815             s2 = match.group('id')
816             try:
817                 # make sure s1 is a valid tracker classname
818                 self._db.getclass(s1)
819                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
820             except KeyError:
821                 return '%s%s'%(s1, s2)
823     def plain(self, escape=0, hyperlink=0):
824         ''' Render a "plain" representation of the property
825             
826             "escape" turns on/off HTML quoting
827             "hyperlink" turns on/off in-text hyperlinking of URLs, email
828                 addresses and designators
829         '''
830         if self._value is None:
831             return ''
832         if escape:
833             s = cgi.escape(str(self._value))
834         else:
835             s = str(self._value)
836         if hyperlink:
837             if not escape:
838                 s = cgi.escape(s)
839             s = self.hyper_re.sub(self._hyper_repl, s)
840         return s
842     def stext(self, escape=0):
843         ''' Render the value of the property as StructuredText.
845             This requires the StructureText module to be installed separately.
846         '''
847         s = self.plain(escape=escape)
848         if not StructuredText:
849             return s
850         return StructuredText(s,level=1,header=0)
852     def field(self, size = 30):
853         ''' Render a form edit field for the property
854         '''
855         if self._value is None:
856             value = ''
857         else:
858             value = cgi.escape(str(self._value))
859             value = '&quot;'.join(value.split('"'))
860         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
862     def multiline(self, escape=0, rows=5, cols=40):
863         ''' Render a multiline form edit field for the property
864         '''
865         if self._value is None:
866             value = ''
867         else:
868             value = cgi.escape(str(self._value))
869             value = '&quot;'.join(value.split('"'))
870         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
871             self._name, rows, cols, value)
873     def email(self, escape=1):
874         ''' Render the value of the property as an obscured email address
875         '''
876         if self._value is None: value = ''
877         else: value = str(self._value)
878         if value.find('@') != -1:
879             name, domain = value.split('@')
880             domain = ' '.join(domain.split('.')[:-1])
881             name = name.replace('.', ' ')
882             value = '%s at %s ...'%(name, domain)
883         else:
884             value = value.replace('.', ' ')
885         if escape:
886             value = cgi.escape(value)
887         return value
889 class PasswordHTMLProperty(HTMLProperty):
890     def plain(self):
891         ''' Render a "plain" representation of the property
892         '''
893         if self._value is None:
894             return ''
895         return _('*encrypted*')
897     def field(self, size = 30):
898         ''' Render a form edit field for the property.
899         '''
900         return '<input type="password" name="%s" size="%s">'%(self._name, size)
902     def confirm(self, size = 30):
903         ''' Render a second form edit field for the property, used for 
904             confirmation that the user typed the password correctly. Generates
905             a field with name ":confirm:name".
906         '''
907         return '<input type="password" name=":confirm:%s" size="%s">'%(
908             self._name, size)
910 class NumberHTMLProperty(HTMLProperty):
911     def plain(self):
912         ''' Render a "plain" representation of the property
913         '''
914         return str(self._value)
916     def field(self, size = 30):
917         ''' Render a form edit field for the property
918         '''
919         if self._value is None:
920             value = ''
921         else:
922             value = cgi.escape(str(self._value))
923             value = '&quot;'.join(value.split('"'))
924         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
926 class BooleanHTMLProperty(HTMLProperty):
927     def plain(self):
928         ''' Render a "plain" representation of the property
929         '''
930         if self._value is None:
931             return ''
932         return self._value and "Yes" or "No"
934     def field(self):
935         ''' Render a form edit field for the property
936         '''
937         checked = self._value and "checked" or ""
938         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
939             checked)
940         if checked:
941             checked = ""
942         else:
943             checked = "checked"
944         s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
945             checked)
946         return s
948 class DateHTMLProperty(HTMLProperty):
949     def plain(self):
950         ''' Render a "plain" representation of the property
951         '''
952         if self._value is None:
953             return ''
954         return str(self._value.local(self._db.getUserTimezone()))
956     def now(self):
957         ''' Return the current time.
959             This is useful for defaulting a new value. Returns a
960             DateHTMLProperty.
961         '''
962         return DateHTMLProperty(self._client, self._nodeid, self._prop,
963             self._name, date.Date('.'))
965     def field(self, size = 30):
966         ''' Render a form edit field for the property
967         '''
968         if self._value is None:
969             value = ''
970         else:
971             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
972             value = '&quot;'.join(value.split('"'))
973         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
975     def reldate(self, pretty=1):
976         ''' Render the interval between the date and now.
978             If the "pretty" flag is true, then make the display pretty.
979         '''
980         if not self._value:
981             return ''
983         # figure the interval
984         interval = date.Date('.') - self._value
985         if pretty:
986             return interval.pretty()
987         return str(interval)
989     _marker = []
990     def pretty(self, format=_marker):
991         ''' Render the date in a pretty format (eg. month names, spaces).
993             The format string is a standard python strftime format string.
994             Note that if the day is zero, and appears at the start of the
995             string, then it'll be stripped from the output. This is handy
996             for the situatin when a date only specifies a month and a year.
997         '''
998         if format is not self._marker:
999             return self._value.pretty(format)
1000         else:
1001             return self._value.pretty()
1003     def local(self, offset):
1004         ''' Return the date/time as a local (timezone offset) date/time.
1005         '''
1006         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1007             self._name, self._value.local(offset))
1009 class IntervalHTMLProperty(HTMLProperty):
1010     def plain(self):
1011         ''' Render a "plain" representation of the property
1012         '''
1013         if self._value is None:
1014             return ''
1015         return str(self._value)
1017     def pretty(self):
1018         ''' Render the interval in a pretty format (eg. "yesterday")
1019         '''
1020         return self._value.pretty()
1022     def field(self, size = 30):
1023         ''' Render a form edit field for the property
1024         '''
1025         if self._value is None:
1026             value = ''
1027         else:
1028             value = cgi.escape(str(self._value))
1029             value = '&quot;'.join(value.split('"'))
1030         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
1032 class LinkHTMLProperty(HTMLProperty):
1033     ''' Link HTMLProperty
1034         Include the above as well as being able to access the class
1035         information. Stringifying the object itself results in the value
1036         from the item being displayed. Accessing attributes of this object
1037         result in the appropriate entry from the class being queried for the
1038         property accessed (so item/assignedto/name would look up the user
1039         entry identified by the assignedto property on item, and then the
1040         name property of that user)
1041     '''
1042     def __init__(self, *args, **kw):
1043         HTMLProperty.__init__(self, *args, **kw)
1044         # if we're representing a form value, then the -1 from the form really
1045         # should be a None
1046         if str(self._value) == '-1':
1047             self._value = None
1049     def __getattr__(self, attr):
1050         ''' return a new HTMLItem '''
1051        #print 'Link.getattr', (self, attr, self._value)
1052         if not self._value:
1053             raise AttributeError, "Can't access missing value"
1054         if self._prop.classname == 'user':
1055             klass = HTMLUser
1056         else:
1057             klass = HTMLItem
1058         i = klass(self._client, self._prop.classname, self._value)
1059         return getattr(i, attr)
1061     def plain(self, escape=0):
1062         ''' Render a "plain" representation of the property
1063         '''
1064         if self._value is None:
1065             return ''
1066         linkcl = self._db.classes[self._prop.classname]
1067         k = linkcl.labelprop(1)
1068         value = str(linkcl.get(self._value, k))
1069         if escape:
1070             value = cgi.escape(value)
1071         return value
1073     def field(self, showid=0, size=None):
1074         ''' Render a form edit field for the property
1075         '''
1076         linkcl = self._db.getclass(self._prop.classname)
1077         if linkcl.getprops().has_key('order'):  
1078             sort_on = 'order'  
1079         else:  
1080             sort_on = linkcl.labelprop()  
1081         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1082         # TODO: make this a field display, not a menu one!
1083         l = ['<select name="%s">'%self._name]
1084         k = linkcl.labelprop(1)
1085         if self._value is None:
1086             s = 'selected '
1087         else:
1088             s = ''
1089         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1091         # make sure we list the current value if it's retired
1092         if self._value and self._value not in options:
1093             options.insert(0, self._value)
1095         for optionid in options:
1096             # get the option value, and if it's None use an empty string
1097             option = linkcl.get(optionid, k) or ''
1099             # figure if this option is selected
1100             s = ''
1101             if optionid == self._value:
1102                 s = 'selected '
1104             # figure the label
1105             if showid:
1106                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1107             else:
1108                 lab = option
1110             # truncate if it's too long
1111             if size is not None and len(lab) > size:
1112                 lab = lab[:size-3] + '...'
1114             # and generate
1115             lab = cgi.escape(lab)
1116             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1117         l.append('</select>')
1118         return '\n'.join(l)
1120     def menu(self, size=None, height=None, showid=0, additional=[],
1121             **conditions):
1122         ''' Render a form select list for this property
1123         '''
1124         value = self._value
1126         # sort function
1127         sortfunc = make_sort_function(self._db, self._prop.classname)
1129         linkcl = self._db.getclass(self._prop.classname)
1130         l = ['<select name="%s">'%self._name]
1131         k = linkcl.labelprop(1)
1132         s = ''
1133         if value is None:
1134             s = 'selected '
1135         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1136         if linkcl.getprops().has_key('order'):  
1137             sort_on = ('+', 'order')
1138         else:  
1139             sort_on = ('+', linkcl.labelprop())
1140         options = linkcl.filter(None, conditions, sort_on, (None, None))
1142         # make sure we list the current value if it's retired
1143         if self._value and self._value not in options:
1144             options.insert(0, self._value)
1146         for optionid in options:
1147             # get the option value, and if it's None use an empty string
1148             option = linkcl.get(optionid, k) or ''
1150             # figure if this option is selected
1151             s = ''
1152             if value in [optionid, option]:
1153                 s = 'selected '
1155             # figure the label
1156             if showid:
1157                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1158             else:
1159                 lab = option
1161             # truncate if it's too long
1162             if size is not None and len(lab) > size:
1163                 lab = lab[:size-3] + '...'
1164             if additional:
1165                 m = []
1166                 for propname in additional:
1167                     m.append(linkcl.get(optionid, propname))
1168                 lab = lab + ' (%s)'%', '.join(map(str, m))
1170             # and generate
1171             lab = cgi.escape(lab)
1172             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1173         l.append('</select>')
1174         return '\n'.join(l)
1175 #    def checklist(self, ...)
1177 class MultilinkHTMLProperty(HTMLProperty):
1178     ''' Multilink HTMLProperty
1180         Also be iterable, returning a wrapper object like the Link case for
1181         each entry in the multilink.
1182     '''
1183     def __len__(self):
1184         ''' length of the multilink '''
1185         return len(self._value)
1187     def __getattr__(self, attr):
1188         ''' no extended attribute accesses make sense here '''
1189         raise AttributeError, attr
1191     def __getitem__(self, num):
1192         ''' iterate and return a new HTMLItem
1193         '''
1194        #print 'Multi.getitem', (self, num)
1195         value = self._value[num]
1196         if self._prop.classname == 'user':
1197             klass = HTMLUser
1198         else:
1199             klass = HTMLItem
1200         return klass(self._client, self._prop.classname, value)
1202     def __contains__(self, value):
1203         ''' Support the "in" operator. We have to make sure the passed-in
1204             value is a string first, not a *HTMLProperty.
1205         '''
1206         return str(value) in self._value
1208     def reverse(self):
1209         ''' return the list in reverse order
1210         '''
1211         l = self._value[:]
1212         l.reverse()
1213         if self._prop.classname == 'user':
1214             klass = HTMLUser
1215         else:
1216             klass = HTMLItem
1217         return [klass(self._client, self._prop.classname, value) for value in l]
1219     def plain(self, escape=0):
1220         ''' Render a "plain" representation of the property
1221         '''
1222         linkcl = self._db.classes[self._prop.classname]
1223         k = linkcl.labelprop(1)
1224         labels = []
1225         for v in self._value:
1226             labels.append(linkcl.get(v, k))
1227         value = ', '.join(labels)
1228         if escape:
1229             value = cgi.escape(value)
1230         return value
1232     def field(self, size=30, showid=0):
1233         ''' Render a form edit field for the property
1234         '''
1235         sortfunc = make_sort_function(self._db, self._prop.classname)
1236         linkcl = self._db.getclass(self._prop.classname)
1237         value = self._value[:]
1238         if value:
1239             value.sort(sortfunc)
1240         # map the id to the label property
1241         if not linkcl.getkey():
1242             showid=1
1243         if not showid:
1244             k = linkcl.labelprop(1)
1245             value = [linkcl.get(v, k) for v in value]
1246         value = cgi.escape(','.join(value))
1247         return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1249     def menu(self, size=None, height=None, showid=0, additional=[],
1250             **conditions):
1251         ''' Render a form select list for this property
1252         '''
1253         value = self._value
1255         # sort function
1256         sortfunc = make_sort_function(self._db, self._prop.classname)
1258         linkcl = self._db.getclass(self._prop.classname)
1259         if linkcl.getprops().has_key('order'):  
1260             sort_on = ('+', 'order')
1261         else:  
1262             sort_on = ('+', linkcl.labelprop())
1263         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1264         height = height or min(len(options), 7)
1265         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1266         k = linkcl.labelprop(1)
1268         # make sure we list the current values if they're retired
1269         for val in value:
1270             if val not in options:
1271                 options.insert(0, val)
1273         for optionid in options:
1274             # get the option value, and if it's None use an empty string
1275             option = linkcl.get(optionid, k) or ''
1277             # figure if this option is selected
1278             s = ''
1279             if optionid in value or option in value:
1280                 s = 'selected '
1282             # figure the label
1283             if showid:
1284                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1285             else:
1286                 lab = option
1287             # truncate if it's too long
1288             if size is not None and len(lab) > size:
1289                 lab = lab[:size-3] + '...'
1290             if additional:
1291                 m = []
1292                 for propname in additional:
1293                     m.append(linkcl.get(optionid, propname))
1294                 lab = lab + ' (%s)'%', '.join(m)
1296             # and generate
1297             lab = cgi.escape(lab)
1298             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1299                 lab))
1300         l.append('</select>')
1301         return '\n'.join(l)
1303 # set the propclasses for HTMLItem
1304 propclasses = (
1305     (hyperdb.String, StringHTMLProperty),
1306     (hyperdb.Number, NumberHTMLProperty),
1307     (hyperdb.Boolean, BooleanHTMLProperty),
1308     (hyperdb.Date, DateHTMLProperty),
1309     (hyperdb.Interval, IntervalHTMLProperty),
1310     (hyperdb.Password, PasswordHTMLProperty),
1311     (hyperdb.Link, LinkHTMLProperty),
1312     (hyperdb.Multilink, MultilinkHTMLProperty),
1315 def make_sort_function(db, classname):
1316     '''Make a sort function for a given class
1317     '''
1318     linkcl = db.getclass(classname)
1319     if linkcl.getprops().has_key('order'):
1320         sort_on = 'order'
1321     else:
1322         sort_on = linkcl.labelprop()
1323     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1324         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1325     return sortfunc
1327 def handleListCGIValue(value):
1328     ''' Value is either a single item or a list of items. Each item has a
1329         .value that we're actually interested in.
1330     '''
1331     if isinstance(value, type([])):
1332         return [value.value for value in value]
1333     else:
1334         value = value.value.strip()
1335         if not value:
1336             return []
1337         return value.split(',')
1339 class ShowDict:
1340     ''' A convenience access to the :columns index parameters
1341     '''
1342     def __init__(self, columns):
1343         self.columns = {}
1344         for col in columns:
1345             self.columns[col] = 1
1346     def __getitem__(self, name):
1347         return self.columns.has_key(name)
1349 class HTMLRequest:
1350     ''' The *request*, holding the CGI form and environment.
1352         "form" the CGI form as a cgi.FieldStorage
1353         "env" the CGI environment variables
1354         "base" the base URL for this instance
1355         "user" a HTMLUser instance for this user
1356         "classname" the current classname (possibly None)
1357         "template" the current template (suffix, also possibly None)
1359         Index args:
1360         "columns" dictionary of the columns to display in an index page
1361         "show" a convenience access to columns - request/show/colname will
1362                be true if the columns should be displayed, false otherwise
1363         "sort" index sort column (direction, column name)
1364         "group" index grouping property (direction, column name)
1365         "filter" properties to filter the index on
1366         "filterspec" values to filter the index on
1367         "search_text" text to perform a full-text search on for an index
1369     '''
1370     def __init__(self, client):
1371         self.client = client
1373         # easier access vars
1374         self.form = client.form
1375         self.env = client.env
1376         self.base = client.base
1377         self.user = HTMLUser(client, 'user', client.userid)
1379         # store the current class name and action
1380         self.classname = client.classname
1381         self.template = client.template
1383         # the special char to use for special vars
1384         self.special_char = '@'
1386         self._post_init()
1388     def _post_init(self):
1389         ''' Set attributes based on self.form
1390         '''
1391         # extract the index display information from the form
1392         self.columns = []
1393         for name in ':columns @columns'.split():
1394             if self.form.has_key(name):
1395                 self.special_char = name[0]
1396                 self.columns = handleListCGIValue(self.form[name])
1397                 break
1398         self.show = ShowDict(self.columns)
1400         # sorting
1401         self.sort = (None, None)
1402         for name in ':sort @sort'.split():
1403             if self.form.has_key(name):
1404                 self.special_char = name[0]
1405                 sort = self.form[name].value
1406                 if sort.startswith('-'):
1407                     self.sort = ('-', sort[1:])
1408                 else:
1409                     self.sort = ('+', sort)
1410                 if self.form.has_key(self.special_char+'sortdir'):
1411                     self.sort = ('-', self.sort[1])
1413         # grouping
1414         self.group = (None, None)
1415         for name in ':group @group'.split():
1416             if self.form.has_key(name):
1417                 self.special_char = name[0]
1418                 group = self.form[name].value
1419                 if group.startswith('-'):
1420                     self.group = ('-', group[1:])
1421                 else:
1422                     self.group = ('+', group)
1423                 if self.form.has_key(self.special_char+'groupdir'):
1424                     self.group = ('-', self.group[1])
1426         # filtering
1427         self.filter = []
1428         for name in ':filter @filter'.split():
1429             if self.form.has_key(name):
1430                 self.special_char = name[0]
1431                 self.filter = handleListCGIValue(self.form[name])
1433         self.filterspec = {}
1434         db = self.client.db
1435         if self.classname is not None:
1436             props = db.getclass(self.classname).getprops()
1437             for name in self.filter:
1438                 if self.form.has_key(name):
1439                     prop = props[name]
1440                     fv = self.form[name]
1441                     if (isinstance(prop, hyperdb.Link) or
1442                             isinstance(prop, hyperdb.Multilink)):
1443                         self.filterspec[name] = lookupIds(db, prop,
1444                             handleListCGIValue(fv))
1445                     else:
1446                         self.filterspec[name] = fv.value
1448         # full-text search argument
1449         self.search_text = None
1450         for name in ':search_text @search_text'.split():
1451             if self.form.has_key(name):
1452                 self.special_char = name[0]
1453                 self.search_text = self.form[name].value
1455         # pagination - size and start index
1456         # figure batch args
1457         self.pagesize = 50
1458         for name in ':pagesize @pagesize'.split():
1459             if self.form.has_key(name):
1460                 self.special_char = name[0]
1461                 self.pagesize = int(self.form[name].value)
1463         self.startwith = 0
1464         for name in ':startwith @startwith'.split():
1465             if self.form.has_key(name):
1466                 self.special_char = name[0]
1467                 self.startwith = int(self.form[name].value)
1469     def updateFromURL(self, url):
1470         ''' Parse the URL for query args, and update my attributes using the
1471             values.
1472         ''' 
1473         self.form = {}
1474         for name, value in cgi.parse_qsl(url):
1475             if self.form.has_key(name):
1476                 if isinstance(self.form[name], type([])):
1477                     self.form[name].append(cgi.MiniFieldStorage(name, value))
1478                 else:
1479                     self.form[name] = [self.form[name],
1480                         cgi.MiniFieldStorage(name, value)]
1481             else:
1482                 self.form[name] = cgi.MiniFieldStorage(name, value)
1483         self._post_init()
1485     def update(self, kwargs):
1486         ''' Update my attributes using the keyword args
1487         '''
1488         self.__dict__.update(kwargs)
1489         if kwargs.has_key('columns'):
1490             self.show = ShowDict(self.columns)
1492     def description(self):
1493         ''' Return a description of the request - handle for the page title.
1494         '''
1495         s = [self.client.db.config.TRACKER_NAME]
1496         if self.classname:
1497             if self.client.nodeid:
1498                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1499             else:
1500                 if self.template == 'item':
1501                     s.append('- new %s'%self.classname)
1502                 elif self.template == 'index':
1503                     s.append('- %s index'%self.classname)
1504                 else:
1505                     s.append('- %s %s'%(self.classname, self.template))
1506         else:
1507             s.append('- home')
1508         return ' '.join(s)
1510     def __str__(self):
1511         d = {}
1512         d.update(self.__dict__)
1513         f = ''
1514         for k in self.form.keys():
1515             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1516         d['form'] = f
1517         e = ''
1518         for k,v in self.env.items():
1519             e += '\n     %r=%r'%(k, v)
1520         d['env'] = e
1521         return '''
1522 form: %(form)s
1523 base: %(base)r
1524 classname: %(classname)r
1525 template: %(template)r
1526 columns: %(columns)r
1527 sort: %(sort)r
1528 group: %(group)r
1529 filter: %(filter)r
1530 search_text: %(search_text)r
1531 pagesize: %(pagesize)r
1532 startwith: %(startwith)r
1533 env: %(env)s
1534 '''%d
1536     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1537             filterspec=1):
1538         ''' return the current index args as form elements '''
1539         l = []
1540         sc = self.special_char
1541         s = '<input type="hidden" name="%s" value="%s">'
1542         if columns and self.columns:
1543             l.append(s%(sc+'columns', ','.join(self.columns)))
1544         if sort and self.sort[1] is not None:
1545             if self.sort[0] == '-':
1546                 val = '-'+self.sort[1]
1547             else:
1548                 val = self.sort[1]
1549             l.append(s%(sc+'sort', val))
1550         if group and self.group[1] is not None:
1551             if self.group[0] == '-':
1552                 val = '-'+self.group[1]
1553             else:
1554                 val = self.group[1]
1555             l.append(s%(sc+'group', val))
1556         if filter and self.filter:
1557             l.append(s%(sc+'filter', ','.join(self.filter)))
1558         if filterspec:
1559             for k,v in self.filterspec.items():
1560                 if type(v) == type([]):
1561                     l.append(s%(k, ','.join(v)))
1562                 else:
1563                     l.append(s%(k, v))
1564         if self.search_text:
1565             l.append(s%(sc+'search_text', self.search_text))
1566         l.append(s%(sc+'pagesize', self.pagesize))
1567         l.append(s%(sc+'startwith', self.startwith))
1568         return '\n'.join(l)
1570     def indexargs_url(self, url, args):
1571         ''' embed the current index args in a URL '''
1572         sc = self.special_char
1573         l = ['%s=%s'%(k,v) for k,v in args.items()]
1574         if self.columns and not args.has_key(':columns'):
1575             l.append(sc+'columns=%s'%(','.join(self.columns)))
1576         if self.sort[1] is not None and not args.has_key(':sort'):
1577             if self.sort[0] == '-':
1578                 val = '-'+self.sort[1]
1579             else:
1580                 val = self.sort[1]
1581             l.append(sc+'sort=%s'%val)
1582         if self.group[1] is not None and not args.has_key(':group'):
1583             if self.group[0] == '-':
1584                 val = '-'+self.group[1]
1585             else:
1586                 val = self.group[1]
1587             l.append(sc+'group=%s'%val)
1588         if self.filter and not args.has_key(':filter'):
1589             l.append(sc+'filter=%s'%(','.join(self.filter)))
1590         for k,v in self.filterspec.items():
1591             if not args.has_key(k):
1592                 if type(v) == type([]):
1593                     l.append('%s=%s'%(k, ','.join(v)))
1594                 else:
1595                     l.append('%s=%s'%(k, v))
1596         if self.search_text and not args.has_key(':search_text'):
1597             l.append(sc+'search_text=%s'%self.search_text)
1598         if not args.has_key(':pagesize'):
1599             l.append(sc+'pagesize=%s'%self.pagesize)
1600         if not args.has_key(':startwith'):
1601             l.append(sc+'startwith=%s'%self.startwith)
1602         return '%s?%s'%(url, '&'.join(l))
1603     indexargs_href = indexargs_url
1605     def base_javascript(self):
1606         return '''
1607 <script language="javascript">
1608 submitted = false;
1609 function submit_once() {
1610     if (submitted) {
1611         alert("Your request is being processed.\\nPlease be patient.");
1612         return 0;
1613     }
1614     submitted = true;
1615     return 1;
1618 function help_window(helpurl, width, height) {
1619     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1621 </script>
1622 '''%self.base
1624     def batch(self):
1625         ''' Return a batch object for results from the "current search"
1626         '''
1627         filterspec = self.filterspec
1628         sort = self.sort
1629         group = self.group
1631         # get the list of ids we're batching over
1632         klass = self.client.db.getclass(self.classname)
1633         if self.search_text:
1634             matches = self.client.db.indexer.search(
1635                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1636         else:
1637             matches = None
1638         l = klass.filter(matches, filterspec, sort, group)
1640         # return the batch object, using IDs only
1641         return Batch(self.client, l, self.pagesize, self.startwith,
1642             classname=self.classname)
1644 # extend the standard ZTUtils Batch object to remove dependency on
1645 # Acquisition and add a couple of useful methods
1646 class Batch(ZTUtils.Batch):
1647     ''' Use me to turn a list of items, or item ids of a given class, into a
1648         series of batches.
1650         ========= ========================================================
1651         Parameter  Usage
1652         ========= ========================================================
1653         sequence  a list of HTMLItems or item ids
1654         classname if sequence is a list of ids, this is the class of item
1655         size      how big to make the sequence.
1656         start     where to start (0-indexed) in the sequence.
1657         end       where to end (0-indexed) in the sequence.
1658         orphan    if the next batch would contain less items than this
1659                   value, then it is combined with this batch
1660         overlap   the number of items shared between adjacent batches
1661         ========= ========================================================
1663         Attributes: Note that the "start" attribute, unlike the
1664         argument, is a 1-based index (I know, lame).  "first" is the
1665         0-based index.  "length" is the actual number of elements in
1666         the batch.
1668         "sequence_length" is the length of the original, unbatched, sequence.
1669     '''
1670     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1671             overlap=0, classname=None):
1672         self.client = client
1673         self.last_index = self.last_item = None
1674         self.current_item = None
1675         self.classname = classname
1676         self.sequence_length = len(sequence)
1677         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1678             overlap)
1680     # overwrite so we can late-instantiate the HTMLItem instance
1681     def __getitem__(self, index):
1682         if index < 0:
1683             if index + self.end < self.first: raise IndexError, index
1684             return self._sequence[index + self.end]
1685         
1686         if index >= self.length:
1687             raise IndexError, index
1689         # move the last_item along - but only if the fetched index changes
1690         # (for some reason, index 0 is fetched twice)
1691         if index != self.last_index:
1692             self.last_item = self.current_item
1693             self.last_index = index
1695         item = self._sequence[index + self.first]
1696         if self.classname:
1697             # map the item ids to instances
1698             if self.classname == 'user':
1699                 item = HTMLUser(self.client, self.classname, item)
1700             else:
1701                 item = HTMLItem(self.client, self.classname, item)
1702         self.current_item = item
1703         return item
1705     def propchanged(self, property):
1706         ''' Detect if the property marked as being the group property
1707             changed in the last iteration fetch
1708         '''
1709         if (self.last_item is None or
1710                 self.last_item[property] != self.current_item[property]):
1711             return 1
1712         return 0
1714     # override these 'cos we don't have access to acquisition
1715     def previous(self):
1716         if self.start == 1:
1717             return None
1718         return Batch(self.client, self._sequence, self._size,
1719             self.first - self._size + self.overlap, 0, self.orphan,
1720             self.overlap)
1722     def next(self):
1723         try:
1724             self._sequence[self.end]
1725         except IndexError:
1726             return None
1727         return Batch(self.client, self._sequence, self._size,
1728             self.end - self.overlap, 0, self.orphan, self.overlap)
1730 class TemplatingUtils:
1731     ''' Utilities for templating
1732     '''
1733     def __init__(self, client):
1734         self.client = client
1735     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1736         return Batch(self.client, sequence, size, start, end, orphan,
1737             overlap)