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.