Welcome, guest | Sign In | My Account | Store | Cart

This recipe combines the delicious PageTemplates package with the flavorful Medusa package to serve up rendered PageTemplates from the file system.

Why not just use Zope you say? As far as I know, the current Zope release isn't an option with Python 2.2 code. The project for which this recipe was devised requires Python 2.2.

Ingredients you'll need:

  1. Python 2.2. If you're using 2.1, try Zope instead, as it does everything this recipe can do plus a whole lot more.

  2. ExtensionClass and friends. There is more than one way to get ExtensionClass installed, but the method I've used successfully is to install StandaloneZODB. That package is available here: http://www.zope.org/Products/StandaloneZODB

  3. PageTemplates, TAL, and ZTUtils packages. These are available in the Zope source releases but must be installed manually. Again, there's more than one way to make these packages available in your system. The method I've used is to copy the package directories from the Zope source archive into the Python lib/site-packages/ directory. The Zope source is available from this link: http://www.zope.org/Products

  4. Medusa. I used Medusa 0.5.2 to develop this recipe, but you may have an equally pleasant experience with other versions. You can get Medusa here: http://oedipus.sourceforge.net/medusa/

  5. A Medusa startup script. As with all Medusa handlers, you must explicitly construct a PageTemplates handler and associate it with an HTTP server. You can modify a copy of the sample startup script included with Medusa or create your own.

  6. Some PageTemplates. The code below reads PageTemplates markup from files stored in your file system. Give your markup files a ".pt" or ".ptx" extension, and the handler will try to render them as PageTemplates before returning their markup.

Once you have all these items in place, modify your Medusa start up script:

  1. Save the code below as "pagetemplate_handler.py", and bring it into your script: import pagetemplate_handler

  2. Construct a pagetemplate_handler.pagetemplate_handler, pagetemplate_handler.pagetemplate_xml_handler, or both. These types need a filesystem object, just like the default_handler.

  3. Associate your pagetemplate_handler with your HTTP server.

Python, 197 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#! /usr/bin/env python

import cgi
import os
import re

import medusa.script_handler
import medusa.xmlrpc_handler
import medusa.producers

import PageTemplates.PageTemplate
import PageTemplates.Expressions


class MedusaPageTemplate(PageTemplates.PageTemplate.PageTemplate):
    """ MedusaPageTemplate -> PageTemplate type for use by Medusa handlers

        This type constructs a PT context from a Medusa request when
        instantiated.  Instances also read in their content from disk when
        created.  Clients supply the path to the page template file,
        along with the HTTP query string and header data.

        The built-ins that this type provides may differ from the built-ins
        provided by ZopePageTemplates.  Specifically:

        * request - Constructed from the query string and form data if present.

        * here - This mapping may be provided by the client.  Default is empty.

        * root - This mapping may be provided by the client.  Default is empty.

        * user - This mapping may be provided by the client.  Default is empty.

        * container - By default, a custom mapping filled with the results of
        stat and other os module functions.  Clients can specify that this data
        not be made available.

        * modules - Instances reuse PageTemplates.Expressions.SecureModuleImporter
        for importing modules.  The behavior of SecureModuleImporter allows an
        unrestricted import of any module, but in practice, modules sometimes
        cannot be imported.
    """
    modules = PageTemplates.Expressions.SecureModuleImporter

    context_keys = ('template', 'nothing', 'options', 'request', 'modules',
                    'here', 'container', 'root', 'user', )

    stat_keys = ('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid',
                 'st_size', 'st_atime', 'st_mtime', 'st_ctime')

    def __init__(self, path, query, header, here={}, options={}, root={},
                 user={}, restricted=0, edit_content_type='text/html'):
        head, tail = os.path.split(path)

        ## these two attributes are so common in PT markup that it would be a
        ## shame to not set them.
        self.id = tail
        self.title = "Page Template '%s'" % (tail, )

        self.here = here
        self.options = options
        self.root = root
        self.template = self
        self.user = user

        ## gather data about the folder that contains this script
        self.container = {}
        if not restricted:
            self.container.update({'is_mount' : os.path.ismount(head),
                                   'is_link' : os.path.islink(head),
                                   'files' : os.listdir(head),
                                   'name' : os.path.split(head)[-1], })
            self.container.update(dict(zip(self.stat_keys, os.stat(head))))

        ## turn a query string and/or form data in the header into a mapping
        self.request = r = {}
        if query:
            if query.startswith('?'):
                query = query[1:]
            r.update(cgi.parse_qs(query, 0, 0))
        if header:
            r.update(cgi.parse_qs(header, 0, 0))
        [r.__setitem__(k, v[0]) for k, v in r.items() if len(v) == 1]

        ## finally, read in the file as a convenience for the client
        self.pt_edit(open(path, 'r').read(), edit_content_type)

    def pt_getContext(self):
        """ redefine pt_getContext() to produce our kind of context """
        return dict([(k, getattr(self, k, None)) for k in self.context_keys])


