You are not logged in Log in Join
You are here: Home » Members » maxm » A list of my How-To's » A Minimal Product - The Sequel

Log in
Name

Password

 

A Minimal Product - The Sequel

Minimal Zope programming How-to - The Sequel

In part one of this minal series I showed how to create a minimal product, with as little fuzz as possible. In this How-To I will show the last few steps in how the minimal class can become a full fledged product.

To understand this How-To you should first read the first in the "minimal" series. click here to see it

Where did I leave off the last time

I made the fully functional product shown below, and I will build on that:

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/__init__.py

        from minimal import minimal

        def initialize(context):
            """Initialize the minimal product.
            This makes the object apear in the product list"""
            context.registerClass(
                minimal,
                constructors = (
                    minimal.manage_addMinimalForm, # The first method is 
                                                   # called when someone 
                                                   # adds the product
                    minimal.manage_addMinimal
                )
            )

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/minimal.py

        from OFS import SimpleItem

        class minimal(SimpleItem.SimpleItem):

            "minimal object"

            meta_type = 'minimal'

            manage_options = (
                {'label': 'View', 'action': 'index_html'},
            )

            def __init__(self, id):
                "initialise a new instance of Minimal"
                self.id = id

            def index_html(self):
                "used to view content of the object"
                return '<html><body>Hello World</body></html>'

        def manage_addMinimal(self, id, RESPONSE=None):
            "Add a Minimal to a folder."
            self._setObject(id, minimal(id))
            RESPONSE.redirect('index_html')

        def manage_addMinimalForm(self):
            "The form used to get the instance' id from the user."
            return """<html>
            <body>
            Please type the id of the minimal instance:<br>
            <form name="form" action="manage_addMinimal"><br>
            <input type="text" name="id"><br>
            <input type="submit" value="add">
            </form>
            </body>
            </html>"""

The first thing that needs to be added, for it to be a more normal product, is dtml support. Dtml is used in Zope to represent data from our class instead of mixing html and Python code, as I have done here in the methods "index_html()" and "manage_addMinimalForm()".

Zope uses the DTMLFile class to support dtml, so I will import that from Globals where it resides:

        from Globals import DTMLFile

I will then convert "index_html()" and "manage_addMinimalForm()" to dml methods by declaring them in the class like this:

        index_html = DTMLFile('www/index_html', globals())

What happens here can seem rather complex. DTMLFile is a class. So index_html is an object in the namespace of the minimal class. When index_html is called through the web it is used as a function. This is a Python speciality where you can call an instance of a class as a function:

        instance = callableClass('squareRoot')
        print instance(9)
        >>> 3

The above example shows another little example of this rather seldomly used behaviour. It's not really that complicated, but I was baffled the first time I saw it. (se the "__call__()" Python method for more info.)

Well to get back to the point, "DTMLFile()" takes two arguments, a file path to a dtml file relative to the product, and a dictionary of the properties you want the dtml file to render. Usually you just pass the current namespace by using the standard Python function "globals()", and the dtml class will automagically render your parameters.

The file name "www/index_html" is a relative path to:

        "<ZOPEDIR>/lib/python/products/minimal/www/index_html.dtml" 

Notice that you should NOT spell out the ".dtml" in the dtml-file name you pass to the DTMLFile class. DTMLFile will add this automagically to your path.

I need somewhere to put my dtml files. For that purpose I created the folder called "www" in the minimal folder. And in that I created my dtml files.

The old index_html looks like this:

        <html><body>Hello World</body></html>

So I put that into a file:

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/index_html.dtml

        <html>
        <body>Hello World</body>
        </html>

If I really want to get creative, I can render a parameter from my class in it using the dtml-var tag:

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/www/index_html.dtml

        <html>
        <body>Hello World<br>
        This object has the id: <dtml-var id>
        </body>
        </html>

I will do the same for my "manage_addMinimalForm()" function. First create the "DTMLFile" instance in my class:

        # Get user id from this form
        manage_addminimalForm = DTMLFile('manage_addminimalForm', globals()) 

And then I save a dtml file in "/lib/python/products/minimal/www/manage_addMinimalForm.dtml":

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/www/manage_addMinimalForm.dtml

        <html>
            <body>
            Please type the id of the minimal instance:<br>
            <form name="form" action="manage_addMinimal"><br>
            <input type="text" name="id"><br>
            <input type="submit" value="add">
            </form>
            </body>
        </html>

In this dtml file you cannot use any dtml tags that tries to render paramters from the class. This is because this page is called before there is any instance of the object. Remember this. At least I got burned a few times before remembering it.

