Chapter 29
Registries with Global Utilities
Difficulty
Contributer
Skills
- Familiarity with the Component Architecture is required.
- Be comfortable with ZCML.
- You should know the message board example, since we are going to use it
in the final step.
Problem/Task
We need registries all the time. It is a very common pattern. In fact, the Component
Architecture itself depends heavily on registered component lookups to provide
functionality. These registries, especially the utility service, can be used to provide
application-level registries as well.
Solution
29.1 Introduction
The goal of this chapter is to develop a mechanism that provides image-representations
of text-based smileys. For example, the text “:-)” should be converted to
. To further complicate the problem, it is undesirable to just store
smileys without any order. Many applications support multiple themes of
smileys, so we want to do that as well. For example, the message board
administrator should be able to choose the desired smiley theme for his message
board.
Based on the requirements above, we want a registry of smiley themes that the
user can choose from. Therefore, we will develop the theme as a utility and register it
with a specific interface, ISmileyTheme. Thus, the entire utility service acts as a
huge registry, but we can easily simulate sub-registries by specifying a particular
interface for a utility. We can then ask the Zope component API to return a list of all
utilities providing ISmileyTheme or simply return a single ISmileyTheme having a
specific name.
Let’s now take a look on how to handle the smileys themselves. Inside the theme,
we simply need a mapping (dictionary) from the text representation to the image.
However, should we really store the image data. In fact, it would better to declare the
image itself as a resource and only store the URL, so that we can (a) support
external links (not done in this chapter) and (b) do not have to worry about
publishing the images.
The code will be rounded off by implementing a couple new ZCML directives to
make the registration of new smiley themes as easy as possible, so that a message
board editor can easily upload his/her favorite theme and use it. We will actually add
a final step to the message board example incorporating smiley themes at the end of
the chapter.
To allow the smiley theme utility to be distributed independently of the message
board application, develop its code in a new package called smileyutility, which
you should place into ZOPE3/src/book. Don’t forget to add an __init__.py file to
the directory.
29.2 Step I: Defining the Interfaces
Before we start coding away, we need to spend some time thinking about the API
that we want to expose. In the next chapter we develop a local/placeful equivalent of
the utility, so our base interface, ISmileyTheme, should be general enough to
support both implementations and not include any implementation-specific methods.
We will then derive another interface, IGlobalSmileyTheme, from the general one
that will specify methods to manage smileys for the global implementation. Note:
The utility will still be registered as a ISmileyTheme.
1 from zope.interface import Interface
2
3 class ISmileyTheme(Interface):
4 """A theme is a collection of smileys having a stylistic theme.
5
6 Themes are intended to be implemented as named utilities, which will be
7 available via a local smiley service.
8 """
9
10 def getSmiley(text, request):
11 """Returns a smiley for the given text and theme.
12
13 If no smiley was found, a ComponentLookupError should be raised.
14 """
15
16 def querySmiley(text, request, default=None):
17 """Returns a smiley for the given text and theme.
18
19 If no smiley was found, the default value is returned.
20 """
21
22 def getSmileysMapping(request):
23 """Return a mapping of text to URL.
24
25 This is incredibly useful when actually attempting to substitute the
26 smiley texts with a URL.
27 """
28
29
30 class IGlobalSmileyTheme(ISmileyTheme):
31 """A global smiley theme that also allows management of smileys."""
32
33 def provideSmiley(text, smiley_path):
34 """Provide a smiley for the utility."""
You might think that this interface seems a bit wordy, but in such widely
available components it is extremely important to document the specific
semantics of each method and the documentation should leave no question or
corner case uncovered. Many people will depend on the correctness of the
API.
- Line 1 & 3: Notice that a utility does not have to inherit any special
interfaces. Until we declare a utility to be a utility, it is just a general
component.
- Line 10-14: Retrieve a smiley given its text representation. Note that we
need the request, since the URL could be built on the fly and we need all
the request information to generate an appropriate URL.
- Line 16-20: Similar to the get-method, except that it returns default
instead of raising an error, if the smiley was not found.
- Line 22-25: Interestingly enough, I did not have this method in my original
design, but noticed that the service would be unusable without it. By
returning a complete list of text-to-URL mappings, the application using
this utility can simply do a search and replace of all smiley occurrences.
In the beginning I envisioned a method that was taking a string as
argument and returns a string with all the smiley occurrences being
replaced by image tags. But this would have been rather limiting, since
the utility would need to guess the usage of the URL; not everyone wants
to generate HTML necessarily. This implementation does not carry this
restriction, since it makes no assumption on how the URLs will be used.
- Line 33-34: As an extension to the ISmileyTheme interface, this method
adds a new smiley to the theme. The smiley_path will be expected to be a
relative path to a resource, something like ++resource++plain__smile.
png. Note that the path must be unique, across all themes, so it is a
good idea to encode the theme name into it by convention. But let’s not
introduce that restriction in the theme.
Now that we have all interfaces defined, let’s look at the implementation, which
should be straightforward.
29.3 Step II: Implementing the Utility
The global theme will use simple dictionary that maps a text representation to the
path of a smiley. When smileys are requested this path is converted to a URL and
returned. The only tricky part of the utility will be to obtain the root URL, since the
utility does not know anything about locations.
However, there is an fast solution. We create a containment root component stub
that implements the IContainmentRoot interface, for which the traversal mechanism
is looking for while generating a path or URL. So here is what I did for obtaining the
root URL:
1 from zope.app import zapi
2 from zope.app.traversing.interfaces import IContainmentRoot
3 from zope.interface import implements
4
5 class Root:
6 implements(IContainmentRoot)
7
8 def getRootURL(request):
9 return str(zapi.getView(Root(), 'absolute_url', request))
Now we have all the pieces to implement the utility. I just used pyskel.py to
create the skeleton and then filled it. Place the following and the getRootURL() code
in a file called globaltheme.py:
1 om zope.component.exceptions import ComponentLookupError
2 om interfaces import IGlobalSmileyTheme
3
4 ass GlobalSmileyTheme(object):
5 """A filesystem based smiley theme."""
6 implements(IGlobalSmileyTheme)
7
8 def __init__(self):
9 self.__smileys = {}
10
11 def getSmiley(self, text, request):
12 "See book.smileyutility.interfaces.ISmileyTheme"
13 smiley = self.querySmiley(text, request)
14 if smiley is None:
15 raise ComponentLookupError, 'Smiley not found.'
16 return smiley
17
18 def querySmiley(self, text, request, default=None):
19 "See book.smileyutility.interfaces.ISmileyTheme"
20 if self.__smileys.get(text) is None:
21 return default
22 return getRootURL(request) + '/' + self.__smileys[text]
23
24 def getSmileysMapping(self, request):
25 "See book.smileyutility.interfaces.ISmileyTheme"
26 smileys = self.__smileys.copy()
27 root_url = getRootURL(request)
28 for name, smiley in smileys.items():
29 smileys[name] = root_url + '/' + smiley
30 return smileys
31
32 def provideSmiley(self, text, smiley_path):
33 "See book.smileyutility.interfaces.IGlobalSmileyTheme"
34 self.__smileys[text] = smiley_path
35
- Line 8-9: Initialize the registry, which is a simple dictionary. Note that I
want this registry to be totally private to this class and noone else should
be able to reach it.
- Line 11-16: This method does not do much, since we turn over all
the responsibility to the next method. All we do is complain with a
ComponentLookupError if there was no result (i.e. None was returned).
- Line 18-22: First, if the theme does not contain the requested smiley, then
simply return the default value. Now that we know that there is a smiley
available, construct the URL by appending the smiley path to the URL
root.
- Line 36-46: We make a copy of all the smiley map. If the theme does not
exist, an empty dictionary is created. In line 43-44 we update every smiley
path with a smiley URL.
- Line 48-51: The smiley path is simply added with the text being the key
of the mapping.
Our utility is now complete. However, we have not created a way to declare a
default theme. To make life simple, the default theme is simply available under the
name “default”.
1 from interfaces import ISmileyTheme
2
3 def declareDefaultSmileyTheme(name):
4 """Declare the default smiley theme."""
5 utilities = zapi.getService(zapi.servicenames.Utilities)
6 theme = zapi.getUtility(ISmileyTheme, name)
7 # register the utility simply without a name
8 utilities.provideUtility(ISmileyTheme, theme, 'default')
In the code above we simply look up the utility by its original name and then
register it again using the name “default”. By the way, this is totally legal and of
practiced. One Utility instance can be registered multiple times using different
interfaces and/or names.
Now, let’s test our new utility.
29.4 Step III: Writing Tests
Writing tests for global utilities is usually fairly simple too, since you usually do not
have to start up the component architecture. In this case, however, we have to do
this, since we are looking up a view when asking for the root URL. We also have to
register this view ( absolute_url) in the first place, so it can be found later. In the
tests package I created a test_doc.py and inserted the set up and tear down code
there:
1 import unittest
2
3 from zope.interface import Interface
4 from zope.testing.doctestunit import DocTestSuite
5
6 from zope.app.tests import ztapi, placelesssetup
7
8 class AbsoluteURL:
9 def __init__(self, context, request):
10 pass
11 def __str__(self):
12 return ''
13
14 def setUp():
15 placelesssetup.setUp()
16 ztapi.browserView(Interface, 'absolute_url', AbsoluteURL)
17
18
19 def test_suite():
20 return unittest.TestSuite((
21 DocTestSuite('book.smileyutility.globaltheme',
22 setUp=setUp, tearDown=placelesssetup.tearDown),
23 ))
24
25 if __name__ == '__main__':
26 unittest.main(defaultTest='test_suite')
- Line 8-12: This is a stub implementation of the absolute URL. We simply
return nothing as root of the url.
- Line 14-16: We have seen a placeless unittest setup before;
placelesssetup.setUp() brings up the basic component architecture
and clears all the registries from possible entries. Line 16 then registers
our stub-implementation of AbsoluteURL as a view.
- Line 21-22: Here we create a doctest suite using the custom setup function.
Now we just have to write the tests. In the docstring of the GlobalSmileyTheme
class add the following doctest code:
1 Let's make sure that the global theme implementation actually fulfills the
2 `ISmileyTheme` API.
3
4 >>> from zope.interface.verify import verifyClass
5 >>> verifyClass(IGlobalSmileyTheme, GlobalSmileyTheme)
6 True
7
8 Initialize the theme and add a couple of smileys.
9
10 >>> theme = GlobalSmileyTheme()
11 >>> theme.provideSmiley(':-)', '++resource++plain__smile.png')
12 >>> theme.provideSmiley(';-)', '++resource++plain__wink.png')
13
14 Let's try to get a smiley out of the registry.
15
16 >>> from zope.publisher.browser import TestRequest
17
18 >>> theme.getSmiley(':-)', TestRequest())
19 '/++resource++plain__smile.png'
20 >>> theme.getSmiley(':-(', TestRequest())
21 Traceback (most recent call last):
22 ...
23 ComponentLookupError: 'Smiley not found.'
24 >>> theme.querySmiley(';-)', TestRequest())
25 '/++resource++plain__wink.png'
26 >>> theme.querySmiley(';-(', TestRequest()) is None
27 True
28
29 And finally we'd like to get a dictionary of all smileys.
30
31 >>> map = theme.getSmileysMapping(TestRequest())
32 >>> len(map)
33 2
34 >>> map[':-)']
35 '/++resource++plain__smile.png'
36 >>> map[';-)']
37 '/++resource++plain__wink.png'
- Line 4-6: It is always good to ensure that the interface was correctly
implemented.
- Line 8-12: Test the provideSmiley() method.
- Line 14-27: Test the simple smiley accessor methods of the utility. Note
how nicely doctests also handle exceptions.
- Line 29-37: Make sure that the getSmileyMapping() method gives the
right output. Note that dictionaries cannot be directly tested in doctests,
since its representation depends on the computer architecture, since the
item order is arbitrary.
Run the tests and make sure that they all pass.
29.5 Step IV: Providing a user-friendly UI
While the current API is functional, it is not very practical to the developer, since
s/he first needs to look up the theme using the component architecture’s utility API
and only then can make use the of the smiley theme features. It would be
much nicer, if we would only need a smiley-theme-related API to work with.
Thus we create some convenience functions in the package’s __init__.py
file:
1 from zope.app import zapi
2
3 from interfaces import ISmileyTheme
4
5 def getSmiley(text, request, theme='default'):
6 theme = zapi.getUtility(ISmileyTheme, theme)
7 return theme.getSmiley(text, request)
8
9 def querySmiley(text, request, theme='default', default=None):
10 theme = zapi.queryUtility(ISmileyTheme, theme)
11 if theme is None:
12 return default
13 return theme.querySmiley(text, request, default)
14
15 def getSmileyThemes():
16 return [name for name, util in zapi.getUtilitiesFor(ISmileyTheme)
17 if name != 'default']
18
19 def getSmileysMapping(request, theme='default'):
20 theme = zapi.getUtility(ISmileyTheme, theme)
21 return theme.getSmileysMapping(request)
The functions integrate the theme utility more tightly in the API.
- Line 15-17: Return a list of names of all available themes, excluding the
“default” one.
The tests for these functions are very similar to the ones of the theme
utility, so I am not going to include them in the text. As always, you
can find the complete code including methods in the code repository (
http://svn.zope.org/book/smileyutility).
29.6 Step V: Implement ZCML Directives
You might have already wondered, how this utility can be useful, if it does not even
deal with the smiley images. This functionality is reserved for the configuration.
When a smiley registration is made, the directive will receive a path to an image, but
does not just register it with the smiley theme. Instead, it first creates a resource
for the image and then passes the resource’s relative path to the smiley
theme.
In case you have not written a ZCML directive yet, there are three steps: creating
the directive schema, implementing the directive handlers and writing the
meta-ZCML configuration. They are represented by the next three sections (a)
through (c).
But first we need to decide what directives we want to create. The first one,
smiley:theme, defines a new theme and allows a sub-directive, smiley:smiley, that
registers new smileys for this theme. A second directive, smiley:smiley, allows you
to register a single smiley for an existing theme, so that other packages can add
additional smileys to a theme. The third and final directive, smiley:defaultTheme,
let’s you specify the theme that will be known as the default one. The specified
theme must exist already.
29.6.1 (a) Declaring the directive schemas
Each ZCML directive is represented by a schema, which defines the type of content
for each element/directive attribute. Each field is also responsible for knowing how to
convert the attribute value into something that is useful. All the usual schema fields
are available. Additionally there are some specific configuration fields that can also
be used. They are listed in the “Intorduction to the Zope Configuration Markup
Language (ZCML)” chapter.
So now that we know what we can use, let’s define the schemas. By convention
they are placed in a file called metadirectives.py:
1 from zope.interface import Interface
2 from zope.configuration.fields import Path
3 from zope.schema import TextLine
4
5 class IThemeDirective(Interface):
6 """Define a new theme."""
7
8 name = TextLine(
9 title=u"Theme Name",
10 description=u"The name of the theme.",
11 default=None,
12 required=False)
13
14 class ISmileySubdirective(Interface):
15 """This directive adds a new smiley using the theme information of the
16 complex smileys directive."""
17
18 text = TextLine(
19 title=u"Smiley Text",
20 description=u"The text that represents the smiley, i.e. ':-)'",
21 required=True)
22
23 file = Path(
24 title=u"Image file",
25 description=u"Path to the image that represents the smiley.",
26 required=True)
27
28 class ISmileyDirective(ISmileySubdirective):
29 """This is a standalone directive registering a smiley for a certain
30 theme."""
31
32 theme = TextLine(
33 title=u"Theme",
34 description=u"The theme the smiley belongs to.",
35 default=None,
36 required=False)
37
38 class IDefaultThemeDirective(IThemeDirective):
39 """Specify the default theme."""
- Line 5-12: The theme directive only requires a “name” attribute that
gives the theme its name.
- Line 13-25: Every smiley is identified by its text representation and the
image file. (The theme is already specified in the sub-directive.)
- Line 27-35: This is the single directive that specifies all information
at once. We simply reuse the previously defined smiley sub-directive
interface and specify the theme.
- Line 37-38: The default theme directive is simple, because it just takes a
theme name.
29.6.2 (b) Implement ZCML directive handlers
Next we implement the directive handlers themselves, which is the real fun part,
since it actually represents some important part of the package’s logic. This code goes
by convention into metaconfigure.py:
1 import os
2
3 from zope.app import zapi
4 from zope.app.component.metaconfigure import utility
5 from zope.app.publisher.browser.resourcemeta import resource
6
7 from interfaces import ISmileyTheme
8 from globaltheme import GlobalSmileyTheme, declareDefaultSmileyTheme
9
10 __registered_resources = []
11
12 def registerSmiley(text, path, theme):
13 theme = zapi.queryUtility(ISmileyTheme, theme)
14 theme.provideSmiley(text, path)
15
16 class theme(object):
17
18 def __init__(self, _context, name):
19 self.name = name
20 utility(_context, ISmileyTheme,
21 factory=GlobalSmileyTheme, name=name)
22
23 def smiley(self, _context, text, file):
24 return smiley(_context, text, file, self.name)
25
26 def __call__(self):
27 return
28
29 def smiley(_context, text, file, theme):
30 name = theme + '__' + os.path.split(file)[1]
31 path = '/++resource++' + name
32
33 if name not in __registered_resources:
34 resource(_context, name, image=file)
35 __registered_resources.append(name)
36
37 _context.action(
38 discriminator = ('smiley', theme, text),
39 callable = registerSmiley,
40 args = (text, path, theme),
41 )
42
43 def defaultTheme(_context, name=None):
44 _context.action(
45 discriminator = ('smiley', 'defaultTheme',),
46 callable = declareDefaultSmileyTheme,
47 args = (name,),
48 )
- Line 10: We want to keep track of all resources that we have already
added, so that we do not register any resource twice, which would raise a
component error.
- Line 12-14: Actually sticking in the smileys into the theme must be delayed
till the configuration actions are executed. This method will be the smiley
registration callable that is called when the smiley registration action is
executed.
- Line 16-27: Since theme is a complex directive (it can contain other
directives inside), it is implemented as a class. The parameters of the
constructor resemble the arguments of the XML element, except for
_context, which is always passed in as first argument and represents the
configuration context.
Each sub-directive (in our case smiley) is a method of the class taking
the element attributes as parameters. In this implementation we forward
the configuration request to the main smiley directive; there is no need
to implement the same code twice.
Every complex directive class must be callable (i.e. implement __call_
_()) . This method is called when the closing element is parsed. Usually
all of the configuration action is happening here, but not in our case.
- Line 29-41: The first task is to separate the filename from the file path
and construct a unique name and path for the smiley. On line 33-35 we
register the resource. We do that only, if we have not registered it before,
which can happen if there are two text representations for a single smiley
image, like “:)” and “:-)”. On line 37-41 we then tell the configuration
system it should add the smiley to the theme. Note that these actions are
not executed at this time, since the configuration mechanism must first
resolve possible overrides and conflict errors.
- Line 43-48: This is a simple handler
for the simple defaultTheme directive. It calls our previously developed
declareDefaultSmileyTheme() function and that’s it.
29.6.3 (c) Writing the meta-ZCML directives
Now that we have completed the Python-side of things, let’s register the new ZCML
directives using the meta namespace in ZCML. By convention the ZCML directives
are placed into a file named metal.zcml:
1 <configure xmlns:meta="http://namespaces.zope.org/meta">
2
3 <meta:directives namespace="http://namespaces.zope.org/smiley">
4
5 <meta:complexDirective
6 name="theme"
7 schema=".metadirectives.IThemeDirective"
8 handler=".metaconfigure.theme">
9
10 <meta:subdirective
11 name="smiley"
12 schema=".metadirectives.ISmileySubdirective" />
13
14 </meta:complexDirective>
15
16 <meta:directive
17 name="smiley"
18 schema=".metadirectives.ISmileyDirective"
19 handler=".metaconfigure.smiley" />
20
21 <meta:directive
22 name="defaultTheme"
23 schema=".metadirectives.IDefaultThemeDirective"
24 handler=".metaconfigure.defaultTheme" />
25
26 </meta:directives>
27
28 </configure>
Each meta directive, whether it is directive, complexDirective or
subdirective, specifies the name of the directive and the schema it represents. The
first two meta directives also take a handler attribute, which describes the callable
object that will execute the directive.
You register this meta ZCML file with the system by placing a file called
smileyutility-meta.zcml in the package-includes directory having the following
content:
1 <include package="book.smileyutility" file="meta.zcml" />
29.6.4 (d) Test Directives
Now we are ready to test the directives. First we create a test ZCML file in tests
called smiley.zcml. We write the directives in a way that we assume we are in the
tests directory during its execution:
1 <configure
2 xmlns:zope="http://namespaces.zope.org/zope"
3 xmlns="http://namespaces.zope.org/smiley">
4
5 <zope:include package="book.smileyutility" file="meta.zcml" />
6
7 <theme name="yazoo">
8 <smiley text=":(" file="../smileys/yazoo/sad.png"/>
9 <smiley text=":)" file="../smileys/yazoo/smile.png"/>
10 </theme>
11
12 <theme name="plain" />
13
14 <smiley
15 theme="plain"
16 text=":("
17 file="../smileys/yazoo/sad.png"/>
18
19 <defaultTheme name="plain" />
20
21 </configure>
- Line 5: First read the meta configuration.
- Line 9-19: Use the three directives.
Now create a module called test_directives.py (the directive tests modules
are usually called this way) and add the following test code:
1 import unittest
2
3 from zope.app import zapi
4 from zope.app.tests.placelesssetup import PlacelessSetup
5 from zope.configuration import xmlconfig
6
7 from book.smileyutility import tests
8 from book.smileyutility.interfaces import ISmileyTheme
9
10 class DirectivesTest(PlacelessSetup, unittest.TestCase):
11
12 def setUp(self):
13 super(DirectivesTest, self).setUp()
14 self.context = xmlconfig.file("smiley.zcml", tests)
15
16 def test_SmileyDirectives(self):
17 self.assertEqual(
18 zapi.getUtility(ISmileyTheme,
19 'default')._GlobalSmileyTheme__smileys,
20 {u':(': u'/++resource++plain__sad.png'})
21 self.assertEqual(
22 zapi.getUtility(ISmileyTheme,
23 'plain')._GlobalSmileyTheme__smileys,
24 {u':(': u'/++resource++plain__sad.png'})
25 self.assertEqual(
26 zapi.getUtility(ISmileyTheme,
27 'yazoo')._GlobalSmileyTheme__smileys,
28 {u':)': u'/++resource++yazoo__smile.png',
29 u':(': u'/++resource++yazoo__sad.png'})
30
31 def test_defaultTheme(self):
32 self.assertEqual(zapi.getUtility(ISmileyTheme, 'default'),
33 zapi.getUtility(ISmileyTheme, 'plain'))
34
35 def test_suite():
36 return unittest.TestSuite((
37 unittest.makeSuite(DirectivesTest),
38 ))
39
40 if __name__ == '__main__':
41 unittest.main()
As we can see, directive unittests can be very compact thanks to the
xmlconfig.file() call.
- Line 4 & 10: Since we are registering resources during the configuration,
we need to create a placeless setup.
- Line 14: Execute the configuration.
- Line 16-29: Make sure that all entries in the smiley themes were created.
- Line 31-33: A quick check that the default theme was set correctly.
- Line 35-41: This is just the necessary unittest boilerplate.
29.7 Step VI: Setting up some Smiley Themes
The service functionality is complete and we are now ready to hook it up to the
system. We need to define the service and provide an implementation to the
component architecture before we add two smiley themes. Therefore, in the
configure.zcml file add:
1 <configure
2 xmlns="http://namespaces.zope.org/smiley"
3 i18n_domain="smileyutility">
4
5 <theme name="plain">
6 <smiley text=":(" file="./smileys/plain/sad.png"/>
7 <smiley text=":-(" file="./smileys/plain/sad.png"/>
8 <smiley text=":)" file="./smileys/plain/smile.png"/>
9 <smiley text=":-)" file="./smileys/plain/smile.png"/>
10 ...
11 </theme>
12
13 <theme name="yazoo">
14 <smiley text=":(" file="./smileys/yazoo/sad.png"/>
15 <smiley text=":-(" file="./smileys/yazoo/sad.png"/>
16 <smiley text=":)" file="./smileys/yazoo/smile.png"/>
17 <smiley text=":-)" file="./smileys/yazoo/smile.png"/>
18 ...
19 </theme>
20
21 <defaultTheme name="plain" />
22
23 </configure>
- Line 5-19: Provide two smiley themes. I abbreviated the list somewhat
from the actual size, since I think you get the picture.
- Line 21: Set the default theme to “plain”.
You can now activate the configuration by placing a file named
smileyutility-configure.zcml in package-includes. It should have the
following content:
1 <include package="book.smileyutility" />
29.8 Step VII: Integrate Smiley Themes into the Message Board
Okay, now we have these smiley themes, but we do not use them anywhere. So that it
will be easier for us to see the smiley themes in action, I decided to extend the
messageboard example by yet another step. The new code consists of two parts: (a)
allow the message board to select one of the available themes and (b) use smileys in
the “Preview” tab of the message board.
29.8.1 (a) The Smiley Theme Selection Adapter
The additional functionality is best implemented using an Adapter and annotations.
The interface that we need is trivially:
1 from zope.schema import Choice
2
3 class ISmileyThemeSpecification(Interface):
4
5 theme = Choice(
6 title=u"Smiley Theme",
7 description=u"The Smiley Theme used in message bodies.",
8 vocabulary=u"Smiley Themes",
9 default=u"default",
10 required=True)
Add this interface to the interfaces.py file of the message board. In the
interface above we refer to a vocabulary called “Smiley Themes” without having
specified it. We expect this vocabulary to provide a list of names of all available
smiley themes. Luckily, creating vocabularies for utilities or utility names can be
easily done using a single ZCML directive:
1 <vocabulary
2 name="Smiley Themes"
3 factory="zope.app.utility.vocabulary.UtilityVocabulary"
4 interface="book.smileyutility.interfaces.ISmileyTheme"
5 nameOnly="true" />
- Line 3: This is a special utility vocabulary class that is used to quickly
create utility-based vocabularies.
- Line 4: This is the interface by which the utilities will be looked up.
- Line 5: If “nameOnly” is specified, the vocabulary will provide utility
names instead of the utility component itself.
Next we create the adapter; place the following class into messageboard.py:
1 from zope.app.annotation.interfaces import IAnnotations
2 from book.messageboard.interfaces import ISmileyThemeSpecification
3
4 class SmileyThemeSpecification(object):
5
6 implements(ISmileyThemeSpecification)
7 __used_for__ = IMessageBoard
8
9 def __init__(self, context):
10 self.context = self.__parent__ = context
11 self._annotations = IAnnotations(context)
12 if self._annotations.get(ThemeKey, None) is None:
13 self._annotations[ThemeKey] = 'default'
14
15 def getTheme(self):
16 return self._annotations[ThemeKey]
17
18 def setTheme(self, value):
19 self._annotations[ThemeKey] = value
20
21 # See .interfaces.ISmileyThemeSpecification
22 theme = property(getTheme, setTheme)
As you can see, this is a very straightforward implementation of the interface
using annotations and the adapter concept, both of which were introduced in the
content components parts before.
The adapter registration and security is a bit tricky, since we must use a trusted
adapter. It is not enough to just specify the “permission” attribute in the adapter
directive, since it will only affect attribute access, but not mutation. Instead of
specifying the “permission” attribute, we need to do a full security declaration using
the zope:class and zope:require directives:
1 <class class=".messageboard.SmileyThemeSpecification">
2 <require
3 permission="book.messageboard.View"
4 interface=".interfaces.ISmileyThemeSpecification"
5 />
6 <require
7 permission="book.messageboard.Edit"
8 set_schema=".interfaces.ISmileyThemeSpecification"
9 />
10 </class>
11
12 <adapter
13 factory=".messageboard.SmileyThemeSpecification"
14 provides=".interfaces.ISmileyThemeSpecification"
15 for=".interfaces.IMessageBoard"
16 trusted="true" />
Last, we need to create a view to set the value. We can simply use the
browser:editform. We configure the view with the following directive in
browser/configure.zcml:
1 <editform
2 name="smileyTheme.html"
3 schema="book.messageboard.interfaces.ISmileyThemeSpecification"
4 for="book.messageboard.interfaces.IMessageBoard"
5 label="Change Smiley Theme"
6 permission="book.messageboard.Edit"
7 menu="zmi_views" title="Smiley Theme" />
By the way, the editform will automatically know how to look up the adapter and
use it instead of the MessageBoard instance. If you now restart Zope 3, you should
be able to change the theme to whatever you like.
29.8.2 (b) Using the Smiley Theme
The very final step is to use all this machinery. To do this, add a method called
body() to the MessageDetails ( browser/message.py) class:
1 def body(self):
2 """Return the body, but mark up smileys."""
3 body = self.context.body
4
5 # Find the messageboard and get the theme preference
6 obj = self.context
7 while not IMessageBoard.providedBy(obj) and \
8 obj is not None:
9 obj = zapi.getParent(obj)
10
11 if obj is None:
12 theme = None
13 else:
14 theme = ISmileyThemeSpecification(obj).theme
15
16 for text, url in getSmileysMapping(self.request, theme).items():
17 body = body.replace(
18 text,
19 '<img src="%s" label="%s"/>' %(url, text))
20
21 return body
- Line 5-14: This code finds the MessageBoard and, when found, gets the
desired theme.
- Line 16-19: Using the theme, get the smiley mapping and convert
one smiley after another from the text representation to an image tag
referencing the smiley.
In the details.pt template, line 33, we now just have to change the call
from context/body to view/body so that the above method is being used.
Once you have done that you are ready to restart Zope 3 and enjoy the
smileys.
Exercises
- The GlobalSmileyTheme currently is really only meant to work with
resources that are defined while executing the smiley directives. However,
it is not very hard to support general URLs as well. To do this, you will
have to change the configuration to allow for another attribute called url
and you have to adjust the theme so that it does not attempt all the time
to add the root URL to the smiley path.
- The directive tests do not check whether resources were created by the
directives. You should enhance the test_directives.py module to do
that.
- I only updated one view to use the smileys. Update the skin as well to
make use of them.