Lesson 2: Writing Page Templates

  1. Files:

Teacher: Paul Carduner

Students: Brittney, Will, Preetam, and Linda

Paul:

Good morning! Is everyone ready for week two of our Zope3 class? Let's begin with a quick look back at what we did last week, followed by a look forward at what we will accomplish today.

Last week we created the interfaces.py file and wrote our first interface: IContact. Recall that an interface is a kind of contract that implementations (in the form of python classes) need to fullfill. With interfaces we can define which attributes and methods the implementation must have. Interfaces also provide us with a good place for documenting the functionality of components in our system.

We also went ahead and made an implementation of the IContact interface in the contact.py file. We then configured this implementation in the configure.zcml file, and even got to the point where we were able to add an instance of the Contact class to the database through the ZMI.

You may have noticed that despite being able to add an instance of the Contact class to the database, we were completely unable to set the lastName variable or to see what it was set to in the first place. That means that our next step is to create a page for displaying information about the Contact object.

The first thing to do is to create a new file called viewcontact.pt . The .pt stands for Page Template. Before we start writing a page template, let's talk about what they are.

All of you have probably had some experience making websites with static web pages. Each page in your website corresponds to a file on the server's file system. If you want the pages to have a certain look and feel, then you might create a template page with empty spaces where you intend to fill in information like heading, content, and a maybe a picture. The template might already contain links to the main pages on your site, so that they can be accessed from anywhere.

With web applications however, most of the pages you see are dynamically generated. That means that each page you see does not have a corresponding file on the server, but rather it is generated from a template, with the empty spaces being filled in based on data from a database.

Zope3 uses ZPT (Zope Page Templates) as its primary templating language. You can write templates in DTML as well, but this is not standard practice nor recommended. A very thurough ZPT reference page is located here: http://www.zope.org/Documentation/Books/ZopeBook/2_6Edition/AppendixC.stx.

So let's start writing a page template and I'll explain how it works as we go. Open up the viewcontact.pt file for editing and type the following lines:

<html>
<body>
  <h1>Z Contact</h1>
  <h3>Last Name: <b tal:content="context/lastName">last name</b></h3>
</body>
</html>

As you can see, what we have written here looks a lot like a regular html file. The only difference is that the <b> tag has this strange tal:content attribute. This attribute is called a tal expression or tales for short. The tal:content attribute tells zope that the content of the b tag (what goes between <b> and </b>) should be filled with the data stored in context/lastName. You might ask what happens to the content of the b tag when last name is already filling it. When zope processes this template and generates the final html page, it will ignore what is already inside the b tag.

Will:

But what about the context/lastName part? How does that access an attribute?

Paul:

context/lastName is a tales path expression. The context part says that we are accessing the object for which this page is being displayed (i.e. the context of the page). Then the lastName part accesses the lastName attribute of the object. This expression is the tales equivalent to the expression in python context.lastName.

If you have managed to follow me this far, then you might want to ask, how do we know what the context variable is? There are several answers to this question, with the answers closer to the truth being much more complicated. So to put it simply but also somewhat inaccurately, when you request a url from the zope server, zope will parse the url to figure out what object you are trying to display. This will make more sense soon!

The beautiful thing about ZPT compared to dtml or php is that you can open up this page template file in your browser and it will render as html. All the tal:content attributes will just be ignored by the web browser. Go ahead and open up this page template file in a web browser and you will see what our page is going to look like.

Now that we have this page template file, it would be nice to see it in action. Before we are able to do that, we have to connect it to our Contact class and configure permissions for it. Open up the configure.zcml file and add the following lines:

  <browser:page
      for="zcontact.interfaces.IContact"
      name="index.html"
      permission="zope.View"
      template="viewcontact.pt"
      />

Be sure to put this tag within the configure tags. The for attribute specifies the interface for which this page can appear. When we connect a page template to an object, we really want to connect it to an interface. For example, if we had multiple classes that all implemented the same interface, then one page template would work for all the different classes because each class provides the same attributes. For that reason we register pages with interfaces. The name attribute specifies how we access this page template through a url. By setting the name to index.html then this page template is the default one used for the object. Then there is the permission attribute, which specifies what permission a user must have to view the page. By default, all users (logged in or not) have the zope.View permission. Finally, the template attribute specifies the path to the file where the template is located.

With the page attribute in the configure.zcml file, we should be able to restart the zope server and try out the page template.

After restarting your zope server, log in, and create a Contact object through the ZMI. To refresh your memory from last week, you first click on the top link under the Navigation section in the upper left hand corner of the screen. This will open up an Add: table below where you clicked. Now click Z Contact Page in this table. This will place you in a new screen which looks like this:

Fill in the Name with a name for the object, say test, and click the Apply button. You will see your new object. In the URL of your browser, replace @@contents.html with index.html, and you should see something like this:

Z Contact
Last Name:

Of course, this still doesn't let us change the empty default string of the lastName attribute. To acheive that final goal, we will have to create a new page template, this time with a form element.

Create and edit a new file called editcontact.pt and add to it the following lines of code:

<html>
<body>
  <h1>Z Contact</h1>
  <form action="index.html">
    <h3>Last Name:
      <input type="text" name="lastName"
             tal:attributes="value context/lastName" />
    </h3>
    <input type="submit" name="SAVE" value="Save" />
  </form>
</body>
</html>

I'm going to assume you already know how html forms work. The only new thing here is the tal:attributes attribute on the input tag. Instead of filling in the content of a tag like the tal:content attribute did, this tales fills in the value for another attribute on the tag. In this case we are setting the value attribute of the input tag (which is what you see in the input box) to whatever the lastName attribute is currently set.