How do Zope remember stuff?

If you are used to writing ordinary dynamic web applications, you are aware of the use of databases. If you are an experienced coder you will develop your business logic in ordinary object oriented fashion, and you will then have to convert the data in those objects into sql tables. You need this to be able to save data between sessions. This is called making the objects persistent.

The difference between the objects and the tables, and the code nessecary to convert to and from the database is called impedance missmatch. In most systems this is a royal pain. If you have tried it, you know why.

In Zope it is so much simpler, as the object oriented database "ZODB" that is part of Zope is directly compatible with Python. So a Python object can be saved in the ZODB without any impedance missmatch. That is GREAT!

More about persistence later. Suffice to say for now that if you inherit from "SimpleItem.SimpleItem", your class will remember any parameters you use in your class.

Now we have another working minimal class

The final files for the minimal class so far are:

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/__init__.py

        from minimal import minimal

        def initialize(context):
            """Initialize the minimal product.
            This makes the object apear in the product list"""
            context.registerClass(
                minimal,
                constructors = (
                    minimal.manage_addMinimalForm, # The first method is 
                                                   # called when someone 
                                                   # adds the product
                    minimal.manage_addMinimal
                )
            )

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/minimal.py

        from OFS import SimpleItem

        from Globals import DTMLFile

        class minimal(SimpleItem.SimpleItem)

            "minimal object"

            meta_type = 'minimal'

            manage_options = (
                {'label': 'View', 'action': 'index_html'},
            ) 

            def __init__(self, id):
                "initialise a new instance of Minimal"
                self.id = id

            # Used to view content of the object
            index_html = DTMLFile('www/index_html', globals()) 

        def manage_addMinimal(self, id, RESPONSE=None):
            "Add a Minimal to a folder."
            self._setObject(id, minimal(id))
            RESPONSE.redirect('index_html')

        manage_addminimalForm = DTMLFile('www/manage_addminimalForm', globals())

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/www/index_html.dtml

        <html>
        <body>Hello World<br>
        This object has the id: <dtml-var id>
        </body>
        </html>

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/www/manage_addMinimalForm.dtml

        <html>
            <body>
                Please type the id of the minimal instance:<br>
                <form name="form" action="manage_addMinimal"><br>
                    <input type="text" name="id"><br>
                    <input type="submit" value="add">
                </form>
            </body>
        </html>

Giving the minimal class just a little bit more functionality

All this minimal bussines is very nice, but I will now add wee bit more functionality to my class, so that it is actually able to do something usefull. It would be very nice to make a minimal news-article class.

Something that would look a little like this in plain Python:

        class minimal:

            def __init__(self, id, title, content):
                self.id      = id
                self.title   = title
                self.content = content

            def index_html(self)
                return """<html>
                <body>
                id: %(id)s<br>
                title: %(title)s<br>
                content: %(content)s<br>
                </body>
                </html>""" % globals()

Actually the minimal product is allready very close in functionality to this news article. I only need to add "title" and "content" as parameters to the class, and some way to edit the content over the web.

First to add the ekstra parameters I just change my "__init__" method to contain them:

        def __init__(self, id, title, content):
            "Inits the product with default values"
            self.id = id
            self.title = title
            self.content = content

I also need to change the "manage_addminimalAction" method so that it remembers to set the parameters during the objets intialisation:

        def manage_addminimalAction(self, id='minimal', title='Title here', 
                                    content='Content here.', REQUEST=None):
            "Add a minimal to a folder."
            self._setObject(id, minimal(id, title, content))
            if REQUEST is not None:
                return self.manage_main(self, REQUEST)

But now that "manage_addminimalAction" takes more parameters I need to send those parameters from the "manage_addMinimalForm". So that will have to be changed too:

        <html>
            <head>
                <title>Add minimal instance</title>
            </head>
            <body bgcolor="#FFFFFF">
                <form name="form" action="manage_addminimalAction" method="post"><br>
                    id:<br>
                    <input type="text" name="id:string" size="30" value="minimal">
                    <br><br>
                    title:<br>
                    <input type="text" name="title:string" size="30" value="Title here">
                    <br><br>
                    content:<br>
                    <textarea name="content:text" cols="40" rows="8" wrap="virtual"
                    >Content here.</textarea>
                    <br><br>
                    <input type="submit" value="  add  ">
                </form>
            </body>
        </html>

