Code

fixed generic template discovery
[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 getItem(self, itemid, num_re=re.compile('\d+')):
345         ''' Get an item of this class by its item id.
346         '''
347         # make sure we're looking at an itemid
348         if not num_re.match(itemid):
349             itemid = self._klass.lookup(itemid)
351         if self.classname == 'user':
352             klass = HTMLUser
353         else:
354             klass = HTMLItem
356         return klass(self._client, self.classname, itemid)
358     def properties(self, sort=1):
359         ''' Return HTMLProperty for all of this class' properties.
360         '''
361         l = []
362         for name, prop in self._props.items():
363             for klass, htmlklass in propclasses:
364                 if isinstance(prop, hyperdb.Multilink):
365                     value = []
366                 else:
367                     value = None
368                 if isinstance(prop, klass):
369                     l.append(htmlklass(self._client, self._classname, '',
370                         prop, name, value, self._anonymous))
371         if sort:
372             l.sort(lambda a,b:cmp(a._name, b._name))
373         return l
375     def list(self):
376         ''' List all items in this class.
377         '''
378         if self.classname == 'user':
379             klass = HTMLUser
380         else:
381             klass = HTMLItem
383         # get the list and sort it nicely
384         l = self._klass.list()
385         sortfunc = make_sort_function(self._db, self.classname)
386         l.sort(sortfunc)
388         l = [klass(self._client, self.classname, x) for x in l]
389         return l
391     def csv(self):
392         ''' Return the items of this class as a chunk of CSV text.
393         '''
394         # get the CSV module
395         try:
396             import csv
397         except ImportError:
398             return 'Sorry, you need the csv module to use this function.\n'\
399                 'Get it from: http://www.object-craft.com.au/projects/csv/'
401         props = self.propnames()
402         p = csv.parser()
403         s = StringIO.StringIO()
404         s.write(p.join(props) + '\n')
405         for nodeid in self._klass.list():
406             l = []
407             for name in props:
408                 value = self._klass.get(nodeid, name)
409                 if value is None:
410                     l.append('')
411                 elif isinstance(value, type([])):
412                     l.append(':'.join(map(str, value)))
413                 else:
414                     l.append(str(self._klass.get(nodeid, name)))
415             s.write(p.join(l) + '\n')
416         return s.getvalue()
418     def propnames(self):
419         ''' Return the list of the names of the properties of this class.
420         '''
421         idlessprops = self._klass.getprops(protected=0).keys()
422         idlessprops.sort()
423         return ['id'] + idlessprops
425     def filter(self, request=None):
426         ''' Return a list of items from this class, filtered and sorted
427             by the current requested filterspec/filter/sort/group args
428         '''
429         # XXX allow direct specification of the filterspec etc.
430         if request is not None:
431             filterspec = request.filterspec
432             sort = request.sort
433             group = request.group
434         else:
435             filterspec = {}
436             sort = (None,None)
437             group = (None,None)
438         if self.classname == 'user':
439             klass = HTMLUser
440         else:
441             klass = HTMLItem
442         l = [klass(self._client, self.classname, x)
443              for x in self._klass.filter(None, filterspec, sort, group)]
444         return l
446     def classhelp(self, properties=None, label='(list)', width='500',
447             height='400', property=''):
448         ''' Pop up a javascript window with class help
450             This generates a link to a popup window which displays the 
451             properties indicated by "properties" of the class named by
452             "classname". The "properties" should be a comma-separated list
453             (eg. 'id,name,description'). Properties defaults to all the
454             properties of a class (excluding id, creator, created and
455             activity).
457             You may optionally override the label displayed, the width and
458             height. The popup window will be resizable and scrollable.
460             If the "property" arg is given, it's passed through to the
461             javascript help_window function.
462         '''
463         if properties is None:
464             properties = self._klass.getprops(protected=0).keys()
465             properties.sort()
466             properties = ','.join(properties)
467         if property:
468             property = '&property=%s'%property
469         return '<a class="classhelp" href="javascript:help_window(\'%s?:'\
470             'template=help&properties=%s%s\', \'%s\', \'%s\')">%s</a>'%(
471             self.classname, properties, property, width, height, label)
473     def submit(self, label="Submit New Entry"):
474         ''' Generate a submit button (and action hidden element)
475         '''
476         return '  <input type="hidden" name=":action" value="new">\n'\
477         '  <input type="submit" name="submit" value="%s">'%label
479     def history(self):
480         return 'New node - no history'
482     def renderWith(self, name, **kwargs):
483         ''' Render this class with the given template.
484         '''
485         # create a new request and override the specified args
486         req = HTMLRequest(self._client)
487         req.classname = self.classname
488         req.update(kwargs)
490         # new template, using the specified classname and request
491         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
493         # use our fabricated request
494         return pt.render(self._client, self.classname, req)
496 class HTMLItem(HTMLPermissions):
497     ''' Accesses through an *item*
498     '''
499     def __init__(self, client, classname, nodeid, anonymous=0):
500         self._client = client
501         self._db = client.db
502         self._classname = classname
503         self._nodeid = nodeid
504         self._klass = self._db.getclass(classname)
505         self._props = self._klass.getprops()
507         # do we prefix the form items with the item's identification?
508         self._anonymous = anonymous
510     def __repr__(self):
511         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
512             self._nodeid)
514     def __getitem__(self, item):
515         ''' return an HTMLProperty instance
516         '''
517         #print 'HTMLItem.getitem', (self, item)
518         if item == 'id':
519             return self._nodeid
521         # get the property
522         prop = self._props[item]
524         # get the value, handling missing values
525         value = None
526         if int(self._nodeid) > 0:
527             value = self._klass.get(self._nodeid, item, None)
528         if value is None:
529             if isinstance(self._props[item], hyperdb.Multilink):
530                 value = []
532         # look up the correct HTMLProperty class
533         for klass, htmlklass in propclasses:
534             if isinstance(prop, klass):
535                 return htmlklass(self._client, self._classname,
536                     self._nodeid, prop, item, value, self._anonymous)
538         raise KeyError, item
540     def __getattr__(self, attr):
541         ''' convenience access to properties '''
542         try:
543             return self[attr]
544         except KeyError:
545             raise AttributeError, attr
546     
547     def submit(self, label="Submit Changes"):
548         ''' Generate a submit button (and action hidden element)
549         '''
550         return '  <input type="hidden" name=":action" value="edit">\n'\
551         '  <input type="submit" name="submit" value="%s">'%label
553     def journal(self, direction='descending'):
554         ''' Return a list of HTMLJournalEntry instances.
555         '''
556         # XXX do this
557         return []
559     def history(self, direction='descending', dre=re.compile('\d+')):
560         l = ['<table class="history">'
561              '<tr><th colspan="4" class="header">',
562              _('History'),
563              '</th></tr><tr>',
564              _('<th>Date</th>'),
565              _('<th>User</th>'),
566              _('<th>Action</th>'),
567              _('<th>Args</th>'),
568             '</tr>']
569         current = {}
570         comments = {}
571         history = self._klass.history(self._nodeid)
572         history.sort()
573         timezone = self._db.getUserTimezone()
574         if direction == 'descending':
575             history.reverse()
576             for prop_n in self._props.keys():
577                 prop = self[prop_n]
578                 if isinstance(prop, HTMLProperty):
579                     current[prop_n] = prop.plain()
580                     # make link if hrefable
581                     if (self._props.has_key(prop_n) and
582                             isinstance(self._props[prop_n], hyperdb.Link)):
583                         classname = self._props[prop_n].classname
584                         try:
585                             find_template(self._db.config.TEMPLATES,
586                                 classname, 'item')
587                         except NoTemplate:
588                             pass
589                         else:
590                             id = self._klass.get(self._nodeid, prop_n, None)
591                             current[prop_n] = '<a href="%s%s">%s</a>'%(
592                                 classname, id, current[prop_n])
593  
594         for id, evt_date, user, action, args in history:
595             date_s = str(evt_date.local(timezone)).replace("."," ")
596             arg_s = ''
597             if action == 'link' and type(args) == type(()):
598                 if len(args) == 3:
599                     linkcl, linkid, key = args
600                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
601                         linkcl, linkid, key)
602                 else:
603                     arg_s = str(args)
605             elif action == 'unlink' and type(args) == type(()):
606                 if len(args) == 3:
607                     linkcl, linkid, key = args
608                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
609                         linkcl, linkid, key)
610                 else:
611                     arg_s = str(args)
613             elif type(args) == type({}):
614                 cell = []
615                 for k in args.keys():
616                     # try to get the relevant property and treat it
617                     # specially
618                     try:
619                         prop = self._props[k]
620                     except KeyError:
621                         prop = None
622                     if prop is not None:
623                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
624                                 isinstance(prop, hyperdb.Link)):
625                             # figure what the link class is
626                             classname = prop.classname
627                             try:
628                                 linkcl = self._db.getclass(classname)
629                             except KeyError:
630                                 labelprop = None
631                                 comments[classname] = _('''The linked class
632                                     %(classname)s no longer exists''')%locals()
633                             labelprop = linkcl.labelprop(1)
634                             hrefable = os.path.exists(
635                                 os.path.join(self._db.config.TEMPLATES,
636                                 classname+'.item'))
638                         if isinstance(prop, hyperdb.Multilink) and args[k]:
639                             ml = []
640                             for linkid in args[k]:
641                                 if isinstance(linkid, type(())):
642                                     sublabel = linkid[0] + ' '
643                                     linkids = linkid[1]
644                                 else:
645                                     sublabel = ''
646                                     linkids = [linkid]
647                                 subml = []
648                                 for linkid in linkids:
649                                     label = classname + linkid
650                                     # if we have a label property, try to use it
651                                     # TODO: test for node existence even when
652                                     # there's no labelprop!
653                                     try:
654                                         if labelprop is not None and \
655                                                 labelprop != 'id':
656                                             label = linkcl.get(linkid, labelprop)
657                                     except IndexError:
658                                         comments['no_link'] = _('''<strike>The
659                                             linked node no longer
660                                             exists</strike>''')
661                                         subml.append('<strike>%s</strike>'%label)
662                                     else:
663                                         if hrefable:
664                                             subml.append('<a href="%s%s">%s</a>'%(
665                                                 classname, linkid, label))
666                                         else:
667                                             subml.append(label)
668                                 ml.append(sublabel + ', '.join(subml))
669                             cell.append('%s:\n  %s'%(k, ', '.join(ml)))
670                         elif isinstance(prop, hyperdb.Link) and args[k]:
671                             label = classname + args[k]
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                             if labelprop is not None and labelprop != 'id':
676                                 try:
677                                     label = linkcl.get(args[k], labelprop)
678                                 except IndexError:
679                                     comments['no_link'] = _('''<strike>The
680                                         linked node no longer
681                                         exists</strike>''')
682                                     cell.append(' <strike>%s</strike>,\n'%label)
683                                     # "flag" this is done .... euwww
684                                     label = None
685                             if label is not None:
686                                 if hrefable:
687                                     old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
688                                 else:
689                                     old = label;
690                                 cell.append('%s: %s' % (k,old))
691                                 if current.has_key(k):
692                                     cell[-1] += ' -> %s'%current[k]
693                                     current[k] = old
695                         elif isinstance(prop, hyperdb.Date) and args[k]:
696                             d = date.Date(args[k]).local(timezone)
697                             cell.append('%s: %s'%(k, str(d)))
698                             if current.has_key(k):
699                                 cell[-1] += ' -> %s' % current[k]
700                                 current[k] = str(d)
702                         elif isinstance(prop, hyperdb.Interval) and args[k]:
703                             d = date.Interval(args[k])
704                             cell.append('%s: %s'%(k, str(d)))
705                             if current.has_key(k):
706                                 cell[-1] += ' -> %s'%current[k]
707                                 current[k] = str(d)
709                         elif isinstance(prop, hyperdb.String) and args[k]:
710                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
711                             if current.has_key(k):
712                                 cell[-1] += ' -> %s'%current[k]
713                                 current[k] = cgi.escape(args[k])
715                         elif not args[k]:
716                             if current.has_key(k):
717                                 cell.append('%s: %s'%(k, current[k]))
718                                 current[k] = '(no value)'
719                             else:
720                                 cell.append('%s: (no value)'%k)
722                         else:
723                             cell.append('%s: %s'%(k, str(args[k])))
724                             if current.has_key(k):
725                                 cell[-1] += ' -> %s'%current[k]
726                                 current[k] = str(args[k])
727                     else:
728                         # property no longer exists
729                         comments['no_exist'] = _('''<em>The indicated property
730                             no longer exists</em>''')
731                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
732                 arg_s = '<br />'.join(cell)
733             else:
734                 # unkown event!!
735                 comments['unknown'] = _('''<strong><em>This event is not
736                     handled by the history display!</em></strong>''')
737                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
738             date_s = date_s.replace(' ', '&nbsp;')
739             # if the user's an itemid, figure the username (older journals
740             # have the username)
741             if dre.match(user):
742                 user = self._db.user.get(user, 'username')
743             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
744                 date_s, user, action, arg_s))
745         if comments:
746             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
747         for entry in comments.values():
748             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
749         l.append('</table>')
750         return '\n'.join(l)
752     def renderQueryForm(self):
753         ''' Render this item, which is a query, as a search form.
754         '''
755         # create a new request and override the specified args
756         req = HTMLRequest(self._client)
757         req.classname = self._klass.get(self._nodeid, 'klass')
758         name = self._klass.get(self._nodeid, 'name')
759         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
760             '&:queryname=%s'%urllib.quote(name))
762         # new template, using the specified classname and request
763         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
765         # use our fabricated request
766         return pt.render(self._client, req.classname, req)
768 class HTMLUser(HTMLItem):
769     ''' Accesses through the *user* (a special case of item)
770     '''
771     def __init__(self, client, classname, nodeid, anonymous=0):
772         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
773         self._default_classname = client.classname
775         # used for security checks
776         self._security = client.db.security
778     _marker = []
779     def hasPermission(self, permission, classname=_marker):
780         ''' Determine if the user has the Permission.
782             The class being tested defaults to the template's class, but may
783             be overidden for this test by suppling an alternate classname.
784         '''
785         if classname is self._marker:
786             classname = self._default_classname
787         return self._security.hasPermission(permission, self._nodeid, classname)
789     def is_edit_ok(self):
790         ''' Is the user allowed to Edit the current class?
791             Also check whether this is the current user's info.
792         '''
793         return self._db.security.hasPermission('Edit', self._client.userid,
794             self._classname) or self._nodeid == self._client.userid
796     def is_view_ok(self):
797         ''' Is the user allowed to View the current class?
798             Also check whether this is the current user's info.
799         '''
800         return self._db.security.hasPermission('Edit', self._client.userid,
801             self._classname) or self._nodeid == self._client.userid
803 class HTMLProperty:
804     ''' String, Number, Date, Interval HTMLProperty
806         Has useful attributes:
808          _name  the name of the property
809          _value the value of the property if any
811         A wrapper object which may be stringified for the plain() behaviour.
812     '''
813     def __init__(self, client, classname, nodeid, prop, name, value,
814             anonymous=0):
815         self._client = client
816         self._db = client.db
817         self._classname = classname
818         self._nodeid = nodeid
819         self._prop = prop
820         self._value = value
821         self._anonymous = anonymous
822         self._name = name
823         if not anonymous:
824             self._formname = '%s%s@%s'%(classname, nodeid, name)
825         else:
826             self._formname = name
827     def __repr__(self):
828         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
829             self._prop, self._value)
830     def __str__(self):
831         return self.plain()
832     def __cmp__(self, other):
833         if isinstance(other, HTMLProperty):
834             return cmp(self._value, other._value)
835         return cmp(self._value, other)
837 class StringHTMLProperty(HTMLProperty):
838     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
839                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
840                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
841     def _hyper_repl(self, match):
842         if match.group('url'):
843             s = match.group('url')
844             return '<a href="%s">%s</a>'%(s, s)
845         elif match.group('email'):
846             s = match.group('email')
847             return '<a href="mailto:%s">%s</a>'%(s, s)
848         else:
849             s = match.group('item')
850             s1 = match.group('class')
851             s2 = match.group('id')
852             try:
853                 # make sure s1 is a valid tracker classname
854                 self._db.getclass(s1)
855                 return '<a href="%s">%s %s</a>'%(s, s1, s2)
856             except KeyError:
857                 return '%s%s'%(s1, s2)
859     def plain(self, escape=0, hyperlink=0):
860         ''' Render a "plain" representation of the property
861             
862             "escape" turns on/off HTML quoting
863             "hyperlink" turns on/off in-text hyperlinking of URLs, email
864                 addresses and designators
865         '''
866         if self._value is None:
867             return ''
868         if escape:
869             s = cgi.escape(str(self._value))
870         else:
871             s = str(self._value)
872         if hyperlink:
873             if not escape:
874                 s = cgi.escape(s)
875             s = self.hyper_re.sub(self._hyper_repl, s)
876         return s
878     def stext(self, escape=0):
879         ''' Render the value of the property as StructuredText.
881             This requires the StructureText module to be installed separately.
882         '''
883         s = self.plain(escape=escape)
884         if not StructuredText:
885             return s
886         return StructuredText(s,level=1,header=0)
888     def field(self, size = 30):
889         ''' Render a form edit field for the property
890         '''
891         if self._value is None:
892             value = ''
893         else:
894             value = cgi.escape(str(self._value))
895             value = '&quot;'.join(value.split('"'))
896         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
898     def multiline(self, escape=0, rows=5, cols=40):
899         ''' Render a multiline form edit field for the property
900         '''
901         if self._value is None:
902             value = ''
903         else:
904             value = cgi.escape(str(self._value))
905             value = '&quot;'.join(value.split('"'))
906         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
907             self._formname, rows, cols, value)
909     def email(self, escape=1):
910         ''' Render the value of the property as an obscured email address
911         '''
912         if self._value is None: value = ''
913         else: value = str(self._value)
914         if value.find('@') != -1:
915             name, domain = value.split('@')
916             domain = ' '.join(domain.split('.')[:-1])
917             name = name.replace('.', ' ')
918             value = '%s at %s ...'%(name, domain)
919         else:
920             value = value.replace('.', ' ')
921         if escape:
922             value = cgi.escape(value)
923         return value
925 class PasswordHTMLProperty(HTMLProperty):
926     def plain(self):
927         ''' Render a "plain" representation of the property
928         '''
929         if self._value is None:
930             return ''
931         return _('*encrypted*')
933     def field(self, size = 30):
934         ''' Render a form edit field for the property.
935         '''
936         return '<input type="password" name="%s" size="%s">'%(self._formname, size)
938     def confirm(self, size = 30):
939         ''' Render a second form edit field for the property, used for 
940             confirmation that the user typed the password correctly. Generates
941             a field with name ":confirm:name".
942         '''
943         return '<input type="password" name=":confirm:%s" size="%s">'%(
944             self._formname, size)
946 class NumberHTMLProperty(HTMLProperty):
947     def plain(self):
948         ''' Render a "plain" representation of the property
949         '''
950         return str(self._value)
952     def field(self, size = 30):
953         ''' Render a form edit field for the property
954         '''
955         if self._value is None:
956             value = ''
957         else:
958             value = cgi.escape(str(self._value))
959             value = '&quot;'.join(value.split('"'))
960         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
962     def __int__(self):
963         ''' Return an int of me
964         '''
965         return int(self._value)
967     def __float__(self):
968         ''' Return a float of me
969         '''
970         return float(self._value)
973 class BooleanHTMLProperty(HTMLProperty):
974     def plain(self):
975         ''' Render a "plain" representation of the property
976         '''
977         if self._value is None:
978             return ''
979         return self._value and "Yes" or "No"
981     def field(self):
982         ''' Render a form edit field for the property
983         '''
984         checked = self._value and "checked" or ""
985         s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
986             checked)
987         if checked:
988             checked = ""
989         else:
990             checked = "checked"
991         s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
992             checked)
993         return s
995 class DateHTMLProperty(HTMLProperty):
996     def plain(self):
997         ''' Render a "plain" representation of the property
998         '''
999         if self._value is None:
1000             return ''
1001         return str(self._value.local(self._db.getUserTimezone()))
1003     def now(self):
1004         ''' Return the current time.
1006             This is useful for defaulting a new value. Returns a
1007             DateHTMLProperty.
1008         '''
1009         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1010             self._formname, date.Date('.'))
1012     def field(self, size = 30):
1013         ''' Render a form edit field for the property
1014         '''
1015         if self._value is None:
1016             value = ''
1017         else:
1018             value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1019             value = '&quot;'.join(value.split('"'))
1020         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1022     def reldate(self, pretty=1):
1023         ''' Render the interval between the date and now.
1025             If the "pretty" flag is true, then make the display pretty.
1026         '''
1027         if not self._value:
1028             return ''
1030         # figure the interval
1031         interval = date.Date('.') - self._value
1032         if pretty:
1033             return interval.pretty()
1034         return str(interval)
1036     _marker = []
1037     def pretty(self, format=_marker):
1038         ''' Render the date in a pretty format (eg. month names, spaces).
1040             The format string is a standard python strftime format string.
1041             Note that if the day is zero, and appears at the start of the
1042             string, then it'll be stripped from the output. This is handy
1043             for the situatin when a date only specifies a month and a year.
1044         '''
1045         if format is not self._marker:
1046             return self._value.pretty(format)
1047         else:
1048             return self._value.pretty()
1050     def local(self, offset):
1051         ''' Return the date/time as a local (timezone offset) date/time.
1052         '''
1053         return DateHTMLProperty(self._client, self._nodeid, self._prop,
1054             self._formname, self._value.local(offset))
1056 class IntervalHTMLProperty(HTMLProperty):
1057     def plain(self):
1058         ''' Render a "plain" representation of the property
1059         '''
1060         if self._value is None:
1061             return ''
1062         return str(self._value)
1064     def pretty(self):
1065         ''' Render the interval in a pretty format (eg. "yesterday")
1066         '''
1067         return self._value.pretty()
1069     def field(self, size = 30):
1070         ''' Render a form edit field for the property
1071         '''
1072         if self._value is None:
1073             value = ''
1074         else:
1075             value = cgi.escape(str(self._value))
1076             value = '&quot;'.join(value.split('"'))
1077         return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1079 class LinkHTMLProperty(HTMLProperty):
1080     ''' Link HTMLProperty
1081         Include the above as well as being able to access the class
1082         information. Stringifying the object itself results in the value
1083         from the item being displayed. Accessing attributes of this object
1084         result in the appropriate entry from the class being queried for the
1085         property accessed (so item/assignedto/name would look up the user
1086         entry identified by the assignedto property on item, and then the
1087         name property of that user)
1088     '''
1089     def __init__(self, *args, **kw):
1090         HTMLProperty.__init__(self, *args, **kw)
1091         # if we're representing a form value, then the -1 from the form really
1092         # should be a None
1093         if str(self._value) == '-1':
1094             self._value = None
1096     def __getattr__(self, attr):
1097         ''' return a new HTMLItem '''
1098        #print 'Link.getattr', (self, attr, self._value)
1099         if not self._value:
1100             raise AttributeError, "Can't access missing value"
1101         if self._prop.classname == 'user':
1102             klass = HTMLUser
1103         else:
1104             klass = HTMLItem
1105         i = klass(self._client, self._prop.classname, self._value)
1106         return getattr(i, attr)
1108     def plain(self, escape=0):
1109         ''' Render a "plain" representation of the property
1110         '''
1111         if self._value is None:
1112             return ''
1113         linkcl = self._db.classes[self._prop.classname]
1114         k = linkcl.labelprop(1)
1115         value = str(linkcl.get(self._value, k))
1116         if escape:
1117             value = cgi.escape(value)
1118         return value
1120     def field(self, showid=0, size=None):
1121         ''' Render a form edit field for the property
1122         '''
1123         linkcl = self._db.getclass(self._prop.classname)
1124         if linkcl.getprops().has_key('order'):  
1125             sort_on = 'order'  
1126         else:  
1127             sort_on = linkcl.labelprop()  
1128         options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1129         # TODO: make this a field display, not a menu one!
1130         l = ['<select name="%s">'%self._formname]
1131         k = linkcl.labelprop(1)
1132         if self._value is None:
1133             s = 'selected '
1134         else:
1135             s = ''
1136         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1138         # make sure we list the current value if it's retired
1139         if self._value and self._value not in options:
1140             options.insert(0, self._value)
1142         for optionid in options:
1143             # get the option value, and if it's None use an empty string
1144             option = linkcl.get(optionid, k) or ''
1146             # figure if this option is selected
1147             s = ''
1148             if optionid == self._value:
1149                 s = 'selected '
1151             # figure the label
1152             if showid:
1153                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1154             else:
1155                 lab = option
1157             # truncate if it's too long
1158             if size is not None and len(lab) > size:
1159                 lab = lab[:size-3] + '...'
1161             # and generate
1162             lab = cgi.escape(lab)
1163             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1164         l.append('</select>')
1165         return '\n'.join(l)
1167     def menu(self, size=None, height=None, showid=0, additional=[],
1168             **conditions):
1169         ''' Render a form select list for this property
1170         '''
1171         value = self._value
1173         # sort function
1174         sortfunc = make_sort_function(self._db, self._prop.classname)
1176         linkcl = self._db.getclass(self._prop.classname)
1177         l = ['<select name="%s">'%self._formname]
1178         k = linkcl.labelprop(1)
1179         s = ''
1180         if value is None:
1181             s = 'selected '
1182         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1183         if linkcl.getprops().has_key('order'):  
1184             sort_on = ('+', 'order')
1185         else:  
1186             sort_on = ('+', linkcl.labelprop())
1187         options = linkcl.filter(None, conditions, sort_on, (None, None))
1189         # make sure we list the current value if it's retired
1190         if self._value and self._value not in options:
1191             options.insert(0, self._value)
1193         for optionid in options:
1194             # get the option value, and if it's None use an empty string
1195             option = linkcl.get(optionid, k) or ''
1197             # figure if this option is selected
1198             s = ''
1199             if value in [optionid, option]:
1200                 s = 'selected '
1202             # figure the label
1203             if showid:
1204                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1205             else:
1206                 lab = option
1208             # truncate if it's too long
1209             if size is not None and len(lab) > size:
1210                 lab = lab[:size-3] + '...'
1211             if additional:
1212                 m = []
1213                 for propname in additional:
1214                     m.append(linkcl.get(optionid, propname))
1215                 lab = lab + ' (%s)'%', '.join(map(str, m))
1217             # and generate
1218             lab = cgi.escape(lab)
1219             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1220         l.append('</select>')
1221         return '\n'.join(l)
1222 #    def checklist(self, ...)
1224 class MultilinkHTMLProperty(HTMLProperty):
1225     ''' Multilink HTMLProperty
1227         Also be iterable, returning a wrapper object like the Link case for
1228         each entry in the multilink.
1229     '''
1230     def __len__(self):
1231         ''' length of the multilink '''
1232         return len(self._value)
1234     def __getattr__(self, attr):
1235         ''' no extended attribute accesses make sense here '''
1236         raise AttributeError, attr
1238     def __getitem__(self, num):
1239         ''' iterate and return a new HTMLItem
1240         '''
1241        #print 'Multi.getitem', (self, num)
1242         value = self._value[num]
1243         if self._prop.classname == 'user':
1244             klass = HTMLUser
1245         else:
1246             klass = HTMLItem
1247         return klass(self._client, self._prop.classname, value)
1249     def __contains__(self, value):
1250         ''' Support the "in" operator. We have to make sure the passed-in
1251             value is a string first, not a *HTMLProperty.
1252         '''
1253         return str(value) in self._value
1255     def reverse(self):
1256         ''' return the list in reverse order
1257         '''
1258         l = self._value[:]
1259         l.reverse()
1260         if self._prop.classname == 'user':
1261             klass = HTMLUser
1262         else:
1263             klass = HTMLItem
1264         return [klass(self._client, self._prop.classname, value) for value in l]
1266     def plain(self, escape=0):
1267         ''' Render a "plain" representation of the property
1268         '''
1269         linkcl = self._db.classes[self._prop.classname]
1270         k = linkcl.labelprop(1)
1271         labels = []
1272         for v in self._value:
1273             labels.append(linkcl.get(v, k))
1274         value = ', '.join(labels)
1275         if escape:
1276             value = cgi.escape(value)
1277         return value
1279     def field(self, size=30, showid=0):
1280         ''' Render a form edit field for the property
1281         '''
1282         sortfunc = make_sort_function(self._db, self._prop.classname)
1283         linkcl = self._db.getclass(self._prop.classname)
1284         value = self._value[:]
1285         if value:
1286             value.sort(sortfunc)
1287         # map the id to the label property
1288         if not linkcl.getkey():
1289             showid=1
1290         if not showid:
1291             k = linkcl.labelprop(1)
1292             value = [linkcl.get(v, k) for v in value]
1293         value = cgi.escape(','.join(value))
1294         return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1296     def menu(self, size=None, height=None, showid=0, additional=[],
1297             **conditions):
1298         ''' Render a form select list for this property
1299         '''
1300         value = self._value
1302         # sort function
1303         sortfunc = make_sort_function(self._db, self._prop.classname)
1305         linkcl = self._db.getclass(self._prop.classname)
1306         if linkcl.getprops().has_key('order'):  
1307             sort_on = ('+', 'order')
1308         else:  
1309             sort_on = ('+', linkcl.labelprop())
1310         options = linkcl.filter(None, conditions, sort_on, (None,None)) 
1311         height = height or min(len(options), 7)
1312         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1313         k = linkcl.labelprop(1)
1315         # make sure we list the current values if they're retired
1316         for val in value:
1317             if val not in options:
1318                 options.insert(0, val)
1320         for optionid in options:
1321             # get the option value, and if it's None use an empty string
1322             option = linkcl.get(optionid, k) or ''
1324             # figure if this option is selected
1325             s = ''
1326             if optionid in value or option in value:
1327                 s = 'selected '
1329             # figure the label
1330             if showid:
1331                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1332             else:
1333                 lab = option
1334             # truncate if it's too long
1335             if size is not None and len(lab) > size:
1336                 lab = lab[:size-3] + '...'
1337             if additional:
1338                 m = []
1339                 for propname in additional:
1340                     m.append(linkcl.get(optionid, propname))
1341                 lab = lab + ' (%s)'%', '.join(m)
1343             # and generate
1344             lab = cgi.escape(lab)
1345             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1346                 lab))
1347         l.append('</select>')
1348         return '\n'.join(l)
1350 # set the propclasses for HTMLItem
1351 propclasses = (
1352     (hyperdb.String, StringHTMLProperty),
1353     (hyperdb.Number, NumberHTMLProperty),
1354     (hyperdb.Boolean, BooleanHTMLProperty),
1355     (hyperdb.Date, DateHTMLProperty),
1356     (hyperdb.Interval, IntervalHTMLProperty),
1357     (hyperdb.Password, PasswordHTMLProperty),
1358     (hyperdb.Link, LinkHTMLProperty),
1359     (hyperdb.Multilink, MultilinkHTMLProperty),
1362 def make_sort_function(db, classname):
1363     '''Make a sort function for a given class
1364     '''
1365     linkcl = db.getclass(classname)
1366     if linkcl.getprops().has_key('order'):
1367         sort_on = 'order'
1368     else:
1369         sort_on = linkcl.labelprop()
1370     def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1371         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1372     return sortfunc
1374 def handleListCGIValue(value):
1375     ''' Value is either a single item or a list of items. Each item has a
1376         .value that we're actually interested in.
1377     '''
1378     if isinstance(value, type([])):
1379         return [value.value for value in value]
1380     else:
1381         value = value.value.strip()
1382         if not value:
1383             return []
1384         return value.split(',')
1386 class ShowDict:
1387     ''' A convenience access to the :columns index parameters
1388     '''
1389     def __init__(self, columns):
1390         self.columns = {}
1391         for col in columns:
1392             self.columns[col] = 1
1393     def __getitem__(self, name):
1394         return self.columns.has_key(name)
1396 class HTMLRequest:
1397     ''' The *request*, holding the CGI form and environment.
1399         "form" the CGI form as a cgi.FieldStorage
1400         "env" the CGI environment variables
1401         "base" the base URL for this instance
1402         "user" a HTMLUser instance for this user
1403         "classname" the current classname (possibly None)
1404         "template" the current template (suffix, also possibly None)
1406         Index args:
1407         "columns" dictionary of the columns to display in an index page
1408         "show" a convenience access to columns - request/show/colname will
1409                be true if the columns should be displayed, false otherwise
1410         "sort" index sort column (direction, column name)
1411         "group" index grouping property (direction, column name)
1412         "filter" properties to filter the index on
1413         "filterspec" values to filter the index on
1414         "search_text" text to perform a full-text search on for an index
1416     '''
1417     def __init__(self, client):
1418         self.client = client
1420         # easier access vars
1421         self.form = client.form
1422         self.env = client.env
1423         self.base = client.base
1424         self.user = HTMLUser(client, 'user', client.userid)
1426         # store the current class name and action
1427         self.classname = client.classname
1428         self.template = client.template
1430         # the special char to use for special vars
1431         self.special_char = '@'
1433         self._post_init()
1435     def _post_init(self):
1436         ''' Set attributes based on self.form
1437         '''
1438         # extract the index display information from the form
1439         self.columns = []
1440         for name in ':columns @columns'.split():
1441             if self.form.has_key(name):
1442                 self.special_char = name[0]
1443                 self.columns = handleListCGIValue(self.form[name])
1444                 break
1445         self.show = ShowDict(self.columns)
1447         # sorting
1448         self.sort = (None, None)
1449         for name in ':sort @sort'.split():
1450             if self.form.has_key(name):
1451                 self.special_char = name[0]
1452                 sort = self.form[name].value
1453                 if sort.startswith('-'):
1454                     self.sort = ('-', sort[1:])
1455                 else:
1456                     self.sort = ('+', sort)
1457                 if self.form.has_key(self.special_char+'sortdir'):
1458                     self.sort = ('-', self.sort[1])
1460         # grouping
1461         self.group = (None, None)
1462         for name in ':group @group'.split():
1463             if self.form.has_key(name):
1464                 self.special_char = name[0]
1465                 group = self.form[name].value
1466                 if group.startswith('-'):
1467                     self.group = ('-', group[1:])
1468                 else:
1469                     self.group = ('+', group)
1470                 if self.form.has_key(self.special_char+'groupdir'):
1471                     self.group = ('-', self.group[1])
1473         # filtering
1474         self.filter = []
1475         for name in ':filter @filter'.split():
1476             if self.form.has_key(name):
1477                 self.special_char = name[0]
1478                 self.filter = handleListCGIValue(self.form[name])
1480         self.filterspec = {}
1481         db = self.client.db
1482         if self.classname is not None:
1483             props = db.getclass(self.classname).getprops()
1484             for name in self.filter:
1485                 if not self.form.has_key(name):
1486                     continue
1487                 prop = props[name]
1488                 fv = self.form[name]
1489                 if (isinstance(prop, hyperdb.Link) or
1490                         isinstance(prop, hyperdb.Multilink)):
1491                     self.filterspec[name] = lookupIds(db, prop,
1492                         handleListCGIValue(fv))
1493                 else:
1494                     if isinstance(fv, type([])):
1495                         self.filterspec[name] = [v.value for v in fv]
1496                     else:
1497                         self.filterspec[name] = fv.value
1499         # full-text search argument
1500         self.search_text = None
1501         for name in ':search_text @search_text'.split():
1502             if self.form.has_key(name):
1503                 self.special_char = name[0]
1504                 self.search_text = self.form[name].value
1506         # pagination - size and start index
1507         # figure batch args
1508         self.pagesize = 50
1509         for name in ':pagesize @pagesize'.split():
1510             if self.form.has_key(name):
1511                 self.special_char = name[0]
1512                 self.pagesize = int(self.form[name].value)
1514         self.startwith = 0
1515         for name in ':startwith @startwith'.split():
1516             if self.form.has_key(name):
1517                 self.special_char = name[0]
1518                 self.startwith = int(self.form[name].value)
1520     def updateFromURL(self, url):
1521         ''' Parse the URL for query args, and update my attributes using the
1522             values.
1523         ''' 
1524         env = {'QUERY_STRING': url}
1525         self.form = cgi.FieldStorage(environ=env)
1527         self._post_init()
1529     def update(self, kwargs):
1530         ''' Update my attributes using the keyword args
1531         '''
1532         self.__dict__.update(kwargs)
1533         if kwargs.has_key('columns'):
1534             self.show = ShowDict(self.columns)
1536     def description(self):
1537         ''' Return a description of the request - handle for the page title.
1538         '''
1539         s = [self.client.db.config.TRACKER_NAME]
1540         if self.classname:
1541             if self.client.nodeid:
1542                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1543             else:
1544                 if self.template == 'item':
1545                     s.append('- new %s'%self.classname)
1546                 elif self.template == 'index':
1547                     s.append('- %s index'%self.classname)
1548                 else:
1549                     s.append('- %s %s'%(self.classname, self.template))
1550         else:
1551             s.append('- home')
1552         return ' '.join(s)
1554     def __str__(self):
1555         d = {}
1556         d.update(self.__dict__)
1557         f = ''
1558         for k in self.form.keys():
1559             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1560         d['form'] = f
1561         e = ''
1562         for k,v in self.env.items():
1563             e += '\n     %r=%r'%(k, v)
1564         d['env'] = e
1565         return '''
1566 form: %(form)s
1567 base: %(base)r
1568 classname: %(classname)r
1569 template: %(template)r
1570 columns: %(columns)r
1571 sort: %(sort)r
1572 group: %(group)r
1573 filter: %(filter)r
1574 search_text: %(search_text)r
1575 pagesize: %(pagesize)r
1576 startwith: %(startwith)r
1577 env: %(env)s
1578 '''%d
1580     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1581             filterspec=1):
1582         ''' return the current index args as form elements '''
1583         l = []
1584         sc = self.special_char
1585         s = '<input type="hidden" name="%s" value="%s">'
1586         if columns and self.columns:
1587             l.append(s%(sc+'columns', ','.join(self.columns)))
1588         if sort and self.sort[1] is not None:
1589             if self.sort[0] == '-':
1590                 val = '-'+self.sort[1]
1591             else:
1592                 val = self.sort[1]
1593             l.append(s%(sc+'sort', val))
1594         if group and self.group[1] is not None:
1595             if self.group[0] == '-':
1596                 val = '-'+self.group[1]
1597             else:
1598                 val = self.group[1]
1599             l.append(s%(sc+'group', val))
1600         if filter and self.filter:
1601             l.append(s%(sc+'filter', ','.join(self.filter)))
1602         if filterspec:
1603             for k,v in self.filterspec.items():
1604                 if type(v) == type([]):
1605                     l.append(s%(k, ','.join(v)))
1606                 else:
1607                     l.append(s%(k, v))
1608         if self.search_text:
1609             l.append(s%(sc+'search_text', self.search_text))
1610         l.append(s%(sc+'pagesize', self.pagesize))
1611         l.append(s%(sc+'startwith', self.startwith))
1612         return '\n'.join(l)
1614     def indexargs_url(self, url, args):
1615         ''' Embed the current index args in a URL
1616         '''
1617         sc = self.special_char
1618         l = ['%s=%s'%(k,v) for k,v in args.items()]
1620         # pull out the special values (prefixed by @ or :)
1621         specials = {}
1622         for key in args.keys():
1623             if key[0] in '@:':
1624                 specials[key[1:]] = args[key]
1626         # ok, now handle the specials we received in the request
1627         if self.columns and not specials.has_key('columns'):
1628             l.append(sc+'columns=%s'%(','.join(self.columns)))
1629         if self.sort[1] is not None and not specials.has_key('sort'):
1630             if self.sort[0] == '-':
1631                 val = '-'+self.sort[1]
1632             else:
1633                 val = self.sort[1]
1634             l.append(sc+'sort=%s'%val)
1635         if self.group[1] is not None and not specials.has_key('group'):
1636             if self.group[0] == '-':
1637                 val = '-'+self.group[1]
1638             else:
1639                 val = self.group[1]
1640             l.append(sc+'group=%s'%val)
1641         if self.filter and not specials.has_key('filter'):
1642             l.append(sc+'filter=%s'%(','.join(self.filter)))
1643         if self.search_text and not specials.has_key('search_text'):
1644             l.append(sc+'search_text=%s'%self.search_text)
1645         if not specials.has_key('pagesize'):
1646             l.append(sc+'pagesize=%s'%self.pagesize)
1647         if not specials.has_key('startwith'):
1648             l.append(sc+'startwith=%s'%self.startwith)
1650         # finally, the remainder of the filter args in the request
1651         for k,v in self.filterspec.items():
1652             if not args.has_key(k):
1653                 if type(v) == type([]):
1654                     l.append('%s=%s'%(k, ','.join(v)))
1655                 else:
1656                     l.append('%s=%s'%(k, v))
1657         return '%s?%s'%(url, '&'.join(l))
1658     indexargs_href = indexargs_url
1660     def base_javascript(self):
1661         return '''
1662 <script language="javascript">
1663 submitted = false;
1664 function submit_once() {
1665     if (submitted) {
1666         alert("Your request is being processed.\\nPlease be patient.");
1667         return 0;
1668     }
1669     submitted = true;
1670     return 1;
1673 function help_window(helpurl, width, height) {
1674     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1676 </script>
1677 '''%self.base
1679     def batch(self):
1680         ''' Return a batch object for results from the "current search"
1681         '''
1682         filterspec = self.filterspec
1683         sort = self.sort
1684         group = self.group
1686         # get the list of ids we're batching over
1687         klass = self.client.db.getclass(self.classname)
1688         if self.search_text:
1689             matches = self.client.db.indexer.search(
1690                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1691         else:
1692             matches = None
1693         l = klass.filter(matches, filterspec, sort, group)
1695         # return the batch object, using IDs only
1696         return Batch(self.client, l, self.pagesize, self.startwith,
1697             classname=self.classname)
1699 # extend the standard ZTUtils Batch object to remove dependency on
1700 # Acquisition and add a couple of useful methods
1701 class Batch(ZTUtils.Batch):
1702     ''' Use me to turn a list of items, or item ids of a given class, into a
1703         series of batches.
1705         ========= ========================================================
1706         Parameter  Usage
1707         ========= ========================================================
1708         sequence  a list of HTMLItems or item ids
1709         classname if sequence is a list of ids, this is the class of item
1710         size      how big to make the sequence.
1711         start     where to start (0-indexed) in the sequence.
1712         end       where to end (0-indexed) in the sequence.
1713         orphan    if the next batch would contain less items than this
1714                   value, then it is combined with this batch
1715         overlap   the number of items shared between adjacent batches
1716         ========= ========================================================
1718         Attributes: Note that the "start" attribute, unlike the
1719         argument, is a 1-based index (I know, lame).  "first" is the
1720         0-based index.  "length" is the actual number of elements in
1721         the batch.
1723         "sequence_length" is the length of the original, unbatched, sequence.
1724     '''
1725     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1726             overlap=0, classname=None):
1727         self.client = client
1728         self.last_index = self.last_item = None
1729         self.current_item = None
1730         self.classname = classname
1731         self.sequence_length = len(sequence)
1732         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1733             overlap)
1735     # overwrite so we can late-instantiate the HTMLItem instance
1736     def __getitem__(self, index):
1737         if index < 0:
1738             if index + self.end < self.first: raise IndexError, index
1739             return self._sequence[index + self.end]
1740         
1741         if index >= self.length:
1742             raise IndexError, index
1744         # move the last_item along - but only if the fetched index changes
1745         # (for some reason, index 0 is fetched twice)
1746         if index != self.last_index:
1747             self.last_item = self.current_item
1748             self.last_index = index
1750         item = self._sequence[index + self.first]
1751         if self.classname:
1752             # map the item ids to instances
1753             if self.classname == 'user':
1754                 item = HTMLUser(self.client, self.classname, item)
1755             else:
1756                 item = HTMLItem(self.client, self.classname, item)
1757         self.current_item = item
1758         return item
1760     def propchanged(self, property):
1761         ''' Detect if the property marked as being the group property
1762             changed in the last iteration fetch
1763         '''
1764         if (self.last_item is None or
1765                 self.last_item[property] != self.current_item[property]):
1766             return 1
1767         return 0
1769     # override these 'cos we don't have access to acquisition
1770     def previous(self):
1771         if self.start == 1:
1772             return None
1773         return Batch(self.client, self._sequence, self._size,
1774             self.first - self._size + self.overlap, 0, self.orphan,
1775             self.overlap)
1777     def next(self):
1778         try:
1779             self._sequence[self.end]
1780         except IndexError:
1781             return None
1782         return Batch(self.client, self._sequence, self._size,
1783             self.end - self.overlap, 0, self.orphan, self.overlap)
1785 class TemplatingUtils:
1786     ''' Utilities for templating
1787     '''
1788     def __init__(self, client):
1789         self.client = client
1790     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1791         return Batch(self.client, sequence, size, start, end, orphan,
1792             overlap)