Code

much nicer error messages when there's a templating error
[roundup.git] / roundup / cgi / templating.py
index 54a592b6341fa30cc21d1eeffc55e4c801edaa48..d29f31d6600aac87eae1d8eae92f55a511c90a2b 100644 (file)
-import sys, cgi, urllib, os, re
+import sys, cgi, urllib, os, re, os.path, time, errno
 
 from roundup import hyperdb, date
 from roundup.i18n import _
 
 
 from roundup import hyperdb, date
 from roundup.i18n import _
 
-
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
 try:
     import StructuredText
 except ImportError:
     StructuredText = None
 
 try:
     import StructuredText
 except ImportError:
     StructuredText = None
 
-# Make sure these modules are loaded
-# I need these to run PageTemplates outside of Zope :(
-# If we're running in a Zope environment, these modules will be loaded
-# already...
-if not sys.modules.has_key('zLOG'):
-    import zLOG
-    sys.modules['zLOG'] = zLOG
-if not sys.modules.has_key('MultiMapping'):
-    import MultiMapping
-    sys.modules['MultiMapping'] = MultiMapping
-if not sys.modules.has_key('ComputedAttribute'):
-    import ComputedAttribute
-    sys.modules['ComputedAttribute'] = ComputedAttribute
-if not sys.modules.has_key('ExtensionClass'):
-    import ExtensionClass
-    sys.modules['ExtensionClass'] = ExtensionClass
-if not sys.modules.has_key('Acquisition'):
-    import Acquisition
-    sys.modules['Acquisition'] = Acquisition
-
-# now it's safe to import PageTemplates and ZTUtils
-from PageTemplates import PageTemplate
-import ZTUtils
+# bring in the templating support
+from roundup.cgi.PageTemplates import PageTemplate
+from roundup.cgi.PageTemplates.Expressions import getEngine
+from roundup.cgi.TAL.TALInterpreter import TALInterpreter
+from roundup.cgi import ZTUtils
+
+# XXX WAH pagetemplates aren't pickleable :(
+#def getTemplate(dir, name, classname=None, request=None):
+#    ''' Interface to get a template, possibly loading a compiled template.
+#    '''
+#    # source
+#    src = os.path.join(dir, name)
+#
+#    # see if we can get a compile from the template"c" directory (most
+#    # likely is "htmlc"
+#    split = list(os.path.split(dir))
+#    split[-1] = split[-1] + 'c'
+#    cdir = os.path.join(*split)
+#    split.append(name)
+#    cpl = os.path.join(*split)
+#
+#    # ok, now see if the source is newer than the compiled (or if the
+#    # compiled even exists)
+#    MTIME = os.path.stat.ST_MTIME
+#    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
+#        # nope, we need to compile
+#        pt = RoundupPageTemplate()
+#        pt.write(open(src).read())
+#        pt.id = name
+#
+#        # save off the compiled template
+#        if not os.path.exists(cdir):
+#            os.makedirs(cdir)
+#        f = open(cpl, 'wb')
+#        pickle.dump(pt, f)
+#        f.close()
+#    else:
+#        # yay, use the compiled template
+#        f = open(cpl, 'rb')
+#        pt = pickle.load(f)
+#    return pt
+
+templates = {}
+
+def getTemplate(dir, name, extension, classname=None, request=None):
+    ''' Interface to get a template, possibly loading a compiled template.
+
+        "name" and "extension" indicate the template we're after, which in
+        most cases will be "name.extension". If "extension" is None, then
+        we look for a template just called "name" with no extension.
+
+        If the file "name.extension" doesn't exist, we look for
+        "_generic.extension" as a fallback.
+    '''
+    # default the name to "home"
+    if name is None:
+        name = 'home'
+
+    # find the source, figure the time it was last modified
+    if extension:
+        filename = '%s.%s'%(name, extension)
+    else:
+        filename = name
+    src = os.path.join(dir, filename)
+    try:
+        stime = os.stat(src)[os.path.stat.ST_MTIME]
+    except os.error, error:
+        if error.errno != errno.ENOENT or not extension:
+            raise
+        # try for a generic template
+        filename = '_generic.%s'%extension
+        src = os.path.join(dir, filename)
+        stime = os.stat(src)[os.path.stat.ST_MTIME]
+
+    key = (dir, filename)
+    if templates.has_key(key) and stime < templates[key].mtime:
+        # compiled template is up to date
+        return templates[key]
+
+    # compile the template
+    templates[key] = pt = RoundupPageTemplate()
+    pt.write(open(src).read())
+    pt.id = filename
+    pt.mtime = time.time()
+    return pt
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
     ''' A Roundup-specific PageTemplate.
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
     ''' A Roundup-specific PageTemplate.