Now I can add an article, setting both the id, title and the content on the very first management page. I like it that way, others prefer to just set the id, and then go to the article and edit the rest of the content. Well feel free to do as you please.

Notice btw. that I dont use "<dtml-var standard_html_header>" in my "manage_addMinimalForm". If you have ever tried to make an error in a standard_html_header so that it doesn't render you wil know why. You stand in risk of not being able to log into the management page. So when it comes to the management pages you must KISS. Keep It Simple Smartie.

In order to use the standard management interface in Zope. You know the one with the tabs in the top. You need to name what those tabs are to be called. You do this by setting a tupple on the class:

        manage_options = (
                {'label': 'Properties', 'action': 'manage_editForm',},
                {'label': 'View', 'action': 'index_html',},
        )

Each item in the tuple is a tab. Each item defined as a dict with a "label" and an "action" key. the value of the label describes what text you see on the tab, and the value of the action is the url that the tab links to.

Another more verbose way to write it could be:

        properties           = {}
        properties['label']  = 'Properties'
        properties['action'] = 'manage_editForm'
        view             = {}
        view['label']    = 'View'
        view['action']   = 'index_html'
        manage_options = (properties, view)

Well ... nobody uses it so I probably shouldn't do it either.

So far the minimal article class looks like this:

        from OFS import SimpleItem
        from Globals import DTMLFile

        class minimal(SimpleItem.SimpleItem):

            """
            A minimal product
            """

            meta_type = 'minimal'

            # manage options are common for all instances so they are set here
            manage_options = (
                {'label': 'Properties', 'action': 'manage_editForm',},
                {'label': 'View', 'action': 'index_html',},
            )

            def __init__(self, id, title, content):
                "Inits the product with default values"
                self.id = id
                self.title = title
                self.content = content

            ##########################
            # The web pages that shows the content.

            # Used to view content of the object
            index_html = DTMLFile('www/index_html', globals()) 

        ##########################
        # constructor pages. Only used when the product is added to a folder.

        def manage_addminimalAction(self, id='minimal', title='Title here',
                                    content='Content here.', REQUEST=None):
            "Add a minimal to a folder."
            self._setObject(id, minimal(id, title, content))
            if REQUEST is not None:
                return self.manage_main(self, REQUEST)

        # Get id and content from this form
        manage_addminimalForm = DTMLFile('manage_addminimalForm', globals()) 

Now I only need to add some way of editing the content of the article. For this purpose I will add a dtml form:

        <html>
        <head>
            <title>Manage minimal</title>
        </head>
        <body bgcolor="#FFFFFF">
            <dtml-var manage_tabs>
            <form name="form" action="." method="post"><br>
                title:<br>
                <input type="text" name="title:string" size="30" value="<dtml-var title
                >">
                <br><br>
                content:<br>
                <textarea name="content:text" cols="40" rows="8" wrap="virtual"
                ><dtml-var content></textarea>
                <br><br>
                <input type="submit" value="Change" name="manage_editAction:method">
            </form>
        </body>
        </html>

In this form I get the content from the object and fill out the form fields with it. When the user hits the submit button, the form will be posted to "manage_editAction". So I guess I will have to make that method to in my minimal class:

        def manage_editAction(self, title, content, RESPONSE=None):
            "Changes the product values"
            self.title = title
            self.content = content
            self._p_changed = 1
            RESPONSE.redirect('manage_editForm')

The thing to notice here is that I don't touch the id of the object. I really don't know what would happen if I did, but the id is stored in two different places. One time in the ObjectManager that holds the instance of the object, and once more in the object itself. I reckon that a missmatch between the two id's could put us in a world of pain.

Another thing to notice here is that I trigger the Persistence by setting:

        self._p_changed = 1

This is the great thing about Zopes object database "ZODB". All you have to remember to save the state of an object between states is to set the "_p_changed" parameter to 1 and everything is stored. Even complicated objects could be stored this way:

        def manage_editAction(self, title, content, RESPONSE=None):
            "Changes the product values"
            self.title = title
            self.content = content
            # Even this code would be stored correctly :-D
            self.authors_and_groups = ['maxm', 'somebody else', 
                                       {'groups', ('programmer','managers')}
                                      ]
            # Though I don't know what I should use it for in this case
            self._p_changed = 1
            RESPONSE.redirect('manage_editForm')

Ain't it great? No SQL fluff in this code.

There are other ways of triggering the persistance mechanism, but I really think that it is ALLWAYS a good idea to set it explicitly like this. Anybody who reads your code can see that it is you intent to store changes in the ZODB in a method with this line. Think of it as a "save()" method.

