.. index:: z3c.zrtresource, zrtresource, css viewlet
z3c.zrtresource and CSS Viewlets
********************************
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.
Using z3c.zrtresource
=====================
Dependencies
------------
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:
.. code-block:: xml
Using the zrt-resource Directive
--------------------------------
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:
.. code-block:: html
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:
.. code-block:: xml
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:
.. code-block:: html
This should satisfy the needs for any designers on the project, but
there is actually more you can do with zrt resources.
String Replacement with zrt-resource
------------------------------------
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:
.. code-block:: html
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:
.. code-block:: css
/* 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_.
Viewlets for CSS Styles
=======================
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.
Creating the Viewlet Manager
----------------------------
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:
.. code-block:: python
class ICSS(IViewletManager):
"""CSS viewlet manager."""
Next we want to register this new viewlet manager in the
``src/zcontact/skin.zcml`` file:
.. code-block:: xml
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:
.. code-block:: xml
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``:
.. code-block:: python
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``:
.. code-block:: xml
Custom Layout Caveat
--------------------
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``):
.. code-block:: xml
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.
Viewlet Customization
---------------------
.. 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:
.. code-block:: python
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:
.. code-block:: xml
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!
.. _zopeproject: http://pypi.python.org/pypi/zopeproject
.. _z3c.zrtresource: http://pypi.python.org/pypi/z3c.zrtresource