Code

nicer invalid property error in HTML templating
[roundup.git] / roundup / cgi / templating.py
1 """Implements the API used in the HTML templating for the web interface.
2 """
3 __docformat__ = 'restructuredtext'
5 from __future__ import nested_scopes
7 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
9 from roundup import hyperdb, date, rcsv
10 from roundup.i18n import _
12 try:
13     import cPickle as pickle
14 except ImportError:
15     import pickle
16 try:
17     import cStringIO as StringIO
18 except ImportError:
19     import StringIO
20 try:
21     import StructuredText
22 except ImportError:
23     StructuredText = None
25 # bring in the templating support
26 from roundup.cgi.PageTemplates import PageTemplate
27 from roundup.cgi.PageTemplates.Expressions import getEngine
28 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
29 from roundup.cgi import ZTUtils
31 class NoTemplate(Exception):
32     pass
34 class Unauthorised(Exception):
35     def __init__(self, action, klass):
36         self.action = action
37         self.klass = klass
38     def __str__(self):
39         return 'You are not allowed to %s items of class %s'%(self.action,
40             self.klass)
42 def find_template(dir, name, extension):
43     ''' Find a template in the nominated dir
44     '''
45     # find the source
46     if extension:
47         filename = '%s.%s'%(name, extension)
48     else:
49         filename = name
51     # try old-style
52     src = os.path.join(dir, filename)
53     if os.path.exists(src):
54         return (src, filename)
56     # try with a .html extension (new-style)
57     filename = filename + '.html'
58     src = os.path.join(dir, filename)
59     if os.path.exists(src):
60         return (src, filename)
62     # no extension == no generic template is possible
63     if not extension:
64         raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
66     # try for a _generic template
67     generic = '_generic.%s'%extension
68     src = os.path.join(dir, generic)
69     if os.path.exists(src):
70         return (src, generic)
72     # finally, try _generic.html
73     generic = generic + '.html'
74     src = os.path.join(dir, generic)
75     if os.path.exists(src):
76         return (src, generic)
78     raise NoTemplate, 'No template file exists for templating "%s" '\
79         'with template "%s" (neither "%s" nor "%s")'%(name, extension,
80         filename, generic)
82 class Templates:
83     templates = {}
85     def __init__(self, dir):
86         self.dir = dir
88     def precompileTemplates(self):
89         ''' Go through a directory and precompile all the templates therein
90         '''
91         for filename in os.listdir(self.dir):
92             if os.path.isdir(filename): continue
93             if '.' in filename:
94                 name, extension = filename.split('.')
95                 self.get(name, extension)
96             else:
97                 self.get(filename, None)
99     def get(self, name, extension=None):
100         ''' Interface to get a template, possibly loading a compiled template.
102             "name" and "extension" indicate the template we're after, which in
103             most cases will be "name.extension". If "extension" is None, then
104             we look for a template just called "name" with no extension.
106             If the file "name.extension" doesn't exist, we look for
107             "_generic.extension" as a fallback.
108         '''
109         # default the name to "home"
110         if name is None:
111             name = 'home'
112         elif extension is None and '.' in name:
113             # split name
114             name, extension = name.split('.')
116         # find the source
117         src, filename = find_template(self.dir, name, extension)
119         # has it changed?
120         try:
121             stime = os.stat(src)[os.path.stat.ST_MTIME]
122         except os.error, error:
123             if error.errno != errno.ENOENT:
124                 raise
126         if self.templates.has_key(src) and \
127                 stime < self.templates[src].mtime:
128             # compiled template is up to date
129             return self.templates[src]
131         # compile the template
132         self.templates[src] = pt = RoundupPageTemplate()
133         # use pt_edit so we can pass the content_type guess too
134         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
135         pt.pt_edit(open(src).read(), content_type)
136         pt.id = filename
137         pt.mtime = time.time()
138         return pt
140     def __getitem__(self, name):
141         name, extension = os.path.splitext(name)
142         if extension:
143             extension = extension[1:]
144         try:
145             return self.get(name, extension)
146         except NoTemplate, message:
147             raise KeyError, message
149 class RoundupPageTemplate(PageTemplate.PageTemplate):
150     '''A Roundup-specific PageTemplate.
152     Interrogate the client to set up the various template variables to
153     be available:
155     *context*
156      this is one of three things:
158      1. None - we're viewing a "home" page
159      2. The current class of item being displayed. This is an HTMLClass
160         instance.
161      3. The current item from the database, if we're viewing a specific
162         item, as an HTMLItem instance.
163     *request*
164       Includes information about the current request, including:
166        - the url
167        - the current index information (``filterspec``, ``filter`` args,
168          ``properties``, etc) parsed out of the form. 
169        - methods for easy filterspec link generation
170        - *user*, the current user node as an HTMLItem instance
171        - *form*, the current CGI form information as a FieldStorage
172     *config*
173       The current tracker config.
174     *db*
175       The current database, used to access arbitrary database items.
176     *utils*
177       This is a special class that has its base in the TemplatingUtils
178       class in this file. If the tracker interfaces module defines a
179       TemplatingUtils class then it is mixed in, overriding the methods
180       in the base class.
181     '''
182     def getContext(self, client, classname, request):
183         # construct the TemplatingUtils class
184         utils = TemplatingUtils
185         if hasattr(client.instance.interfaces, 'TemplatingUtils'):
186             class utils(client.instance.interfaces.TemplatingUtils, utils):
187                 pass
189         c = {
190              'options': {},
191              'nothing': None,
192              'request': request,
193              'db': HTMLDatabase(client),
194              'config': client.instance.config,
195              'tracker': client.instance,
196              'utils': utils(client),
197              'templates': Templates(client.instance.config.TEMPLATES),
198         }
199         # add in the item if there is one
200         if client.nodeid:
201             if classname == 'user':
202                 c['context'] = HTMLUser(client, classname, client.nodeid,
203                     anonymous=1)
204             else:
205                 c['context'] = HTMLItem(client, classname, client.nodeid,
206                     anonymous=1)
207         elif client.db.classes.has_key(classname):
208             if classname == 'user':
209                 c['context'] = HTMLUserClass(client, classname, anonymous=1)
210             else:
211                 c['context'] = HTMLClass(client, classname, anonymous=1)
212         return c
214     def render(self, client, classname, request, **options):
215         """Render this Page Template"""
217         if not self._v_cooked:
218             self._cook()
220         __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
222         if self._v_errors:
223             raise PageTemplate.PTRuntimeError, \
224                 'Page Template %s has errors.'%self.id
226         # figure the context
227         classname = classname or client.classname
228         request = request or HTMLRequest(client)
229         c = self.getContext(client, classname, request)
230         c.update({'options': options})
232         # and go
233         output = StringIO.StringIO()
234         TALInterpreter(self._v_program, self.macros,
235             getEngine().getContext(c), output, tal=1, strictinsert=0)()
236         return output.getvalue()
238     def __repr__(self):
239         return '<Roundup PageTemplate %r>'%self.id
241 class HTMLDatabase:
242     ''' Return HTMLClasses for valid class fetches
243     '''
244     def __init__(self, client):
245         self._client = client
246         self._db = client.db
248         # we want config to be exposed
249         self.config = client.db.config
251     def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
252         # check to see if we're actually accessing an item
253         m = desre.match(item)
254         if m:
255             self._client.db.getclass(m.group('cl'))
256             return HTMLItem(self._client, m.group('cl'), m.group('id'))
257         else:
258             self._client.db.getclass(item)
259             if item == 'user':
260                 return HTMLUserClass(self._client, item)
261             return HTMLClass(self._client, item)
263     def __getattr__(self, attr):
264         try:
265             return self[attr]
266         except KeyError:
267             raise AttributeError, attr
269     def classes(self):
270         l = self._client.db.classes.keys()
271         l.sort()
272         r = []
273         for item in l:
274             if item == 'user':
275                 m.append(HTMLUserClass(self._client, item))
276             m.append(HTMLClass(self._client, item))
277         return r
279 def lookupIds(db, prop, ids, fail_ok=False, num_re=re.compile('-?\d+')):
280     ''' "fail_ok" should be specified if we wish to pass through bad values
281         (most likely form values that we wish to represent back to the user)
282     '''
283     cl = db.getclass(prop.classname)
284     l = []
285     for entry in ids:
286         if num_re.match(entry):
287             l.append(entry)
288         else:
289             try:
290                 l.append(cl.lookup(entry))
291             except (TypeError, KeyError):
292                 if fail_ok:
293                     # pass through the bad value
294                     l.append(entry)
295     return l
297 class HTMLPermissions:
298     ''' Helpers that provide answers to commonly asked Permission questions.
299     '''
300     def is_edit_ok(self):
301         ''' Is the user allowed to Edit the current class?
302         '''
303         return self._db.security.hasPermission('Edit', self._client.userid,
304             self._classname)
306     def is_view_ok(self):
307         ''' Is the user allowed to View the current class?
308         '''
309         return self._db.security.hasPermission('View', self._client.userid,
310             self._classname)
312     def is_only_view_ok(self):
313         ''' Is the user only allowed to View (ie. not Edit) the current class?
314         '''
315         return self.is_view_ok() and not self.is_edit_ok()
317     def view_check(self):
318         ''' Raise the Unauthorised exception if the user's not permitted to
319             view this class.
320         '''
321         if not self.is_view_ok():
322             raise Unauthorised("view", self._classname)
324     def edit_check(self):
325         ''' Raise the Unauthorised exception if the user's not permitted to
326             edit this class.
327         '''
328         if not self.is_edit_ok():
329             raise Unauthorised("edit", self._classname)
331 def input_html4(**attrs):
332     """Generate an 'input' (html4) element with given attributes"""
333     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
335 def input_xhtml(**attrs):
336     """Generate an 'input' (xhtml) element with given attributes"""
337     return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
339 class HTMLInputMixin:
340     ''' requires a _client property '''
341     def __init__(self):
342         html_version = 'html4'
343         if hasattr(self._client.instance.config, 'HTML_VERSION'):
344             html_version = self._client.instance.config.HTML_VERSION
345         if html_version == 'xhtml':
346             self.input = input_xhtml
347         else:
348             self.input = input_html4
350 class HTMLClass(HTMLInputMixin, HTMLPermissions):
351     ''' Accesses through a class (either through *class* or *db.<classname>*)
352     '''
353     def __init__(self, client, classname, anonymous=0):
354         self._client = client
355         self._db = client.db
356         self._anonymous = anonymous
358         # we want classname to be exposed, but _classname gives a
359         # consistent API for extending Class/Item
360         self._classname = self.classname = classname
361         self._klass = self._db.getclass(self.classname)
362         self._props = self._klass.getprops()
364         HTMLInputMixin.__init__(self)
366     def __repr__(self):
367         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
369     def __getitem__(self, item):
370         ''' return an HTMLProperty instance
371         '''
372        #print 'HTMLClass.getitem', (self, item)
374         # we don't exist
375         if item == 'id':
376             return None
378         # get the property
379         try:
380             prop = self._props[item]
381         except KeyError:
382             raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
384         # look up the correct HTMLProperty class
385         form = self._client.form
386         for klass, htmlklass in propclasses:
387             if not isinstance(prop, klass):
388                 continue
389             if form.has_key(item):
390                 if isinstance(prop, hyperdb.Multilink):
391                     value = lookupIds(self._db, prop,
392                         handleListCGIValue(form[item]), fail_ok=True)
393                 elif isinstance(prop, hyperdb.Link):
394                     value = form[item].value.strip()
395                     if value:
396                         value = lookupIds(self._db, prop, [value],
397                             fail_ok=True)[0]
398                     else:
399                         value = None
400                 else:
401                     value = form[item].value.strip() or None
402             else:
403                 if isinstance(prop, hyperdb.Multilink):
404                     value = []
405                 else:
406                     value = None
407             return htmlklass(self._client, self._classname, '', prop, item,
408                 value, self._anonymous)
410         # no good
411         raise KeyError, item
413     def __getattr__(self, attr):
414         ''' convenience access '''
415         try:
416             return self[attr]
417         except KeyError:
418             raise AttributeError, attr
420     def designator(self):
421         ''' Return this class' designator (classname) '''
422         return self._classname
424     def getItem(self, itemid, num_re=re.compile('-?\d+')):
425         ''' Get an item of this class by its item id.
426         '''
427         # make sure we're looking at an itemid
428         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
429             itemid = self._klass.lookup(itemid)
431         if self.classname == 'user':
432             klass = HTMLUser
433         else:
434             klass = HTMLItem
436         return klass(self._client, self.classname, itemid)
438     def properties(self, sort=1):
439         ''' Return HTMLProperty for all of this class' properties.
440         '''
441         l = []
442         for name, prop in self._props.items():
443             for klass, htmlklass in propclasses:
444                 if isinstance(prop, hyperdb.Multilink):
445                     value = []
446                 else:
447                     value = None
448                 if isinstance(prop, klass):
449                     l.append(htmlklass(self._client, self._classname, '',
450                         prop, name, value, self._anonymous))
451         if sort:
452             l.sort(lambda a,b:cmp(a._name, b._name))
453         return l
455     def list(self, sort_on=None):
456         ''' List all items in this class.
457         '''
458         if self.classname == 'user':
459             klass = HTMLUser
460         else:
461             klass = HTMLItem
463         # get the list and sort it nicely
464         l = self._klass.list()
465         sortfunc = make_sort_function(self._db, self.classname, sort_on)
466         l.sort(sortfunc)
468         l = [klass(self._client, self.classname, x) for x in l]
469         return l
471     def csv(self):
472         ''' Return the items of this class as a chunk of CSV text.
473         '''
474         if rcsv.error:
475             return rcsv.error
477         props = self.propnames()
478         s = StringIO.StringIO()
479         writer = rcsv.writer(s, rcsv.comma_separated)
480         writer.writerow(props)
481         for nodeid in self._klass.list():
482             l = []
483             for name in props:
484                 value = self._klass.get(nodeid, name)
485                 if value is None:
486                     l.append('')
487                 elif isinstance(value, type([])):
488                     l.append(':'.join(map(str, value)))
489                 else:
490                     l.append(str(self._klass.get(nodeid, name)))
491             writer.writerow(l)
492         return s.getvalue()
494     def propnames(self):
495         ''' Return the list of the names of the properties of this class.
496         '''
497         idlessprops = self._klass.getprops(protected=0).keys()
498         idlessprops.sort()
499         return ['id'] + idlessprops
501     def filter(self, request=None, filterspec={}, sort=(None,None),
502             group=(None,None)):
503         ''' Return a list of items from this class, filtered and sorted
504             by the current requested filterspec/filter/sort/group args
506             "request" takes precedence over the other three arguments.
507         '''
508         if request is not None:
509             filterspec = request.filterspec
510             sort = request.sort
511             group = request.group
512         if self.classname == 'user':
513             klass = HTMLUser
514         else:
515             klass = HTMLItem
516         l = [klass(self._client, self.classname, x)
517              for x in self._klass.filter(None, filterspec, sort, group)]
518         return l
520     def classhelp(self, properties=None, label='(list)', width='500',
521             height='400', property=''):
522         ''' Pop up a javascript window with class help
524             This generates a link to a popup window which displays the 
525             properties indicated by "properties" of the class named by
526             "classname". The "properties" should be a comma-separated list
527             (eg. 'id,name,description'). Properties defaults to all the
528             properties of a class (excluding id, creator, created and
529             activity).
531             You may optionally override the label displayed, the width and
532             height. The popup window will be resizable and scrollable.
534             If the "property" arg is given, it's passed through to the
535             javascript help_window function.
536         '''
537         if properties is None:
538             properties = self._klass.getprops(protected=0).keys()
539             properties.sort()
540             properties = ','.join(properties)
541         if property:
542             property = '&amp;property=%s'%property
543         return '<a class="classhelp" href="javascript:help_window(\'%s?'\
544             '@startwith=0&amp;@template=help&amp;properties=%s%s\', \'%s\', \
545             \'%s\')">%s</a>'%(self.classname, properties, property, width,
546             height, label)
548     def submit(self, label="Submit New Entry"):
549         ''' Generate a submit button (and action hidden element)
550         '''
551         self.view_check()
552         if self.is_edit_ok():
553             return self.input(type="hidden",name="@action",value="new") + \
554                    '\n' + self.input(type="submit",name="submit",value=label)
555         return ''
557     def history(self):
558         self.view_check()
559         return 'New node - no history'
561     def renderWith(self, name, **kwargs):
562         ''' Render this class with the given template.
563         '''
564         # create a new request and override the specified args
565         req = HTMLRequest(self._client)
566         req.classname = self.classname
567         req.update(kwargs)
569         # new template, using the specified classname and request
570         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
572         # use our fabricated request
573         args = {
574             'ok_message': self._client.ok_message,
575             'error_message': self._client.error_message
576         }
577         return pt.render(self._client, self.classname, req, **args)
579 class HTMLItem(HTMLInputMixin, HTMLPermissions):
580     ''' Accesses through an *item*
581     '''
582     def __init__(self, client, classname, nodeid, anonymous=0):
583         self._client = client
584         self._db = client.db
585         self._classname = classname
586         self._nodeid = nodeid
587         self._klass = self._db.getclass(classname)
588         self._props = self._klass.getprops()
590         # do we prefix the form items with the item's identification?
591         self._anonymous = anonymous
593         HTMLInputMixin.__init__(self)
595     def __repr__(self):
596         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
597             self._nodeid)
599     def __getitem__(self, item):
600         ''' return an HTMLProperty instance
601         '''
602         #print 'HTMLItem.getitem', (self, item)
603         if item == 'id':
604             return self._nodeid
606         # get the property
607         prop = self._props[item]
609         # get the value, handling missing values
610         value = None
611         if int(self._nodeid) > 0:
612             value = self._klass.get(self._nodeid, item, None)
613         if value is None:
614             if isinstance(self._props[item], hyperdb.Multilink):
615                 value = []
617         # look up the correct HTMLProperty class
618         for klass, htmlklass in propclasses:
619             if isinstance(prop, klass):
620                 return htmlklass(self._client, self._classname,
621                     self._nodeid, prop, item, value, self._anonymous)
623         raise KeyError, item
625     def __getattr__(self, attr):
626         ''' convenience access to properties '''
627         try:
628             return self[attr]
629         except KeyError:
630             raise AttributeError, attr
632     def designator(self):
633         """Return this item's designator (classname + id)."""
634         return '%s%s'%(self._classname, self._nodeid)
635     
636     def submit(self, label="Submit Changes"):
637         """Generate a submit button.
639         Also sneak in the lastactivity and action hidden elements.
640         """
641         return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
642                self.input(type="hidden", name="@action", value="edit") + '\n' + \
643                self.input(type="submit", name="submit", value=label)
645     def journal(self, direction='descending'):
646         ''' Return a list of HTMLJournalEntry instances.
647         '''
648         # XXX do this
649         return []
651     def history(self, direction='descending', dre=re.compile('\d+')):
652         self.view_check()
654         l = ['<table class="history">'
655              '<tr><th colspan="4" class="header">',
656              _('History'),
657              '</th></tr><tr>',
658              _('<th>Date</th>'),
659              _('<th>User</th>'),
660              _('<th>Action</th>'),
661              _('<th>Args</th>'),
662             '</tr>']
663         current = {}
664         comments = {}
665         history = self._klass.history(self._nodeid)
666         history.sort()
667         timezone = self._db.getUserTimezone()
668         if direction == 'descending':
669             history.reverse()
670             for prop_n in self._props.keys():
671                 prop = self[prop_n]
672                 if isinstance(prop, HTMLProperty):
673                     current[prop_n] = prop.plain()
674                     # make link if hrefable
675                     if (self._props.has_key(prop_n) and
676                             isinstance(self._props[prop_n], hyperdb.Link)):
677                         classname = self._props[prop_n].classname
678                         try:
679                             template = find_template(self._db.config.TEMPLATES,
680                                 classname, 'item')
681                             if template[1].startswith('_generic'):
682                                 raise NoTemplate, 'not really...'
683                         except NoTemplate:
684                             pass
685                         else:
686                             id = self._klass.get(self._nodeid, prop_n, None)
687                             current[prop_n] = '<a href="%s%s">%s</a>'%(
688                                 classname, id, current[prop_n])
689  
690         for id, evt_date, user, action, args in history:
691             date_s = str(evt_date.local(timezone)).replace("."," ")
692             arg_s = ''
693             if action == 'link' and type(args) == type(()):
694                 if len(args) == 3:
695                     linkcl, linkid, key = args
696                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
697                         linkcl, linkid, key)
698                 else:
699                     arg_s = str(args)
701             elif action == 'unlink' and type(args) == type(()):
702                 if len(args) == 3:
703                     linkcl, linkid, key = args
704                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
705                         linkcl, linkid, key)
706                 else:
707                     arg_s = str(args)
709             elif type(args) == type({}):
710                 cell = []
711                 for k in args.keys():
712                     # try to get the relevant property and treat it
713                     # specially
714                     try:
715                         prop = self._props[k]
716                     except KeyError:
717                         prop = None
718                     if prop is None:
719                         # property no longer exists
720                         comments['no_exist'] = _('''<em>The indicated property
721                             no longer exists</em>''')
722                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
723                         continue
725                     if args[k] and (isinstance(prop, hyperdb.Multilink) or
726                             isinstance(prop, hyperdb.Link)):
727                         # figure what the link class is
728                         classname = prop.classname
729                         try:
730                             linkcl = self._db.getclass(classname)
731                         except KeyError:
732                             labelprop = None
733                             comments[classname] = _('''The linked class
734                                 %(classname)s no longer exists''')%locals()
735                         labelprop = linkcl.labelprop(1)
736                         try:
737                             template = find_template(self._db.config.TEMPLATES,
738                                 classname, 'item')
739                             if template[1].startswith('_generic'):
740                                 raise NoTemplate, 'not really...'
741                             hrefable = 1
742                         except NoTemplate:
743                             hrefable = 0
745                     if isinstance(prop, hyperdb.Multilink) and args[k]:
746                         ml = []
747                         for linkid in args[k]:
748                             if isinstance(linkid, type(())):
749                                 sublabel = linkid[0] + ' '
750                                 linkids = linkid[1]
751                             else:
752                                 sublabel = ''
753                                 linkids = [linkid]
754                             subml = []
755                             for linkid in linkids:
756                                 label = classname + linkid
757                                 # if we have a label property, try to use it
758                                 # TODO: test for node existence even when
759                                 # there's no labelprop!
760                                 try:
761                                     if labelprop is not None and \
762                                             labelprop != 'id':
763                                         label = linkcl.get(linkid, labelprop)
764                                 except IndexError:
765                                     comments['no_link'] = _('''<strike>The
766                                         linked node no longer
767                                         exists</strike>''')
768                                     subml.append('<strike>%s</strike>'%label)
769                                 else:
770                                     if hrefable:
771                                         subml.append('<a href="%s%s">%s</a>'%(
772                                             classname, linkid, label))
773                                     else:
774                                         subml.append(label)
775                             ml.append(sublabel + ', '.join(subml))
776                         cell.append('%s:\n  %s'%(k, ', '.join(ml)))
777                     elif isinstance(prop, hyperdb.Link) and args[k]:
778                         label = classname + args[k]
779                         # if we have a label property, try to use it
780                         # TODO: test for node existence even when
781                         # there's no labelprop!
782                         if labelprop is not None and labelprop != 'id':
783                             try:
784                                 label = linkcl.get(args[k], labelprop)
785                             except IndexError:
786                                 comments['no_link'] = _('''<strike>The
787                                     linked node no longer
788                                     exists</strike>''')
789                                 cell.append(' <strike>%s</strike>,\n'%label)
790                                 # "flag" this is done .... euwww
791                                 label = None
792                         if label is not None:
793                             if hrefable:
794                                 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
795                             else:
796                                 old = label;
797                             cell.append('%s: %s' % (k,old))
798                             if current.has_key(k):
799                                 cell[-1] += ' -> %s'%current[k]
800                                 current[k] = old
802                     elif isinstance(prop, hyperdb.Date) and args[k]:
803                         d = date.Date(args[k]).local(timezone)
804                         cell.append('%s: %s'%(k, str(d)))
805                         if current.has_key(k):
806                             cell[-1] += ' -> %s' % current[k]
807                             current[k] = str(d)
809                     elif isinstance(prop, hyperdb.Interval) and args[k]:
810                         d = date.Interval(args[k])
811                         cell.append('%s: %s'%(k, str(d)))
812                         if current.has_key(k):
813                             cell[-1] += ' -> %s'%current[k]
814                             current[k] = str(d)
816                     elif isinstance(prop, hyperdb.String) and args[k]:
817                         cell.append('%s: %s'%(k, cgi.escape(args[k])))
818                         if current.has_key(k):
819                             cell[-1] += ' -> %s'%current[k]
820                             current[k] = cgi.escape(args[k])
822                     elif not args[k]:
823                         if current.has_key(k):
824                             cell.append('%s: %s'%(k, current[k]))
825                             current[k] = '(no value)'
826                         else:
827                             cell.append('%s: (no value)'%k)
829                     else:
830                         cell.append('%s: %s'%(k, str(args[k])))
831                         if current.has_key(k):
832                             cell[-1] += ' -> %s'%current[k]
833                             current[k] = str(args[k])
835                 arg_s = '<br />'.join(cell)
836             else:
837                 # unkown event!!
838                 comments['unknown'] = _('''<strong><em>This event is not
839                     handled by the history display!</em></strong>''')
840                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
841             date_s = date_s.replace(' ', '&nbsp;')
842             # if the user's an itemid, figure the username (older journals
843             # have the username)
844             if dre.match(user):
845                 user = self._db.user.get(user, 'username')
846             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
847                 date_s, user, action, arg_s))
848         if comments:
849             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
850         for entry in comments.values():
851             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
852         l.append('</table>')
853         return '\n'.join(l)
855     def renderQueryForm(self):
856         ''' Render this item, which is a query, as a search form.
857         '''
858         # create a new request and override the specified args
859         req = HTMLRequest(self._client)
860         req.classname = self._klass.get(self._nodeid, 'klass')
861         name = self._klass.get(self._nodeid, 'name')
862         req.updateFromURL(self._klass.get(self._nodeid, 'url') +
863             '&@queryname=%s'%urllib.quote(name))
865         # new template, using the specified classname and request
866         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
868         # use our fabricated request
869         return pt.render(self._client, req.classname, req)
871 class HTMLUserPermission:
873     def is_edit_ok(self):
874         ''' Is the user allowed to Edit the current class?
875             Also check whether this is the current user's info.
876         '''
877         return self._user_perm_check('Edit')
879     def is_view_ok(self):
880         ''' Is the user allowed to View the current class?
881             Also check whether this is the current user's info.
882         '''
883         return self._user_perm_check('View')
885     def _user_perm_check(self, type):
886         # some users may view / edit all users
887         s = self._db.security
888         userid = self._client.userid
889         if s.hasPermission(type, userid, self._classname):
890             return 1
892         # users may view their own info
893         is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
894         if getattr(self, '_nodeid', None) == userid and not is_anonymous:
895             return 1
897         # may anonymous users register?
898         if (is_anonymous and s.hasPermission('Web Registration', userid,
899                 self._classname)):
900             return 1
902         # nope, no access here
903         return 0
905 class HTMLUserClass(HTMLUserPermission, HTMLClass):
906     pass
908 class HTMLUser(HTMLUserPermission, HTMLItem):
909     ''' Accesses through the *user* (a special case of item)
910     '''
911     def __init__(self, client, classname, nodeid, anonymous=0):
912         HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
913         self._default_classname = client.classname
915         # used for security checks
916         self._security = client.db.security
918     _marker = []
919     def hasPermission(self, permission, classname=_marker):
920         ''' Determine if the user has the Permission.
922             The class being tested defaults to the template's class, but may
923             be overidden for this test by suppling an alternate classname.
924         '''
925         if classname is self._marker:
926             classname = self._default_classname
927         return self._security.hasPermission(permission, self._nodeid, classname)
929 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
930     ''' String, Number, Date, Interval HTMLProperty
932         Has useful attributes:
934          _name  the name of the property
935          _value the value of the property if any
937         A wrapper object which may be stringified for the plain() behaviour.
938     '''
939     def __init__(self, client, classname, nodeid, prop, name, value,
940             anonymous=0):
941         self._client = client
942         self._db = client.db
943         self._classname = classname
944         self._nodeid = nodeid
945         self._prop = prop
946         self._value = value
947         self._anonymous = anonymous
948         self._name = name
949         if not anonymous:
950             self._formname = '%s%s@%s'%(classname, nodeid, name)
951         else:
952             self._formname = name
954         HTMLInputMixin.__init__(self)
956     def __repr__(self):
957         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
958             self._prop, self._value)
959     def __str__(self):
960         return self.plain()
961     def __cmp__(self, other):
962         if isinstance(other, HTMLProperty):
963             return cmp(self._value, other._value)
964         return cmp(self._value, other)
966     def is_edit_ok(self):
967         ''' Is the user allowed to Edit the current class?
968         '''
969         thing = HTMLDatabase(self._client)[self._classname]
970         if self._nodeid:
971             # this is a special-case for the User class where permission's
972             # on a per-item basis :(
973             thing = thing.getItem(self._nodeid)
974         return thing.is_edit_ok()
976     def is_view_ok(self):
977         ''' Is the user allowed to View the current class?
978         '''
979         thing = HTMLDatabase(self._client)[self._classname]
980         if self._nodeid:
981             # this is a special-case for the User class where permission's
982             # on a per-item basis :(
983             thing = thing.getItem(self._nodeid)
984         return thing.is_view_ok()
986 class StringHTMLProperty(HTMLProperty):
987     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
988                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
989                           r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
990     def _hyper_repl(self, match):
991         if match.group('url'):
992             s = match.group('url')
993             return '<a href="%s">%s</a>'%(s, s)
994         elif match.group('email'):
995             s = match.group('email')
996             return '<a href="mailto:%s">%s</a>'%(s, s)
997         else:
998             s = match.group('item')
999             s1 = match.group('class')
1000             s2 = match.group('id')
1001             try:
1002                 # make sure s1 is a valid tracker classname
1003                 cl = self._db.getclass(s1)
1004                 if not cl.hasnode(s2):
1005                     raise KeyError, 'oops'
1006                 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1007             except KeyError:
1008                 return '%s%s'%(s1, s2)
1010     def hyperlinked(self):
1011         ''' Render a "hyperlinked" version of the text '''
1012         return self.plain(hyperlink=1)
1014     def plain(self, escape=0, hyperlink=0):
1015         '''Render a "plain" representation of the property
1016             
1017         - "escape" turns on/off HTML quoting
1018         - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1019           addresses and designators
1020         '''
1021         self.view_check()
1023         if self._value is None:
1024             return ''
1025         if escape:
1026             s = cgi.escape(str(self._value))
1027         else:
1028             s = str(self._value)
1029         if hyperlink:
1030             # no, we *must* escape this text
1031             if not escape:
1032                 s = cgi.escape(s)
1033             s = self.hyper_re.sub(self._hyper_repl, s)
1034         return s
1036     def stext(self, escape=0):
1037         ''' Render the value of the property as StructuredText.
1039             This requires the StructureText module to be installed separately.
1040         '''
1041         self.view_check()
1043         s = self.plain(escape=escape)
1044         if not StructuredText:
1045             return s
1046         return StructuredText(s,level=1,header=0)
1048     def field(self, size = 30):
1049         ''' Render the property as a field in HTML.
1051             If not editable, just display the value via plain().
1052         '''
1053         self.view_check()
1055         if self._value is None:
1056             value = ''
1057         else:
1058             value = cgi.escape(str(self._value))
1060         if self.is_edit_ok():
1061             value = '&quot;'.join(value.split('"'))
1062             return self.input(name=self._formname,value=value,size=size)
1064         return self.plain()
1066     def multiline(self, escape=0, rows=5, cols=40):
1067         ''' Render a multiline form edit field for the property.
1069             If not editable, just display the plain() value in a <pre> tag.
1070         '''
1071         self.view_check()
1073         if self._value is None:
1074             value = ''
1075         else:
1076             value = cgi.escape(str(self._value))
1078         if self.is_edit_ok():
1079             value = '&quot;'.join(value.split('"'))
1080             return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1081                 self._formname, rows, cols, value)
1083         return '<pre>%s</pre>'%self.plain()
1085     def email(self, escape=1):
1086         ''' Render the value of the property as an obscured email address
1087         '''
1088         self.view_check()
1090         if self._value is None:
1091             value = ''
1092         else:
1093             value = str(self._value)
1094         if value.find('@') != -1:
1095             name, domain = value.split('@')
1096             domain = ' '.join(domain.split('.')[:-1])
1097             name = name.replace('.', ' ')
1098             value = '%s at %s ...'%(name, domain)
1099         else:
1100             value = value.replace('.', ' ')
1101         if escape:
1102             value = cgi.escape(value)
1103         return value
1105 class PasswordHTMLProperty(HTMLProperty):
1106     def plain(self):
1107         ''' Render a "plain" representation of the property
1108         '''
1109         self.view_check()
1111         if self._value is None:
1112             return ''
1113         return _('*encrypted*')
1115     def field(self, size = 30):
1116         ''' Render a form edit field for the property.
1118             If not editable, just display the value via plain().
1119         '''
1120         self.view_check()
1122         if self.is_edit_ok():
1123             return self.input(type="password", name=self._formname, size=size)
1125         return self.plain()
1127     def confirm(self, size = 30):
1128         ''' Render a second form edit field for the property, used for 
1129             confirmation that the user typed the password correctly. Generates
1130             a field with name "@confirm@name".
1132             If not editable, display nothing.
1133         '''
1134         self.view_check()
1136         if self.is_edit_ok():
1137             return self.input(type="password",
1138                 name="@confirm@%s"%self._formname, size=size)
1140         return ''
1142 class NumberHTMLProperty(HTMLProperty):
1143     def plain(self):
1144         ''' Render a "plain" representation of the property
1145         '''
1146         self.view_check()
1148         return str(self._value)
1150     def field(self, size = 30):
1151         ''' Render a form edit field for the property.
1153             If not editable, just display the value via plain().
1154         '''
1155         self.view_check()
1157         if self._value is None:
1158             value = ''
1159         else:
1160             value = cgi.escape(str(self._value))
1162         if self.is_edit_ok():
1163             value = '&quot;'.join(value.split('"'))
1164             return self.input(name=self._formname,value=value,size=size)
1166         return self.plain()
1168     def __int__(self):
1169         ''' Return an int of me
1170         '''
1171         return int(self._value)
1173     def __float__(self):
1174         ''' Return a float of me
1175         '''
1176         return float(self._value)
1179 class BooleanHTMLProperty(HTMLProperty):
1180     def plain(self):
1181         ''' Render a "plain" representation of the property
1182         '''
1183         self.view_check()
1185         if self._value is None:
1186             return ''
1187         return self._value and "Yes" or "No"
1189     def field(self):
1190         ''' Render a form edit field for the property
1192             If not editable, just display the value via plain().
1193         '''
1194         self.view_check()
1196         if not is_edit_ok():
1197             return self.plain()
1199         checked = self._value and "checked" or ""
1200         if self._value:
1201             s = self.input(type="radio", name=self._formname, value="yes",
1202                 checked="checked")
1203             s += 'Yes'
1204             s +=self.input(type="radio", name=self._formname, value="no")
1205             s += 'No'
1206         else:
1207             s = self.input(type="radio", name=self._formname, value="yes")
1208             s += 'Yes'
1209             s +=self.input(type="radio", name=self._formname, value="no",
1210                 checked="checked")
1211             s += 'No'
1212         return s
1214 class DateHTMLProperty(HTMLProperty):
1215     def plain(self):
1216         ''' Render a "plain" representation of the property
1217         '''
1218         self.view_check()
1220         if self._value is None:
1221             return ''
1222         return str(self._value.local(self._db.getUserTimezone()))
1224     def now(self):
1225         ''' Return the current time.
1227             This is useful for defaulting a new value. Returns a
1228             DateHTMLProperty.
1229         '''
1230         self.view_check()
1232         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1233             self._prop, self._formname, date.Date('.'))
1235     def field(self, size = 30):
1236         ''' Render a form edit field for the property
1238             If not editable, just display the value via plain().
1239         '''
1240         self.view_check()
1242         if self._value is None:
1243             value = ''
1244         else:
1245             tz = self._db.getUserTimezone()
1246             value = cgi.escape(str(self._value.local(tz)))
1248         if is_edit_ok():
1249             value = '&quot;'.join(value.split('"'))
1250             return self.input(name=self._formname,value=value,size=size)
1251         
1252         return self.plain()
1254     def reldate(self, pretty=1):
1255         ''' Render the interval between the date and now.
1257             If the "pretty" flag is true, then make the display pretty.
1258         '''
1259         self.view_check()
1261         if not self._value:
1262             return ''
1264         # figure the interval
1265         interval = self._value - date.Date('.')
1266         if pretty:
1267             return interval.pretty()
1268         return str(interval)
1270     _marker = []
1271     def pretty(self, format=_marker):
1272         ''' Render the date in a pretty format (eg. month names, spaces).
1274             The format string is a standard python strftime format string.
1275             Note that if the day is zero, and appears at the start of the
1276             string, then it'll be stripped from the output. This is handy
1277             for the situatin when a date only specifies a month and a year.
1278         '''
1279         self.view_check()
1281         if format is not self._marker:
1282             return self._value.pretty(format)
1283         else:
1284             return self._value.pretty()
1286     def local(self, offset):
1287         ''' Return the date/time as a local (timezone offset) date/time.
1288         '''
1289         self.view_check()
1291         return DateHTMLProperty(self._client, self._classname, self._nodeid,
1292             self._prop, self._formname, self._value.local(offset))
1294 class IntervalHTMLProperty(HTMLProperty):
1295     def plain(self):
1296         ''' Render a "plain" representation of the property
1297         '''
1298         self.view_check()
1300         if self._value is None:
1301             return ''
1302         return str(self._value)
1304     def pretty(self):
1305         ''' Render the interval in a pretty format (eg. "yesterday")
1306         '''
1307         self.view_check()
1309         return self._value.pretty()
1311     def field(self, size = 30):
1312         ''' Render a form edit field for the property
1314             If not editable, just display the value via plain().
1315         '''
1316         self.view_check()
1318         if self._value is None:
1319             value = ''
1320         else:
1321             value = cgi.escape(str(self._value))
1323         if is_edit_ok():
1324             value = '&quot;'.join(value.split('"'))
1325             return self.input(name=self._formname,value=value,size=size)
1327         return self.plain()
1329 class LinkHTMLProperty(HTMLProperty):
1330     ''' Link HTMLProperty
1331         Include the above as well as being able to access the class
1332         information. Stringifying the object itself results in the value
1333         from the item being displayed. Accessing attributes of this object
1334         result in the appropriate entry from the class being queried for the
1335         property accessed (so item/assignedto/name would look up the user
1336         entry identified by the assignedto property on item, and then the
1337         name property of that user)
1338     '''
1339     def __init__(self, *args, **kw):
1340         HTMLProperty.__init__(self, *args, **kw)
1341         # if we're representing a form value, then the -1 from the form really
1342         # should be a None
1343         if str(self._value) == '-1':
1344             self._value = None
1346     def __getattr__(self, attr):
1347         ''' return a new HTMLItem '''
1348        #print 'Link.getattr', (self, attr, self._value)
1349         if not self._value:
1350             raise AttributeError, "Can't access missing value"
1351         if self._prop.classname == 'user':
1352             klass = HTMLUser
1353         else:
1354             klass = HTMLItem
1355         i = klass(self._client, self._prop.classname, self._value)
1356         return getattr(i, attr)
1358     def plain(self, escape=0):
1359         ''' Render a "plain" representation of the property
1360         '''
1361         self.view_check()
1363         if self._value is None:
1364             return ''
1365         linkcl = self._db.classes[self._prop.classname]
1366         k = linkcl.labelprop(1)
1367         value = str(linkcl.get(self._value, k))
1368         if escape:
1369             value = cgi.escape(value)
1370         return value
1372     def field(self, showid=0, size=None):
1373         ''' Render a form edit field for the property
1375             If not editable, just display the value via plain().
1376         '''
1377         self.view_check()
1379         if not self.is_edit_ok():
1380             return self.plain()
1382         # edit field
1383         linkcl = self._db.getclass(self._prop.classname)
1384         if self._value is None:
1385             value = ''
1386         else:
1387             k = linkcl.getkey()
1388             if k:
1389                 label = linkcl.get(self._value, k)
1390             else:
1391                 label = self._value
1392             value = cgi.escape(str(self._value))
1393             value = '&quot;'.join(value.split('"'))
1394         return '<input name="%s" value="%s" size="%s">'%(self._formname,
1395             label, size)
1397     def menu(self, size=None, height=None, showid=0, additional=[],
1398             sort_on=None, **conditions):
1399         ''' Render a form select list for this property
1401             If not editable, just display the value via plain().
1402         '''
1403         self.view_check()
1405         if not self.is_edit_ok():
1406             return self.plain()
1408         value = self._value
1410         linkcl = self._db.getclass(self._prop.classname)
1411         l = ['<select name="%s">'%self._formname]
1412         k = linkcl.labelprop(1)
1413         s = ''
1414         if value is None:
1415             s = 'selected="selected" '
1416         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1417         if linkcl.getprops().has_key('order'):  
1418             sort_on = ('+', 'order')
1419         else:  
1420             if sort_on is None:
1421                 sort_on = ('+', linkcl.labelprop())
1422             else:
1423                 sort_on = ('+', sort_on)
1424         options = linkcl.filter(None, conditions, sort_on, (None, None))
1426         # make sure we list the current value if it's retired
1427         if self._value and self._value not in options:
1428             options.insert(0, self._value)
1430         for optionid in options:
1431             # get the option value, and if it's None use an empty string
1432             option = linkcl.get(optionid, k) or ''
1434             # figure if this option is selected
1435             s = ''
1436             if value in [optionid, option]:
1437                 s = 'selected="selected" '
1439             # figure the label
1440             if showid:
1441                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1442             else:
1443                 lab = option
1445             # truncate if it's too long
1446             if size is not None and len(lab) > size:
1447                 lab = lab[:size-3] + '...'
1448             if additional:
1449                 m = []
1450                 for propname in additional:
1451                     m.append(linkcl.get(optionid, propname))
1452                 lab = lab + ' (%s)'%', '.join(map(str, m))
1454             # and generate
1455             lab = cgi.escape(lab)
1456             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1457         l.append('</select>')
1458         return '\n'.join(l)
1459 #    def checklist(self, ...)
1461 class MultilinkHTMLProperty(HTMLProperty):
1462     ''' Multilink HTMLProperty
1464         Also be iterable, returning a wrapper object like the Link case for
1465         each entry in the multilink.
1466     '''
1467     def __init__(self, *args, **kwargs):
1468         HTMLProperty.__init__(self, *args, **kwargs)
1469         if self._value:
1470             sortfun = make_sort_function(self._db, self._prop.classname)
1471             self._value.sort(sortfun)
1472     
1473     def __len__(self):
1474         ''' length of the multilink '''
1475         return len(self._value)
1477     def __getattr__(self, attr):
1478         ''' no extended attribute accesses make sense here '''
1479         raise AttributeError, attr
1481     def __getitem__(self, num):
1482         ''' iterate and return a new HTMLItem
1483         '''
1484        #print 'Multi.getitem', (self, num)
1485         value = self._value[num]
1486         if self._prop.classname == 'user':
1487             klass = HTMLUser
1488         else:
1489             klass = HTMLItem
1490         return klass(self._client, self._prop.classname, value)
1492     def __contains__(self, value):
1493         ''' Support the "in" operator. We have to make sure the passed-in
1494             value is a string first, not a HTMLProperty.
1495         '''
1496         return str(value) in self._value
1498     def reverse(self):
1499         ''' return the list in reverse order
1500         '''
1501         l = self._value[:]
1502         l.reverse()
1503         if self._prop.classname == 'user':
1504             klass = HTMLUser
1505         else:
1506             klass = HTMLItem
1507         return [klass(self._client, self._prop.classname, value) for value in l]
1509     def plain(self, escape=0):
1510         ''' Render a "plain" representation of the property
1511         '''
1512         self.view_check()
1514         linkcl = self._db.classes[self._prop.classname]
1515         k = linkcl.labelprop(1)
1516         labels = []
1517         for v in self._value:
1518             labels.append(linkcl.get(v, k))
1519         value = ', '.join(labels)
1520         if escape:
1521             value = cgi.escape(value)
1522         return value
1524     def field(self, size=30, showid=0):
1525         ''' Render a form edit field for the property
1527             If not editable, just display the value via plain().
1528         '''
1529         self.view_check()
1531         if not self.is_edit_ok():
1532             return self.plain()
1534         linkcl = self._db.getclass(self._prop.classname)
1535         value = self._value[:]
1536         # map the id to the label property
1537         if not linkcl.getkey():
1538             showid=1
1539         if not showid:
1540             k = linkcl.labelprop(1)
1541             value = [linkcl.get(v, k) for v in value]
1542         value = cgi.escape(','.join(value))
1543         return self.input(name=self._formname,size=size,value=value)
1545     def menu(self, size=None, height=None, showid=0, additional=[],
1546             sort_on=None, **conditions):
1547         ''' Render a form select list for this property
1549             If not editable, just display the value via plain().
1550         '''
1551         self.view_check()
1553         if not self.is_edit_ok():
1554             return self.plain()
1556         value = self._value
1558         linkcl = self._db.getclass(self._prop.classname)
1559         if sort_on is None:
1560             sort_on = ('+', find_sort_key(linkcl))
1561         else:
1562             sort_on = ('+', sort_on)
1563         options = linkcl.filter(None, conditions, sort_on)
1564         height = height or min(len(options), 7)
1565         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1566         k = linkcl.labelprop(1)
1568         # make sure we list the current values if they're retired
1569         for val in value:
1570             if val not in options:
1571                 options.insert(0, val)
1573         for optionid in options:
1574             # get the option value, and if it's None use an empty string
1575             option = linkcl.get(optionid, k) or ''
1577             # figure if this option is selected
1578             s = ''
1579             if optionid in value or option in value:
1580                 s = 'selected="selected" '
1582             # figure the label
1583             if showid:
1584                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1585             else:
1586                 lab = option
1587             # truncate if it's too long
1588             if size is not None and len(lab) > size:
1589                 lab = lab[:size-3] + '...'
1590             if additional:
1591                 m = []
1592                 for propname in additional:
1593                     m.append(linkcl.get(optionid, propname))
1594                 lab = lab + ' (%s)'%', '.join(m)
1596             # and generate
1597             lab = cgi.escape(lab)
1598             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1599                 lab))
1600         l.append('</select>')
1601         return '\n'.join(l)
1603 # set the propclasses for HTMLItem
1604 propclasses = (
1605     (hyperdb.String, StringHTMLProperty),
1606     (hyperdb.Number, NumberHTMLProperty),
1607     (hyperdb.Boolean, BooleanHTMLProperty),
1608     (hyperdb.Date, DateHTMLProperty),
1609     (hyperdb.Interval, IntervalHTMLProperty),
1610     (hyperdb.Password, PasswordHTMLProperty),
1611     (hyperdb.Link, LinkHTMLProperty),
1612     (hyperdb.Multilink, MultilinkHTMLProperty),
1615 def make_sort_function(db, classname, sort_on=None):
1616     '''Make a sort function for a given class
1617     '''
1618     linkcl = db.getclass(classname)
1619     if sort_on is None:
1620         sort_on = find_sort_key(linkcl)
1621     def sortfunc(a, b):
1622         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1623     return sortfunc
1625 def find_sort_key(linkcl):
1626     if linkcl.getprops().has_key('order'):
1627         return 'order'
1628     else:
1629         return linkcl.labelprop()
1631 def handleListCGIValue(value):
1632     ''' Value is either a single item or a list of items. Each item has a
1633         .value that we're actually interested in.
1634     '''
1635     if isinstance(value, type([])):
1636         return [value.value for value in value]
1637     else:
1638         value = value.value.strip()
1639         if not value:
1640             return []
1641         return value.split(',')
1643 class ShowDict:
1644     ''' A convenience access to the :columns index parameters
1645     '''
1646     def __init__(self, columns):
1647         self.columns = {}
1648         for col in columns:
1649             self.columns[col] = 1
1650     def __getitem__(self, name):
1651         return self.columns.has_key(name)
1653 class HTMLRequest(HTMLInputMixin):
1654     '''The *request*, holding the CGI form and environment.
1656     - "form" the CGI form as a cgi.FieldStorage
1657     - "env" the CGI environment variables
1658     - "base" the base URL for this instance
1659     - "user" a HTMLUser instance for this user
1660     - "classname" the current classname (possibly None)
1661     - "template" the current template (suffix, also possibly None)
1663     Index args:
1665     - "columns" dictionary of the columns to display in an index page
1666     - "show" a convenience access to columns - request/show/colname will
1667       be true if the columns should be displayed, false otherwise
1668     - "sort" index sort column (direction, column name)
1669     - "group" index grouping property (direction, column name)
1670     - "filter" properties to filter the index on
1671     - "filterspec" values to filter the index on
1672     - "search_text" text to perform a full-text search on for an index
1673     '''
1674     def __init__(self, client):
1675         # _client is needed by HTMLInputMixin
1676         self._client = self.client = client
1678         # easier access vars
1679         self.form = client.form
1680         self.env = client.env
1681         self.base = client.base
1682         self.user = HTMLUser(client, 'user', client.userid)
1684         # store the current class name and action
1685         self.classname = client.classname
1686         self.template = client.template
1688         # the special char to use for special vars
1689         self.special_char = '@'
1691         HTMLInputMixin.__init__(self)
1693         self._post_init()
1695     def _post_init(self):
1696         ''' Set attributes based on self.form
1697         '''
1698         # extract the index display information from the form
1699         self.columns = []
1700         for name in ':columns @columns'.split():
1701             if self.form.has_key(name):
1702                 self.special_char = name[0]
1703                 self.columns = handleListCGIValue(self.form[name])
1704                 break
1705         self.show = ShowDict(self.columns)
1707         # sorting
1708         self.sort = (None, None)
1709         for name in ':sort @sort'.split():
1710             if self.form.has_key(name):
1711                 self.special_char = name[0]
1712                 sort = self.form[name].value
1713                 if sort.startswith('-'):
1714                     self.sort = ('-', sort[1:])
1715                 else:
1716                     self.sort = ('+', sort)
1717                 if self.form.has_key(self.special_char+'sortdir'):
1718                     self.sort = ('-', self.sort[1])
1720         # grouping
1721         self.group = (None, None)
1722         for name in ':group @group'.split():
1723             if self.form.has_key(name):
1724                 self.special_char = name[0]
1725                 group = self.form[name].value
1726                 if group.startswith('-'):
1727                     self.group = ('-', group[1:])
1728                 else:
1729                     self.group = ('+', group)
1730                 if self.form.has_key(self.special_char+'groupdir'):
1731                     self.group = ('-', self.group[1])
1733         # filtering
1734         self.filter = []
1735         for name in ':filter @filter'.split():
1736             if self.form.has_key(name):
1737                 self.special_char = name[0]
1738                 self.filter = handleListCGIValue(self.form[name])
1740         self.filterspec = {}
1741         db = self.client.db
1742         if self.classname is not None:
1743             props = db.getclass(self.classname).getprops()
1744             for name in self.filter:
1745                 if not self.form.has_key(name):
1746                     continue
1747                 prop = props[name]
1748                 fv = self.form[name]
1749                 if (isinstance(prop, hyperdb.Link) or
1750                         isinstance(prop, hyperdb.Multilink)):
1751                     self.filterspec[name] = lookupIds(db, prop,
1752                         handleListCGIValue(fv))
1753                 else:
1754                     if isinstance(fv, type([])):
1755                         self.filterspec[name] = [v.value for v in fv]
1756                     else:
1757                         self.filterspec[name] = fv.value
1759         # full-text search argument
1760         self.search_text = None
1761         for name in ':search_text @search_text'.split():
1762             if self.form.has_key(name):
1763                 self.special_char = name[0]
1764                 self.search_text = self.form[name].value
1766         # pagination - size and start index
1767         # figure batch args
1768         self.pagesize = 50
1769         for name in ':pagesize @pagesize'.split():
1770             if self.form.has_key(name):
1771                 self.special_char = name[0]
1772                 self.pagesize = int(self.form[name].value)
1774         self.startwith = 0
1775         for name in ':startwith @startwith'.split():
1776             if self.form.has_key(name):
1777                 self.special_char = name[0]
1778                 self.startwith = int(self.form[name].value)
1780     def updateFromURL(self, url):
1781         ''' Parse the URL for query args, and update my attributes using the
1782             values.
1783         ''' 
1784         env = {'QUERY_STRING': url}
1785         self.form = cgi.FieldStorage(environ=env)
1787         self._post_init()
1789     def update(self, kwargs):
1790         ''' Update my attributes using the keyword args
1791         '''
1792         self.__dict__.update(kwargs)
1793         if kwargs.has_key('columns'):
1794             self.show = ShowDict(self.columns)
1796     def description(self):
1797         ''' Return a description of the request - handle for the page title.
1798         '''
1799         s = [self.client.db.config.TRACKER_NAME]
1800         if self.classname:
1801             if self.client.nodeid:
1802                 s.append('- %s%s'%(self.classname, self.client.nodeid))
1803             else:
1804                 if self.template == 'item':
1805                     s.append('- new %s'%self.classname)
1806                 elif self.template == 'index':
1807                     s.append('- %s index'%self.classname)
1808                 else:
1809                     s.append('- %s %s'%(self.classname, self.template))
1810         else:
1811             s.append('- home')
1812         return ' '.join(s)
1814     def __str__(self):
1815         d = {}
1816         d.update(self.__dict__)
1817         f = ''
1818         for k in self.form.keys():
1819             f += '\n      %r=%r'%(k,handleListCGIValue(self.form[k]))
1820         d['form'] = f
1821         e = ''
1822         for k,v in self.env.items():
1823             e += '\n     %r=%r'%(k, v)
1824         d['env'] = e
1825         return '''
1826 form: %(form)s
1827 base: %(base)r
1828 classname: %(classname)r
1829 template: %(template)r
1830 columns: %(columns)r
1831 sort: %(sort)r
1832 group: %(group)r
1833 filter: %(filter)r
1834 search_text: %(search_text)r
1835 pagesize: %(pagesize)r
1836 startwith: %(startwith)r
1837 env: %(env)s
1838 '''%d
1840     def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1841             filterspec=1):
1842         ''' return the current index args as form elements '''
1843         l = []
1844         sc = self.special_char
1845         s = self.input(type="hidden",name="%s",value="%s")
1846         if columns and self.columns:
1847             l.append(s%(sc+'columns', ','.join(self.columns)))
1848         if sort and self.sort[1] is not None:
1849             if self.sort[0] == '-':
1850                 val = '-'+self.sort[1]
1851             else:
1852                 val = self.sort[1]
1853             l.append(s%(sc+'sort', val))
1854         if group and self.group[1] is not None:
1855             if self.group[0] == '-':
1856                 val = '-'+self.group[1]
1857             else:
1858                 val = self.group[1]
1859             l.append(s%(sc+'group', val))
1860         if filter and self.filter:
1861             l.append(s%(sc+'filter', ','.join(self.filter)))
1862         if filterspec:
1863             for k,v in self.filterspec.items():
1864                 if type(v) == type([]):
1865                     l.append(s%(k, ','.join(v)))
1866                 else:
1867                     l.append(s%(k, v))
1868         if self.search_text:
1869             l.append(s%(sc+'search_text', self.search_text))
1870         l.append(s%(sc+'pagesize', self.pagesize))
1871         l.append(s%(sc+'startwith', self.startwith))
1872         return '\n'.join(l)
1874     def indexargs_url(self, url, args):
1875         ''' Embed the current index args in a URL
1876         '''
1877         sc = self.special_char
1878         l = ['%s=%s'%(k,v) for k,v in args.items()]
1880         # pull out the special values (prefixed by @ or :)
1881         specials = {}
1882         for key in args.keys():
1883             if key[0] in '@:':
1884                 specials[key[1:]] = args[key]
1886         # ok, now handle the specials we received in the request
1887         if self.columns and not specials.has_key('columns'):
1888             l.append(sc+'columns=%s'%(','.join(self.columns)))
1889         if self.sort[1] is not None and not specials.has_key('sort'):
1890             if self.sort[0] == '-':
1891                 val = '-'+self.sort[1]
1892             else:
1893                 val = self.sort[1]
1894             l.append(sc+'sort=%s'%val)
1895         if self.group[1] is not None and not specials.has_key('group'):
1896             if self.group[0] == '-':
1897                 val = '-'+self.group[1]
1898             else:
1899                 val = self.group[1]
1900             l.append(sc+'group=%s'%val)
1901         if self.filter and not specials.has_key('filter'):
1902             l.append(sc+'filter=%s'%(','.join(self.filter)))
1903         if self.search_text and not specials.has_key('search_text'):
1904             l.append(sc+'search_text=%s'%self.search_text)
1905         if not specials.has_key('pagesize'):
1906             l.append(sc+'pagesize=%s'%self.pagesize)
1907         if not specials.has_key('startwith'):
1908             l.append(sc+'startwith=%s'%self.startwith)
1910         # finally, the remainder of the filter args in the request
1911         for k,v in self.filterspec.items():
1912             if not args.has_key(k):
1913                 if type(v) == type([]):
1914                     l.append('%s=%s'%(k, ','.join(v)))
1915                 else:
1916                     l.append('%s=%s'%(k, v))
1917         return '%s?%s'%(url, '&'.join(l))
1918     indexargs_href = indexargs_url
1920     def base_javascript(self):
1921         return '''
1922 <script type="text/javascript">
1923 submitted = false;
1924 function submit_once() {
1925     if (submitted) {
1926         alert("Your request is being processed.\\nPlease be patient.");
1927         event.returnValue = 0;    // work-around for IE
1928         return 0;
1929     }
1930     submitted = true;
1931     return 1;
1934 function help_window(helpurl, width, height) {
1935     HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1937 </script>
1938 '''%self.base
1940     def batch(self):
1941         ''' Return a batch object for results from the "current search"
1942         '''
1943         filterspec = self.filterspec
1944         sort = self.sort
1945         group = self.group
1947         # get the list of ids we're batching over
1948         klass = self.client.db.getclass(self.classname)
1949         if self.search_text:
1950             matches = self.client.db.indexer.search(
1951                 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1952         else:
1953             matches = None
1954         l = klass.filter(matches, filterspec, sort, group)
1956         # return the batch object, using IDs only
1957         return Batch(self.client, l, self.pagesize, self.startwith,
1958             classname=self.classname)
1960 # extend the standard ZTUtils Batch object to remove dependency on
1961 # Acquisition and add a couple of useful methods
1962 class Batch(ZTUtils.Batch):
1963     ''' Use me to turn a list of items, or item ids of a given class, into a
1964         series of batches.
1966         ========= ========================================================
1967         Parameter  Usage
1968         ========= ========================================================
1969         sequence  a list of HTMLItems or item ids
1970         classname if sequence is a list of ids, this is the class of item
1971         size      how big to make the sequence.
1972         start     where to start (0-indexed) in the sequence.
1973         end       where to end (0-indexed) in the sequence.
1974         orphan    if the next batch would contain less items than this
1975                   value, then it is combined with this batch
1976         overlap   the number of items shared between adjacent batches
1977         ========= ========================================================
1979         Attributes: Note that the "start" attribute, unlike the
1980         argument, is a 1-based index (I know, lame).  "first" is the
1981         0-based index.  "length" is the actual number of elements in
1982         the batch.
1984         "sequence_length" is the length of the original, unbatched, sequence.
1985     '''
1986     def __init__(self, client, sequence, size, start, end=0, orphan=0,
1987             overlap=0, classname=None):
1988         self.client = client
1989         self.last_index = self.last_item = None
1990         self.current_item = None
1991         self.classname = classname
1992         self.sequence_length = len(sequence)
1993         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1994             overlap)
1996     # overwrite so we can late-instantiate the HTMLItem instance
1997     def __getitem__(self, index):
1998         if index < 0:
1999             if index + self.end < self.first: raise IndexError, index
2000             return self._sequence[index + self.end]
2001         
2002         if index >= self.length:
2003             raise IndexError, index
2005         # move the last_item along - but only if the fetched index changes
2006         # (for some reason, index 0 is fetched twice)
2007         if index != self.last_index:
2008             self.last_item = self.current_item
2009             self.last_index = index
2011         item = self._sequence[index + self.first]
2012         if self.classname:
2013             # map the item ids to instances
2014             if self.classname == 'user':
2015                 item = HTMLUser(self.client, self.classname, item)
2016             else:
2017                 item = HTMLItem(self.client, self.classname, item)
2018         self.current_item = item
2019         return item
2021     def propchanged(self, property):
2022         ''' Detect if the property marked as being the group property
2023             changed in the last iteration fetch
2024         '''
2025         if (self.last_item is None or
2026                 self.last_item[property] != self.current_item[property]):
2027             return 1
2028         return 0
2030     # override these 'cos we don't have access to acquisition
2031     def previous(self):
2032         if self.start == 1:
2033             return None
2034         return Batch(self.client, self._sequence, self._size,
2035             self.first - self._size + self.overlap, 0, self.orphan,
2036             self.overlap)
2038     def next(self):
2039         try:
2040             self._sequence[self.end]
2041         except IndexError:
2042             return None
2043         return Batch(self.client, self._sequence, self._size,
2044             self.end - self.overlap, 0, self.orphan, self.overlap)
2046 class TemplatingUtils:
2047     ''' Utilities for templating
2048     '''
2049     def __init__(self, client):
2050         self.client = client
2051     def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2052         return Batch(self.client, sequence, size, start, end, orphan,
2053             overlap)