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.pyfile 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
IContactinterface in thecontact.pyfile. We then configured this implementation in theconfigure.zcmlfile, and even got to the point where we were able to add an instance of theContactclass to the database through the ZMI.You may have noticed that despite being able to add an instance of the
Contactclass to the database, we were completely unable to set thelastNamevariable 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 theContactobject.The first thing to do is to create a new file called
viewcontact.pt. The.ptstands 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.ptfile 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 strangetal:contentattribute. This attribute is called a tal expression or tales for short. Thetal:contentattribute tells zope that the content of the b tag (what goes between <b> and </b>) should be filled with the data stored incontext/lastName. You might ask what happens to the content of the b tag whenlast nameis 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/lastNamepart? How does that access an attribute?
Paul:
context/lastNameis a tales path expression. Thecontextpart says that we are accessing the object for which this page is being displayed (i.e. the context of the page). Then thelastNamepart accesses thelastNameattribute of the object. This expression is the tales equivalent to the expression in pythoncontext.lastName.If you have managed to follow me this far, then you might want to ask,
how do we know what theThere 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!contextvariable is?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
Contactclass and configure permissions for it. Open up theconfigure.zcmlfile 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
forattribute 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. Thenameattribute specifies how we access this page template through a url. By setting the name toindex.htmlthen this page template is the default one used for the object. Then there is thepermissionattribute, which specifies what permission a user must have to view the page. By default, all users (logged in or not) have thezope.Viewpermission. Finally, thetemplateattribute specifies the path to the file where the template is located.With the
pageattribute in theconfigure.zcmlfile, 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
toplink under theNavigationsection in the upper left hand corner of the screen. This will open up anAdd:table below where you clicked. Now clickZ Contact Pagein this table. This will place you in a new screen which looks like this:
Fill in the
Namewith a name for the object, saytest, and click theApplybutton. You will see your new object. In the URL of your browser, replace@@contents.htmlwithindex.html, and you should see something like this:Z ContactLast Name:Of course, this still doesn't let us change the empty default string of the
lastNameattribute. 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.ptand 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:attributesattribute on the input tag. Instead of filling in the content of a tag like thetal:contentattribute did, this tales fills in the value for another attribute on the tag. In this case we are setting thevalueattribute 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:pagetag to theconfigure.zcmlfile 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
pagetag 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.ptfile 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.htmlafter your content object name, you should get a page tat looks something like:Z ContactLast 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.pyand 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:
- User inputs data into form
- User submits data using the Save button
- Data gets sent to the index.html page via the url (looks like http://localhost/contact/index.html?lastName=Whatever)
- The browser view class for the index.html page gets instantiated and processes the data, which is stored in a request object
- 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.zcmlfile and add an attribute to the firstbrowser:pagetag (the one for index.html). The line should beclass="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
classattribute, 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.pyfor 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.pyfile 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.Persistentwhen 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
Contactclass, 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 yourcontact.pyfile. 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.
- zcontact/interfaces.py
- zcontact/contact.py
- zcontact/zcontact-configure.zcml
- zcontact/configure.zcml
- zcontact/__init__.py
- zcontact/viewcontact.pt
- zcontact/editcontact.pt
- zcontact/browser.py
A tarball of all these files is located here: lesson02.tgz
