Teacher: Paul Carduner
Students: Brittney, Will, Preetam, and Linda
Paul:
Today we are going to learn how to develop custom skins for our application. So far we have been using the ZMI to control our application, even though the ZMI provides much more functionality than we want. So for the sake of our end users, we want to create our own simplified version of the ZMI that only has the functionality we required.
Skins can be a somewhat complicated topic because the skinning architecture has several different parts that interfact with each other to provide flexible skinning capabilities. The first and somewhat all-encompassing part of skinning is a layer.
I like to think of layers as boxes that can contain a bunch of stuff, including other boxes. Primarily, the boxes/layers hold things we want to display on our web page. These
thingsor what we might call resources, can be anything from images to stylesheets to files to special views and macros. Although we have not used it yet, the configuration tags used to create pages and resources take an optionallayerattribute which can be used to specify which layer they are in (or which box they are in).Having multiple layers is especially useful when we have things like mutliple skins. Imagine having a web application that was used by a bunch of different schools. Each school is going to want their own logo to appear in the application. Since we don't want to worry about all the different logos when writing the application, we simply create new layers for each of the schools. Then we can put a different logo image or stylesheet or anything into each of the schools' layer. Then when the school logs in, zope will look up the logo image in the layer for that school.
Will:
Does this mean you can have mutiple layers?
Paul:
The really interesting part about layers is that they can inherit from other layers (like having boxes inside boxes). So this answers your question, Will, about the layers. Then if zope doesn't find a logo image in a certain layer, it will look in the parent layers too. This way we could have a default logo if the school did not provide their own.
Since we aren't planning on deploying Z Contact with a bunch of different looks, we only need to create one layer.
Linda:
So is it like a Java inheritance where it overwrites new values with old?
Paul:
Like python inheritance? Yes.
The other part of skinning with Zope3 is an actual
skinitself. In Zope terminology, the skin is what figures out which layer to use. Remember how each school would see a different layer? We have to be able to tell Zope which layer to use and that is done with a skin. The skin is in fact just a marker that gets attached to the request object (the users HTTP request from their web browser). Then when zope inspects the request object, it sees this special marker and looks for resources or views in one of the layers specified by the skin. Thespecial markeris actually just an interface. This sounds somewhat complicated, and it is, but you will see how all this fits together next.In the simplest case (which is our case), we are only going to have one layer and one skin. The way that skins specify which layers should be looked in for a given resource is by inheritance. That is, if our skin wants to tell zope to look in layers A, B, and C, then the interface defining our skin would inherit layers A, B, and C (just like normal python inheritance). This means that skins are actually layers too. You might think of them as a grouping of other layers.
So enough talk, let's get to creating our skin. As I said earlier, skins are just marker interfaces. So open up the
interfaces.pyfile and create an interface calledIZContactSkin. Now, the only layer we want to inherit from is the default layer. The default layer is the layer that things get registered for when you don't specify a specific layer in the configuration tag. At the moment, all our pages, and all of zope's views as well, are registered on this default layer. And even though we are writing our own custom skin, we will still want to be able to use a lot of the views zope provides for us (which live in the default layer). The default layer is just another interfaces located atzope.publisher.interfaces.browser.IDefaultBrowserLayer, so make the IZContactSkin inherit from this interface. My skin definition looks like this:class IZContactSkin(zope.publisher.interfaces.browser.IDefaultBrowserLayer): """The ZContact skin."""That's it. That is our skin. So any request object that provides the IZContactSkin interface will use our skin. So how does a request object go about providing this interface? Well, zope has this handy thing called a traversal adapter.An example and a statement: The traverser
traversesthe url. This is what the url would look like:http://localhost/++skin++ZContact/path/to/whatever. When zope traverses this url, that is, when zope looks up the url, it will see the ++skin++ part and know that what follows is the name of the skin to use. It will then lookup the interface for that name, and make the request object provide that interface. Then zope machinery handles the rest.To give our skin a name so it can be accessed through the URL, we have to register it in our configure.zcml file. Open up
configure.zcmland add the following tag:<interface interface="zcontact.interfaces.IZContactSkin" type="zope.publisher.interfaces.browser.IBrowserSkinType" name="ZContact" />This tells zope that the skin we made is in fact a skin (that is theIBrowserSkinTypepart) and that the skin can be looked up using the nameZContact. With that written, we can access our skin through a url. I would recommend restarting our zope server now and checking it out. Go to any place in the ZMI, then add ++skin++ZContact to the beginning of the url (just after the server:port) and see what the page looks like using our skin.What you should see is something pretty ugly:
Of course our skin looks this way because we are no longer using zope's built in macros. This naturally brings us to the last part of skinning: Macros.
Macros are what people might normally think of when you say
skinbecause it defines how and what things get layed out on the screen. It is the visual part of a skin. Macros are defined in the same way that we create page templates. In fact, you might think of them as meta templates - templates for templates. Remember in lesson 3 when we had the line<html metal:use-macro="context/@@standard_macros/view">? Most views in Zope use theviewmacro, which is specified in thestandard_macrosview. So if we want our skin to work with existing zope components, we will make our ownstandard_macrosview, with our ownviewmacro.Notice that
@@standard_macrosis just a view lookup. Remember that a view is a lot like a page with a class and template. The first thing we need to do to create macros is add astandard_macrospage to the new layer we created,IZContactSkin. So go back to yourconfigure.zcmlfile and add the following lines:<browser:page for="*" name="standard_macros" template="zcontactmacros.pt" permission="zope.View" layer="zcontact.interfaces.IZContactSkin" />Let's go through this tag line by line. We specifyfor="*"to say that this view should be available no matter what type of object we are looking at. Thename="standard_macros"is just the name of the view (what comes after@@in the view lookup. Thetemplate="zcontactmacros.pt"line specifies the template where our macros will be defined. We want to use the zope.View permission because if you can view a page you should be able to view the macro for it (otherwise it won't display correctly). Finally, thelayer="zcontact.interfaces.IZContactSkin"line puts this view in the new layer we created (don't forget that the skin is a layer itself). If we didn't include this line, the this zcml registration would conflict with the standard_macros view registration from inside zope, because you can't have to views with the same name in the same layer.Now we need to write the actual page template. As we specified in zcml, we need to create a new file called
zcontactmacros.pt. The first macro we want to create should be calledview. Start by writing the following:<html metal:define-macro="view"> Hello World For Macros! </html>Note: Be sure to look at a management screen (which uses the view macro). If you look at a non-management screen it will try to look up the page macro and fail. You could rename this macro topageto have it display there.Notice that for anything involving macros, we use the metal namespace. Now restart your server and open up any page using this skin. You should get an empty screen that just says
Hello World For Macros!If you look at the other page templates we have created, like
viewcontact.pt, you will see other metal attributes, like metal:fill-slot. In our macro we can defineslotswhere content will go. Page templates which use the macro can thenfillthose slots using the fill-slot attribute. To create a slot we use the define-slot attribute in our macro. So, we might want to add a default header to our macro, then present the information from the body slot. Edit your viewcontact.pt file to look like this:<html metal:define-macro="view"> <head><title>ZContact</title></head> <body> <h1>Z Contact</h1> <h4>The Hottest Contact Manager on the Planet</h4> <hr /> <div metal:define-slot="body"> Hello World For Macros! </div> </body> </html>Since our macro is just a page template, we don't need to restart the zope server to see our changes. So check out your page again. Now you should see what we put in the macro as well as the contents defined by the template for the page we are looking at.Since this still looks exceedingly ugly, we are going to want to use some cascading style sheets. Casecading style sheets present an excellent usecase for resources. In zope, a resource is just a file that lives on the server's file system, and not in Zope's object database. These might include CSS files, images, javascript libraries, and more. Since I generally use quite a number of resources in my web applications, I like to have a folder where I keep all the resources. So go ahead and create a new directory within
zcontactcalledresources. Then we need to register this directory as containing a bunch of resources. To do this, open upconfigure.zcmland add the following tag:<browser:resourceDirectory directory="resources" name="zcontact_resources" layer="zcontact.interfaces.IZContactSkin" />To access a file in the resources directory from the web, we use the path http:/localhost/@@/zcontact_resources/. Now lets add a link to the css file in our html code:<html metal:define-macro="view"> <head>Now you should restart your zope server (so the resourceDirectory zcml tag gets registered), and take a look at the page again.<link rel="stylesheet" type="text/css" href="/@@/zcontact_resources/style.css" tal:attributes="href context/++resource++zcontact_resources/style.css"/><title>ZContact</title> </head> <body> <h1>Z Contact</h1> <h4>The Hottest Contact Manager on the Planet</h4> <hr /> <div metal:define-slot="body"> Hello World For Macros! </div> </body> </html>
Files:
When you click on the links below 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/tests.py
- zcontact/ftests.py
- zcontact/README.txt
- zcontact/browser.txt
- zcontact/contactcontainer.pt
- zcontact/zcontactmacros.pt
- zcontact/resources/style.css
A tarball of all these files is located here: lesson07.tgz
