Code

- Always sort MultilinkHTMLProperty in the correct order, usually
[roundup.git] / roundup / cgi / templating.py
1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9     import cPickle as pickle
10 except ImportError:
11     import pickle
12 try:
13     import cStringIO as StringIO
14 except ImportError:
15     import StringIO
16 try:
17     import StructuredText
18 except ImportError:
19     StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 class NoTemplate(Exception):
28     pass
30 def find_template(dir, name, extension):
31     ''' Find a template in the nominated dir
32     '''
33     # find the source
34     if extension:
35         filename = '%s.%s'%(name, extension)
36     else:
37         filename = name
39     # try old-style
40     src = os.path.join(dir, filename)
41     if os.path.exists(src):
42         return (src, filename)
44     # try with a .html extension (new-style)
45     filename = filename + '.html'
46     src = os.path.join(dir, filename)
47     if os.path.exists(src):
48         return (src, filename)
50     # no extension == no generic template is possible
51     if not extension:
52         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
54     # try for a _generic template
55     generic = '_generic.%s'%extension
56     src = os.path.join(dir, generic)
57     if os.path.exists(src):
58         return (src, generic)
60     # finally, try _generic.html
61     generic = generic + '.html'
62     src = os.path.join(dir, generic)
63     if os.path.exists(src):
64         return (src, generic)
66     raise NoTemplate, 'No template file exists for templating "%s" '\
67         'with template "%s" (neither "%s" nor "%s")'%(name, extension,
68         filename, generic)
70 class Templates:
71     templates = {}
73     def __init__(self, dir):
74         self.dir = dir
76     def precompileTemplates(self):
77         ''' Go through a directory and precompile all the templates therein
78         '''
79         for filename in os.listdir(self.dir):
80             if os.path.isdir(filename): continue
81             if '.' in filename:
82                 name, extension = filename.split('.')
83                 self.get(name, extension)
84             else:
85                 self.get(filename, None)
87     def get(self, name, extension=None):
88         ''' Interface to get a template, possibly loading a compiled template.
90             "name" and "extension" indicate the template we're after, which in
91             most cases will be "name.extension". If "extension" is None, then
92             we look for a template just called "name" with no extension.
94             If the file "name.extension" doesn't exist, we look for
95             "_generic.extension" as a fallback.
96         '''
97         # default the name to "home"
98         if name is None:
99             name = 'home'
100         elif extension is None and '.' in name:
101             # split name
102             name, extension = name.split('.')
104         # find the source
105         src, filename = find_template(self.dir, name, extension)
107         # has it changed?
108         try:
109             stime = os.stat(src)[os.path.stat.ST_MTIME]
110         except os.error, error:
111             if error.errno != errno.ENOENT:
112                 raise
114         if self.templates.has_key(src) and \
115                 stime < self.templates[src].mtime:
116             # compiled template is up to date
117             return self.templates[src]
119         # compile the template
120         self.templates[src] = pt = RoundupPageTemplate()
121         # use pt_edit so we can pass the content_type guess too
122         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
123         pt.pt_edit(open(src).read(), content_type)
124         pt.id = filename
125         pt.mtime = time.time()
126         return pt
128     def __getitem__(self, name):
129         name, extension = os.path.splitext(name)
130         if extension:
131             extension = extension[1:]
132         try:
133             return self.get(name, extension)
134         except NoTemplate, message:
135             raise KeyError, message
137 class RoundupPageTemplate(PageTemplate.PageTemplate):
138     ''' A Roundup-specific PageTemplate.
140         Interrogate the client to set up the various template variables to
141         be available:
143         *context*
144          this is one of three things:
145          1. None - we're viewing a "home" page
146          2. The current class of item being displayed. This is an HTMLClass
147             instance.
148          3. The current item from the database, if we're viewing a specific
149             item, as an HTMLItem instance.
150         *request*
151           Includes information about the current request, including:
152            - the url
153            - the current index information (``filterspec``, ``filter`` args,
154              ``properties``, etc) parsed out of the form. 
155            - methods for easy filterspec link generation
156            - *user*, the current user node as an HTMLItem instance
157            - *form*, the current CGI form information as a FieldStorage
158         *config*
159           The current tracker config.
160         *db*
161           The current database, used to access arbitrary database items.
162         *utils*
163           This is a special class that has its base in the TemplatingUtils
164           class in this file. If the tracker interfaces module defines a
165           TemplatingUtils class then it is mixed in, overriding the methods
166           in the base class.
167     '''
168     def getContext(self, client, classname, request):
169         # construct the TemplatingUtils class
170         utils = TemplatingUtils
171         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
172             class utils(client.instance.interfaces.TemplatingUtils, utils):
173                 pass
175         c = {
176              'options': {},
177              'nothing': None,
178              'request': request,
179              'db': HTMLDatabase(client),
180              'config': client.instance.config,
181              'tracker': client.instance,
182              'utils': utils(client),
183              'templates': Templates(client.instance.config.TEMPLATES),
184         }
185         # add in the item if there is one
186         if client.nodeid:
187             if classname == 'user':
188                 c['context'] = HTMLUser(client, classname, client.nodeid,
189                     anonymous=1)
190             else:
191                 c['context'] = HTMLItem(client, classname, client.nodeid,
192                     anonymous=1)
193         elif client.db.classes.has_key(classname):
194             c['context'] = HTMLClass(client, classname, anonymous=1)
195         return c
197     def render(self, client, classname, request, **options):
198         """Render this Page Template"""
200         if not self._v_cooked:
201             self._cook()
203         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
205         if self._v_errors:
206             raise PageTemplate.PTRuntimeError, \
207                 'Page Template %s has errors.'%self.id
209         # figure the context
210         classname = classname or client.classname
211         request = request or HTMLRequest(client)
212         c = self.getContext(client, classname, request)
213         c.update({'options': options})
215         # and go
216         output = StringIO.StringIO()
217         TALInterpreter(self._v_program, self.macros,
218             getEngine().getContext(c), output, tal=1, strictinsert=0)()
219         return output.getvalue()
221 class HTMLDatabase:
222     ''' Return HTMLClasses for valid class fetches
223     '''
224     def __init__(self, client):
225         self._client = client
226         self._db = client.db
228         # we want config to be exposed
229         self.config = client.db.config
231     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
232         # check to see if we're actually accessing an item
233         m = desre.match(item)
234         if m:
235             self._client.db.getclass(m.group('cl'))
236             return HTMLItem(self._client, m.group('cl'), m.group('id'))
237         else:
238             self._client.db.getclass(item)
239             return HTMLClass(self._client, item)
241     def __getattr__(self, attr):
242         try:
243             return self[attr]
244         except KeyError:
245             raise AttributeError, attr
247     def classes(self):
248         l = self._client.db.classes.keys()
249         l.sort()
250         return [HTMLClass(self._client, cn) for cn in l]
252 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
253     cl = db.getclass(prop.classname)
254     l = []
255     for entry in ids:
256         if num_re.match(entry):
257             l.append(entry)
258         else:
259             try:
260                 l.append(cl.lookup(entry))
261             except KeyError:
262                 # ignore invalid keys
263                 pass
264     return l
266 class HTMLPermissions:
267     ''' Helpers that provide answers to commonly asked Permission questions.
268     '''
269     def is_edit_ok(self):
270         ''' Is the user allowed to Edit the current class?
271         '''
272         return self._db.security.hasPermission('Edit', self._client.userid,
273             self._classname)
274     def is_view_ok(self):
275         ''' Is the user allowed to View the current class?
276         '''
277         return self._db.security.hasPermission('View', self._client.userid,
278             self._classname)
279     def is_only_view_ok(self):
280         ''' Is the user only allowed to View (ie. not Edit) the current class?
281         '''
282         return self.is_view_ok() and not self.is_edit_ok()
284 class HTMLClass(HTMLPermissions):
285     ''' Accesses through a class (either through *class* or *db.<classname>*)
286     '''
287     def __init__(self, client, classname, anonymous=0):
288         self._client = client
289         self._db = client.db
290         self._anonymous = anonymous
292         # we want classname to be exposed, but _classname gives a
293         # consistent API for extending Class/Item
294         self._classname = self.classname = classname
295         self._klass = self._db.getclass(self.classname)
296         self._props = self._klass.getprops()
298     def __repr__(self):
299         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
301     def __getitem__(self, item):
302         ''' return an HTMLProperty instance
303         '''
304        #print 'HTMLClass.getitem', (self, item)
306         # we don't exist
307         if item == 'id':
308             return None
310         # get the property
311         prop = self._props[item]
313         # look up the correct HTMLProperty class
314         form = self._client.form
315         for klass, htmlklass in propclasses:
316             if not isinstance(prop, klass):
317                 continue
318             if form.has_key(item):
319                 if isinstance(prop, hyperdb.Multilink):
320                     value = lookupIds(self._db, prop,
321                         handleListCGIValue(form[item]))
322                 elif isinstance(prop, hyperdb.Link):
323                     value = form[item].value.strip()
324                     if value:
325                         value = lookupIds(self._db, prop, [value])[0]
326                     else:
327                         value = None
328                 else:
329                     value = form[item].value.strip() or None
330             else:
331                 if isinstance(prop, hyperdb.Multilink):
332                     value = []
333                 else:
334                     value = None
335             return htmlklass(self._client, self._classname, '', prop, item,
336                 value, self._anonymous)
338         # no good
339         raise KeyError, item
341     def __getattr__(self, attr):
342         ''' convenience access '''
343         try:
344             return self[attr]
345         except KeyError:
346             raise AttributeError, attr
348     def designator(self):
349         ''' Return this class' designator (classname) '''
350         return self._classname
352     def getItem(self, itemid, num_re=re.compile('-?\d+')):
353         ''' Get an item of this class by its item id.
354         '''
355         # make sure we're looking at an itemid
356         if not num_re.match(itemid):
357             itemid = self._klass.lookup(itemid)
359         if self.classname == 'user':
360             klass = HTMLUser
361         else:
362             klass = HTMLItem
364         return klass(self._client, self.classname, itemid)
366     def properties(self, sort=1):
367         ''' Return HTMLProperty for all of this class' properties.
368         '''
369         l = []
370         for name, prop in self._props.items():
371             for klass, htmlklass in propclasses:
372                 if isinstance(prop, hyperdb.Multilink):
373                     value = []
374                 else:
375                     value = None
376                 if isinstance(prop, klass):
377                     l.append(htmlklass(self._client, self._classname, '',
378                         prop, name, value, self._anonymous))
379         if sort:
380             l.sort(lambda a,b:cmp(a._name, b._name))
381         return l
383     def list(self):
384         ''' List all items in this class.
385         '''
386         if self.classname == 'user':
387             klass = HTMLUser
388         else:
389             klass = HTMLItem
391         # get the list and sort it nicely
392         l = self._klass.list()
393         sortfunc = make_sort_function(self._db, self.classname)
394         l.sort(sortfunc)
396         l = [klass(self._client, self.classname, x) for x in l]
397         return l
399     def csv(self):
400         ''' Return the items of this class as a chunk of CSV text.
401         '''
402         if rcsv.error:
403             return rcsv.error
405         props = self.propnames()
406         s = StringIO.StringIO()
407         writer = rcsv.writer(s, rcsv.comma_separated)
408         writer.writerow(props)
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             writer.writerow(l)
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 = '&amp;property=%s'%property
473         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
474             '@startwith=0&amp;@template=help&amp;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                             template = find_template(self._db.config.TEMPLATES,
595                                 classname, 'item')
596                             if template[1].startswith('_generic'):
597                                 raise NoTemplate, 'not really...'
598                         except NoTemplate:
599                             pass
600                         else:
601                             id = self._klass.get(self._nodeid, prop_n, None)
602                             current[prop_n] = '<a href="%s%s">%s</a>'%(
603                                 classname, id, current[prop_n])
604  
605         for id, evt_date, user, action, args in history:
606             date_s = str(evt_date.local(timezone)).replace("."," ")
607             arg_s = ''
608             if action == 'link' and type(args) == type(()):
609                 if len(args) == 3:
610                     linkcl, linkid, key = args
611                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
612                         linkcl, linkid, key)
613                 else:
614                     arg_s = str(args)
616             elif action == 'unlink' and type(args) == type(()):
617                 if len(args) == 3:
618                     linkcl, linkid, key = args
619                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
620                         linkcl, linkid, key)
621                 else:
622                     arg_s = str(args)
624             elif type(args) == type({}):
625                 cell = []
626                 for k in args.keys():
627                     # try to get the relevant property and treat it
628                     # specially
629                     try:
630                         prop = self._props[k]
631                     except KeyError:
632                         prop = None
633                     if prop is None:
634                         # property no longer exists
635                         comments['no_exist'] = _('''<em>The indicated property
636                             no longer exists</em>''')
637                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
638                         continue
640                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
641                             isinstance(prop, hyperdb.Link)):
642                         # figure what the link class is
643                         classname = prop.classname
644                         try:
645                             linkcl = self._db.getclass(classname)
646                         except KeyError:
647                             labelprop = None
648                             comments[classname] = _('''The linked class
649                                 %(classname)s no longer exists''')%locals()
650                         labelprop = linkcl.labelprop(1)
651                         try:
652                             template = find_template(self._db.config.TEMPLATES,
653                                 classname, 'item')
654                             if template[1].startswith('_generic'):
655                                 raise NoTemplate, 'not really...'
656                             hrefable = 1
657                         except NoTemplate:
658                             hrefable = 0
660                     if isinstance(prop, hyperdb.Multilink) and args[k]:
661                         ml = []
662                         for linkid in args[k]:
663                             if isinstance(linkid, type(())):
664                                 sublabel = linkid[0] + ' '
665                                 linkids = linkid[1]
666                             else:
667                                 sublabel = ''
668                                 linkids = [linkid]
669                             subml = []
670                             for linkid in linkids:
671                                 label = classname + linkid
672                                 # if we have a label property, try to use it
673                                 # TODO: test for node existence even when
674                                 # there's no labelprop!
675                                 try:
676                                     if labelprop is not None and \
677                                             labelprop != 'id':
678                                         label = linkcl.get(linkid, labelprop)
679                                 except IndexError:
680                                     comments['no_link'] = _('''<strike>The
681                                         linked node no longer
682                                         exists</strike>''')
683                                     subml.append('<strike>%s</strike>'%label)
684                                 else:
685                                     if hrefable:
686                                         subml.append('<a href="%s%s">%s</a>'%(
687                                             classname, linkid, label))
688                                     else:
689                                         subml.append(label)
690                             ml.append(sublabel + ', '.join(subml))
691                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
692                     elif isinstance(prop, hyperdb.Link) and args[k]:
693                         label = classname + args[k]
694                         # if we have a label property, try to use it
695                         # TODO: test for node existence even when
696                         # there's no labelprop!
697                         if labelprop is not None and labelprop != 'id':
698                             try:
699                                 label = linkcl.get(args[k], labelprop)
700                             except IndexError:
701                                 comments['no_link'] = _('''<strike>The
702                                     linked node no longer
703                                     exists</strike>''')
704                                 cell.append(' <strike>%s</strike>,\n'%label)
705                                 # "flag" this is done .... euwww
706                                 label = None
707                         if label is not None:
708                             if hrefable:
709                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
710                             else:
711                                 old = label;
712                             cell.append('%s: %s' % (k,old))
713                             if current.has_key(k):
714                                 cell[-1] += ' -> %s'%current[k]
715                                 current[k] = old
717                     elif isinstance(prop, hyperdb.Date) and args[k]:
718                         d = date.Date(args[k]).local(timezone)
719                         cell.append('%s: %s'%(k, str(d)))
720                         if current.has_key(k):
721                             cell[-1] += ' -> %s' % current[k]
722                             current[k] = str(d)
724                     elif isinstance(prop, hyperdb.Interval) and args[k]:
725                         d = date.Interval(args[k])
726                         cell.append('%s: %s'%(k, str(d)))
727                         if current.has_key(k):
728                             cell[-1] += ' -> %s'%current[k]
729                             current[k] = str(d)
731                     elif isinstance(prop, hyperdb.String) and args[k]:
732                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
733                         if current.has_key(k):
734                             cell[-1] += ' -> %s'%current[k]
735                             current[k] = cgi.escape(args[k])
737                     elif not args[k]:
738                         if current.has_key(k):
739                             cell.append('%s: %s'%(k, current[k]))
740                             current[k] = '(no value)'
741                         else:
742                             cell.append('%s: (no value)'%k)
744                     else:
745                         cell.append('%s: %s'%(k, str(args[k])))
746                         if current.has_key(k):
747                             cell[-1] += ' -> %s'%current[k]
748                             current[k] = str(args[k])
750                 arg_s = '<br />'.join(cell)
751             else:
752                 # unkown event!!
753                 comments['unknown'] = _('''<strong><em>This event is not
754                     handled by the history display!</em></strong>''')
755                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
756             date_s = date_s.replace(' ', '&nbsp;')
757             # if the user's an itemid, figure the username (older journals
758             # have the username)
759             if dre.match(user):
760                 user = self._db.user.get(user, 'username')
761             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
762                 date_s, user, action, arg_s))
763         if comments:
764             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
765         for entry in comments.values():
766             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
767         l.append('</table>')
768         return '\n'.join(l)
770     def renderQueryForm(self):
771         ''' Render this item, which is a query, as a search form.
772         '''
773         # create a new request and override the specified args
774         req = HTMLRequest(self._client)
775         req.classname = self._klass.get(self._nodeid, 'klass')
776         name = self._klass.get(self._nodeid, 'name')
777         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
778             '&@queryname=%s'%urllib.quote(name))
780         # new template, using the specified classname and request
781         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
783         # use our fabricated request
784         return pt.render(self._client, req.classname, req)
786 class HTMLUser(HTMLItem):
787     ''' Accesses through the *user* (a special case of item)
788     '''
789     def __init__(self, client, classname, nodeid, anonymous=0):
790         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
791         self._default_classname = client.classname
793         # used for security checks
794         self._security = client.db.security
796     _marker = []
797     def hasPermission(self, permission, classname=_marker):
798         ''' Determine if the user has the Permission.
800             The class being tested defaults to the template's class, but may
801             be overidden for this test by suppling an alternate classname.
802         '''
803         if classname is self._marker:
804             classname = self._default_classname
805         return self._security.hasPermission(permission, self._nodeid, classname)
807     def is_edit_ok(self):
808         ''' Is the user allowed to Edit the current class?
809             Also check whether this is the current user's info.
810         '''
811         return self._db.security.hasPermission('Edit', self._client.userid,
812             self._classname) or (self._nodeid == self._client.userid and
813             self._db.user.get(self._client.userid, 'username') != 'anonymous')
815     def is_view_ok(self):
816         ''' Is the user allowed to View the current class?
817             Also check whether this is the current user's info.
818         '''
819         return self._db.security.hasPermission('Edit', self._client.userid,
820             self._classname) or (self._nodeid == self._client.userid and
821             self._db.user.get(self._client.userid, 'username') != 'anonymous')
823 class HTMLProperty:
824     ''' String, Number, Date, Interval HTMLProperty
826         Has useful attributes:
828          _name  the name of the property
829          _value the value of the property if any
831         A wrapper object which may be stringified for the plain() behaviour.
832     '''
833     def __init__(self, client, classname, nodeid, prop, name, value,
834             anonymous=0):
835         self._client = client
836         self._db = client.db
837         self._classname = classname
838         self._nodeid = nodeid
839         self._prop = prop
840         self._value = value
841         self._anonymous = anonymous
842         self._name = name
843         if not anonymous:
844             self._formname = '%s%s@%s'%(classname, nodeid, name)
845         else:
846             self._formname = name
847     def __repr__(self):
848         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
849             self._prop, self._value)
850     def __str__(self):
851         return self.plain()
852     def __cmp__(self, other):
853         if isinstance(other, HTMLProperty):
854             return cmp(self._value, other._value)
855         return cmp(self._value, other)
857 class StringHTMLProperty(HTMLProperty):
858     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
859                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
860                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
861     def _hyper_repl(self, match):
862         if match.group('url'):
863             s = match.group('url')
864             return '<a href="%s">%s</a>'%(s, s)
865         elif match.group('email'):
866             s = match.group('email')
867             return '<a href="mailto:%s">%s</a>'%(s, s)
868         else:
869             s = match.group('item')
870             s1 = match.group('class')
871             s2 = match.group('id')
872             try:
873                 # make sure s1 is a valid tracker classname
874                 self._db.getclass(s1)
875                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
876             except KeyError:
877                 return '%s%s'%(s1, s2)
879     def hyperlinked(self):
880         ''' Render a "hyperlinked" version of the text '''
881         return self.plain(hyperlink=1)
883     def plain(self, escape=0, hyperlink=0):
884         ''' Render a "plain" representation of the property
885             
886             "escape" turns on/off HTML quoting
887             "hyperlink" turns on/off in-text hyperlinking of URLs, email
888                 addresses and designators
889         '''
890         if self._value is None:
891             return ''
892         if escape:
893             s = cgi.escape(str(self._value))
894         else:
895             s = str(self._value)
896         if hyperlink:
897             # no, we *must* escape this text
898             if not escape:
899                 s = cgi.escape(s)
900             s = self.hyper_re.sub(self._hyper_repl, s)
901         return s
903     def stext(self, escape=0):
904         ''' Render the value of the property as StructuredText.
906             This requires the StructureText module to be installed separately.
907         '''
908         s = self.plain(escape=escape)
909         if not StructuredText:
910             return s
911         return StructuredText(s,level=1,header=0)
913     def field(self, size = 30):
914         ''' Render a form edit field for the property
915         '''
916         if self._value is None:
917             value = ''
918         else:
919             value = cgi.escape(str(self._value))
920             value = '&quot;'.join(value.split('"'))
921         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
923     def multiline(self, escape=0, rows=5, cols=40):
924         ''' Render a multiline form edit field for the property
925         '''
926         if self._value is None:
927             value = ''
928         else:
929             value = cgi.escape(str(self._value))
930             value = '&quot;'.join(value.split('"'))
931         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
932             self._formname, rows, cols, value)
934     def email(self, escape=1):
935         ''' Render the value of the property as an obscured email address
936         '''
937         if self._value is None: value = ''
938         else: value = str(self._value)
939         if value.find('@') != -1:
940             name, domain = value.split('@')
941             domain = ' '.join(domain.split('.')[:-1])
942             name = name.replace('.', ' ')
943             value = '%s at %s ...'%(name, domain)
944         else:
945             value = value.replace('.', ' ')
946         if escape:
947             value = cgi.escape(value)
948         return value
950 class PasswordHTMLProperty(HTMLProperty):
951     def plain(self):
952         ''' Render a "plain" representation of the property
953         '''
954         if self._value is None:
955             return ''
956         return _('*encrypted*')
958     def field(self, size = 30):
959         ''' Render a form edit field for the property.
960         '''
961         return '<input type="password" name="%s" size="%s">'%(self._formname, size)
963     def confirm(self, size = 30):
964         ''' Render a second form edit field for the property, used for 
965             confirmation that the user typed the password correctly. Generates
966             a field with name "@confirm@name".
967         '''
968         return '<input type="password" name="@confirm@%s" size="%s">'%(
969             self._formname, size)
971 class NumberHTMLProperty(HTMLProperty):
972     def plain(self):
973         ''' Render a "plain" representation of the property
974         '''
975         return str(self._value)
977     def field(self, size = 30):
978         ''' Render a form edit field for the property
979         '''
980         if self._value is None:
981             value = ''
982         else:
983             value = cgi.escape(str(self._value))
984             value = '&quot;'.join(value.split('"'))
985         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
987     def __int__(self):
988         ''' Return an int of me
989         '''
990         return int(self._value)
992     def __float__(self):
993         ''' Return a float of me
994         '''
995         return float(self._value)
998 class BooleanHTMLProperty(HTMLProperty):
999     def plain(self):
1000         ''' Render a "plain" representation of the property
1001         '''
1002         if self._value is None:
1003             return ''
1004         return self._value and "Yes" or "No"
1006     def field(self):
1007         ''' Render a form edit field for the property
1008         '''
1009         checked = self._value and "checked" or ""
1010         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
1011             checked)
1012         if checked:
1013             checked = ""
1014         else:
1015             checked = "checked"
1016         s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
1017             checked)
1018         return s
1020 class DateHTMLProperty(HTMLProperty):
1021     def plain(self):
1022         ''' Render a "plain" representation of the property
1023         '''
1024         if self._value is None:
1025             return ''
1026         return str(self._value.local(self._db.getUserTimezone()))
1028     def now(self):
1029         ''' Return the current time.
1031             This is useful for defaulting a new value. Returns a
1032             DateHTMLProperty.
1033         '''
1034         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1035             self._formname, date.Date('.'))
1037     def field(self, size = 30):
1038         ''' Render a form edit field for the property
1039         '''
1040         if self._value is None:
1041             value = ''
1042         else:
1043             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1044             value = '&quot;'.join(value.split('"'))
1045         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1047     def reldate(self, pretty=1):
1048         ''' Render the interval between the date and now.
1050             If the "pretty" flag is true, then make the display pretty.
1051         '''
1052         if not self._value:
1053             return ''
1055         # figure the interval
1056         interval = date.Date('.') - self._value
1057         if pretty:
1058             return interval.pretty()
1059         return str(interval)
1061     _marker = []
1062     def pretty(self, format=_marker):
1063         ''' Render the date in a pretty format (eg. month names, spaces).
1065             The format string is a standard python strftime format string.
1066             Note that if the day is zero, and appears at the start of the
1067             string, then it'll be stripped from the output. This is handy
1068             for the situatin when a date only specifies a month and a year.
1069         '''
1070         if format is not self._marker:
1071             return self._value.pretty(format)
1072         else:
1073             return self._value.pretty()
1075     def local(self, offset):
1076         ''' Return the date/time as a local (timezone offset) date/time.
1077         '''
1078         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1079             self._formname, self._value.local(offset))
1081 class IntervalHTMLProperty(HTMLProperty):
1082     def plain(self):
1083         ''' Render a "plain" representation of the property
1084         '''
1085         if self._value is None:
1086             return ''
1087         return str(self._value)
1089     def pretty(self):
1090         ''' Render the interval in a pretty format (eg. "yesterday")
1091         '''
1092         return self._value.pretty()
1094     def field(self, size = 30):
1095         ''' Render a form edit field for the property
1096         '''
1097         if self._value is None:
1098             value = ''
1099         else:
1100             value = cgi.escape(str(self._value))
1101             value = '&quot;'.join(value.split('"'))
1102         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1104 class LinkHTMLProperty(HTMLProperty):
1105     ''' Link HTMLProperty
1106         Include the above as well as being able to access the class
1107         information. Stringifying the object itself results in the value
1108         from the item being displayed. Accessing attributes of this object
1109         result in the appropriate entry from the class being queried for the
1110         property accessed (so item/assignedto/name would look up the user
1111         entry identified by the assignedto property on item, and then the
1112         name property of that user)
1113     '''
1114     def __init__(self, *args, **kw):
1115         HTMLProperty.__init__(self, *args, **kw)
1116         # if we're representing a form value, then the -1 from the form really
1117         # should be a None
1118         if str(self._value) == '-1':
1119             self._value = None
1121     def __getattr__(self, attr):
1122         ''' return a new HTMLItem '''
1123        #print 'Link.getattr', (self, attr, self._value)
1124         if not self._value:
1125             raise AttributeError, "Can't access missing value"
1126         if self._prop.classname == 'user':
1127             klass = HTMLUser
1128         else:
1129             klass = HTMLItem
1130         i = klass(self._client, self._prop.classname, self._value)
1131         return getattr(i, attr)
1133     def plain(self, escape=0):
1134         ''' Render a "plain" representation of the property
1135         '''
1136         if self._value is None:
1137             return ''
1138         linkcl = self._db.classes[self._prop.classname]
1139         k = linkcl.labelprop(1)
1140         value = str(linkcl.get(self._value, k))
1141         if escape:
1142             value = cgi.escape(value)
1143         return value
1145     def field(self, showid=0, size=None):
1146         ''' Render a form edit field for the property
1147         '''
1148         linkcl = self._db.getclass(self._prop.classname)
1149         if linkcl.getprops().has_key('order'):  
1150             sort_on = 'order'  
1151         else:  
1152             sort_on = linkcl.labelprop()  
1153         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1154         # TODO: make this a field display, not a menu one!
1155         l = ['<select name="%s">'%self._formname]
1156         k = linkcl.labelprop(1)
1157         if self._value is None:
1158             s = 'selected '
1159         else:
1160             s = ''
1161         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1163         # make sure we list the current value if it's retired
1164         if self._value and self._value not in options:
1165             options.insert(0, self._value)
1167         for optionid in options:
1168             # get the option value, and if it's None use an empty string
1169             option = linkcl.get(optionid, k) or ''
1171             # figure if this option is selected
1172             s = ''
1173             if optionid == self._value:
1174                 s = 'selected '
1176             # figure the label
1177             if showid:
1178                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1179             else:
1180                 lab = option
1182             # truncate if it's too long
1183             if size is not None and len(lab) > size:
1184                 lab = lab[:size-3] + '...'
1186             # and generate
1187             lab = cgi.escape(lab)
1188             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1189         l.append('</select>')
1190         return '\n'.join(l)
1192     def menu(self, size=None, height=None, showid=0, additional=[],
1193             **conditions):
1194         ''' Render a form select list for this property
1195         '''
1196         value = self._value
1198         linkcl = self._db.getclass(self._prop.classname)
1199         l = ['<select name="%s">'%self._formname]
1200         k = linkcl.labelprop(1)
1201         s = ''
1202         if value is None:
1203             s = 'selected '
1204         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1205         if linkcl.getprops().has_key('order'):  
1206             sort_on = ('+', 'order')
1207         else:  
1208             sort_on = ('+', linkcl.labelprop())
1209         options = linkcl.filter(None, conditions, sort_on, (None, None))
1211         # make sure we list the current value if it's retired
1212         if self._value and self._value not in options:
1213             options.insert(0, self._value)
1215         for optionid in options:
1216             # get the option value, and if it's None use an empty string
1217             option = linkcl.get(optionid, k) or ''
1219             # figure if this option is selected
1220             s = ''
1221             if value in [optionid, option]:
1222                 s = 'selected '
1224             # figure the label
1225             if showid:
1226                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1227             else:
1228                 lab = option
1230             # truncate if it's too long
1231             if size is not None and len(lab) > size:
1232                 lab = lab[:size-3] + '...'
1233             if additional:
1234                 m = []
1235                 for propname in additional:
1236                     m.append(linkcl.get(optionid, propname))
1237                 lab = lab + ' (%s)'%', '.join(map(str, m))
1239             # and generate
1240             lab = cgi.escape(lab)
1241             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1242         l.append('</select>')
1243         return '\n'.join(l)
1244 #    def checklist(self, ...)
1246 class MultilinkHTMLProperty(HTMLProperty):
1247     ''' Multilink HTMLProperty
1249         Also be iterable, returning a wrapper object like the Link case for
1250         each entry in the multilink.
1251     '''
1252     def __init__(self, *args, **kwargs):
1253         HTMLProperty.__init__(self, *args, **kwargs)
1254         if self._value:
1255             self._value.sort(make_sort_function(self._db, self._prop.classname))
1256     
1257     def __len__(self):
1258         ''' length of the multilink '''
1259         return len(self._value)
1261     def __getattr__(self, attr):
1262         ''' no extended attribute accesses make sense here '''
1263         raise AttributeError, attr
1265     def __getitem__(self, num):
1266         ''' iterate and return a new HTMLItem
1267         '''
1268        #print 'Multi.getitem', (self, num)
1269         value = self._value[num]
1270         if self._prop.classname == 'user':
1271             klass = HTMLUser
1272         else:
1273             klass = HTMLItem
1274         return klass(self._client, self._prop.classname, value)
1276     def __contains__(self, value):
1277         ''' Support the "in" operator. We have to make sure the passed-in
1278             value is a string first, not a *HTMLProperty.
1279         '''
1280         return str(value) in self._value
1282     def reverse(self):
1283         ''' return the list in reverse order
1284         '''
1285         l = self._value[:]
1286         l.reverse()
1287         if self._prop.classname == 'user':
1288             klass = HTMLUser
1289         else:
1290             klass = HTMLItem
1291         return [klass(self._client, self._prop.classname, value) for value in l]
1293     def plain(self, escape=0):
1294         ''' Render a "plain" representation of the property
1295         '''
1296         linkcl = self._db.classes[self._prop.classname]
1297         k = linkcl.labelprop(1)
1298         labels = []
1299         for v in self._value:
1300             labels.append(linkcl.get(v, k))
1301         value = ', '.join(labels)
1302         if escape:
1303             value = cgi.escape(value)
1304         return value
1306     def field(self, size=30, showid=0):
1307         ''' Render a form edit field for the property
1308         '''
1309         linkcl = self._db.getclass(self._prop.classname)
1310         value = self._value[:]
1311         # map the id to the label property
1312         if not linkcl.getkey():
1313             showid=1
1314         if not showid:
1315             k = linkcl.labelprop(1)
1316             value = [linkcl.get(v, k) for v in value]
1317         value = cgi.escape(','.join(value))
1318         return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1320     def menu(self, size=None, height=None, showid=0, additional=[],
1321             **conditions):
1322         ''' Render a form select list for this property
1323         '''
1324         value = self._value
1326         linkcl = self._db.getclass(self._prop.classname)
1327         sort_on = ('+', find_sort_key(linkcl))
1328         options = linkcl.filter(None, conditions, sort_on)
1329         height = height or min(len(options), 7)
1330         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1331         k = linkcl.labelprop(1)
1333         # make sure we list the current values if they're retired
1334         for val in value:
1335             if val not in options:
1336                 options.insert(0, val)
1338         for optionid in options:
1339             # get the option value, and if it's None use an empty string
1340             option = linkcl.get(optionid, k) or ''
1342             # figure if this option is selected
1343             s = ''
1344             if optionid in value or option in value:
1345                 s = 'selected '
1347             # figure the label
1348             if showid:
1349                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1350             else:
1351                 lab = option
1352             # truncate if it's too long
1353             if size is not None and len(lab) > size:
1354                 lab = lab[:size-3] + '...'
1355             if additional:
1356                 m = []
1357                 for propname in additional:
1358                     m.append(linkcl.get(optionid, propname))
1359                 lab = lab + ' (%s)'%', '.join(m)
1361             # and generate
1362             lab = cgi.escape(lab)
1363             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1364                 lab))
1365         l.append('</select>')
1366         return '\n'.join(l)
1368 # set the propclasses for HTMLItem
1369 propclasses = (
1370     (hyperdb.String, StringHTMLProperty),
1371     (hyperdb.Number, NumberHTMLProperty),
1372     (hyperdb.Boolean, BooleanHTMLProperty),
1373     (hyperdb.Date, DateHTMLProperty),
1374     (hyperdb.Interval, IntervalHTMLProperty),
1375     (hyperdb.Password, PasswordHTMLProperty),
1376     (hyperdb.Link, LinkHTMLProperty),
1377     (hyperdb.Multilink, MultilinkHTMLProperty),
1380 def make_sort_function(db, classname):
1381     '''Make a sort function for a given class
1382     '''
1383     linkcl = db.getclass(classname)
1384     sort_on = find_sort_key(linkcl)
1385     def sortfunc(a, b):
1386         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1387     return sortfunc
1389 def find_sort_key(linkcl):
1390     if linkcl.getprops().has_key('order'):
1391         return 'order'
1392     else:
1393         return linkcl.labelprop()
1395 def handleListCGIValue(value):
1396     ''' Value is either a single item or a list of items. Each item has a
1397         .value that we're actually interested in.
1398     '''
1399     if isinstance(value, type([])):
1400         return [value.value for value in value]
1401     else:
1402         value = value.value.strip()
1403         if not value:
1404             return []
1405         return value.split(',')
1407 class ShowDict:
1408     ''' A convenience access to the :columns index parameters
1409     '''
1410     def __init__(self, columns):
1411         self.columns = {}
1412         for col in columns:
1413             self.columns[col] = 1
1414     def __getitem__(self, name):
1415         return self.columns.has_key(name)
1417 class HTMLRequest:
1418     ''' The *request*, holding the CGI form and environment.
1420         "form" the CGI form as a cgi.FieldStorage
1421         "env" the CGI environment variables
1422         "base" the base URL for this instance
1423         "user" a HTMLUser instance for this user
1424         "classname" the current classname (possibly None)
1425         "template" the current template (suffix, also possibly None)
1427         Index args:
1428         "columns" dictionary of the columns to display in an index page
1429         "show" a convenience access to columns - request/show/colname will
1430                be true if the columns should be displayed, false otherwise
1431         "sort" index sort column (direction, column name)
1432         "group" index grouping property (direction, column name)
1433         "filter" properties to filter the index on
1434         "filterspec" values to filter the index on
1435         "search_text" text to perform a full-text search on for an index
1437     '''
1438     def __init__(self, client):
1439         self.client = client
1441         # easier access vars
1442         self.form = client.form
1443         self.env = client.env
1444         self.base = client.base
1445         self.user = HTMLUser(client, 'user', client.userid)
1447         # store the current class name and action
1448         self.classname = client.classname
1449         self.template = client.template
1451         # the special char to use for special vars
1452         self.special_char = '@'
1454         self._post_init()
1456     def _post_init(self):
1457         ''' Set attributes based on self.form
1458         '''
1459         # extract the index display information from the form
1460         self.columns = []
1461         for name in ':columns @columns'.split():
1462             if self.form.has_key(name):
1463                 self.special_char = name[0]
1464                 self.columns = handleListCGIValue(self.form[name])
1465                 break
1466         self.show = ShowDict(self.columns)
1468         # sorting
1469         self.sort = (None, None)
1470         for name in ':sort @sort'.split():
1471             if self.form.has_key(name):
1472                 self.special_char = name[0]
1473                 sort = self.form[name].value
1474                 if sort.startswith('-'):
1475                     self.sort = ('-', sort[1:])
1476                 else:
1477                     self.sort = ('+', sort)
1478                 if self.form.has_key(self.special_char+'sortdir'):
1479                     self.sort = ('-', self.sort[1])
1481         # grouping
1482         self.group = (None, None)
1483         for name in ':group @group'.split():
1484             if self.form.has_key(name):
1485                 self.special_char = name[0]
1486                 group = self.form[name].value
1487                 if group.startswith('-'):
1488                     self.group = ('-', group[1:])
1489                 else:
1490                     self.group = ('+', group)
1491                 if self.form.has_key(self.special_char+'groupdir'):
1492                     self.group = ('-', self.group[1])
1494         # filtering
1495         self.filter = []
1496         for name in ':filter @filter'.split():
1497             if self.form.has_key(name):
1498                 self.special_char = name[0]
1499                 self.filter = handleListCGIValue(self.form[name])
1501         self.filterspec = {}
1502         db = self.client.db
1503         if self.classname is not None:
1504             props = db.getclass(self.classname).getprops()
1505             for name in self.filter:
1506                 if not self.form.has_key(name):
1507                     continue
1508                 prop = props[name]
1509                 fv = self.form[name]
1510                 if (isinstance(prop, hyperdb.Link) or
1511                         isinstance(prop, hyperdb.Multilink)):
1512                     self.filterspec[name] = lookupIds(db, prop,
1513                         handleListCGIValue(fv))
1514                 else:
1515                     if isinstance(fv, type([])):
1516                         self.filterspec[name] = [v.value for v in fv]
1517                     else:
1518                         self.filterspec[name] = fv.value
1520         # full-text search argument
1521         self.search_text = None
1522         for name in ':search_text @search_text'.split():
1523             if self.form.has_key(name):
1524                 self.special_char = name[0]
1525                 self.search_text = self.form[name].value
1527         # pagination - size and start index
1528         # figure batch args
1529         self.pagesize = 50
1530         for name in ':pagesize @pagesize'.split():
1531             if self.form.has_key(name):
1532                 self.special_char = name[0]
1533                 self.pagesize = int(self.form[name].value)
1535         self.startwith = 0
1536         for name in ':startwith @startwith'.split():
1537             if self.form.has_key(name):
1538                 self.special_char = name[0]
1539                 self.startwith = int(self.form[name].value)
1541     def updateFromURL(self, url):
1542         ''' Parse the URL for query args, and update my attributes using the
1543             values.
1544         ''' 
1545         env = {'QUERY_STRING': url}
1546         self.form = cgi.FieldStorage(environ=env)
1548         self._post_init()
1550     def update(self, kwargs):
1551         ''' Update my attributes using the keyword args
1552         '''
1553         self.__dict__.update(kwargs)
1554         if kwargs.has_key('columns'):
1555             self.show = ShowDict(self.columns)
1557     def description(self):
1558         ''' Return a description of the request - handle for the page title.
1559         '''
1560         s = [self.client.db.config.TRACKER_NAME]
1561         if self.classname:
1562             if self.client.nodeid:
1563                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1564             else:
1565                 if self.template == 'item':
1566                     s.append('- new %s'%self.classname)
1567                 elif self.template == 'index':
1568                     s.append('- %s index'%self.classname)
1569                 else:
1570                     s.append('- %s %s'%(self.classname, self.template))
1571         else:
1572             s.append('- home')
1573         return ' '.join(s)
1575     def __str__(self):
1576         d = {}
1577         d.update(self.__dict__)
1578         f = ''
1579         for k in self.form.keys():
1580             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1581         d['form'] = f
1582         e = ''
1583         for k,v in self.env.items():
1584             e += '\n     %r=%r'%(k, v)
1585         d['env'] = e
1586         return '''
1587 form: %(form)s
1588 base: %(base)r
1589 classname: %(classname)r
1590 template: %(template)r
1591 columns: %(columns)r
1592 sort: %(sort)r
1593 group: %(group)r
1594 filter: %(filter)r
1595 search_text: %(search_text)r
1596 pagesize: %(pagesize)r
1597 startwith: %(startwith)r
1598 env: %(env)s
1599 '''%d
1601     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1602             filterspec=1):
1603         ''' return the current index args as form elements '''
1604         l = []
1605         sc = self.special_char
1606         s = '<input type="hidden" name="%s" value="%s">'
1607         if columns and self.columns:
1608             l.append(s%(sc+'columns', ','.join(self.columns)))
1609         if sort and self.sort[1] is not None:
1610             if self.sort[0] == '-':
1611                 val = '-'+self.sort[1]
1612             else:
1613                 val = self.sort[1]
1614             l.append(s%(sc+'sort', val))
1615         if group and self.group[1] is not None:
1616             if self.group[0] == '-':
1617                 val = '-'+self.group[1]
1618             else:
1619                 val = self.group[1]
1620             l.append(s%(sc+'group', val))
1621         if filter and self.filter:
1622             l.append(s%(sc+'filter', ','.join(self.filter)))
1623         if filterspec:
1624             for k,v in self.filterspec.items():
1625                 if type(v) == type([]):
1626                     l.append(s%(k, ','.join(v)))
1627                 else:
1628                     l.append(s%(k, v))
1629         if self.search_text:
1630             l.append(s%(sc+'search_text', self.search_text))
1631         l.append(s%(sc+'pagesize', self.pagesize))
1632         l.append(s%(sc+'startwith', self.startwith))
1633         return '\n'.join(l)
1635     def indexargs_url(self, url, args):
1636         ''' Embed the current index args in a URL
1637         '''
1638         sc = self.special_char
1639         l = ['%s=%s'%(k,v) for k,v in args.items()]
1641         # pull out the special values (prefixed by @ or :)
1642         specials = {}
1643         for key in args.keys():
1644             if key[0] in '@:':
1645                 specials[key[1:]] = args[key]
1647         # ok, now handle the specials we received in the request
1648         if self.columns and not specials.has_key('columns'):
1649             l.append(sc+'columns=%s'%(','.join(self.columns)))
1650         if self.sort[1] is not None and not specials.has_key('sort'):
1651             if self.sort[0] == '-':
1652                 val = '-'+self.sort[1]
1653             else:
1654                 val = self.sort[1]
1655             l.append(sc+'sort=%s'%val)
1656         if self.group[1] is not None and not specials.has_key('group'):
1657             if self.group[0] == '-':
1658                 val = '-'+self.group[1]
1659             else:
1660                 val = self.group[1]
1661             l.append(sc+'group=%s'%val)
1662         if self.filter and not specials.has_key('filter'):
1663             l.append(sc+'filter=%s'%(','.join(self.filter)))
1664         if self.search_text and not specials.has_key('search_text'):
1665             l.append(sc+'search_text=%s'%self.search_text)
1666         if not specials.has_key('pagesize'):
1667             l.append(sc+'pagesize=%s'%self.pagesize)
1668         if not specials.has_key('startwith'):
1669             l.append(sc+'startwith=%s'%self.startwith)
1671         # finally, the remainder of the filter args in the request
1672         for k,v in self.filterspec.items():
1673             if not args.has_key(k):
1674                 if type(v) == type([]):
1675                     l.append('%s=%s'%(k, ','.join(v)))
1676                 else:
1677                     l.append('%s=%s'%(k, v))
1678         return '%s?%s'%(url, '&'.join(l))
1679     indexargs_href = indexargs_url
1681     def base_javascript(self):
1682         return '''
1683 <script type="text/javascript">
1684 submitted = false;
1685 function submit_once() {
1686     if (submitted) {
1687         alert("Your request is being processed.\\nPlease be patient.");
1688         return 0;
1689     }
1690     submitted = true;
1691     return 1;
1694 function help_window(helpurl, width, height) {
1695     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1697 </script>
1698 '''%self.base
1700     def batch(self):
1701         ''' Return a batch object for results from the "current search"
1702         '''
1703         filterspec = self.filterspec
1704         sort = self.sort
1705         group = self.group
1707         # get the list of ids we're batching over
1708         klass = self.client.db.getclass(self.classname)
1709         if self.search_text:
1710             matches = self.client.db.indexer.search(
1711                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1712         else:
1713             matches = None
1714         l = klass.filter(matches, filterspec, sort, group)
1716         # return the batch object, using IDs only
1717         return Batch(self.client, l, self.pagesize, self.startwith,
1718             classname=self.classname)
1720 # extend the standard ZTUtils Batch object to remove dependency on
1721 # Acquisition and add a couple of useful methods
1722 class Batch(ZTUtils.Batch):
1723     ''' Use me to turn a list of items, or item ids of a given class, into a
1724         series of batches.
1726         ========= ========================================================
1727         Parameter  Usage
1728         ========= ========================================================
1729         sequence  a list of HTMLItems or item ids
1730         classname if sequence is a list of ids, this is the class of item
1731         size      how big to make the sequence.
1732         start     where to start (0-indexed) in the sequence.
1733         end       where to end (0-indexed) in the sequence.
1734         orphan    if the next batch would contain less items than this
1735                   value, then it is combined with this batch
1736         overlap   the number of items shared between adjacent batches
1737         ========= ========================================================
1739         Attributes: Note that the "start" attribute, unlike the
1740         argument, is a 1-based index (I know, lame).  "first" is the
1741         0-based index.  "length" is the actual number of elements in
1742         the batch.
1744         "sequence_length" is the length of the original, unbatched, sequence.
1745     '''
1746     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1747             overlap=0, classname=None):
1748         self.client = client
1749         self.last_index = self.last_item = None
1750         self.current_item = None
1751         self.classname = classname
1752         self.sequence_length = len(sequence)
1753         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1754             overlap)
1756     # overwrite so we can late-instantiate the HTMLItem instance
1757     def __getitem__(self, index):
1758         if index < 0:
1759             if index + self.end < self.first: raise IndexError, index
1760             return self._sequence[index + self.end]
1761         
1762         if index >= self.length:
1763             raise IndexError, index
1765         # move the last_item along - but only if the fetched index changes
1766         # (for some reason, index 0 is fetched twice)
1767         if index != self.last_index:
1768             self.last_item = self.current_item
1769             self.last_index = index
1771         item = self._sequence[index + self.first]
1772         if self.classname:
1773             # map the item ids to instances
1774             if self.classname == 'user':
1775                 item = HTMLUser(self.client, self.classname, item)
1776             else:
1777                 item = HTMLItem(self.client, self.classname, item)
1778         self.current_item = item
1779         return item
1781     def propchanged(self, property):
1782         ''' Detect if the property marked as being the group property
1783             changed in the last iteration fetch
1784         '''
1785         if (self.last_item is None or
1786                 self.last_item[property] != self.current_item[property]):
1787             return 1
1788         return 0
1790     # override these 'cos we don't have access to acquisition
1791     def previous(self):
1792         if self.start == 1:
1793             return None
1794         return Batch(self.client, self._sequence, self._size,
1795             self.first - self._size + self.overlap, 0, self.orphan,
1796             self.overlap)
1798     def next(self):
1799         try:
1800             self._sequence[self.end]
1801         except IndexError:
1802             return None
1803         return Batch(self.client, self._sequence, self._size,
1804             self.end - self.overlap, 0, self.orphan, self.overlap)
1806 class TemplatingUtils:
1807     ''' Utilities for templating
1808     '''
1809     def __init__(self, client):
1810         self.client = client
1811     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1812         return Batch(self.client, sequence, size, start, end, orphan,
1813             overlap)