Lesson 3: Schemas, Macros and Auto Generated Forms

  1. Files:

Teacher: Paul Carduner

Students: Brittney, Will, Preetam, and Linda

Paul:

Hello, and welcome to the third week of our Zope3 class. First we will briefly go over what we covered last time, and then get into some work.

Last week we made our first page templates and a browser view class. This allowed us to display information about a contact and have the user input information about a contact with html forms. We also made our object persistent and learned how to register pages in ZCML.

Today we will continue working on how information is displayed to and inputted by the user. Our major goal for this application is to have a well functioning and slender address book. But at the moment, all we can enter about a person is their last name (a bit to slender of an address book to be useful). We want to continue by adding more peices of information to store about a contact - specifically their first name, phone number, email address, and street address.

Can anyone tell me what the first step is going to be?

Will:

Do we add corresponding attributes to the class definition?

Paul:

Almost, but not quite. You might look back to where we first started. What was the first thing we wrote (besides the __init__.py file)? It was an interface. When you modify the description of an object (not necessarily the implementation) then you have to first change the interface - which describes an object. So the first thing you should do is open up the interfaces.py file for editing.

Now your first instinct is probably going to be to create more Attributes. But in fact we will actually be creating what is called a schema. Schemas are a lot like interfaces except that instead of creating a contract for a class implementation, schemas form a contract for each individual attribute in a class.

Let's just write the schema, and I will explain how it works once we have something to look at. The first thing to do is to add a import zope.schema line at the top of the interfaces.py file. Next, modify the lastName attribute to look like this:

lastName = zope.schema.TextLine(
        title=u"Last Name",
        description=u"A person's last name.",
        required=True)

Linda:

Does that mean we still have to do import zope.interface at the top of the program?

Paul:

Yes. You have to remember that IContact is still inheriting from the zope.interface.Interface class. Also, schemas don't exist outside of interfaces (ie., no schema is not an interface). And because schemas have to be interited, it is like it is being stored in the interface.

With that code in place, the IContact interface is now also a schema. Schemas are built right into interface definitions because they essentially extend an interface. It is reasonable to think the difference between interfaces and schemas is pretty arbitrary and only a matter of nomenclature. For now, it's fine to think of schemas and interfaces as the same thing because they are so intertwined. Just note that they are in fact somewhat different. As Stephan Richter writes in his Zope 3 book, The methods of an interface describe the functionality of a component, while the schema's fields represent the state.

