[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.