@@ -77,36 +146,46 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
           python modules made available (XXX: not sure what's actually in
           there tho)
     '''
           python modules made available (XXX: not sure what's actually in
           there tho)
     '''
-    def __init__(self, client, classname=None, request=None):
-        ''' Extract the vars from the client and install in the context.
-        '''
-        self.client = client
-        self.classname = classname or self.client.classname
-        self.request = request or HTMLRequest(self.client)
-
-    def pt_getContext(self):
+    def getContext(self, client, classname, request):
         c = {
         c = {
-             'klass': HTMLClass(self.client, self.classname),
+             'klass': HTMLClass(client, classname),
              'options': {},
              'nothing': None,
              'options': {},
              'nothing': None,
-             'request': self.request,
-             'content': self.client.content,
-             'db': HTMLDatabase(self.client),
-             'instance': self.client.instance
+             'request': request,
+             'content': client.content,
+             'db': HTMLDatabase(client),
+             'instance': client.instance
         }
         # add in the item if there is one
         }
         # add in the item if there is one
-        if self.client.nodeid:
-            c['item'] = HTMLItem(self.client.db, self.classname,
-                self.client.nodeid)
-            c[self.classname] = c['item']
+        if client.nodeid:
+            c['item'] = HTMLItem(client.db, classname, client.nodeid)
+            c[classname] = c['item']
         else:
         else:
-            c[self.classname] = c['klass']
+            c[classname] = c['klass']
         return c
         return c
-   
-    def render(self, *args, **kwargs):
-        if not kwargs.has_key('args'):
-            kwargs['args'] = args
-        return self.pt_render(extra_context={'options': kwargs})
+
+    def render(self, client, classname, request, **options):
+        """Render this Page Template"""
+
+        if not self._v_cooked:
+            self._cook()
+
+        __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
+
+        if self._v_errors:
+            raise PTRuntimeError, 'Page Template %s has errors.' % self.id
+
+        # figure the context
+        classname = classname or client.classname
+        request = request or HTMLRequest(client)
+        c = self.getContext(client, classname, request)
+        c.update({'options': options})
+
+        # and go
+        output = StringIO.StringIO()
+        TALInterpreter(self._v_program, self._v_macros,
+            getEngine().getContext(c), output, tal=1, strictinsert=0)()
+        return output.getvalue()
 
 class HTMLDatabase:
     ''' Return HTMLClasses for valid class fetches
 
 class HTMLDatabase:
     ''' Return HTMLClasses for valid class fetches
@@ -115,7 +194,10 @@ class HTMLDatabase:
         self.client = client
         self.config = client.db.config
     def __getattr__(self, attr):
         self.client = client
         self.config = client.db.config
     def __getattr__(self, attr):
-        self.client.db.getclass(attr)
+        try:
+            self.client.db.getclass(attr)
+        except KeyError:
+            raise AttributeError, attr
         return HTMLClass(self.client, attr)
     def classes(self):
         l = self.client.db.classes.keys()
         return HTMLClass(self.client, attr)
     def classes(self):
         l = self.client.db.classes.keys()
@@ -183,6 +265,40 @@ class HTMLClass:
         l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
         return l
 
         l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
         return l
 
+    def csv(self):
+        ''' Return the items of this class as a chunk of CSV text.
+        '''
+        # get the CSV module
+        try:
+            import csv
+        except ImportError:
+            return 'Sorry, you need the csv module to use this function.\n'\
+                'Get it from: http://www.object-craft.com.au/projects/csv/'
+
+        props = self.propnames()
+        p = csv.parser()
+        s = StringIO.StringIO()
+        s.write(p.join(props) + '\n')
+        for nodeid in self.klass.list():
+            l = []
+            for name in props:
+                value = self.klass.get(nodeid, name)
+                if value is None:
+                    l.append('')
+                elif isinstance(value, type([])):
+                    l.append(':'.join(map(str, value)))
+                else:
+                    l.append(str(self.klass.get(nodeid, name)))
+            s.write(p.join(l) + '\n')
+        return s.getvalue()
+
+    def propnames(self):
+        ''' Return the list of the names of the properties of this class.
+        '''
+        idlessprops = self.klass.getprops(protected=0).keys()
+        idlessprops.sort()
+        return ['id'] + idlessprops
+
     def filter(self, request=None):
         ''' Return a list of items from this class, filtered and sorted
             by the current requested filterspec/filter/sort/group args
     def filter(self, request=None):
         ''' Return a list of items from this class, filtered and sorted
             by the current requested filterspec/filter/sort/group args
@@ -206,9 +322,9 @@ class HTMLClass:
            You may optionally override the label displayed, the width and
            height. The popup window will be resizable and scrollable.
         '''
            You may optionally override the label displayed, the width and
            height. The popup window will be resizable and scrollable.
         '''
