In Chapter 3, "Using Basic Zope Objects" and Chapter 4, "Dynamic Content with DTML" you learned about basic Zope objects and DTML. In this chapter you'll see how you can build simple but powerful web applications using these tools. In later chapters of the book you'll discover more complex objects and more complex DTML. However, the design techniques covered in this chapter are still relevant.
Note: in chapter 3, "Basic Zope Objects", we explained how Zope Page Templates are new to Zope and should be used for presentation. We have not yet converted this chapter over to use Page Templates instead of DTML. We will be rewriting this chapter soon to reflect new methedologies based on page templates soon.
Folders are the "basic building blocks" of Zope applications. Folders allow you to organize your Zope objects, and actively participate in your web applications. Folders are given behavior by adding scripts to them.
Scripts and folders work together to build simple applications. Folders provide structure for your information and also provide a framework for your site's behavior. Later in this chapter, an example of a simple guest book application based on this design concept is given. A folder is used to hold the methods, scripts and data of the guest book application, the scripts provide behavior that define how the application works, and the methods provide presentation to the application.
For example, suppose you have an Invoices folder to hold invoices. You could create objects inside that folder named addInvoice and editInvoice to allow you to add and edit invoices. Now your Invoices folder becomes a small application.
Zope's simple and expressive URLs are used to work with the invoices application. As you've seen, you can display a Zope object by going to its URL in your browser. So for example, the URL http://localhost:8080/Invoices/addInvoice calls the addInvoice object on the Invoices folder. This URL might take you to a screen that lets you add an invoice. Likewise, the URL http://localhost:8080/Invoices/editInvoice?invoice_number=42 calls the editInvoice object on the Invoices folder and passes it the argument invoice_number with a value of 42. This URL could allow you to edit invoice number 42.
The invoices example demonstrates a powerful Zope feature. You can call an object on a folder by going to a URL that consists of the folder's URL followed by the id of the object. This facility is used throughout Zope and is a very general design pattern. In fact you are not just restricted to calling objects on folders. You'll see later how you can call objects on all kinds of Zope objects using the same URL technique.
For example suppose you want to call an object named viewFolder on one of your folders. Perhaps you have many different viewFolder objects in different locations. Zope figures out which one you want by first looking in the folder that you are calling the object on. If it can't find the object there it goes up one level and looks in the folder's containing folder. If the object can't be found there it goes up another level. This process continues until Zope finds the object or gets to the root folder. If Zope can't find the object in the root it gives up and raises an exception.
You'll see this kind of dynamic behavior in many different places in Zope. This technique is called acquisition. A folder is said to acquire a object by searching for the object in its containers.
As you've seen, folders can acquire all kinds of objects. There is one special object that Zope uses to display a folder. This object is named index_html.
The index_html object provides a default view of the folder. This is analogous to how an index.html file provides a default view for a directory in Apache and other web servers.
For example, if you create an index_html object in your Invoices folder and view the folder by clicking the View tab or by visiting the URL http://localhost:8080/Invoices/, Zope will call the index_html object on the Invoices folder.
A folder can also acquire an index_html object from its parent folders just as it can acquire any object. You can use this behavior to create a default view for a bunch of folders all in one place. If you want a different default view of a given folder, just create a custom index_html object in that folder. This way you can override the index_html object defined higher up.
In this section, you'll create a simple web site for the Zope Zoo. As the Zoo webmaster, it is your job to make the web site easy to use and manage. Here are some things you'll need:
In order for your navigation system to work, your site will need some basic structure through which to navigate. Create some folders in your Zope system that represent the structure of your site. Let's use a zoo structure with the following layout, as shown in Figure 5-1.
Figure 5-1 Zoo folder structure.
The main structure of the Zope Zoo contains three top level folders, Reptiles, Mammals and Fish. To navigate your site, users should first go to your home page and click on one of the top level folders to enter that particular part of the Zoo. They should also be able to use a very similar interface to keep going deeper into the site; i.e. the snakes section. Also, the user should be able to back out of a section and go up to the parent section.
You can accomplish this easily with Zope. In your ZopeZoo folder, create a DTML Method called navigation:
<ul> <dtml-in expr="objectValues('Folder')"> <li><a href="&dtml-absolute_url;"><dtml-var title_or_id></a></li><br> </dtml-in> </ul>
The method you just created shows a list of links to the various sub-sections of the zoo. It's important to notice that this method can work on any zoo folder since it makes no assumptions about the folder. Also since we placed this method in the ZopeZoo folder, all the zoo folders can acquire it.
Now, you need to incorporate this method into the site. Let's put a reference to it in the standard_html_header object so that the navigation system is available on every page of the site. Your standard_html_header could look like this:
<html> <head><title><dtml-var title></title></head> <body> <dtml-var navigation>
Next we need to add a front page to the Zoo site and then we can view the site and verify that the navigation works correctly.
Now, you need a front page that serves as the welcome screen for Zoo visitors. Let's create a DTML Method in the ZopeZoo folder called index_html with the following content:
<dtml-var standard_html_header> <h1>Welcome to the Zope Zoo</h1> <p>Here you will find all kinds of cool animals. You are in the <b><dtml-var getId></b> section.</p> <dtml-var standard_html_footer>
Take a look at how your site appears by clicking on the View tab in the root folder, as shown in Figure 5-2.
Figure 5-2 Zope Zoo front page.
Here you start to see how things come together. At the top of your main page you see a list of links to the various subsections. These links are created by the navigation method that is called by the standard_html_header method.
You can use the navigation links to travel through the various sections of the Zoo. Use this navigation interface to find the reptiles section.
Zope builds this page to display a folder by looking for the default folder view method ,index_html. It walks up the zoo site folder by folder until it finds the index_html method in the ZopeZoo folder. It then calls this method on the Reptiles folder. The index_html method calls the standard_html_header method which in turn calls the navigation method. Finally, the index_html method displays a welcome message and calls the standard_html_footer.
What if you want the reptile page to display something besides the welcome message? You can replace the index_html method in the reptile section with a more appropriate display method and still take advantage of the zoo header and footer including navigation.
In the Reptile folder create a DTML Method named index_html. Give it some content more appropriate to reptiles:
<dtml-var standard_html_header> <h1>The Reptile House</h1> <p>Welcome to the Reptile House.</p> <p>We are open from 6pm to midnight Monday through Friday.</p> <dtml-var standard_html_footer>
Now take a look at the reptile page by going to the Reptile folder and clicking the View tab.
Since the index_html method in the Reptile folder includes the standard headers and footers, the reptile page still includes your navigation system.
Click on the Snakes link on the reptile page to see what the Snakes section looks like. The snakes page looks like the Reptiles page because the Snakes folder acquires its index_html display method from the Reptiles folder.
The navigation system for the zoo works pretty well, but it has one big problem. Once you go deeper into the site you need to use your browser's back button to go back. There are no navigation links to allow you to navigate up the folder hierarchy. Let's add a navigation link to allow you to go up the hierarchy. Change the navigation method in the root folder:
<a href="..">Return to parent</a><br> <ul> <dtml-in expr="objectValues('Folder')"> <li><a href="&dtml-absolute_url;"><dtml-var title_or_id></a><br></li> </dtml-in> </ul>
Now browse the Zoo site to see how this new link works, as shown in Figure Figure 5-3.
Figure 5-3 Improved zoo navigation controls.
As you can see, the Return to parent link allows you to go back up from a section of the site to its parent. However some problems remain; when you are at the top level of the site you still get a Return to parent link which leads nowhere. Let's fix this by changing the navigation method to hide the parent link when you're in the ZopeZoo folder:
<dtml-if expr="_.len(PARENTS) > 2"> <a href="..">Return to parent</a><br> </dtml-if> <ul> <dtml-in expr="objectValues('Folder')"> <li><a href="&dtml-absolute_url;"><dtml-var title_or_id></a><br></li> </dtml-in> </ul>
Now the method tests to see if the current object has any parents before it display a link to the parent. PARENTS is a list of the current object's parents, and len is a utility function which returns the length of a list. See Appendix A for more information on DTML utility functions. Now view the site. Notice that now there is no parent link when you're viewing the main zoo page.
There are still some things that could be improved about the navigation system. For example, it's pretty hard to tell what section of the Zoo you're in. You've changed the reptile section, but the rest of the site all looks pretty much the same with the exception of having different navigation links. It would be nice to have each page tell you what part of the Zoo you're in.
Let's change the navigation method once again to display where you are:
<dtml-if expr="_.len(PARENTS) > 2"> <h2><dtml-var title_or_id> Section</h2> <a href="..">Return to parent</a><br> </dtml-if> <ul> <dtml-in expr="objectValues('Folder')"> <li><a href="&dtml-absolute_url;"><dtml-var title_or_id></a><br></li> </dtml-in> </ul>
Now view the site again.
Figure 5-4 Zoo page with section information.
As you can see in Figure 5-4, the navigation method now tells you what section you're in along with links to go to different sections of the zoo.
Zoo pages are built by collections of methods that operate on folders. For example, the header method calls the navigation method to display navigation links on all pages. In addition to factoring out shared behavior such as navigation controls, you can use different Zope objects to factor out shared content.
Suppose you'd like to use CSS (Cascading Style Sheets ) to tailor the look and feel of the zoo site. One way to do this would be to include the CSS tags in the standard_html_header method. This way every page of the site would have the CSS information. This is a good way to reuse content, however, this is not a flexible solution since you may want a different look and feel in different parts of your site. Suppose you want the background of the snakes page to be green, while the rest of the site should have a white background. You'd have to override the standard_html_header in the Snakes folder and make it exactly the same as the normal header with the exception of the style information. This is an inflexible solution since you can't vary the CSS information without changing the entire header.
You can create a more flexible way to define CSS information by factoring it out into a separate object that the header will insert. Create a DTML Document in the ZopeZoo folder named style_sheet. Change the contents of the document to include some style information:
<style type="text/css"> h1{ font-size: 24pt; font-family: sans-serif; } p{ color: #220000; } body{ background: #FFFFDD; } </style>
This is a CSS style sheet that defines how to display h1, p and body HTML tags. Now let's include this content into our web site by inserting it into the standard_html_header method:
<html> <head> <dtml-var style_sheet> </head> <body> <dtml-var navigation>
Now, when you look at documents on your site, all of their paragraphs will be dark red, and the headers will be in a sans-serif font.
To change the style information in a part of the zoo site, just create a new style_sheet document and drop it into a folder. All the pages in that folder and its sub-folders will use the new style sheet.
File libraries are common on web sites since many sites distribute files of some sort. The old fashioned way to create a file library is to upload your files, then create a web page that contains links to those files. With Zope you can dynamically create links to files. When you upload, change or delete files, the file library's links can change automatically.
Create a folder in the ZopeZoo folder called Files. This folder contains all of the file you want to distribute to your web visitors.
In the Files folder create some empty file objects with names like DogGrooming or HomeScienceExperiments, just to give you some sample data to work with. Add some descriptive titles to these files.
DTML can help you save time maintaining this library. Create an index_html DTML Method in the Files folder to list all the files in the library:
<dtml-var standard_html_header> <h1>File Library</h1> <ul> <dtml-in expr="objectValues('File')"> <li><a href="&dtml-absolute_url;"><dtml-var title_or_id></a></li> </dtml-in> </ul> <dtml-var standard_html_footer>
Now view the Files folder. You should see a list of links to the files in the Files folder as shown in Figure 5-5.
Figure 5-5 File library contents page.
If you add another file, Zope will dynamically adjust the file library page. You may also want to try changing the titles of the files, uploading new files, or deleting some of the files.
The file library as it stands is functional but Spartan. The library doesn't let you know when a file was created, and it doesn't let you sort the files in any way. Let's make the library a little fancier.
Most Zope objects have a bobobase_modification_time method that returns the time the object was last modified. We can use this method in the file library's index_html method:
<dtml-var standard_html_header> <h1>File Library</h1> <table> <tr> <th>File</th> <th>Last Modified</th> </tr> <dtml-in expr="objectValues('File')"> <tr> <td><a href="&dtml-absolute_url;"><dtml-var title_or_id></a></td> <td><dtml-var bobobase_modification_time fmt="aCommon"></td> </tr> </dtml-in> </table> <dtml-var standard_html_footer>
The new file library method uses an HTML table to display the files and their modification times.
Finally let's add the ability to sort this list by file name or by modification date. Change the index_html method again:
<dtml-var standard_html_header> <h1>File Library</h1> <table> <tr> <th><a href="&dtml-URL0;?sort=name">File</a></th> <th><a href="&dtml-URL0;?sort=date">Last Modified</a></th> </tr> <dtml-if expr="_.has_key('sort') and sort=='date'"> <dtml-in expr="objectValues('File')" sort="bobobase_modification_time" reverse> <tr> <td><a href="&dtml-absolute_url;"><dtml-var title_or_id></a></td> <td><dtml-var bobobase_modification_time fmt="aCommon"><td> </tr> </dtml-in> <dtml-else> <dtml-in expr="objectValues('File')" sort="id"> <tr> <td><a href="&dtml-absolute_url;"><dtml-var title_or_id></a></td> <td><dtml-var bobobase_modification_time fmt="aCommon"><td> </tr> </dtml-in> </dtml-if> </table> <dtml-var standard_html_footer>
Now view the file library and click on the File and Last Modified links to sort the files. This method works with two sorting loops. One uses the in tag to sort on an object's id. The other does a reverse sort on an object's bobobase_modification_time method. The index_html method decides which loop to use by looking for the sort variable. If there is a sort variable and if it has a value of date then the files are sorted by modification time. Otherwise the files are sorted by id.
A guest book is a common and useful web application that allows visitors to your site to leave messages. Figure Figure 5-6 shows what the guest book you're going to write looks like.
Figure 5-6 Zoo guest book.
Start by creating a folder called GuestBook in the root folder.
Give this folder the title The Zope Zoo Guest Book
. The
GuestBook folder will hold the guest book entries and methods to
view and add entries. The folder will hold everything the guest
book needs. After the guest book is done you will be able to copy
and paste it elsewhere in your site to create new guest books.
You can use Zope to create a guest book several ways, but for this example, you'll use one of the simplest. The GuestBook folder will hold a bunch of DTML Documents, one document for each guest book entry. When a new entry is added to the guest book, a new document is created in the GuestBook folder. To delete an unwanted entry, just go into the GuestBook folder and delete the unwanted document using the management interface.
Let's create a method that displays all of the entries. Call this method index_html so that it is the default view of the GuestBook folder:
<dtml-var standard_html_header> <h2><dtml-var title_or_id></h2> <!-- Provide a link to add a new entry, this link goes to the addEntryForm method --> <p> <a href="addEntryForm">Sign the guest book</a> </p> <!-- Iterate over each DTML Document in the folder starting with the newest documents first. --> <dtml-in expr="objectValues('DTML Document')" sort="bobobase_modification_time" reverse> <!-- Display the date, author and contents of each document --> <p> <b>On <dtml-var bobobase_modification_time fmt="aCommon">, <dtml-var guest_name html_quote null="Anonymous"> said:</b><br> <dtml-var sequence-item html_quote newline_to_br> <!-- Make sure we use html_quote so the users can't sneak any HTML onto our page --> </p> </dtml-in> <dtml-var standard_html_footer>
This method loops over all the documents in the folder and displays each one. Notice that this method assumes that each document will have a guest_name property. If that property doesn't exist or is empty, then Zope will use Anonymous as the guest name. When you create a entry document you'll have to make sure to set this property.
Next, let's create a form that your site visitors will use to add new guest book entries. In the index_html method above we already created a link to this form. In your GuestBook folder create a new DTML Method named addEntryForm:
<dtml-var standard_html_header> <p>Type in your name and your comments and we'll add it to the guest book.</p> <form action="addEntryAction" method="POST"> <p> Your name: <input type="text" name="guest_name" value="Anonymous"> </p> <p> Your comments: <br> <textarea name="comments" rows="10" cols="60"></textarea> </p> <p> <input type="submit" value="Send Comments"> </p> </form> <dtml-var standard_html_footer>
Now when you click on the Sign Guest Book link on the guest book page you'll see a form allowing you to type in your comments. This form collects the user's name and comments and submits this information to a method named addEntryAction.
Now create an addEntryAction DTML Method in the GuestBook folder to handle the form. This form will create a new entry document and return a confirmation message:
<dtml-var standard_html_header> <dtml-call expr="addEntry(guest_name, comments)"> <h1>Thanks for signing our guest book!</h1> <p><a href="<dtml-var URL1>">Return</a> to the guest book.</p> <dtml-var standard_html_footer>
This method creates a new entry by calling the addEntry method and returns a message letting the user know that their entry has been added.
The last remaining piece of the puzzle is to write the script that will create a document and sets its contents and properties. We'll do this in Python since it is much clearer than doing it in DTML. Create a Python-based Script in the GuestBook folder called addEntry with parameters guest_name and comments:
## Script (Python) "addEntry" ##parameters=guest_name, comments ## """ Create a guest book entry. """ # create a unique document id id='entry_%d' % len(context.objectIds()) # create the document context.manage_addProduct['OFSP'].manage_addDTMLDocument(id, title="", file=comments) # add a guest_name string property doc=getattr(context, id) doc.manage_addProperty('guest_name', guest_name, 'string')
This script uses Zope API calls to create a DTML Document and to create a property on that document. This script performs the same sort of actions in a script that you could do manually; it creates a document, edits it and sets a property.
The guest book is now almost finished. To use the simple guest book, just visit http://localhost:8080/GuestBook/.
One final thing is needed to make the guest book complete. More than likely your security policy will not allow anonymous site visitors to create documents. However the guest book application should be able to be used by anonymous visitors. In Chapter 7, User and Security, we'll explore this scenario more fully. The solution is to grant special permission to the addEntry method to allow it to do its work of creating a document. You can do this by setting the Proxy role of the method to Manager. This means that when the method runs it will work as though it was run by a manager regardless of who is actually running the method. To change the proxy roles go to the Proxy view of the addEntry method, as shown in Figure 5-7.
Figure 5-7 Setting proxy roles for the addEntry method.
Now select Manager from the list of proxy roles and click Change.
Congratulations, you've just completed a functional web application. The guest book is complete and can be copied to different sites if you want.
All Zope objects can create XML. It's fairly easy to create XML with DTML. XML is just a way of describing information. The power of XML is that it lets you easily exchange information across the network. Here's a simple way that you could represent your guest book in XML:
<guestbook> <entry> <comments>My comments</comments> </entry> <entry> <comments>I like your web page</comments> </entry> <entry> <comments>Please no blink tags</comments> </entry> </guestbook>
This XML document may not be that complex but it's easy to generate. Create a DTML Method named "entries.xml" in your guest book folder with the following contents:
<guestbook> <dtml-in expr="objectValues('DTML Document')"> <entry> <comments><dtml-var document_src html_quote></comments> </entry> </dtml-in> </guestbook>
As you can see, DTML is equally adept at creating XML as it is at creating HTML. Simply embed DTML tags among XML tags and you're set. The only tricky thing that you may wish to do is to set the content-type of the response to text/xml, which can be done with this DTML code:
<dtml-call expr="RESPONSE.setHeader('content-type', 'text/xml')">
The whole point of generating XML is producing data in a format that can be understood by other systems. Therefore you will probably want to create XML in an existing format understood by the systems you want to communicate with. In the case of the guest book a reasonable format may be the RSS (Rich Site Summary) XML format. RSS is a format developed by Netscape for its my.netscape.com site, which has since gained popularity among other web logs and news sites. The Zope.org web site uses DTML to build a dynamic RSS document.
Congratulations! You've XML-enabled your guest book in just a couple minutes. Pat yourself on the back. If you want extra credit, research RSS enough to figure out how to change entries.xml to generate RSS.
This chapter shows how simple web applications can be made. Zope has many more features in addition to these, but these simple examples should get you started on create well managed, complex web sites.
In the next chapter, we'll see how the Zope security system lets Zope work with many different users at the same time and allows them to collaborate together on the same projects.