Code

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