When we first created our layout for ZContact, we just included the CSS stylesheet with the whole layout. In general, this is bad practice for more reasons than I’ll go into here. Now we want to separate out our CSS into its own file. We are going to do this using another z3c.* library: z3c.zrtresource.
One big goal we have in developing any web application is for designers who do not know anything about programming to be able to modify the purely visual aspects of the application. This includes stylesheets. Designers typically develop in a local environment without ever running any code so we must somehow handle the disparity between accessing resources like stylesheets from the local file system versus from a running web application. Page templates already help us a lot; just open up layout.pt directly in a browser and you will see a very simple display of what the application looks like even though it is not running. z3c.zrtresource will help us do this with stylesheets and other resources.
The first step in using any new z3c.* package is adding it as a dependency to our application. Open setup.py and add "z3c.zrtresource" to the install_requires parameter and rerun ./bin/buildout. z3c.zrtresource defines a new zcml directive that we’ll see in a moment, so we first have to include the package’s meta.zcml file in our own src/zcontact/configure.zcml with the line:
<include package="z3c.zrtresource" file="meta.zcml" />
Now we can go ahead and remove the CSS from the layout.pt file and stick it into a new file at src/zcontact/style.css. In place of the style tags in layout.pt, we will add a link tag like this:
<link rel="stylesheet" type="text/css" href="style.css" />
This is just a place holder allowing the stylesheet to be accessed from the file system. In a moment we’ll add some code for a dynamically generated path to the style sheet.
If you start up the server now and go to http://localhost:8080/++skin++ZContact/ the style will be broken. That is because http://localhost:8080/++skin++ZContact/style.css does not resolve to an actual resource. First we have to add the stylesheet as a resource in zope. Open skin.zcml and add the zrt-resource directive at the bottom:
<browser:zrt-resource
name="style.css"
file="style.css"
layer="zcontact.layer.IZContactBrowserLayer"
/>
Now you can restart the server and you should be able to access the stylesheet at http://localhost:8080/++skin++ZContact/++resource++style.css
Now we can change our stylesheet link in layout.pt to look like this:
<link rel="stylesheet" type="text/css" href="style.css"
tal:attributes="href string:++resource++style.css"/>
This should satisfy the needs for any designers on the project, but there is actually more you can do with zrt resources.
So far we have not done anything with zrt resources that you can’t do with the regular resource directive that is part of zope core. But if you go to the z3c.zrtresource pypi page you’ll find documentation on some cool templating features. As an example of what we can do (not that you would necessarily want to do this) we’ll go ahead and have the background color for our page dynamically generated from the request.
Our goal is to have the url http://localhost:8080/++skin++ZContact/@@index.html?color=LightGoldenRodYellow give us a page with a LightGoldenRodYellow background. First we have to pass this request data to the stylesheet itself by changing the stylesheet link in layout.pt to look like this:
<link rel="stylesheet" type="text/css" href="style.css"
tal:attributes="href string:++resource++style.css?color=${request/color|string:white}"/>
So when the page gets rendered, the stylesheet link will be pointing to ++resource++style.css?color=LightGoldenRodYellow. Now we can edit style.css by adding a comment at the top and modify the body style:
/* zrt-replace: "requestcolor" tal"request/color|string:white" */
body {
padding: 1em;
background: requestcolor;
}
Now try going to http://localhost:8080/++skin++ZContact/@@index.html?color=LightGoldenRodYellow and you should see a magnificent light golden rod yellow background. With access to tal code, there is no limit to what you could do with this. See more documentation with examples at z3c.zrtresource.
We’ve seen one example of how viewlets come in handy with navigation menus, but there are many other ways we can make our application more manigable using viewlets.
One of the things we want is a way to include the right CSS on every page including the occasional custom CSS for very specific pages. Furthermore, we want to easily include CSS stylesheets defined in other packages, for example z3c.form, without having to copy files over. Enter viewlets, which allow for dynamically generated html to appear on any page according to a whole slew of parameters including context, skins/layers, and whatever other rules you feel like. One common paradigm these days is to register all CSS style sheets inside of viewlets that can be managed with zcml. Let’s go through such a setup.
First we want to create a new viewlet manager to collect our CSS viewlets. We’ll do this the same way as with the navigation menu viewlet manager. Open up src/zcontact/skin.py and add these two lines:
class ICSS(IViewletManager):
"""CSS viewlet manager."""
Next we want to register this new viewlet manager in the src/zcontact/skin.zcml file:
<browser:viewletManager
name="ICSS"
provides="zcontact.skin.ICSS"
class="zope.viewlet.manager.WeightOrderedViewletManager"
layer="zcontact.layer.IZContactBrowserLayer"
permission="zope.Public"
/>
With a new viewlet manager named ICSS, we can modify our main layout template to replace the link tag with a tal block that displays the relevant CSS viewlets. Open up src/zcontact/layout.pt and make the link tag look like this:
<tal:block tal:content="structure provider:ICSS">
<link rel="stylesheet" type="text/css" href="style.css"/>
</tal:block>
One thing you should notice here is that we still let the original link tag stick around. When the page gets generated in zope, it will be replaced with our CSS viewlets, but when a designer opens the layout.pt file directly, the style sheet will still be loaded, albeit not dynamically.
Now we are ready to add the CSS viewlets. Just as with the z3c.menu viewlets, there is already a viewlet class that works specifically for CSS viewlets, rendering them with link tags. Let’s create such a viewlet by adding two lines to src/zcontact/skin.py:
from zope.viewlet.viewlet import CSSViewlet
ZContactCSSViewlet = CSSViewlet('style.css')
The argument we pass to the CSSViewlet class specifies the name of the css resource not the file itself. Since we typically name the resource with the same name as the file, this might be confusing. Finally we must register the viewlet with zcml in src/zcontact/skin.zcml:
<browser:viewlet
name="style.css"
for="*"
manager="zcontact.skin.ICSS"
class="zcontact.skin.ZContactCSSViewlet"
permission="zope.Public"
layer="zcontact.layer.IZContactBrowserLayer"
/>
Usually we would be finished at this point and you could restart the server and see the stylesheet still loaded. Unfortunately, there is one caveat involved with an earlier step in the tutorial related to using a layer that does not inherit from the default layer.
When the CSSViewlet class renders the viewlet in html, the url it uses to access the resource (style.css) is not /++resource++style.css but rather the simpler /@@/style.css. Unfortunately, @@ is actually a view registered on the default layer and therefore does not exist in our IZContactBrowserLayer, which in turn means that the resource cannot be accessed inside our skin with that url.
There are a few solutions to this problem, but the simplest is to simply reregister the @@ view in our own skin. The zcml registration for this view should look like this (in src/zcontact/skin.zcml):
<browser:page
name=""
for="zope.app.component.interfaces.ISite"
class="zope.app.publisher.browser.resources.Resources"
permission="zope.Public"
allowed_interface="zope.publisher.interfaces.browser.IBrowserPublisher"
layer="zcontact.layer.IZContactBrowserLayer"
/>
In case you’re curious, the original view is defined in zope/app/publisher/browser/configure.zcml.
With that final piece in place, you can restart the server to see exactly the same thing that you had before, but now in the much more powerful and flexible viewlet way.
Note
This next part is not for the faint of heart.
But what about all that cool stuff we just did with z3c.zrtresource? Now that the viewlet is rendering the link tag, it won’t include the color request variable for setting the background color. To add this feature back again, we will have to customize the css viewlet with our own viewlet class.
I will give you the code first, and the explanation afterwords. In src/zcontact/skin.py, I’ve added the following code:
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.viewlet.viewlet import CSSResourceViewletBase
from zope.viewlet.viewlet import ViewletBase
import zope.viewlet.viewlet
class RequestCSSResourceViewlet(CSSResourceViewletBase, ViewletBase):
index = ViewPageTemplateFile(os.path.join(os.path.dirname(zope.viewlet.viewlet.__file__), 'css_viewlet.pt'))
requestKey = ''
default = ''
path = ''
def getURL(self):
self._path = self.path
baseURL = super(RequestCSSResourceViewlet, self).getURL()
fullURL = baseURL + '?%s=%s' % (self.requestKey,
self.request.get(self.requestKey, self.default))
return fullURL
While reading this over, I highly recommend looking at the code for the base classes CSSResourceViewletBase and ViewletBase. There are three parts that we are overriding from the base classes, the index attribute which points to a page template for rendering the link tag, the path attribute which gives the name of the resource we are looking for, and the getURL method, which actually renders the url that we want. For the index page template, we want to reuse the existing template from the zope.viewlet.viewlet package called css_viewlet.pt. The getURL method just performs the same action as in the tal tag we had earlier: concatenating the request parameters onto the url.
We’ve also defined this class in a generic manner so that it can be used for parameters other than just color. A nice thing about viewlets that you will see in a moment is that you can specify any class attribute with zcml by simply adding additional attributes of the same name to the browser:viewlet tag. In this case, we will pass three extra attributes: requestKey, default, and path. Now open src/zcontact/skin.zcml and modify the existing style.css viewlet tag to look like this:
<browser:viewlet
name="style.css"
for="*"
manager="zcontact.skin.ICSS"
class="zcontact.skin.RequestCSSResourceViewlet"
permission="zope.Public"
layer="zcontact.layer.IZContactBrowserLayer"
requestKey="color"
default="white"
path="style.css"
/>
You can see here that we have just changed the class attribute and added the other attributes we need for the viewlet. That concludes our foray into customizing a CSS viewlet and you should now be able to restart your server and go to http://localhost:8080/++skin++ZContact/@@index.html?color=LightGoldenRodYellow where you should see a Light Golden Rod Yellow background!