class pagetemplate_handler_base(medusa.script_handler.script_handler):
    """ pagetemplate_handler_base -> handler that serves up rendered PT files

        This handler type reuses the majority of the behavior defined by
        medusa.script_handler.script_handler.  The 'match' function is the most
        notable of the reused methods.

        The 'handle_request' method defined below mirrors the script_handler
        behavior.  It also allows POST verbs, and uses the collector from
        medusa.xmlrpc_handler to gather POST data.

        Clients should not use this class directly.  Instead, clients should
        instantiate or subclass the two concrete classes defined below,
        pagetemplate_handler and pagetemplate_xml_handler.
    """
    pt_exceptions = (PageTemplates.PageTemplate.PTRuntimeError,
                     PageTemplates.TALES.TALESError,)

    def __init__(self, filesystem):
        ## explicit call to the super; don't want clients to forget to do this
        medusa.script_handler.script_handler.__init__(self, filesystem)

    def handle_request(self, request):
        [path, params, query, fragment] = request.split_uri()

        if not self.filesystem.isfile(path):
            request.error(404)
            return

        self.hits.increment()
        request.script_filename = self.filesystem.translate(path)

        if request.command in ('post', 'put'):
            cl = request.get_header('content-length')
            length = int(cl)
            if not cl:
                request.error(411)
                return
            else:
                col = medusa.xmlrpc_handler.collector(self, request)
                request.collector = col
        elif request.command in ('get', ):
            self.continue_request(None, request)
        else:
            request.error(405)
            return

    def continue_request(self, data, request):
        """ implements the signature expected by xmlrpc_handler.collector """
        path, params, query, fragment = request.split_uri()

        try:
            pt_obj = self.get_pagetemplate(request.script_filename, query, data)
            response = self.render_pagetemplate(request, pt_obj)
        except self.pt_exceptions, ex:
            print '%r' % ex, ex
            response = '<exception>%s</exception>' % (ex, )

        request['Content-Type'] = self.content_type
        request['Content-Length'] = len(response)
        request.push(response)
        request.done()
        return

    def get_pagetemplate(self, filename, query, data):
        """ get_pagetemplate -> return a PT object.  subclasses can override """
        pt = MedusaPageTemplate(filename, query, data)
        return pt

    def render_pagetemplate(self, request, pagetemplate):
        """ render_pagetemplate -> render a PT object.  subclasses can override """
        options = {}
        res = pagetemplate(**options)
        return res

    def __repr__(self):
        return '<page template request handler at %s>' % id(self)

    def status(self):
        return medusa.producers.simple_producer("""
            <li>PageTemplate Handler
                <ul><li><b>Hits:</b> %s</li>
                    <li><b>Exceptions:</b> %s</li>
                </ul>
            </li>""" % (self.hits, self.exceptions, ))


class pagetemplate_handler(pagetemplate_handler_base):
    """ pagetemplate_handler  -> Medusa HTTP handler

        Concrete handler that returns rendered PT markup as HTML. 
    """
    content_type = 'text/html'
    extension = 'pt'
    script_regex = re.compile(r'.*/([^/]+\.%s)' % extension, re.IGNORECASE)


class pagetemplate_xml_handler(pagetemplate_handler_base):
    """ pagetemplate_xml_handler -> Medusa HTTP handler

        Concrete handler that returns rendered PT markup as XML.
    """
    content_type = 'text/xml'
    extension = 'ptx'
    script_regex = re.compile(r'.*/([^/]+\.%s)' % extension, re.IGNORECASE)

There are a few caveats, of course: A. Most of the ZPT idioms don't work, e.g., "container/title".

B. METAL macros don't work, and I don't know why.

C. Access to modules via "module" is spotty.

This recipe isn't perfect, but it can be made better. Please send me any feedback you have. Enjoy!