Finally

Now the products does something real and features most of the stuff used in any production products.

Here is the content of all the files, notice that index_html has also been changed. The two files "version.txt" and "README.txt" shows up as tabs in the management interface at "Product at /Control_Panel/Products/minimal " "README.txt" is the place where you wil document the product, using structured text:

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/__init__.py

        import minimal

        def initialize(context): 

            """Initialize the minimal product.
            This makes the object apear in the product list"""

            context.registerClass(
                minimal.minimal,
                constructors = (
                    minimal.manage_addminimalForm,
                    minimal.manage_addminimalAction,
                ),
                icon=None
            )

        ##########################################
        # <ZOPEDIR>/lib/python/products/minimal/minimal.py

        from OFS import SimpleItem
        from Globals import DTMLFile

        class minimal(SimpleItem.SimpleItem):

            """
            A minimal product
            """

            meta_type = 'minimal'

            manage_options = (
                {'label': 'Properties', 'action': 'manage_editForm',},
                {'label': 'View', 'action': 'index_html',},
            )

            def __init__(self, id, title, content):
                "Inits the product with default values"
                self.id = id
                self.title = title
                self.content = content

            def manage_editAction(self, title, content, RESPONSE=None):
                "Changes the product values"
                self.title = title
                self.content = content
                self._p_changed = 1
                RESPONSE.redirect('manage_editForm')

            # The web pages that shows content. Put your own in the www folder.

            index_html = DTMLFile('www/index_html',
                                  globals()) # Used to view content of the object

            manage_editForm = DTMLFile('www/manage_editForm',
                                        globals()) # Edit the content of the object

        # constructor pages. Only used when the product is added to a folder.

        def manage_addminimalAction(self, id='minimal', title='Title here',
                                    content='Content here.', REQUEST=None):
            "Add a minimal to a folder."
            self._setObject(id, minimal(id, title, content))
            if REQUEST is not None:
                return self.manage_main(self, REQUEST)

        manage_addminimalForm = DTMLFile('manage_addminimalForm',
                                          globals()) # Get user id from this form

        ##########################
        # <ZOPEDIR>/lib/python/products/minimal/manage_addminimalForm.dtml

        <html>
        <head>
            <title>Add minimal instance</title>
        </head>
        <body bgcolor="#FFFFFF">

            <form name="form" action="manage_addminimalAction" method="post"><br>

                id:<br>
                <input type="text" name="id:string" size="30" value="minimal">
                <br><br>

                title:<br>
                <input type="text" name="title:string" size="30" value="Title here">
                <br><br>

                content:<br>
                <textarea name="content:text" cols="40" rows="8" wrap="virtual"
                >Content here.</textarea>
                <br><br>

                <input type="submit" value="  add  ">

            </form>

        </body>
        </html>

        ##########################
        # <ZOPEDIR>/lib/python/products/minimal/www/manage_editForm.dtml

        <html>
        <head>
            <title>Manage minimal</title>
        </head>
        <body bgcolor="#FFFFFF">

            <dtml-var manage_tabs>

            <form name="form" action="." method="post"><br>

                title:<br>
                <input type="text" name="title:string" size="30" value="<dtml-var title
                >">
                <br><br>

                content:<br>
                <textarea name="content:text" cols="40" rows="8" wrap="virtual"
                ><dtml-var content></textarea>
                <br><br>

                <input type="submit" value="Change" name="manage_editAction:method">

            </form>

        </body>
        </html>

        ##########################
        # <ZOPEDIR>/lib/python/products/minimal/www/index_html.dtml

        <dtml-var standard_html_header>

        <dtml-if id>
            <b>id:</b><br>
            <dtml-var id>

        <br><br></dtml-if>

        <dtml-if title>
            <b>title:</b><br>
            <dtml-var title>

        <br><br></dtml-if>

        <dtml-if content>
            <b>content:</b><br>
            <dtml-var content>

        <br><br></dtml-if>

        <dtml-var standard_html_footer>

        ##########################
        # <ZOPEDIR>/lib/python/products/minimal/README.txt

        Minimal Article product

            This is a minimal article product. Just fill out the id, title and content.

        ##########################
        # <ZOPEDIR>/lib/python/products/minimal/version.txt

        0.0.1

Keep in mind that the only reason I have for writing these How-To's is if I get any feedback. So if you have any comments, be it positive or negative don't hesitate to send them.

Regards

Max M

Source to the How-To can be found here, if you want to submit patches or the like.