12. Writing a Product in Python
Chapter 12
Writing a product for Plone allows you to do almost anything you'd like to do with Plone. Using Python to write content types or tools is the best way to provide ultimate flexibility. If you have a burning need for Plone to do something specific and it isn't covered elsewhere, then this is your opportunity to add this feature by writing a product. This could be storing some type of content specific to your company or some manipulation unique to you. In the previous chapter, I showed how you can customize a content type. This customization can take you only so far, though; you can't actually add new attributes to your content type, for example. Instead, you'll probably want to write your own content type.
In this chapter, I run through two examples: a content type and a tool. Both of these examples will be relatively straightforward but will get you ready for the next chapter, where I'll show you how to use Archetypes, a framework for Plone that allows you to generate content types quickly and simply with a minimal amount of code.
Specifically, I create a custom content type in Plone and step through all the code used to create this content type. It's a quite interesting content type—it uses several building blocks it pulls from a third-party C module and incorporates them into your Plone site. I show how to create the content type initially and then add permissions, search integration, new user interface elements, and installation scripts. Finally, I cover how to create a Plone tool, which is a way to add new tools to a site. Both of the examples in this chapter are available online for you to download, install, and study. Also, Appendix B lists all the code.
Writing a Custom Content Type
For the Plone book Web site (http://plone-book.agmweb.ca), I wanted to be able to show the code from this book online. I could've taken the code and simply placed it into documents, but that code would just show up without syntax highlighting. Also, all the whitespace in Python would have been removed. For a great product such as Plone, I needed something that looked good. So I needed a content type that would take code, syntax highlight it nicely, and allow users to easily view it online. Figure 12-1 shows the sample finished product.
Figure 12-1. An example Python script uploaded into Plone
From this design, you can extrapolate a few requirements for this product. Specifically, this product will have the following attributes:
- ID: Each piece of code will have a unique ID. This attribute is required.
- Title: Each piece of code should have a title. This attribute is required.
- Description: Each piece of code should have a description describing what it should do. This attribute is optional.
- Source code: Each piece of code will have one source code attribute that contains the source for that content type. This will be optional, but making it required is reasonable.
- Language: This is the language for the source code—for example, Perl, Python, Hypertext Markup Language (HTML), and so on.
Of course, the content type should interact with Plone so that you can use the power of Plone. You'll need to ensure that the product can be searched, can interact with security, can interact with workflow, and is correctly persisted. Further, it'd be nice if users could upload scripts directly from their hard drives rather than trying to cut and paste into a text area.
When investigating this code, I needed to find a simple way to turn code into HTML. This is pretty easy to do for a language with simple syntax such as Python (in fact, Python can 'lex” its own code), but really you want to be able to do this for multiple languages, such as HTML (page templates), JavaScript, Cascading Style Sheets (CSS), and so on. Fortunately, the SilverCity module does this already and is available from SourceForge (http://silvercity.sf.net/). It uses C libraries from the Scintilla text editor to lex the code. Without having to worry too much implementation, the upshot is that it'll happily spit out syntax-highlighted HTML for nearly a dozen programming languages.
Looking at the list of requirements, you'll see that they're pretty straightforward. In fact, the ID, title, and description are all defined in the Dublin Core implementation in Plone. So you have to worry only about the source code and language. Plone requires an ID and a title, and it really helps to have a description.
Starting the Content Type
Now that you have an idea of the content type you'll create in this chapter, you can start building it by writing Python on the file system. This content type is also a product, so you create a new directory in your product directory. The name of the directory you'll create is the name of the product that Zope will import, so choose your name wisely. I toyed with the idea of calling the product SourceCode or PloneSourceCode but decided those would be too confusing (they could also mean that the product is the actual source code for Plone). Instead, PloneSilverCity seemed to be a nice name that gave credit to its origins and was sufficiently obscure that no one would confuse it with something else.
After creating the directory, I usually add a few files and directories that I'll need. Every package needs an __init__.py file in it. The name of this file comes from Python and indicates that this directory is a Python package and hence importable. When the package is imported, Zope executes this file. Inside that file, you'll insert the product registration code so that the product will be registered with Zope.
Being user friendly, you can also add a few text files such as readme.txt, install.txt, and so on. One other text file that's also useful to add is refresh.txt. This file lets you hook into Zope's Refresh module and lets you dynamically reload the product as you write it. This is mind-bogglingly useful for your first few steps in writing a class, and I'll show how to configure this in Zope later.
At the moment, you have a directory called PloneSilverCity in the product directory that contains the following files, all empty: readme.txt, refresh.txt, install.txt, and __init__.py. This is now a valid Python package that does absolutely nothing (but not for long).
Developing with ZClasses
You're creating the content type using Python, but you've probably heard about ZClasses in other documentation or on the Internet. ZClasses are an existing framework in Zope 2 for developing classes through the Web. Many people have developed and distributed ZClasses successfully, and there can be a role for them for rapid development. However, I really don't recommend them. It's hard to develop them using existing tools, place them in source code, distribute them, and so on. Almost everyone I've talked to about ZClasses agrees that it's worth the effort to learn how to develop with Python, and I've seen more than one presentation that has ZClasses in the list of mistakes people have made.
If you do see documentation or other information relating to ZClasses, then I really recommend resisting the temptation to use it. For this reason, there's no mention of developing using ZClasses. If you're looking for a quick way to develop, then take a look at Archetypes, which is a slightly different approach.
Integrating SilverCity
Before you get too far into the Zope code, it may be useful to figure out how to use SilverCity. In any software development, writing layers that allow testing at atomic layers is absolutely vital. For this reason, you should start by making sure that you can use SilverCity from a Python module. If that works, you then simply have to add the Zope layer.
So, look into SilverCity for a moment. First, you have to install SilverCity; fortunately, this module corresponds to the install instructions for Python modules as outlined in Chapter 10. To install on Windows, download the file SilverCity-0.9.5.win32-py2.3.exe from http://silvercity.sf.net and run the graphical installer. To install on Linux, download the file SilverCity-0.9.5.tar.gz from http://silvercity.sf.net and save it to disk. Then unpack it and run the setup.py program. For example:
$ tar -zxf SilverCity-0.9.2.tgz $ cd SilverCity-0.9.2 $ python setup.py install ...
After doing this, you can quickly test that it works from the following Python prompt in Windows or Linux:
$ python Python 2.3.2 (#1, Oct 6 2003, 10:07:16) [GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import SilverCity >>>
This means SilverCity has been successfully installed. If you don't get a similar result and can't import SilverCity, stop and solve this issue first; otherwise nothing else will run.
Now you need to figure out the Application Programming Interface (API) for this module; being lazy, I went and read an example script located in PySilverCity/Scripts called source2html.py. This script does exactly what you want: It spits out HTML for a given piece of code. A really cheeky way to see this in operation is to feed this script to itself, like so:
$python source2html.py source2html.py --generator=python
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>source2html.py</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link
rel="stylesheet"
href="default.css" />
</head>
...
This means you just need to look at this API and alter it slightly. Add a module called source.py in the PloneSilverCity directory. In this you'll write the code that will provide the interface to the library; this new module contains no Zope-specific or Plone-specific code at this point. This module has three main modules: it'll tell you all the possible languages you can use, it'll take some text and return the correct parser, and finally it'll actually perform the translation.
First, add the following create_generator function, which gives you the correct parser:
from SilverCity import LanguageInfo
from StringIO import StringIO
def create_generator(source_file_name=None, generator_name=None):
""" Make a generator from the given information
about the object, such as its source and type """
if generator_name:
return LanguageInfo.find_generator_by_name(generator_name)()
else:
if source_file_name:
h = LanguageInfo.guess_language_for_file(source_file_name)
return h.get_default_html_generator()()
else:
raise ValueError, "Unknown file type, cannot create lexer"
Second, when you're in Plone, you need to be able to figure out exactly what languages are available so you can show them to the users. Write the following function to return that list, and call it list_generators:
def list_generators():
""" This returns a list of generators, a generator
is a valid language, so these are things like perl,
python, xml etc..."""
lexers = LanguageInfo.get_generator_names()
return lexers
Finally, the generate_html function takes a source file as a string, an optional generator, and an optional filename. SilverCity requires a file such as buffer to write the content out, so you can use Python's StringIO module to accomplish this. The following is the generate_html function:
def generate_html(source_file, generator=None, source_file_name=None):
""" From the source make a generator
and then make the html """
# SilverCity requires a file like object
target_file = StringIO()
generator = create_generator(source_file_name, generator)
generator.generate_html(target_file, source_file)
# return the html back
return target_file.getvalue(), generator.name
You'll note that this calls the create_generator function you wrote earlier to figure out the correct generator for this language. That's all the code you need to able to generate the HTML for a file. I haven't gotten into any of the specifics of actually lexing through the source or producing the HTML; the SilverCity library does this all for you. To reiterate the earlier point, in this module you have no reference to Zope or Plone; this module is completely independent. The actual details of this module aren't necessary to know, as long as you understand you're importing a third-party library.
It's traditional in Python scripts to put in at least one piece of test code. You could write a complete unit test suite, but that's outside of the current topic. Instead, you'll add a little bit of code to test two things: that this works and the languages that are available, like so:
if __name__ == "__main__":
import sys
myself = sys.argv[0]
file = open(myself, 'r').read()
print generate_html(file, generator="python")
print list_generators()
If you run this script, it'll open itself and feed that into the HTML syntax highlighter. A bunch of HTML will be spit out. You could just place this in the Zope-specific module you're about to write; however, having it all in a separate script makes it easy to test and alter later.
Writing the Class
A content type in Plone is just an object that has some particular attributes and some particular base classes. You don't even need to worry about reading and writing from the database—that's all handled by the Persistencebase classes. For the moment, create a module called PloneSilverCity.py in the package.
First, import the source.py module you wrote a few moments ago. That's one simple line because the module is in the same package. The line to import the functions is as follows:
from source import generate_html, list_generators
Second, you'll need a PloneSilverCity class that allows you to encapsulate the functionality you need. You need to worry about the following four attributes on this class:
- id: This stores the unique ID of this instance of the PloneSilverCity class.
- _raw: This stores the raw source code in the class.
- _raw_as_html: This stores the source code after it has been lexed into HTML.
- _raw_language: This stores the language of this source code.
For each of these attributes, you'll write an accessor, which is a function that returns the value of that attribute so that rather than calling the attribute, you call the accessor function. An example accessor function is getLanguage, which returns the value of the language. Writing an accessor is usually a good idea, especially because you'll apply security to these accessor methods later. In Zope, any method or attribute that begins within an underscore isn't available to Web-based methods such as page templates or Script (Python) objects. A good practice is to start all your attributes with an underscore and then put security on the accessing method.
Listing 12-1 shows the basic class.
Listing 12-1. The Basic Python Class
class PloneSilverCity:
def __init__(self, id):
self.id = id
self._raw = ""
self._raw_as_html = ""
self._raw_language = None
def getLanguage(self):
""" Returns the language this code has been lexed with """
return self._raw_language
def getRawCode(self):
""" Returns the raw code """
return self._raw
def getHTMLCode(self):
""" Returns the html code """
return self._raw_as_html
def getLanguages(self):
""" Returns the list of languages available """
langs = []
for name, description in list_generators():
langs.append( {'name':name, 'value':description} )
langs.sort()
return langs
You'll have to add one other method, which is an edit method that allows you to upload a file or a string. This one method will read the file and see if there's anything in the file; if there is, then it will be read and a filename determined. Then the code, language, and filename will be passed to the generate function. You'll store all this in the attributes mentioned earlier, as shown in Listing 12-2.
Listing 12-2. The Method for Handling Edits
def edit(self, language, raw_code, file=""):
""" The edit function, that sets
all our parameters, and turns the code
into pretty HTML """
filename = ""
if file:
file_code = file.read()
# if there is a file and it's not blank...
if file_code:
raw_code = file_code
if hasattr(file, "name"):
filename = file.name
else:
filename = file.filename
# set the language to None so set by SilverCity
language = None
self._raw = raw_code
# our function, generate_html does the hard work here
html, language = generate_html(raw_code, language, filename)
self._raw_as_html = html
self._raw_language = language
NOTE Well-versed Python developers may raise an issue with using file.name and file.filename. Zope file objects have an attribute called filename, which represents the filename, while in Python the attribute is called name. This code will then work in straight Python or Zope.
So now you have a Python class that encapsulates the object. At this point, you should be able to run this from the Python prompt quite easily and test that it does what you want. For example:
$ python
Python 2.3.2 (#1, Oct 6 2003, 10:07:16)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from PloneSilverCity import PloneSilverCity
>>> p = PloneSilverCity("test.py")
>>> p.edit("python", "print 'hello world'")
>>> p.getRawCode()
"print 'hello world'"
>>> p.getHTMLCode()
'<span class="p_word">print</span>
<span class="p_default">nbsp;</span>
<span class="p_character">\'hellonbsp;world\'</span>'
>>> p.getLanguage()
'python'
Turning the Package into a Product
Now you have a simple package, but this isn't yet a Plone product. You have to initialize it with Plone. This means adding extra information to the PloneSilverCity.py module. Specifically, you need to add a factory function. Using a factory is a well-known pattern in object-oriented design, and it defines how an object will be created. So, to the PloneSilverCity.py module, add the following constructor to the module:
def addPloneSilverCity(self, id, REQUEST=None):
""" This is our factory function and creates
an empty PloneSilverCity object """
obj = PloneSilverCity(id)
self._setObject(id, obj)
The addPloneSilverCity function isn't part of the PloneSilverCity class. As a constructor for the class, it's placed in the module outside the class. This function is the first Plone-specific function. Three parameters are passed to the method: the self object, the ID string for the object, and REQUEST. The self object is actually the context you've seen before, just by a different name. Since the objects will always be created inside folder, self will refer to the folder in which this object will be created. This function creates an instance of PloneSilverCity called obj and passes it to the _setObject method of the folder. The _setObject method is particular to Zope; it instantiates the object in the database and registers the object in the containing folder.
Next, add the factory type information covered in Chapter 11 (this is your first chance to create it yourself). The factory type information contains all the information about the content type in a dictionary; this information is loaded into portal_types when the product is installed into your Plone instance. This information will mirror what you saw in earlier, where you altered factory type information through the Web.
Before building the factory information, I usually create a configuration file that contains all the repeated variables for the product. This file is called config.py, and in there you put the names of the product, the name of its layer in the skins, and the name as it will appear to the user, like so:
product_name = "Source Code" plone_product_name = "PloneSilverCity" layer_name = "silvercity" layer_location = "PloneSilverCity/skins"
Then you can set up the factory type information and use these strings. For example, the ID will be Source Code since this is shown in Plone to the users. The actions section of the type information is a tuple of dictionaries of all the actions that can occur with this object. When this factory is loaded into Plone, the Actions tab inside the portal_types tool will be populated with this content. Each of those actions has a corresponding method, template, or script that will be called; most of these directly correspond to page templates, which I discuss later in this section.
As you know by now, an action is something that users can do to an item in the Plone database. Thinking of this example application, users can do two obvious things to the source code. They can view it and see the nicely highlighted code, and they can edit the item and upload some source code. Actually, Plone requires that there's one action called view and one called edit, so these two fit nicely. You also want a third action—it's nice to be able to download the source in its original form. With languages such as Python where the formatting is key, this is really useful. This action points directly to getRawCode, which is the method for getting the raw code back again.
Each action has a permission associated, as shown in Listing 12-3 (I show exactly where that comes from later in this section).
Listing 12-3. The Factory Type Information and Actions
factory_type_information = {
'id': plone_product_name,
'meta_type': product_name,
'description': ('Provides syntax highlighted HTML of source code.'),
'product': product_name,
'factory': 'addPloneSilverCity',
'content_icon': 'silvercity.gif',
'immediate_view': 'view',
'actions': (
{'id': 'view',
'name': 'View',
'action': 'silvercity_view_form',
'permissions': (view_permission,)},
{'id': 'edit',
'name': 'Edit',
'action': 'silvercity_edit_form',
'permissions': (edit_permission,)},
{'id': 'source',
'name': 'Source',
'action': 'getRawCode',
'permissions': (view_permission,)},
),
}
NOTE At this point, the product can't be imported from the Python prompt because the code is incomplete.
Setting Up Permissions
A fundamental concept when dealing with Web sites is that everything and everybody is untrusted. Before any property is accessed or any method is called, you must first check if the party wanting to perform an action is allowed to do so. In most systems, three permissions exist: the permission to add an item, the permission to delete an item, and the permission to edit an item. One other permission applies to Plone: the right to view an item through the Web (or other protocol). The containing folder handles deleting, which is a permission handed out in Plone to the containing folder. If you can delete anything in the folder, you can then also delete the content type you're adding here.
This leaves you with three permissions to worry about. It's normal to use the ones that come with the CMFCore package: Add portal content, Modify portal content, and View. Returning to the config file, you can add the permissions you need, like so:
from Products.CMFCore import CMFCorePermissions add_permission = CMFCorePermissions.AddPortalContent edit_permission = CMFCorePermissions.ModifyPortalContent view_permission = CMFCorePermissions.View
This means the add_permission variable references the permission imported from CMFCorePermissions. There's nothing magical about the permissions—each permission is just a string. Using the built-in permission is convenient and understandable for your users. Plone is already configured to allow the right person to add content using the Add portal content permission. Further, the default workflow is defined to use and alter these permissions. These permissions were the ones you added to the factory type information.
If you wanted to make your own permission, you could do so quite easily. Suppose you wanted the Add… permission to be Add Source Code and have its own permission. Then you'd change the file to read as follows:
add_permission = "Add Source Code"
After importing the product, you'd have a new permission in the Security tab matching that Add Source Code permission. Why would you want to do this? Well, using a permission that everyone else uses is convenient. However, it may be that you want more granularity or different security. For this reason, you can just create your own security settings.
Completing the Initialization
Now you need to set up the initialization of the product. You do this in the __init__.py module so that when Zope reads this file at startup, it'll complete the initialization of the product, as shown in Listing 12-4.
Listing 12-4. The __init__.py Module
import PloneSilverCity
from Products.CMFCore import utils
from Products.CMFCore.DirectoryView import registerDirectory
from config import product_name, add_permission
contentConstructors = (PloneSilverCity.addPloneSilverCity,)
contentClasses = (PloneSilverCity.PloneSilverCity,)
contentFTI = (PloneSilverCity.factory_type_information,)
registerDirectory('skins', globals())
def initialize(context):
product = utils.ContentInit(product_name,
content_types = contentClasses,
permission = add_permission,
extra_constructors = contentConstructors,
fti = contentFTI)
product.initialize(context)
What's happening in this code? Well, actually not that much—it's just a little verbose. First, you make references to the classes and constructors that are going to be used in contentClasses and contentConstructors. These map to the factory function for creating the objects and the actual class. These are then passed into the ContentInit function, inside initialize, which is a special function that's called during the product initialization. ContentInit does all the work to set up the product within Plone. The parameters to this function are as follows:
- product_name: This is the name of the product, as defined in the config file (in this case, PloneSilverCity).
- content_types: This is the tuple of classes that this product defines; usually this is just one class, but it may be more.
- permission: This is the permission that's needed to create an instance of this object; in this case, it's the add_permission variable I've defined in config.py*.*
- fti: This stands for factory type information and is the dictionary of factory type information you defined in the PloneSilverCity.py module for the content.
Altering the Product Modules
Now you can return to the PloneSilverCity.py module and complete the task of turning this into a Plone product. At the start of the module, you'll create the import statements. These import statements pull various Plone initialization requirements from various locations, as follows:
from Globals import InitializeClass from AccessControl import ClassSecurityInfo from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl from Products.CMFCore.PortalContent import PortalContent
These imports provide the base functionality for the product and are common across most content types. The definitions of imports are as follows:
InitializeClass: This function initializes the class and applies all the security declarations that it'll have. You specify those security declarations by using the ClassSecurityInfo class.
ClassSecurityInfo: This class provides a series of security methods that will allow you to restrict access to methods of the content type.
DefaultDublinCoreImpl: This class provides an implementation of Dublin Core metadata. Chapter 11 covered Dublin Core; this gives an object all the Dublin Core attributes and methods to access them such as Title, Description, Creator, and so on.
PortalContent: This provides the base class for all content in a Plone site and some of the key attributes it needs. Using this base class gives the object a whole host of functionality such as making the object persist inside the database, cataloging the object for searching inside the portal_catalog object, and making it registerable with the portal_types tool.
You'll also need to import the configuration variables and permissions as well. So that takes the following two lines:
from config import plone_product_name, product_name from config import add_permission, edit_permission, view_permission
Returning to the class, you have to add two base classes to make it fully Plone compatible: PortalContent and DefaultDublinCoreImpl. You also need to give the class a meta_type. Each product in Zope has a unique meta_type:
class PloneSilverCity(PortalContent, DefaultDublinCoreImpl):
meta_type = product_name
One requirement of Plone is that it knows what base classes the content type implements. Other parts of the application will need to know what classes are implemented. So, explicitly state what classes the content type implements, like so:
__implements__ = (
PortalContent.__implements__,
DefaultDublinCoreImpl.__implements__
)
Adding Security to the Class
If you've already decided to give the actions security, you also need to apply this security to the class. In an object-publishing environment such as Plone, anyone can call any method of the class through the Web, unless it begins with an underscore. This is obviously bad, and you need to protect all your methods.
To do this inside the class, make an instance of the ClassSecurityInfo class. You do this with the following line:
security = ClassSecurityInfo()
This security object provides an interface into Zope security machinery. You can then apply methods to the object. My favorite method for doing this is to add a line applying the security directly above the method. This way it's easy to remember where the security is applied, and you won't forget to update it later, if you need to do so. The declareProtected method takes the permission and the method name to protect the edit method. So that only people who actually have the edit permission can call it, you do the following:
security.declareProtected(edit_permission, "edit")
Repeat this for each method, giving the appropriate permission and method name. The only one that needs to be protected is __init__ because this begins with an underscore. To apply all this security, you must initialize the class. Without doing this one step, all the security further declared won't be applied, and your object will be public.
In other words, don't forget this line:
InitializeClass(PloneSilverCity)
The API for ClassSecurityInfo provides the following methods for the class:
- declarePublic: This takes a list of names. All the names are declared publicly accessible for all users through restricted code and through the Web.
- declarePrivate: This takes a list of names. All the names are private and can't be accessed through restricted code.
- declareProtected: This takes a permission and any number of names. All the names can be accessed only with the permission given.
- declareObjectPublic: This sets the entire object as publicly accessible.
- declareObjectPrivate: This sets the entire object to private and inaccessible to restricted code.
With these methods it's possible to set almost any security you'd like. However, I've almost always found that explicitly setting the protection of each method with a permission has been sufficient.
Integrating with Search
In the previous chapter I showed you how the search works and the indexes that exist. Since the indexes work against Dublin Core objects and you've used Dublin Core as a base class, your object's title, description, creator, modification date, and so on, will all be indexed for you—no extra work is needed. Further, by inheriting from the PortalContent class every time the object is altered, the catalog will be updated for you; again, you don't need to worry about anything.
However, one index does need a little help, and that's SearchableText. As I demonstrated previously, the SearchableText index provides the full-text index that Plone uses when a search is run. It'd be nice if the search would also index the source code, so if somebody uploaded a piece of code with import in it, the search would pick it up. Because the catalog looks at the object and tries to find an attribute or method matching the index name, all you need to do is provide a method with that name that returns the value you want.
The easiest way to do this is to make a string out of the fields you want—for example, the title, the description, and the raw code. This can be protected by the View permission, since anyone viewing the object can happily see the contents anyway. The following is a SearchableText method that performs this task:
security.declareProtected(view_permission, "SearchableText")
def SearchableText(self):
""" Used by the catalog for basic full text indexing """
return "%s %s %s" % ( self.Title()
, self.Description()
, self._raw
)
The Difference Between a Python Class and a Plone Class
As you can see, there's quite a difference between a normal Python product and one registered in Plone. However, most of those differences are about registering the product and asserting the security. The actual class remains similar. Listing 12-5 highlights all the differences between the pure Python implementation and the Plone implementation.
Listing 12-5. The Plone Version of the Class
from Globals import InitializeClass**
from AccessControl import ClassSecurityInfo
from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent
from config import plone_product_name, product_name
from config import add_permission, edit_permission, view_permission
from source import generate_html, list_generators
factory_type_information = {
'id': plone_product_name,
'meta_type': product_name,
'description': ('Provides syntax highlighted HTML of source code.'),
'product': product_name,
'factory': 'addPloneSilverCity',
'content_icon': 'silvercity.gif',
'immediate_view': 'view',
'actions': (
{'id': 'view',
'name': 'View',
'action': 'silvercity_view_form',
'permissions': (view_permission,)},
{'id': 'source',
'name': 'View source',
'action': 'getRawCode',
'permissions': (view_permission,)},
{'id': 'edit',
'name': 'Edit',
'action': 'silvercity_edit_form',
'permissions': (edit_permission,)},
),
}
def addPloneSilverCity(self, id, REQUEST=None):
""" This is our factory function and creates
an empty PloneSilverCity object inside our Plone
site """
obj = PloneSilverCity(id)
self._setObject(id, obj)
class PloneSilverCity(PortalContent, DefaultDublinCoreImpl):
meta_type = product_name
__implements__ = (
PortalContent.__implements__,
DefaultDublinCoreImpl.__implements__
)
security = ClassSecurityInfo()
def __init__(self, id):
DefaultDublinCoreImpl.__init__(self)
self.id = id
self._raw = ""
self._raw_as_html = ""
self._raw_language = None
security.declareProtected(edit_permission, "edit")
def edit(self, language, raw_code, file=""):
""" The edit function, that sets
all our parameters, and turns the code
into pretty HTML """
filename = ""
if file:
file_code = file.read()
# if there is a file and its not blank...
if file_code:
raw_code = file_code
if hasattr(file, "name"):
filename = file.name
else:
filename = file.filename
# set the language to None so set by SilverCity
language = None
self._raw = raw_code
# our function, generate_html does the hard work here
html, language = generate_html(raw_code, language, filename)
self._raw_as_html = html
self._raw_language = language
security.declareProtected(view_permission, "getLanguage")
def getLanguage(self):
""" Returns the language that code has been lexed with """
return self._raw_language
security.declareProtected(view_permission, "getLanguages")
def getLanguages(self):
""" Returns the list of languages available """
langs = []
for name, description in list_generators():
# these names are normally in uppercase
langs.append( {'name':lang, 'value':language } )
langs.sort()
return langs
security.declareProtected(view_permission, "getRawCode")
def getRawCode(self):
""" Returns the raw code """
return self._raw
security.declareProtected(view_permission, "getHTMLCode")
def getHTMLCode(self):
""" Returns the html code """
return self._raw_as_html
security.declareProtected(view_permission, "SearchableText")
def SearchableText(self):
""" Used by the catalog for basic full text indexing """
return "%s %s %s" % ( self.Title()
, self.Description()
, self._raw
)
InitializeClass(PloneSilverCity)
Adding Skins
So now that you've got the main code, you have two things left to do: build the skins and create an installation method. The skins are actually one of the easier parts because so much of the work has been done already in the Plone framework. I covered skins in detail in earlier chapters, where I discussed how to make a skin for the Plone site on the file system. Each product that needs to provide custom User Interface (UI) does so by making its own File System Directory View (FSDV), so you'll do the same again here.
The skins are placed in the skins directory of the product. This directory name is defined in the __init__.py file where you register the directory using the registerDirectory function. If you wanted to change the name, make sure to register it—you can register as many directories as you like, but it's recursive and will register everything in and below that registered directory.
The easiest of all your jobs for this product is to add an icon for the object that will appear in Plone. The name of this icon is already defined in the factory type information with the line 'content_icon': 'silvercity.gif', so all you have to do is add an icon to the skins directory called silvercity.gif. This icon will display whenever you see the object in the Plone user interface. When SilverCity lexes a file, it outputs HTML using CSS tags, so you have ensure that the particular CSS file is available. For this product you simply copy the CSS out of the SilverCity product and place it in the skins directory with the name silvercity.css.
These two items are now done. Next, you actually have to write the view and edit pages. Previously I discussed how this is similar to a document, so when you're looking for view and edit pages, the best place to look is the pages for a document. Those pages are document_view.pt and document_edit_form.cpt and are located in the CMFPlone/skins/plone_content directory.
Making the View Page
To alter the view page, you take the view page for a document, copy it into your product's skins directory, and rename it to silvercity_view.pt. There's no point in re-creating the entire page when the view page is so similar; all you need to do is to make two minor changes.
As mentioned, SilverCity spits out HTML where all the code has been highlighted using CSS and you have a custom style sheet. You need to make sure that the view page inserts that CSS, and the main template has a slot for CSS called css_slot. To put the custom CSS file into that slot, you just have to provide a value for it. For example:
<metal:cssslot fill-slot="css_slot"> <link rel="stylesheet" href="" tal:attributes="href string:$portal_url/silvercity.css" /> </metal:cssslot>
Here you're referencing a CSS file called silvercity.css. That file is located in the skins directory, and you'll be accessing it from the skin when it's rendered. The original document shows a property called cookedBody, which is an attribute of a document. I removed that part of the code and instead inserted the code. As you've seen by now, the function getHTMLCode returns the HTML, so all you have to do is the following:
<div id="bodyContent">
<div tal:replace="structure here/getHTMLCode" />
</div>
If you want to change anything else specific in this page template, now is your opportunity. It could be nice to show the language that it was written in, to show an icon, or to change history, for example.
Making the Edit Page
Like the view page, you can take the edit page, copy it into the skin, and rename it to silvercity_edit_form.cpt. The biggest problem is that the edit form is designed to be used with a What You See Is What You Get (WYSIWYG) editor such as Epoz. Until a good WYSIWYG editor for source code is available for Web browsers, you'll have to turn this off because you can't write SQL in an HTML editor.
This is quite a lengthy change of the page template—remember, you can get this off the Web site. In this template, remove all mentions of the editors and replace them with a simple text area. Keep the name of the HTML field the same because there's no real need to change it. Also, leaving it the same means it plays nicely with the script for handling the form later. A document has at the bottom a series of selections for the format, which are normally items such as Plain Text, HTML, and so on. You'll replace this with a drop-down box for all the languages that the main SilverCity library has available. The getLanguages method written earlier returns a list of all the languages. Each item is a dictionary that contains the value (for example, CPP) and a nice name (for example, C or C++).
Listing 12-6 loops through the getLanguages method written earlier. You can also define a variable for the current language so that as it loops through the languages, you can highlight the current language.
Listing 12-6. Adding a Drop-Down List for Selecting the Language
<div class="field">
<label
for="language"
i18n:translate="label_silvercity_language">
Language</label>
<div class="formHelp" i18n:translate="help_silvercity_language">
Select the name of the language that you are adding
</div>
<select name="text_format"
tal:define="l here/getLanguage">
<option tal:repeat="item here/getLanguages"
tal:content="item/name"
tal:attributes="value item/value;
selected python:test(item['value'] == l, 1, 0)" />
</select>
</div>
When the edit page gets submitted, you need to set up the validators and actions to do something with the form. The validation should check that a valid title and a valid ID have been given. To the silvercity_edit.cpt.metadata file, add the following:
[validators] validators..Save = validate_id,validate_title validators..Cancel =
Where did those validations come from? Well, I was cheeky and again looked at the validations for a document. That calls three validations, but you need only two of them. By checking what that validation evaluated to, you can see which ones are needed and which ones aren't. You'll find all the validations in plone_skins/plone_form_scripts, and the object name starts with validation.
So now you need the action, so take the edit script for a document (document_edit.cpy) and copy it into SilverCity. Mostly the script is just fine, so you can keep it with one modification. Change the messages to Source code instead of Document. Listing 12-7 shows the edit script.
Listing 12-7. The Edit Script
##parameters=text_format, text, file='', SafteyBelt='', ~CCC
title='', description='', id=''
##title=Edit a document
filename=getattr(file,'filename', '')
if file and filename:
# if there is no id, use the filename as the id
if not id:
id = filename[max( filename.rfind('/')
, filename.rfind('\\')
, filename.rfind(':') )+1:]
file.seek(0)
# if there is no id specified, keep the current one
if not id:
id = context.getId()
new_context = context.portal_factory.doCreate(context, id)
new_context.edit( text_format
, text
, file
, safety_belt=SafetyBelt )
from Products.CMFPlone import transaction_note
transaction_note('Edited source code %s at %s' % ~CCC
(new_context.title_or_id(), new_context.absolute_url()) ~CCC
)
new_context.plone_utils.contentEdit( new_context
, id=id
, title=title
, description=description )
return state.set(context=new_context, ~CCC
portal_status_message='Source code changes saved.')
The script does a few things. First, it gets the filename if one exists; if no ID is given, then the ID is set to that filename. This means if a user uploads library.c, the ID for that object will be library.c. Second, it tells portal_factory to create an object (see the 'Portal Factory” sidebar for more information on what that means). Then it calls the edit method on the object (something you wrote earlier), and it calls contentEdit on the plone_utils tool. Without looking into the depths of the plone_utils tool, contentEdit takes the keywords given, and if the class implements Dublin Core, then it will change those attributes. Since you set up the __implements__ attribute earlier, the edit method in Listing 12-7 will do the work for you. Any changes to title, ID, or description will be changed in the object.
Portal Factory
One problem exists with the way objects are created. Before you can even get to the edit form, you have to create an object. Then the edit form for that object will display. In practice, people accidentally create objects, get to the edit form, and then realize it was the wrong type. This is annoying and leaves spare objects lying around in your database. It's like creating a file on the file system, realizing it's wrong, and then leaving it there.
To solve this, the portal_factory tool allows you to temporarily create objects. It'll create a temporary object and then let you edit it. Only once you've clicked the edit button will your object be created. To assign an object to portal_factory, go to the portal_factory tool, and in the form select all the content types for which you'd like to use this tool. The only catch is that you must ensure your edit scripts correctly integrate with the tool, as shown in this example.
Installing the Product into Plone
You have a standard way for installing a product into Plone you go to the Plone control panel and click the product to install it. That script uses the portal_quickinstaller tool to do the installation. For this product to work, you need to expose functionality that the tool can read. After all, you want as many people to use the product as possible. If you're writing something that's for internal use only and you're never going to distribute to anyone else, you can skip this stage. But you'll need to do these steps by hand anyway, and it's always better to have a script for the installation.
NOTE Quick Installer makes an external method of this install function and runs it for you behind the scenes. It performs a few other tasks, as well. This means you could make an external method to do this if you wanted. That's why the installation instructions for many products tell you to create an external method.
To integrate with the Quick Installer, you need to make a specific module called Install.py in the Extensions directory. That module has to contain a function called install. The Quick Installer tool runs the install function, and the output is placed in a file on the server. The install method has to install the product into the portal types, so add an FSDV that points to the skins directory, and add this new directory to the skin layers.
Now import the functions and set up the variables as usual. You have to import the factory_type_information from the product so that you can use it in the script, as shown in Listing 12-8.
Listing 12-8. The Start of the Installation Function
from Products.CMFCore.TypesTool import ContentFactoryMetadata
from Products.CMFCore.DirectoryView import createDirectoryView
from Products.CMFCore.utils import getToolByName
from Products.PloneSilverCity.PloneSilverCity import factory_type_information
from Products.PloneSilverCity.config import plone_product_name, product_name
from Products.PloneSilverCity.config import layer_name, layer_location
def install(self):
""" Install this product """
After this, everything is generic and could be run on any product—unless of course you want it to do something special on the installation. To add your product to the portal_types tool, you first check that your product isn't already registered. It could be that someone has registered another product of the same name. For this you'll call the manage_addTypeInformation method, as shown in Listing 12-9.
Listing 12-9. Remainder of the Installation Function
out = []
typesTool = getToolByName(self, 'portal_types')
skinsTool = getToolByName(self, 'portal_skins')
if id not in typesTool.objectIds():
typesTool.manage_addTypeInformation(
add_meta_type = factory_type_information['meta_type'],
id = factory_type_information['id']
)
out.append('Registered with the types tool')
else:
out.append('Object "%s" already existed in the types tool' % (id))
Next you need to add an FSDV to the skins directory. Again, the first thing you check is that you don't already have one; then you add the directory view with the following:
if skinname not in skinsTool.objectIds():
createDirectoryView(skinsTool, skinlocation, skinname)
out.append('Added "%s" directory view to portal_skins' % skinname)
Finally, you loop through all the skins and add your new FSDV to each of the skins. This is a generic function; each skin is listed as string with each layer separated by commas. All you have to do is split the string up and insert your new skin after the layer named custom, as shown in Listing 12-10.
Listing 12-10. Setting the Skin in the Installation Method
skins = skinsTool.getSkinSelections()
for skin in skins:
path = skinsTool.getSkinPath(skin)
path = [ p.strip() for p in path.split(',') ]
if skinname not in path:
path.insert(path.index('custom')+1, skinname)
path = ", ".join(path)
skinsTool.addSkinSelection(skin, path)
out.append('Added "%s" to "%s" skins' % (skinname, skin))
else:
out.append('Skipping "%s" skin' % skin)
return "\n".join(out)
That's it. Your product is now ready to run.
Testing the Product
To test, restart your Plone instance so that it'll read the product directory in. If you haven't already developed your product in the appropriate products folder, then place it there now as part of your standard installation process. If there are any problems with your product, then Zope may start, but the product may show up as broken in the control panel.
Then install into Plone using the Add/Remove Products page in the Plone control panel. Now you should be able to go to a folder and add a source code object. The icon will be your icon in the skin, and the name is what you defined in the file system. After adding this, you'll get the edit page. Note that the URL now has silvercity_edit_form on the end and shows the nicely altered edit form.
You could add some code, select a language, and click Save, or you could upload a file from your computer. After clicking Save, you'll be taken back to the view function, and, sure enough, the code will be shown with the syntax highlighted.
This product is a little example of how simple writing a product in Plone is. Although it has been a lot of pages, most of it has been setting up the infrastructure and the skins. One of the first things people do is compare this to other Web scripting languages such as PHP. You have to remember that by having your code in Plone, you've achieved quite a few things without having to rewrite them. Specifically, you've achieved the following:
- Full-text searching of the content
- Integration with the workflow
- Integration with portal membership and authentication
- Persistence through the Plone database without having to write SQL or do other work
Further, it really does let your complete product scale later. For example, if you need a bug-tracking system, drop in the Collector product, and if you need a photo management product, drop in CMFPhoto. By utilizing the framework, you can give your overall site a great deal of flexibility and scalability.
Although this product is a little cheeky by using lots of existing code, it demonstrates quite a few key functions of writing a product in Plone.
Debugging Development
If you're developing your own product, then two things will happen to you at some point (unless you have so much Zen that you should be writing the Zope core code): your product will break, and you'll need to debug it.
During development, you may want to try importing the product into the Python prompt to see how it works. Unfortunately, you'll probably get an error. This is because when you do this import, you'll get a cascade of Zope-related imports. You can cope with some of this but not all of it. One of the common problems is that you'll get the following error:
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "PloneSilverCity/__init__.py", line 1, in ?
import PloneSilverCity
File "PloneSilverCity/PloneSilverCity.py", line 4, in ?
from Globals import InitializeClass
File "/opt/Zope-2.7/lib/python/Globals.py", line 23, in ?
import Acquisition, ComputedAttribute, App.PersistentExtra, os
File "/opt/Zope-2.7/lib/python/App/PersistentExtra.py", line 15, in ?
from Persistence import Persistent
ImportError: cannot import name Persistent
You can solve this by making sure that before you import PloneSilverCity you import Zope and run the startup method. To be able to import Zope, you must have the directory containing Zope in your path; on my computer, this is /opt/Zope-2.7/lib/python. However, you'll then run across errors trying to import CMFCore if you have an instance home configured, which you probably will.
The easiest way to import PloneSilverCity is to run Zope from the command line in debug mode using zopectl. This will open a Python prompt that will let you access the Zope database directly from Python. Chapter 14 covers this in more detail, but it can be done easily now (assuming your Zope isn't currently running). You can find the zopectl script in the bin directory of your Zope instance; for example, on my computer, this is /var/zope/bin. Listing 12-11 shows an example running zopectl with PloneSilverCity.
NOTE At the time of writing, zopectl doesn't work on Windows. However, on Linux, it's a convenient way to test your code. Unfortunately, using zopectl requires locking the Zope Object Database (ZODB) and, unless you're running ZEO (something I'll discuss in Chapter 14), can't be done while Zope is running.
Listing 12-11. Debugging the Product Using Zope
$ cd /var/zope/bin
$ ./zopectl debug
Starting debugger (the name "app" is bound to the top-level Zope object)
>>> from PloneSilverCity.PloneSilverCity import PloneSilverCity
>>> p = PloneSilverCity("test")
>>> p.edit("python", "import test")
>>> p.getRawCode()
'import test'
>>> p
<PloneSilverCity at test>
When your product breaks, you'll get a traceback to one of two places, either the error log page or one of the event logs. If you've really broken it, then Plone won't start; this normally happens when an import fails. If that's the case, then Plone will just not start at all. I recommend starting Plone from the command line, be it Windows or Linux. Having the console output of that error will give you an immediate output of the error. For example, Listing 12-12 shows what happens when you try to run Plone SilverCity with a deliberate error in the import.
Listing 12-12. An Example Error on Startup
$ bin/runzope
------
2003-12-19T17:44:05 INFO(0) ZServer HTTP server started at Fri Dec 19 17:44:05 2003
Hostname: laptop
Port: 8080
------
2003-12-19T17:44:05 INFO(0) ZServer FTP server started at Fri Dec 19 17:44:05
2003
Hostname: basil.agmweb.ca
Port: 8021
------
2003-12-19T17:44:16 ERROR(200) Zope Could not import Products.PloneSilverCity
Traceback (most recent call last):
File "/opt/Zope-2.7/lib/python/OFS/Application.py", line 533, in import_product
product=__import__(pname, global_dict, global_dict, silly)
File "/var/zope.zeo/Products/PloneSilverCity/__init__.py", line 1, in ?
import PloneSilverCity
File "/var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py", line 1
import ThisModuleDoesNotExist
^
ImportError: No module named ThismoduleDoesNotExist
At this point, Zope stops; you'll have to fix this import before starting again. This is probably the easiest error to fix but will likely occur only when you install that a whiz-bang new product off the Internet only to find it has a dozen dependencies.
The next kind of error that can occur is a programming or logic error, which occurs inside the code. Suppose your product adds two numbers together, but one of them is a string (this is an error in Python). An error will be raised, which Plone will report back to the user interface with an error value and error type. At this point, you should click plone setup and click Error Log to see the traceback, find the bug, and fix the issue.
If you change something in a product, the change doesn't get reflected in Python right away; you need to use a product called Refresh to force that change. This is an amazingly useful tool for new developers, and you enable it by having a file called refresh.txt in your Products directory. You'll note that PloneSilverCity has one. Next, in the ZMI, select Control Panel, select Products, select PloneSilverCity (or your product name), and click the Refresh tab. If your product has a refresh.txt file, you can click the Refresh button. Plone will then dynamically reload your product with all the new code. If you're running Zope in debug mode, then you can set the product to dynamically check every time it's run, rather than having to come back to this screen each time.
Unfortunately, everything with refresh isn't all rosy. It does do some rather 'interesting” things behind the scenes to Python to enable this to happen. In fact, the Refresh product can produce unexpected results in your product—nothing that restarting Zope won't fix, though. Relatively simple products that just do data manipulation will be fine, but some products aren't. Chances are if you're just starting out, your products will be simple, and you'll be fine.
Finally, if it something goes wrong and you can't figure it out, you'll need to pull out a debugger. You have so many ways to debug Zope that I'll just discuss the one I use the most—the Python debugger. You can call the Python debugger by adding the following line to a piece of code:
import pdb; pdb.set_trace()
TIP: It's uncommon in Python to put two lines into one by using a semicolon, but here it's handy so that when you come to delete or comment it out later, you have only one line to comment.
That will cause a breakpoint in the code execution, and Zope will stop processing and open the debugger. This is why you really want to run Plone from a console when developing. This doesn't work with a service or a daemon because there's no console to which to connect. Now if you re-create your problem, you'll be dropped into the Python debugger, and you can debug your product. For example, in my now fixed-up and correctly importing PloneSilverCity, I added the following pdb trace function to the getLanguages method:
def getLanguages(self):
""" Returns the list of languages available """
import pdb; pdb.set_trace()
langs = []
...
Now when you run Plone and connect to the skin (something you'll add a moment), this function will be called, and, sure enough, on the console you started Zope with, you'll see the following:
--Return-- > /var/tmp/python2.3-2.3.2-root/usr/lib/python2.3/pdb.py(992)set_trace()->None -> Pdb().set_trace() (Pdb)
You can type help to get a list of help options. The two main choices are n for next and s for steppng into an item. For example:
(Pdb) n > /var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py(97)getLanguages() -> langs = [] (Pdb) n > /var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py(99)getLanguages() -> for value, description in list_generators(): (Pdb) langs []
For more information on the debugger, I recommend the online documentation at the Python Web site (http://python.org/doc/current/lib/module-pdb.html). You have other ways to debug Zope, such as using ZEO to get an interpreter, for example. Chapter 14 covers using ZEO. Integrated developer environments such as Wing (http://wingide.com/wingide) or Komodo (http://www.activestate.com/Products/Komodo) can remotely debug Zope instances and have nice graphical interfaces.
Writing a Custom Tool
Writing a tool is simpler than writing a content type, mostly because there's little to do in terms of registering the product and because the user interface is simple. For an example, I use simple statistics tool on my ZopeZen Web site (http://www.zopezen.org) for giving information about the amount of content, number of users, and so on. This simple tool prints a few numbers that interest me as a manager of the site. Figure 12-2 shows my ZopeZen stats.
Figure 12-2. PloneStats on ZopeZen
These are Web site statistics—I can get those by parsing the Web logs for my Plone server. Tools—such as Analog, Webalizer, WebTrends, and so on—can happily parse your Plone or Apache Web logs for you. Again, you can find the entire code for this project in the Collective at http://sf.net/projects/collective in the PloneStats package.
Starting the Tool
You should place the tool in a product directory the same way you did with the content type—by making a directory inside the instance home product directory. Into that folder add the refresh.txt, install.txt, readme.txt, and __init__.py files.
In that directory, the main module is called stats.py, which contains all the code for creating the stats. Again, I'll cover how the module looks without any extra Zope code. However, since you're directly plugging into the other Plone tools, running outside of Zope will make little sense.
Listing 12-13 shows the start of the tool. This is a simple version that has two methods: one to return the number of content types by type and by workflow state and the other for users that returns the total number of users in the site.
Listing 12-13. The Basic Stats Object
class Stats:
def getContentTypes(self):
""" Returns the number of documents by type """
pc = getToolByName(self, "portal_catalog")
# call the catalog and loop through the records
results = pc()
numbers = {"total":len(results),"bytype":{},"bystate":{}}
for result in results:
# set the number for the type
ctype = str(result.Type)
num = numbers["bytype"].get(ctype, 0)
num += 1
numbers["bytype"][ctype] = num
# set the number for the state
state = str(result.review_state)
num = numbers["bystate"].get(state, 0)
num += 1
numbers["bystate"][state] = num
return numbers
def getUserCount(self):
""" The number of users """
pm = getToolByName(self, "portal_membership")
count = len(pm.listMemberIds())
return count
Turning the Package into a Tool
To turn the package into a tool, you have to do the same process for the content type. In other words, you have to register the tool in the __init__.py module. Just like in the content type example, make a config.py file that contains all the configurations. The config.py file looks like this:
from Products.CMFCore import CMFCorePermissions view_permission = CMFCorePermissions.ManagePortal product_name = "PloneStats" unique_id = "plone_stats"
The security for this product is simpler, but that's because the product is quite simple—all it does is interact with other tools and produce some statistics. There's nothing for users to add, edit, delete, or otherwise interact with. This means you have really only one permission, ManagePortal, which is the permission to actually manage Plone's configuration and is usually given only to managers. For this purpose, this means only managers can go into the ZMI and see the information the tool provides. You could quite easily add a nice-looking skin for the Plone control panel or a portlet that displays this information in your site if you wanted.
Returning to __init__.py, add the initialization code for the tool. There's a special initialization script for tools, called ToolInit. In this tool, the __init__.py file looks like this:
from Products.CMFCore import utils
from stats import Stats
from config import product_name
tools = (stats.Stats,)
def initialize(context):
init = utils.ToolInit( product_name,
tools = tools,
product_name = product_name,
icon='tool.gif'
)
init.initialize(context)
The ToolInit function can take multiple tools; in this case, you have only one tool. If you have multiple tools, you can have only one product name and icon to show in the ZMI. This is all that's needed to register the tool. Now you have to complete the main module to turn it into an actual tool object.
Altering the Tool Code
Next, add the code to the class to turn into a tool. Like the content type, this is just a matter of adding security inheriting from the correct base classes, like so:
from Globals import InitializeClass from OFS.SimpleItem import SimpleItem from AccessControl import ClassSecurityInfo from Products.CMFCore.utils import UniqueObject, getToolByName
The SimpleItem class is the default base class for a simple (not a folder) object in Zope. Actually, all content types inherit from a class that, somewhere in its class hierarchy, inherits from SimpleItem; it's just that you don't need all the extra attributes those other classes provide. UniqueObject ensures that there will be one and only one instance of this object inside your Plone site and that it can't be renamed or moved around. This means your object will always be available.
Next, you import the variables from the config file as usual. By assigning the ID of your object, you ensure that the tool will have the ID of whatever unique_id is in the config file—in this case, plone_stats. The two base classes for the tool are the UniqueObject and SimpleItem classes, which are the minimum it needs. For example:
from config import view_permission, product_name, unique_id
class Stats(UniqueObject, SimpleItem):
""" Prints out statistics for a Plone site """
meta_type = product_name
id = unique_id
Next, you need to set up the security, and again you'll use the ClassSecurityInfo class to set explicit permissions on the methods. For example:
security = ClassSecurityInfo()
security.declareProtected(view_permission, 'getContentTypes')
def getContentTypes(self):
...
Adding Some User Interface Elements
The main code is complete, so it'd be nice to show some response to the user when they click the tool in the ZMI, such as giving an example of how to use the product. For this, you'll alter the ZMI so that you can display something.
Specifically, you write a page template that does what you want it to do. In this example, this is a simple page template that hooks into the ZMI. The ZMI is an unsophisticated user interface that just spits back Web pages to the user, so no fancy macros or slots render the page. All you need to do is write a HTML and add the following:
<span tal:replace="structure here/manage_tabs" />
This one tal:replace function gets the management tabs and makes them appear at the top of the page. My ZMI page loops through the two methods of the plone_stats tool and spits out the results for the user, as shown in Listing 12-14.
Listing 12-14. A Page to Show in the Management Interface
<html>
<body>
<span tal:replace="structure here/manage_tabs" />
<p>Statistics for this Plone site.</p>
<h3>Content Types</h3>
<span tal:define="numbers here/getContentTypes">
<p>
Total count: <i tal:replace="numbers/total" /><br />
Content types by type:
</p>
<span tal:repeat="type python:numbers['bytype'].keys()">
<ul>
<li>
<span tal:replace="type" />:
<i tal:replace="python: numbers['bytype'][type]" />
</li>
</ul>
</span>
<p>Content types by state:</p>
<span tal:repeat="type python:numbers['bystate'].keys()">
<ul>
<li>
<span tal:replace="type" />:
<i tal:replace="python: numbers['bystate'][type]" />
</li>
</ul>
</span>
</span>
<h3>Users</h3>
<p>
User count: <i tal:replace="here/getUserCount" />
</p>
</body>
</html>
This is called output.pt and placed inside the www directory. You don't have to use a separate directory, but doing so makes it easier to remember.
The last step is to hook this up into the ZMI for your product. You do this by returning to the Stats class and adding the following (first import a PageTemplateFile class that can handle the template from the file system):
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
Then you register the page template as a method for the product that can be accessed. In the following, the method outputPage can now be called through the Web, and the matching page template is returned:
outputPage = PageTemplateFile('www/output.pt', globals())
security.declareProtected(view_permission, 'outputPage')
Finally, the tabs at the top of the ZMI are determined by a tuple called manage_options that maintains a list of all the tabs to be shown on a page. You need to insert the new management page in there, so you do the following:
manage_options = (
{'label':'output', 'action':'outputPage'},
) + SimpleItem.manage_options
Testing the Tool
Now that the tool is done, you can test that it works. First, restart your Plone instance so that it'll read the product directory in and register your new tool. Second, access the ZMI and go to the Add drop-down box in the top-right corner. You'll notice that PloneStats is now in the drop-down list, so select this option and click Add. The next form will list the tools available in the PloneStats product; in this case, just one appears, as shown in Figure 12-3.
Figure 12-3. Adding the tool
Select the tool, and click Add. To test that the tool works, click it. You should see a series of statistics, as you saw earlier on ZopeZen.
This tool is simple because I'm really not sure what presentation people want; if I make a standard reporting tool, then you can use it as you want. Some ideas that spring to mind are a page in the control panel, a little portlet box, a Portable Document Format (PDF) that contains pretty graphs and is e-mailed to the manager, or an API that's an external reporting tool, such as Crystal Reports could use. At this point, I'll wait and see what happens in the future.
Andy McKay: The Definitive Guide to Plone. Apress 2004
It was last updated by lallo on 2005-06-11 02:43 from the cvs source using
cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/plone-docs co PloneBook.