Again we will add a browser:page tag to the configure.zcml file that looks like this:

  <browser:page
      for="zcontact.interfaces.IContact"
      permission="zope.ManageContent"
      template="editcontact.pt"
      <class="zcontact.browser.ContactView"
      />

Since this looks almost exactly the same as the previous page tag we made, I won't explain it.

Again you should be able to restart your zope server, and browse to the contact object. Since we will be given the index.html page, it might be nice to have a link on that page to our edit form. So you should modify the viewcontact.pt file to look like this:

<html>
<body>
  <h1>Z Contact</h1>
  <h3>Last Name: <b tal:content="context/lastName">last name</b></h3>
  <a href="edit.html">Edit</a>
</body>
</html>

Note: When you only change a page template file, it is not necessary to restart your zope server.

So if you enter /edit.html after your content object name, you should get a page tat looks something like:

Z Contact
Last Name:

Now the problem is that clicking on the Save button doesn't actually do anything! We need to write what is called a browser view class to process the information entered into the form.

Create and edit a new file called browser.py and add the following lines:

import zope.security.proxy

class ContactView(object):
    """View for showing and modifying a contact"""
    def __init__(self, context, request):
        self.context = context
        self.request = request

        if self.request.get("SAVE"):
            context = zope.security.proxy.removeSecurityProxy(self.context)
            context.lastName = self.request.get("lastName")

To better understand how this is supposed to work, I will write out what happens in steps:

  1. User inputs data into form
  2. User submits data using the Save button
  3. Data gets sent to the index.html page via the url (looks like http://localhost/contact/index.html?lastName=Whatever)
  4. The browser view class for the index.html page gets instantiated and processes the data, which is stored in a request object
  5. Finally, the index.html page gets rendered with the updated information

When browser view classes are instantiated, they get sent the context object (the same one the page templates access) and the request object (with all the data in it). That is why we have to put context and request in the __init__ method (and always in that order). The request object is like a python directory. The self.request.get("SAVE") checks that we clicked on the save button.

Linda:

But what about the removeSecurityProxy? Do we have to make the code input safe (or is it already), or does Zope take care of that?

Paul:

Zope takes care of the safe input part. removeSecurityProxy doesn't handle input at all. Normally web pages (page templates and browser vieiws) are not allowed to modify the data stored in an object because of a protective wrapper around the object called a security proxy. So in order to change data on the context object, we have to remove the security proxy.

Now edit the configure.zcml file and add an attribute to the first browser:page tag (the one for index.html). The line should be class="zcontact.browser.ContactView" and the tag should look like

  <browser:page
    for="zcontact.interfaces.IContact"
    name="index.html"
    permission="zope.View"
    template="viewcontact.pt"
    class="zcontact.browser.ContactView"
    />

When we specify a class attribute, zope will create an instance of the class before generating the html from the page template.

Brittney:

Will we have to create a separate class in browser.py for every class that can be updated?

Paul:

No, but we'll get to that later.

We specifiy the attribute for the index.html page and not for edit.html because the index.html page is where the form data gets sent when we click Save, so index.html processes the data.

However, you will see that if you restart your zope server, the last name that you inputted is not kept! This is because if the class is not persistent, then all the data stored in it will disappear when zope is restarted.

To fix this, open up the contact.py file and import persistent.

import zope.interface
import persistent
import interfaces

class Contact(persistent.Persistent):
    """Implementation of IContact"""
    zope.interface.implements(interfaces.IContact)
    lastName = u''
Now, when you look at your contact object in the ZMI, there should be an error like:
TypeError: ('object.__new__(Contact) is not safe,
use persistent.Persistent.__new__()', <function _reconstructor
at 0xb7deded4>, (<class 'zcontact.contact.Contact'>,
<type 'object'>, None))

Preetam:

Is there a reason why my zope instance doesn't work because of that error? I find that it works if I don't import from persistent.Persistent.

Paul:

Correct, Preetam. That is the problem with inheriting from persistent.Persistent when you already have an instance of the object in the database. When zope stores objects in its database, it saves all the attributes from the object. But if you change the definition of a class and then restart zope, the database won't necessarily conform to your class definition. When this happens, problems can occur. This exercise is to let you remember to use persistence from the start.

Fortunately, we have only created one instance of the Contact class, so we are not going to lose a lot of data by making the new class definition incompatible with what's in the database. To resolve this incompatibility, take out the persistent inheritance in the class definition in your contact.py file. Restart your zope server and then delete all of your instances of the Contact class in your zope server through the ZMI. Finally, add persistence back in, restart your zope server and everything should work.

Brittney:

Is there any way to avoid this? In case you forgot to include persistence?

Paul:

Yes, but the solution is somewhat complicated and involves things called generation scripts. These scripts won't come up until much later.

Now that you can add the Contact to your zope server and changed the data stored in it, we can conclude the second class.

Files:

When you click on the links belove you will be presented with text versions of these files. If you choose to save these files from your web browser, be sure to put the files in the proper directory structure.

  1. zcontact/interfaces.py
  2. zcontact/contact.py
  3. zcontact/zcontact-configure.zcml
  4. zcontact/configure.zcml
  5. zcontact/__init__.py
  6. zcontact/viewcontact.pt
  7. zcontact/editcontact.pt
  8. zcontact/browser.py

A tarball of all these files is located here: lesson02.tgz

Previous Lesson

Front Page

Next Lesson