[ZDP] Documentation (like we need any)
Michel Pelletier
nont@interfold.com
Thu, 15 Apr 1999 01:56:10 -0600
Note:
This is a documentation burst for peer review, this is also some interesting
stuff anyone might want to read. FYI, this is a section of a chapter in the
upcoming 'The Zen of Zope'. Some of the Python code indenting might be a little
wrong from copy and paste mangling. Please inform me of any errors you notice.
-Michel
Zope Products
Zope Products are a way of extending Zope with third party
software. In Zope parlance, a 'Product' is one or more
modules, which define one or more objects for
Zope to use. Typicly a Product is either created through the
web , in the control panel, or is written in Python as an
Package in the lib/python/Products directory.
Products are a very powerful way of extending Zope. As an
example, let's say your business is dealing with Snarfs. You
could create a website with all of Zopes usual tools, but your
collection of Snarfs would have to be a loosely collected
structure of Folders and DTML Methods and other things.
Products let you make your own object types that define
exactly what you want in a Snarf, and give you an easy way to
add these Snarf objects anywhere you want them. A more
relistic example is a message board, where the manager can add
boards and edit messages using the management interface.
Instead of thinking of a 'board' as a folder full of content,
you can think of it as a board object. When you create that
object, you don't need to be hauling around all of your little
components, you can package them into one big one and give it
a name.
Many third party products in Zope are created as Products.
Before version 1.11, Products that defined new object
definitions (Python classes) could only be written in Python.
Version 1.11 introduces the concept of a ZClass, which is
exactly like a Python class, but it can be created and built
through the managment interface. This removes the need for
Product developers to know Python or have access to the
filesystem.
ZClasses also give an excellent way of prototyping Zope
applications. Since ZClasses work just like Python classes,
Zope cannot tell the different between them. A ZClass can be
built just like a Python class, and when your prototype is
finalized it is easy to map the concepts and existing code
from a ZClass to a Python class. This also aids in rapid
development; quick results can be gotten because the managment
interface provides an existing framework and due to the time
saved not needing to restart Zope every time you change your
Product.
There is still a need, however, to develop many Products in
Python. Products that deal with the low level issues of Zope
are not suited well for DTML, and many things, like opening
files or importing python services, cannot be done at all from
DTML. To extend a ZClass with Python would require an
External Method, which is not as clean a method of development
if the Product requires a lot of low level work.
There are two types of Python Product:
DTML Extensions -
A is DTML Extension is any Product that defines a DTML
tag. A DTML Extension can only be written in Python.
Zope Extensions -
A Zope Extension is any Product that defines objects and
methods to manage those object. A Product must call a
special Python interface to register the objects and
methods to manage those objects in Zope's Product
Database. Zope Extension can be written in Python, or
created through the web with ZClasses.
Products can be eith Zope or DTML Extensions, or both if they
are written in Python. For example, the MailHost Product that
ships with Zope defined two DTML Extensions, the
<!--#sendmail--> and <!--#mime--> tags, and it also defines a
Zope Extension that defines the MailHost object.
DTML Extensions are rare, and although you are more than
welcome to extend your DTML in any way you choose (and
distribute those changes as a Product) be aware that the DTML
namespace is clean, and should remain that way unless a good
argument can be made against not extending DTML. Typicly, it
is not acceptable to do one minor thing with DTML that could
be done easily with other methods. Extending DTML should be
used only for major extensions to the functionality of the
language. For example, the <!--#sendmail--> tag was created
in order to format and send email messages when written in
DTML, and the <!--#mime --> tag was added to construct mime
transportable information within Zope. One third party
developer has written a <!--#calendar --> tag which the DTML
program can use to easily create custom calendars. These are
basic building blocks, and have therefore been wise additions
to DTML.
Creating a DTML tag involves defining a class which will be
your tag object. When a chunch of DTML is first saved in an
object, it is parsed and compiled, so that all the ocorances
of a tag are replaced by their compiled objects. Therefore:
<h1>This is a chunk of <!--#var DTML--></h1>
becomes internaly represented as:
<h1>This is a chunk of <instance of a var tag></h1>
When ever the DTML method or document is called, the instances
of tag objects are gone through and their 'render' methods are
called. Here, let's show how the <!--#mime --> tag is
defined. Create a Product called 'MIMETools' and in MIMETools
create and edit MIMETag.py::
from DocumentTemplate.DT_Util import *
from DocumentTemplate.DT_String import String
from MimeWriter import MimeWriter
from cStringIO import StringIO
import string, mimetools
MIMEError = "MIME Tag Error"
class MIMETag:
'''
'''
name='mime'
blockContinuations=('boundary',)
encode=None
MIMETag.py defines a class, called MIMETag. The 'name'
attribute defines the tag name, this is used by the parser to
recognize the tag. Setting it to 'mime' tells the parser that
we are defining the <!--#mime--> tag.
'blockContinuation' is tuple of the various tags that can be
used to delimit blocks within the information contained within
the tag. Some tags are standalone, like <!--#var-->, Some
tags like <!--#with --> are containers consisting of only one
block of content and require an ending <!--#/with --> tag, and
some tags are multi-block, like the
<!--#if--><!--#else--><!--#/if--> tags. With one 'else' tag,
an 'if' construct contains two bocks. 'if' tags can also be
broken up by 'elif'.
So by setting 'name' to 'mime' and 'blockContinuations' to
'('boundary',) we are telling the DTML parser that 'mime'
constructs can look like:
<!--#mime -->
...
<!--#/mime-->
or:
<!--#mime -->
...
<!--#boundary-->
...
<!--#boundary-->
...
<!--#boundary-->
...
<!--#/mime-->
The 'mime' tag may have any many blocks within it as there are
boundary tags, plus one.
When the DTML parser find a tag, it breaks it up into blocks
(if it's a blockish tag) and constructs a tag object. In this
case, it would instanciate an object of type 'MIMETag'. To do
this, it would call the objects constructor, '__init__'.
def __init__(self, blocks):
self.sections = []
for tname, args, section in blocks:
# using the 'for' construct, iterate of the
# sequence of (tname, args, section) tuples.
# each block in the tag gets looped over here
# once. 'tname' is the tagname of this
# iteration, 'args' is the RH expression
# of an attribute, and 'section' is the
# unrendered content of the block
args = parse_params(args, type=None, disposition=None,
encode=None, name=None)
# parse_params is provided by DT_Util, it
# specifies the type of attributes the tag
# expects or can accept. In this case, the
# 'mime' tag can have 'type', 'disposition'
# or 'name' attribute.
has_key=args.has_key
if has_key('type'):
type = args['type']
else:
type = 'application/octet-stream'
# if the tag has a 'type' attribute, snif that,
# otherwise assume it's
# 'application/octet-stream'
if has_key('disposition'):
disposition = args['disposition']
else:
disposition = ''
# sniff for the 'disposition' attribute,
# and set it to an empty string if there
# isn't one.
if has_key('encode'):
encode = args['encode']
else:
encode = 'base64'
# if no encoding is specified, assume
# it's 'base64'
if has_key('name'):
name = args['name']
else:
name = ''
# get the name, or set it to a empty string
if encode not in \
('base64', 'quoted-printable', 'uuencode', 'x-uuencode',
'uue', 'x-uue', '7bit'):
raise MIMEError, (
'An unsupported encoding was specified in tag')
# make sure the encoding was sane
self.sections.append((type, disposition, encode,
name, section.blocks))
# append the type, disposition, encoding, name,
# and section information as a tuple to the
# the 'sections' sequence.
New 'mime' objects are instanciated whenever the DTML parser
fines a '<!--#mime-->' tag construct in DTML. It is
important to understand that the above '__init__' method is
called when the DTML is actualy inserted into the DTML
object. It is *compiled*. It is interesting to know that
when you edit a DTML Method or Document, clicking on
'Change', thus commiting your changes to the object, is when
the various tag's '__init__' methods are called; this is
when the tag objects are instanciated.
Whenever a DTML Method is called, or when a DTML Document's
'index_html' method is called, the compiled DTML code is
executed. The DTML engine runs through the DTML, either
calling the tag object directly or calling it's 'render'
method. (footnote: Jim can never remember which of these
works, so currently all tags define both). The 'mime' tag's
render method would be:
def render(self, md):
mw = MimeWriter(StringIO())
# MimeWriter is a standard Python 1.5.1
# module for formatting data in mime
outer = mw.startmultipartbody('mixed')
# create the initial, outer mime layer
for x in self.sections:
# iterate over each (type, disposition,
# encoding, name, block) tuple in the
# sections sequence.
inner = mw.nextpart()
t, d, e, n, b = x
# create an inner part, and unpack the tuple
if d:
inner.addheader('Content-Disposition', d)
inner.addheader('Content-Transfer-Encoding', e)
# add some headers
if n:
plist = [('name', n)]
else:
plist = []
innerfile = inner.startbody(t, plist, 1)
# set up to write to body
output = StringIO()
if e == '7bit':
innerfile.write(render_blocks(b, md))
else:
mimetools.encode(StringIO(render_blocks(b,
md)), output, e)
output.seek(0)
innerfile.write(output.read())
# first, called render_blocks on the block,
# which renders any DTML tag objects the block
# man contain, and then encode the result of
# that
if x is self.sections[-1]:
mw.lastpart()
# if this is the last block, wrap up our MimeWriter
# object.
outer.seek(0)
return outer.read()
# return the formatted and encoded data
__call__=render
# if the tag is called, call the render method
The 'render' method has two jobs, one, make sure than any
DTML tags that it contains get rendered, and then to take
all of that rendered text and encode stuff it into the
MIMEWriter object. If any of the blocks had contained DTML,
then that DTML would have been executed with each iteration
around the 'for' loop 'render_blocks' was called on the
section of DTML. By calling 'render_blocks' DTML tags can
be tested in themselves or other DTML tags.