-        return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
-            'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
-            properties, width, height, label)
+        return '<a href="javascript:help_window(\'%s?:template=help&' \
+            ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
+            '(%s)</b></a>'%(self.classname, properties, width, height, label)
 
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
 
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
@@ -225,19 +341,15 @@ class HTMLClass:
         # create a new request and override the specified args
         req = HTMLRequest(self.client)
         req.classname = self.classname
         # create a new request and override the specified args
         req = HTMLRequest(self.client)
         req.classname = self.classname
-        req.__dict__.update(kwargs)
+        req.update(kwargs)
 
         # new template, using the specified classname and request
 
         # new template, using the specified classname and request
-        pt = RoundupPageTemplate(self.client, self.classname, req)
-
-        # use the specified template
-        name = self.classname + '.' + name
-        pt.write(open('/tmp/test/html/%s'%name).read())
-        pt.id = name
+        pt = getTemplate(self.db.config.TEMPLATES, self.classname, name)
 
         # XXX handle PT rendering errors here nicely
         try:
 
         # XXX handle PT rendering errors here nicely
         try:
-            return pt.render()
+            # use our fabricated request
+            return pt.render(self.client, self.classname, req)
         except PageTemplate.PTRuntimeError, message:
             return '<strong>%s</strong><ol>%s</ol>'%(message,
                 cgi.escape('<li>'.join(pt._v_errors)))
         except PageTemplate.PTRuntimeError, message:
             return '<strong>%s</strong><ol>%s</ol>'%(message,
                 cgi.escape('<li>'.join(pt._v_errors)))
@@ -851,6 +963,16 @@ def handleListCGIValue(value):
     else:
         return value.value.split(',')
 
     else:
         return value.value.split(',')
 
+class ShowDict:
+    ''' A convenience access to the :columns index parameters
+    '''
+    def __init__(self, columns):
+        self.columns = {}
+        for col in columns:
+            self.columns[col] = 1
+    def __getitem__(self, name):
+        return self.columns.has_key(name)
+
 class HTMLRequest:
     ''' The *request*, holding the CGI form and environment.
 
 class HTMLRequest:
     ''' The *request*, holding the CGI form and environment.
 
@@ -860,10 +982,12 @@ class HTMLRequest:
         "base" the base URL for this instance
         "user" a HTMLUser instance for this user
         "classname" the current classname (possibly None)
         "base" the base URL for this instance
         "user" a HTMLUser instance for this user
         "classname" the current classname (possibly None)
-        "template_type" the current template type (suffix, also possibly None)
+        "template" the current template (suffix, also possibly None)
 
         Index args:
         "columns" dictionary of the columns to display in an index page
 
         Index args:
         "columns" dictionary of the columns to display in an index page
+        "show" a convenience access to columns - request/show/colname will
+               be true if the columns should be displayed, false otherwise
         "sort" index sort column (direction, column name)
         "group" index grouping property (direction, column name)
         "filter" properties to filter the index on
         "sort" index sort column (direction, column name)
         "group" index grouping property (direction, column name)
         "filter" properties to filter the index on
@@ -883,13 +1007,13 @@ class HTMLRequest:
 
         # store the current class name and action
         self.classname = client.classname
 
         # store the current class name and action
         self.classname = client.classname
-        self.template_type = client.template_type
+        self.template = client.template
 
         # extract the index display information from the form
 
         # extract the index display information from the form
-        self.columns = {}
+        self.columns = []
         if self.form.has_key(':columns'):
         if self.form.has_key(':columns'):
-            for entry in handleListCGIValue(self.form[':columns']):
-                self.columns[entry] = 1
+            self.columns = handleListCGIValue(self.form[':columns'])
+        self.show = ShowDict(self.columns)
 
         # sorting
         self.sort = (None, None)
 
         # sorting
         self.sort = (None, None)
@@ -935,6 +1059,35 @@ class HTMLRequest:
         if self.form.has_key(':search_text'):
             self.search_text = self.form[':search_text'].value
 
         if self.form.has_key(':search_text'):
             self.search_text = self.form[':search_text'].value
 
+        # pagination - size and start index
+        # figure batch args
+        if self.form.has_key(':pagesize'):
+            self.pagesize = int(self.form[':pagesize'].value)
+        else:
+            self.pagesize = 50
+        if self.form.has_key(':startwith'):
+            self.startwith = int(self.form[':startwith'].value)
+        else:
+            self.startwith = 0
+
+    def update(self, kwargs):
+        self.__dict__.update(kwargs)
+        if kwargs.has_key('columns'):
+            self.show = ShowDict(self.columns)
+
+    def description(self):
+        ''' Return a description of the request - handle for the page title.
+        '''
+        s = [self.client.db.config.INSTANCE_NAME]
+        if self.classname:
+            if self.client.nodeid:
+                s.append('- %s%s'%(self.classname, self.client.nodeid))
+            else:
+                s.append('- index of '+self.classname)
+        else:
+            s.append('- home')
+        return ' '.join(s)
+
     def __str__(self):
         d = {}
         d.update(self.__dict__)
     def __str__(self):
         d = {}
         d.update(self.__dict__)
