Code

Keep a cache of compiled PageTemplates.
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 3 Sep 2002 02:58:11 +0000 (02:58 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Tue, 3 Sep 2002 02:58:11 +0000 (02:58 +0000)
Reinstated query saving.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1033 57a73879-2fb5-44c3-a270-3262357dd7e2

TODO.txt
roundup/cgi/client.py
roundup/cgi/templating.py
roundup/templates/classic/html/home
roundup/templates/classic/html/issue.index
roundup/templates/classic/html/issue.search
roundup/templates/classic/html/page

index 2ef63fd68dabce508ffe02f6901a29022609dc46..be2860c2adf5878dd3401debd48134eb7d726c60 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -42,16 +42,15 @@ pending web: Quick help links next to the property labels giving a
              form element too, eg. how to use the nosy list edit box.
 pending web: clicking on a group header should filter for that type of entry
 pending web: re-enable auth by basic http auth
+pending web: search "refinement"
+             - pre-fill the search page with the current search parameters)
+             - add a drop-down with all queries which fills form with selected
+               query values
 
 New templating TODO:
 . generic class editing
 . classhelp
-. query saving
-  - add ":queryname" to search form submission, and handle it in search action
-  - ?add a drop-down on search page with all queries that fills form with
-     each query's values?
-. search "refinement" (pre-fill the search page with the current search
-  parameters)
+. rewritten documentation (can come after the beta though so stuff is settled)
 
 ongoing: any bugs
 
index 9683e6724c589bd8f91c2f9a65309ec3acaf9790..f50b06da105fae60ba950277c17fbc9d8e54dfee 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.6 2002-09-02 07:46:55 richard Exp $
+# $Id: client.py,v 1.7 2002-09-03 02:58:11 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -10,8 +10,9 @@ import binascii, Cookie, time, random
 from roundup import roundupdb, date, hyperdb, password
 from roundup.i18n import _
 
-from roundup.cgi.templating import RoundupPageTemplate
+from roundup.cgi.templating import getTemplate, HTMLRequest
 from roundup.cgi import cgitb
+
 from PageTemplates import PageTemplate
 
 class Unauthorised(ValueError):
@@ -280,13 +281,11 @@ class Client:
     def template(self, name, **kwargs):
         ''' Return a PageTemplate for the named page
         '''
-        pt = RoundupPageTemplate(self)
-        # make errors nicer
-        pt.id = name
-        pt.write(open(os.path.join(self.instance.TEMPLATES, name)).read())
-        # XXX handle PT rendering errors here nicely
+        pt = getTemplate(self.instance.TEMPLATES, name)
+        # XXX handle PT rendering errors here more nicely
         try:
-            return pt.render(**kwargs)
+            # let the template render figure stuff out
+            return pt.render(self, None, None, **kwargs)
         except PageTemplate.PTRuntimeError, message:
             return '<strong>%s</strong><ol>%s</ol>'%(message,
                 '<li>'.join(pt._v_errors))
@@ -777,6 +776,9 @@ class Client:
             Set the form ":filter" variable based on the values of the
             filter variables - if they're set to anything other than
             "dontcare" then add them to :filter.
+
+            Also handle the ":queryname" variable and save off the query to
+            the user's query list.
         '''
         # generic edit is per-class only
         if not self.searchPermission():
@@ -790,6 +792,32 @@ class Client:
             if not self.form[key].value: continue
             self.form.value.append(cgi.MiniFieldStorage(':filter', key))
 
+        # handle saving the query params
+        if self.form.has_key(':queryname'):
+            queryname = self.form[':queryname'].value.strip()
+            if queryname:
+                # parse the environment and figure what the query _is_
+                req = HTMLRequest(self)
+                url = req.indexargs_href('', {})
+
+                # handle editing an existing query
+                try:
+                    qid = self.db.query.lookup(queryname)
+                    self.db.query.set(qid, klass=self.classname, url=url)
+                except KeyError:
+                    # create a query
+                    qid = self.db.query.create(name=queryname,
+                        klass=self.classname, url=url)
+
+                    # and add it to the user's query multilink
+                    queries = self.db.user.get(self.userid, 'queries')
+                    queries.append(qid)
+                    self.db.user.set(self.userid, queries=queries)
+
+                # commit the query change to the database
+                self.db.commit()
+
+
     def searchPermission(self):
         ''' Determine whether the user has permission to search this class.
 
@@ -1004,13 +1032,10 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
                             'value': value, 'classname': link}
         elif isinstance(proptype, hyperdb.Multilink):
             value = form[key]
-            if hasattr(value, 'value'):
-                # Quite likely to be a FormItem instance
-                value = value.value
             if not isinstance(value, type([])):
                 value = [i.strip() for i in value.split(',')]
             else:
-                value = [i.strip() for i in value]
+                value = [i.value.strip() for i in value]
             link = cl.properties[key].classname
             l = []
             for entry in map(str, value):