Looking at the code, we see that instead of creating an Attribute object as we had before, we have a TextLine field. When we refer to attributes that are set with schemas, we call them schema fields. Schemas allow us to specify what kind of data should be stored in a particular variable, whether it is string data, numerical data, a date, a url, a list of things, etc. For a longer list of schema fields, look at Chapter 8.3 of the Zope 3 Book ( http://wiki.zope.org/zope3/schema.html). Schemas also allow us to specify documentation in the form of a title and a description. What you put in title and description can end up in the user interface as we will see later in the lesson. Finally, there is the required parameter which specified whether or not the attribute must be filled in with data - the alternative being to leave it empty (None).

So now that we understand how that works, lets go ahead and add more schema fields for each of our new attributes: firstName, phone, email and address. These will all be using the TextLine schema field. The only required attribute should be lastName. Your interfaces.py file should look like this:

import zope.interface
import zope.schema

class IContact(zope.interface.Interface):
    """The interface for a contact."""

    lastName = zope.schema.TextLine(
        title=u"Last Name",
        description=u"A contact's last name.",
        required=True)

    firstName = zope.schema.TextLine(
        title=u"First Name",
        description=u"A contact's first name.",
        required=False)

    phone = zope.schema.TextLine(
        title=u"Phone",
        description=u"A contact's phone number.",
        required=False)

    email = zope.schema.TextLine(
        title=u"Email",
        description=u"A contact's email address.",
        required=False)

    address = zope.schema.TextLine(
        title=u"Address",
        description=u"A contact's address.",
        required=False)

Now that we have modified our interface and extended it into a schema, we need to change the implementation to match the schema. So open up the contact.py file and add the new attributes. At the end the contact.py file should look like this:

import zope.interface
import persistent

import interfaces

class Contact(persistent.Persistent):
    """Implementation of IContact"""
    zope.interface.implements(interfaces.IContact)

    lastName = u''
    firstName = u''
    phone = u''
    email = u''
    address = u''

Can anyone tell me what's next?

Will:

We have to modify the page template so that it displays data from these new attributes right?

Paul:

Yes, that's correct. Open up the viewcontact.pt and modify your file to look like this:

<html>
<body>
  <h1>Z Contact</h1>
  <h3>Name:
    <b tal:content="string:${context/lastName}, ${context/firstName}">name</b>
  </h3>
  <h4>Email: <span tal:replace="context/email"/></h4>
  <h4>Phone: <span tal:replace="context/phone"/></h4>
  <h4>Address: <span tal:replace="context/address"/></h4>
</body>
</html>

Pay particularly close attention to the tal:content attribute for displaying the first and last name. When we start with string:, that tells the page template parser that what follows is not a path expression. Rather it is a string expression. String expressions return plain strings. If we want to include dynamic variables in the string that the expression returns, then we have to wrap it up in ${}. In python, this expression might look like "%s, %s" % (context.lastName, context.firstName). Instead of using %s, we just put the variable right inside the string expression.

So that should be it for the viewcontact.pt file, but what about the editcontact.pt file?

Will:

Oh no, are we going to have to add input fields for all the variables, and then change the browser view to work with all those variables?

Paul:

Fortunately, no. Once you start having objects that store lots of different data, writing and rewriting forms can become a pain in the butt really fast. To combat this problem, Zope 3 is capable of auto generating forms, and even browser views that handle the form input! In fact, we don't even need to write a page template at all. The only thing we need to do is change some zcml registration. That said, open up the configure.zcml file.

Scroll down to place where we registered the edit.html page for editing a contact. I want you to replace that entire tag with a new one that looks like this:

  <browser:editform
      label="Edit Contact"
      for="zcontact.interfaces.IContact"
      name="edit.html"
      permission="zope.ManageContent"
      schema="zcontact.interfaces.IContact"
      />

You will notice in the code that we kept the the attributes name, for and permission the same. We got rid of the template file becase we are no longer going to need it, and we added a label and a schema. The label attribute just tells Zope what to display at the top of the form (so that the user knows what they are looking at). The schema attribute obviously tells zope which schema to generate the form from.

Zope will look at the attributes in the scema, look at what we put for title and description, and look at the type of schema field it is (text input, date input, etc) and display the appropriate widget in html (input box, radio buttons, drop down list, etc.) As I said before, the auto generated forms also automatically handle form input. That means that all the data the user enters will be automatically stored into the right object. On top of that, before storing any data, it will verify that the input is of the correct type (a valid date, a number vs. characters, a url, etc.). If the input is not correct, it automatically spits an error back to the user, asking them to fill in the correct information. Auto generated forms do some other stuff, but I think you get the idea: they are Awesome!

Will:

Wow, that is really awesome! How come we didn't use auto generated forms in the first place?!

Paul:

Well, I wanted to expose you to a somewhat lower level version of how to get data from the user and into your object. Also, there will come a time when you want a super highly customized AJAXified super form, and the auto generated forms just won't cut it. But for our purposes right now, they work like a charm.

At this point, you should restart your zope server and try out that edit form!

While trying out that edit form, you probably noticed that it showed up in the ZMI. The auto generated forms use what is called a page macro, which essentially sticks it into the ZMI. Or it might be better to say that the macro sticks the ZMI around the auto generated form. It would be really cool if our entire contact system were built right into the ZMI. This is one of the cool things about Zope. Since we are talking about a component architecture, we often want to build one component into another one. That is, we want out Z Contact application to work seemlessly with all the other components in Zope - especially when it comes to navigation.

Fortunately, adding the page macro to our application is almost trivial. We only have one page template now, the viewcontact.pt file. Open up that file for editing and change the top two lines, which include the html tag and the body tag, to:

<html metal:use-macro="context/@@standard_macros/view">
<body metal:fill-slot="body">
Here we are using the metal namespace. Macros are a lot like page templates in that they define a general layout for something. The difference is that instead of filling in tags with data from a database, a page macro fills in slots with html generated from a page template. This way we can have page templates generate html for multiple parts of a page, and they can be glued together using the macro. For example, in the ZMI, one of the slots is the body, which is what you see in the main pane. The other slots include the tabs at the top, the navigation on the left, and the add menu on the left, among other things.

The metal:fill-slot attribute on the body tag tells the page tempalte rendering engine that we are going to take everything inside this body tag, and stick it into the body slot in the macro. The fact that the name of the tag and the name of the slot are the same is totally arbitrary. We could just have easily used a div instead of a body tag.

Now that you have changed the page template, go ahead and reload the page which displays the contact object. Remember, you do not have to restart your zope server if you only changed a page template. The page should now look something like this:

If you have explored around the ZMI a bit, you will have noticed that a lot of navigation is accessed through the tabs you see at the top of every page. Since we want out contact application to be built right into the ZMI, it would be handy if the navigation worked in the same way. To add these tabs for the main display page and the edit page, we will have to edit the page configurations in the configure.zcml file. Go ahead and open up the configure.zcml and modify it like so: (new lines are highlighted)

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

  <browser:editform
      label="Edit Contact"
      for="zcontact.interfaces.IContact"
      name="edit.html"
      permission="zope.ManageContent"
      schema="zcontact.interfaces.IContact"
      menu="zmi_views" title="Edit Contact"
      />

zmi_views is the name for the menu used in the ZMI. It is possible to create your own menus, but we will start by just using the one for the ZMI. The title simply specifies what will appear as the link. Again you can restart your zope server and see what the tabs look like.

Before we finish our lesson, you should remove the editcontact.pt file since we no longer need it. With that done, our lesson will be over for the day.

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 and to also change the endings of the file names from .py to .py and from .zcml to .zcml.

  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

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

Previous Lesson

Front Page

Next Lesson