Code

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