Code

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