index 8f4bf32892c44dfd2d4b77e95f89791ec3bde3cc..27ab4769096ae4c372b96353db48d0655d42a419 100644 (file)
@@ -1,9 +1,16 @@
-import sys, cgi, urllib, os, re, os.path
+import sys, cgi, urllib, os, re, os.path, time
 
 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:
@@ -29,10 +36,69 @@ if not sys.modules.has_key('Acquisition'):
     import Acquisition
     sys.modules['Acquisition'] = Acquisition
 
-# now it's safe to import PageTemplates and ZTUtils
+# now it's safe to import PageTemplates, TAL and ZTUtils
 from PageTemplates import PageTemplate
+from PageTemplates.Expressions import getEngine
+from TAL.TALInterpreter import TALInterpreter
 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, classname=None, request=None):
+    ''' Interface to get a template, possibly loading a compiled template.
+    '''
+    # find the source, figure the time it was last modified
+    src = os.path.join(dir, name)
+    stime = os.stat(src)[os.path.stat.ST_MTIME]
+
+    key = (dir, name)
+    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 = name
+    pt.mtime = time.time()
+    return pt
+
 class RoundupPageTemplate(PageTemplate.PageTemplate):
     ''' A Roundup-specific PageTemplate.
 
@@ -77,36 +143,46 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
           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 = {
-             'klass': HTMLClass(self.client, self.classname),
+             'klass': HTMLClass(client, classname),
              '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
-        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:
-            c[self.classname] = c['klass']
+            c[classname] = c['klass']
         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
@@ -225,19 +301,16 @@ class HTMLClass:
         # 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
-        pt = RoundupPageTemplate(self.client, self.classname, req)
-
-        # use the specified template
         name = self.classname + '.' + name
-        pt.write(open(os.path.join(self.db.config.TEMPLATES, name)).read())
-        pt.id = name
+        pt = getTemplate(self.db.config.TEMPLATES, name)
 
         # 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)))
@@ -851,6 +924,16 @@ def handleListCGIValue(value):
     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.
 
@@ -864,6 +947,8 @@ class HTMLRequest:
 
         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
@@ -886,10 +971,10 @@ class HTMLRequest:
         self.template_type = client.template_type
 
         # extract the index display information from the form
-        self.columns = {}
+        self.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)
@@ -935,6 +1020,22 @@ class HTMLRequest:
         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 __str__(self):
         d = {}
         d.update(self.__dict__)
@@ -956,7 +1057,9 @@ 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
 
@@ -966,14 +1069,14 @@ env: %(env)s
         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 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:
@@ -984,20 +1087,24 @@ env: %(env)s
         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()]
         if self.columns:
-            l.append(':columns=%s'%(','.join(self.columns.keys())))
-        if self.sort is not None:
+            l.append(':columns=%s'%(','.join(self.columns)))
+        if self.sort[1] is not None:
             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:
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
@@ -1007,6 +1114,10 @@ env: %(env)s
             l.append(':filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
             l.append('%s=%s'%(k, ','.join(v)))
+        if self.search_text:
+            l.append(':search_text=%s'%self.search_text)
+        l.append(':pagesize=%s'%self.pagesize)
+        l.append(':startwith=%s'%self.startwith)
         return '%s?%s'%(url, '&'.join(l))
 
     def base_javascript(self):
@@ -1044,18 +1155,9 @@ function help_window(helpurl, width, height) {
             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 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
index 8dc99f76ac03a1bfe3cde08843f988bc5716021f..03fe21bbaf55ffadab473eb2a04d541a8cddf08f 100644 (file)
@@ -6,7 +6,6 @@
 -->
 <span tal:replace="structure python:db.issue.renderWith('index',
     sort=('-', 'activity'), group=('+', 'priority'), filter=['status'],
-    columns={'id':1,'activity':1,'title':1,'creator':1,'assignedto':1,
-             'status':1},
+    columns=['id','activity','title','creator','assignedto', 'status'],
     filterspec={'status':['-1','1','2','3','4','5','6','7']})" />
 
index fc47be7ef458aa6728cec87bf018f38e7e90cfa2..420d0e6a3b57494749126ae491c6dc7d634e136a 100644 (file)
@@ -2,13 +2,13 @@
 <tal:block tal:define="batch request/batch">
  <table class="list">
   <tr>
-   <th tal:condition="exists:request/columns/priority">Priority</th>
-   <th tal:condition="exists:request/columns/id">ID</th>
-   <th tal:condition="exists:request/columns/activity">Activity</th>
-   <th tal:condition="exists:request/columns/title">Title</th>
-   <th tal:condition="exists:request/columns/status">Status</th>
-   <th tal:condition="exists:request/columns/creator">Created&nbsp;By</th>
-   <th tal:condition="exists:request/columns/assignedto">Assigned&nbsp;To</th>
+   <th tal:condition="request/show/priority">Priority</th>
+   <th tal:condition="request/show/id">ID</th>
+   <th tal:condition="request/show/activity">Activity</th>
+   <th tal:condition="request/show/title">Title</th>
+   <th tal:condition="request/show/status">Status</th>
+   <th tal:condition="request/show/creator">Created&nbsp;By</th>
+   <th tal:condition="request/show/assignedto">Assigned&nbsp;To</th>
   </tr>
  <tal:block tal:repeat="i batch">
   <tr tal:condition="python:request.group[1] and
    </th>
   </tr>
   <tr tal:attributes="class python:['normal', 'alt'][repeat['i'].even()]">
-   <td tal:condition="exists:request/columns/priority"
-      tal:content="i/priority"></td>
-   <td tal:condition="exists:request/columns/id"
-      tal:content="i/id"></td>
-   <td tal:condition="exists:request/columns/activity"
-      tal:content="i/activity/reldate"></td>
-   <td tal:condition="exists:request/columns/title">
+   <td tal:condition="request/show/priority" tal:content="i/priority"></td>
+   <td tal:condition="request/show/id" tal:content="i/id"></td>
+   <td tal:condition="request/show/activity"
+       tal:content="i/activity/reldate"></td>
+   <td tal:condition="request/show/title">
     <a tal:attributes="href string:issue${i/id}"
        tal:content="python:i.title.value or '[no title]'">title</a>
    </td>
-   <td tal:condition="exists:request/columns/status"
-       tal:content="i/status"></td>
-   <td tal:condition="exists:request/columns/creator"
-       tal:content="i/creator"></td>
-   <td tal:condition="exists:request/columns/assignedto"
-       tal:content="i/assignedto"></td>
+   <td tal:condition="request/show/status" tal:content="i/status"></td>
+   <td tal:condition="request/show/creator" tal:content="i/creator"></td>
+   <td tal:condition="request/show/assignedto" tal:content="i/assignedto"></td>
   </tr>
  </tal:block>
  <tr>
    <td>
     <select name=":sort">
      <option value="">- nothing -</option>
-     <option tal:repeat="col python:request.columns.keys()"
+     <option tal:repeat="col request/columns"
              tal:attributes="value col; selected python:col == request.sort[1]"
              tal:content="col">column</option>
-     </select>
+    </select>
    </td>
    <th>Descending:</th>
    <td><input type="checkbox" name=":sortdir"
   <tr>
    <th>Group on:</th>
    <td>
-     <select name=":group">
-      <option value="">- nothing -</option>
-      <option tal:repeat="col python:request.columns.keys()"
+    <select name=":group">
+     <option value="">- nothing -</option>
+     <option tal:repeat="col request/columns"
              tal:attributes="value col; selected python:col == request.group[1]"
              tal:content="col">column</option>
-     </select>
+    </select>
    </td>
    <th>Descending:</th>
    <td><input type="checkbox" name=":groupdir"
index 30ed9884978fd2d8314a4a3343251a843ec66a63..3bd1ca140bf65046b9fad10ca5aeba550de970e8 100644 (file)
@@ -57,7 +57,7 @@
  <th>Activity:</th>
  <td><input name="activity"></td>
  <td><input type="checkbox" name=":columns" value="activity" checked></td>
- <td><input type="radio" name=":sort" value="activity" checked></td>
+ <td><input type="radio" name=":sort" value="activity"></td>
  <td>&nbsp;</td>
 </tr>
 
@@ -74,7 +74,7 @@
  </td>
  <td><input type="checkbox" name=":columns" value="priority"></td>
  <td><input type="radio" name=":sort" value="priority"></td>
- <td><input type="radio" name=":group" value="priority" checked></td>
+ <td><input type="radio" name=":group" value="priority"></td>
 </tr>
 
 <tr>
 </td>
 </tr>
 
+<tr>
+<th>Query name**:</th>
+<td><input name=":queryname">
+</td>
+</tr>
+
 <tr><td>&nbsp;</td>
 <td><input type="submit" value="Search"></td>
 </tr>
 
 <tr><td>&nbsp;</td>
- <td colspan="4" class="help">*: The "all text" field will look in message
-   bodies and issue titles</td>
+ <td colspan="4" class="help">
+   *: The "all text" field will look in message bodies and issue titles<br>
+   **: If you supply a name, the query will be saved off and available as a
+       link in the sidebar
+ </td>
 </tr>
 </table>
 
index c69172b6ad1548e19d940328065f1e42cf395d22..4c0de68aeeb475d23313e7037880830a9006edbb 100644 (file)
 
 <tr>
  <td rowspan="2" valign="top" nowrap class="sidebar">
+  <p class="classblock" tal:condition="request/user/queries">
+   <b>Your Queries</b><br>
+   <a tal:repeat="qs request/user/queries"
+      tal:attributes="href python:'%s%s'%(qs['klass'], qs['url'])"
+      tal:content="qs/name">link</a>
+  </p>
+
   <p class="classblock"
        tal:condition="python:request.user.hasPermission('View', 'issue')">
    <b>Issues</b><br>