@@ -951,12 +1104,14 @@ form: %(form)s
 url: %(url)r
 base: %(base)r
 classname: %(classname)r
 url: %(url)r
 base: %(base)r
 classname: %(classname)r
-template_type: %(template_type)r
+template: %(template)r
 columns: %(columns)r
 sort: %(sort)r
 group: %(group)r
 filter: %(filter)r
 columns: %(columns)r
 sort: %(sort)r
 group: %(group)r
 filter: %(filter)r
-filterspec: %(filterspec)r
+search_text: %(search_text)r
+pagesize: %(pagesize)r
+startwith: %(startwith)r
 env: %(env)s
 '''%d
 
 env: %(env)s
 '''%d
 
@@ -966,14 +1121,14 @@ env: %(env)s
         l = []
         s = '<input type="hidden" name="%s" value="%s">'
         if columns and self.columns:
         l = []
         s = '<input type="hidden" name="%s" value="%s">'
         if columns and self.columns:
-            l.append(s%(':columns', ','.join(self.columns.keys())))
-        if sort and self.sort is not None:
+            l.append(s%(':columns', ','.join(self.columns)))
+        if sort and self.sort[1] is not None:
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
             l.append(s%(':sort', val))
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
             l.append(s%(':sort', val))
-        if group and self.group is not None:
+        if group and self.group[1] is not None:
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
@@ -984,29 +1139,40 @@ env: %(env)s
         if filterspec:
             for k,v in self.filterspec.items():
                 l.append(s%(k, ','.join(v)))
         if filterspec:
             for k,v in self.filterspec.items():
                 l.append(s%(k, ','.join(v)))
+        if self.search_text:
+            l.append(s%(':search_text', self.search_text))
+        l.append(s%(':pagesize', self.pagesize))
+        l.append(s%(':startwith', self.startwith))
         return '\n'.join(l)
 
     def indexargs_href(self, url, args):
         ''' embed the current index args in a URL '''
         l = ['%s=%s'%(k,v) for k,v in args.items()]
         return '\n'.join(l)
 
     def indexargs_href(self, url, args):
         ''' embed the current index args in a URL '''
         l = ['%s=%s'%(k,v) for k,v in args.items()]
-        if self.columns:
-            l.append(':columns=%s'%(','.join(self.columns.keys())))
-        if self.sort is not None:
+        if self.columns and not args.has_key(':columns'):
+            l.append(':columns=%s'%(','.join(self.columns)))
+        if self.sort[1] is not None and not args.has_key(':sort'):
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
             l.append(':sort=%s'%val)
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
             l.append(':sort=%s'%val)
-        if self.group is not None:
+        if self.group[1] is not None and not args.has_key(':group'):
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
                 val = self.group[1]
             l.append(':group=%s'%val)
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
                 val = self.group[1]
             l.append(':group=%s'%val)
-        if self.filter:
+        if self.filter and not args.has_key(':columns'):
             l.append(':filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
             l.append(':filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
-            l.append('%s=%s'%(k, ','.join(v)))
+            if not args.has_key(k):
+                l.append('%s=%s'%(k, ','.join(v)))
+        if self.search_text and not args.has_key(':search_text'):
+            l.append(':search_text=%s'%self.search_text)
+        if not args.has_key(':pagesize'):
+            l.append(':pagesize=%s'%self.pagesize)
+        if not args.has_key(':startwith'):
+            l.append(':startwith=%s'%self.startwith)
         return '%s?%s'%(url, '&'.join(l))
 
     def base_javascript(self):
         return '%s?%s'%(url, '&'.join(l))
 
     def base_javascript(self):
@@ -1044,18 +1210,9 @@ function help_window(helpurl, width, height) {
             matches = None
         l = klass.filter(matches, filterspec, sort, group)
 
             matches = None
         l = klass.filter(matches, filterspec, sort, group)
 
-        # figure batch args
-        if self.form.has_key(':pagesize'):
-            size = int(self.form[':pagesize'].value)
-        else:
-            size = 50
-        if self.form.has_key(':startwith'):
-            start = int(self.form[':startwith'].value)
-        else:
-            start = 0
-
         # return the batch object
         # return the batch object
-        return Batch(self.client, self.classname, l, size, start)
+        return Batch(self.client, self.classname, l, self.pagesize,
+            self.startwith)
 
 
 # extend the standard ZTUtils Batch object to remove dependency on
 
 
 # extend the standard ZTUtils Batch object to remove dependency on