Writing CMF content types using Python
This How-to applies to:
Any version.
This How-to is intended for:
Developers, Advanced Developers
CMF Type Creation in Python
Shows the creation of new CMF object types from scratch using Python. Useful if you want to know what's going on behind the scenes in Archetypes, or can't use Archetypes for some reason.
Summary
- Aims of the Tutorial
- Introduction
- Product Installation
- How does it work?
- Installation Method
- DTML Methods
- Conclusion
- Links
Aims of the Tutorial
The goal of this tutorial is to easily create new CMF object types, not through the ZClass intermediary, but by using Python directly in your favorite text editor.I'm assuming that you already know Python and CMF.
I wrote this tutorial while first using the CMF, by wanting to figure out how to write products in Python. To this end, I mainly studied the CMFCore and CMFDefault products and used the http://www.gnu.org/copyleft/fdl.html tutorial.
I hope that this will succeed in bringing missing documentation to this subject..... (notably in French!)
Don't hesitate to send me your comments via e-mail
Introduction
The CMF gives by default several different types of objects (news, links, etc...) by creating your site's container management. This is most of the time efficient and sufficient to create your portal, but it is often that you have the need to use an object type that is not already created. Fortunately, CMF allows us to create new type of objects, notably in Python
We are going to create here a new type of object: Tutorial
A Tutorial is similar to a file ... identified by its author, its contributors, its release date, its title and a short description. It can one of several types: Case Study, White Paper...
We're going to see how to create this product (though a CMF type is a product in ZOPE), how to initialize it in ZOPE and how to use it throughout a CMF site.
Product Installation
Before we see how to build the product, we're going to test it briefly.
For this, download the product archive here. Copy it into your $ZOPEPATH/directory, then uncompress it. Then go to the CMF site root where you want to use the new type and create an External Method with the following parameters:
Id: TutorialInstall
Title: (optional)
Module Name: TutorialInstall
Function Name: install
This method allows you to declare the new object type according to the types available in your CMF site; we'll get to that later.
After this method executes, you can add new objects of the Tutorial type in your CMF Folder.
The edit/modification methods are, as you will see, similar to some of the native CMF objects, and this tutorial creates a workflow mechanism.
How does it work?
Let's focus on the Python Product Source Code before seeing how to get to the result.Once the product is decompressed, you must put it within the directory structure:
$ZOPEPATH/lib/python/Products/CMFTutorial/ :
CMFTutorial.py
INSTALL.txt
LICENSE.txt
__init__.py
skins/
history.txt
versions.txt
As you may suspect, CMFTutorial.py contains the code of our product.
__init__.py contains the instructions that allow Zope to recognize this product when the server starts.
The skins folder contains the DTML methods necessary to display and edit our type.
At the same time, the TutorialInstall.py file adds the directory $ZOPEPATH/Extensions/
This Python script (used as an External Method) allows the automation of the recognition procedure of our type within our CMF instance.
CMFTutorial.py
This is the first file to create when you want to define a new product type, by having taken care to define the necessary attributes of this object (here: author, release date, etc...) and its actions (publication, visualization)
Like most Python scripts, this file begins with a series of imports.
ZOPE Imports:
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
import OFS.Image
Although the last line is specific to our product (import relating to the operations on files), the first two deal with a number of Zope products that call the product initialization methods and are the ones concerning security restrictions.
CMF Imports:
from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent
from Products.CMFCore.WorkflowCore import WorkflowAction
from Products.CMFCore import CMFCorePermissions
from Products.CMFDefault.File import File
The first two lines deal with relative CMF classes that the CMFTutorial is going to come from. The third item concerns the workflow of our object; the fourth is relative to the permission of the product's security. The last is there specifically for our product (file operations)
The first things to define in our new CMF type are the attributes and base actions, all as one defines the attributes and object methods in OOP before its implementation.
These are classified through a Python list named factory_type_information.
This one is defined as:
First of all, the attributes:
factory_type_information = (
{id:Tutorial,meta_type:CMFTutorial,description: (Tutorials are downloadable files with information about their release.),icon:tutorial_icon.gif,product:CMFTutorial,factory:addCMFTutorial,
'immediate_view':metadata_edit_form,
- id : The object type's unique ID. This is this that will be indicated to the user in the list of CMF object types in this Folder
- meta_type : the name of the product that tells the user see in Control_Panel / Products
- description : as its name indicates, the object's description as such is indicated to the user
- icon : the icon that corresponds to the product (optional). It must be put in the skins/CMFTutorial/ directory.
- product : The product that our type belongs to (i.e., the name of the directory)
- factory : the method that will create the object instance in our CMF site.
- immediate_view : the DTML method that will be called after the creation of our object.
You must be very careful about the values of these attributes. In fact, if the product doesn't correspond to the product where our new type is found (i.e., the name of the directory), our product will be recognized by Zope and CMF, but it will not be possible to create an instance of this type in our CMF site...
And the actions:
actions: (
{id:view,
name:View,
action:cmftutorial_view,
permissions: (CMFCorePermissions.View,),
},
{id:edit,
name:Edit,
action:cmftutorial_edit_form,
permissions: (CMFCorePermissions.ModifyPortalContent,),
},
{id:metadata,
name:Metadata,
action:metadata_edit_form,
permissions: (CMFCorePermissions.ModifyPortalContent,),
},
{id:download,
name:Download,
action: '',
permissions: (CMFCorePermissions.View,),
},
)
Our object is going to have three actions: View, Edit, and Metadata. For each action, you find:
- id : the action's unique internet reference
- name : its name (such that it will be attached to the management interface of our CMF object)
- action : the associated DTML method
- permissions : the necessary permissions to call this method
As far permissions are concerned, these are the attributes from the CMFCorePermissions class.
View allows for a method to be accessible to everyone and ModifyPortalContent uniquely to the people having the possibility to modify the content of the object (i.e., the content managers and the object's authors).
After these definitions, we pass the method to add a tutorial.
def addCMFTutorial(self,
id,
title='',
description='',
type='',
author='',
contribs='',
file='',
filetype='',
):""" Create an empty CMFTutorial """
tutorial_object = CMFTutorial(id, title, description, type, \
author, contribs, file, filetype)
self._setObject(id, tutorial_object)
This method creates a CMFTutorial instance and adds this instance to the ZODB(_setObject), i.e. in our CMF site. We will see in a moment what this method is called.
As we have already seen, this is the DTML Document metadata_edit_form that is called implicitly after the execution of this method.
Now we turn back to the CMFTutorial class. It's a Python class, which has a constructor, attributes and methods.
class CMFTutorial(DefaultDublinCoreImpl,
PortalContent,
OFS.Image.File,
File,
):
Our class has 4 parents here. You can choose what to inherit for your product from other classes according to your needs.
DefaultDublinCore allows us to have, among other things, a workflow for the object, so as well as to have attributes for our object given to them by native CMF objects (date of last modification, author, etc...)
PortalContent is also necessary for our object to be usable by the CMF.
The inheritence of OFS.Image.File and of File is necessary for the operations concerning the attached file.
The first attribute for our class is the meta_type, which is new...(qui est a nouveau indiqu�)
This is necessary so that the object instances of the created objects are listed in your folder after their creation.
Next we find the instruction:
ClassSecurityInfo().declareObjectPublic()
which allows us to declare the object as being public.
Then comes our class constructor:
def __init__(self,
id,
title='',
description='',
type='',
author='',
contribs='',
release='',
file='',
filetype='',
):""" Initialize an instance of the class """
## constructor parents
DefaultDublinCoreImpl.__init__(self, title, description = description)
## set info
self.id = id
self.type = type
self.author = author
self.contribs = contribs
self.release = release
self.filename = ''
self.filetype = filetype
Here we call the DefaultDublinCoreImpl constructor, which gives our instance all the properties of the base CMF objects (creation date, author of the object, etc...). The title of our tutorial and its description are stocked with attributes from DefaultDublinCoreImpl and are considered as the metadata of our object. In fact, all CMF objects have attributes like metadata, we get these for free, rather than redefining them separately. (nous en profitons donc, plutot que de les redéfinir séparement.)
Then we initialize our instance variables, which are this time specified by our product (type of tutorial, author, etc...)
Now that we have initialized the attributes, let's see how to modify them.
This is the edit method that will allow us to edit the object's properties.
def edit(self,
type='',
author='',
contribs='',
file='',
fileType=None,
):""" Edit the instance's properties """
## update info
self.type = type
self.author = author
self.contribs = contribs
## upload file + get info
if self._isNotEmpty(file):
self.manage_upload(file)
self.filename = file.filename
self.filetype = filetype or self._getType(file.headers)
This method only consists of updating the attributes of our instance.
As mentioned in the previous section, the first part manages general information of this one and the second loads itself with the one concerning the attached file.
Note that manage_upload() is inherited from OFS.Image.File and _isNotEmpty from File.
Notice that this publishing method doesn't take the title or the description of our Tutorial. In fact, as discussed above, these attributes are the metadata of the object, and we do not have to take care of their management, which is inherited from DefaultDublinCoreImpl (and makes itself, at the user interface level, by the Metadata action and not by Edit).
The instruction
edit = WorkflowAction(edit)
takes care of managing the workflow after publishing.
The following method, SearchableText, allows users to search for different instances of this object. It must resend the desired values that this search is possible (this comes back to indexing an object in the ZCatalog according to certain properties).
def SearchableText(text):
""" Method used by search engine """
return "%s %s %s %s %s" % (self.title, \
self.description, self.type, self.author, self.contribs)
The last method, our own new type, is the altered index_html. It is not always necessary to redefine this method; however, it does allow us to force the method call index_html from OFS.Image.File (i.e. the file download) and to specify that the file name to down download is the real file name (and not its id, which would be the case without defining the header). It verifies that the file exists, in order to create a Python exception at the server level, in the case where you're attempting download a non-existant file.
def index_html(self, REQUEST, RESPONSE):""" Force tutorial download with correct filename """
if self.filename != '': RESPONSE.setHeader(
Content-Disposition,attachement; \ filename="%s"%self.filename) return OFS.Image.File.index_html(self, REQUEST, RESPONSE)
I won't go back into the following method since it's a Python method that does not concern the creation of the CMF type as such, but nothing keeps you from looking at it, either. (rien ne vous y empeche de jeter un oeil, bien au contraire.)
We have thus created our product, but we still have to make our CMF instance and ZOPE recognize each other. And that's our next step.
__init__.py
We're going to see how Zope recognizes a product. If you have already worked on Zope products in Python, you have without doubt already had to make the __init__.py method.
We must first import our product from certain CMFCore classes
import CMFTutorial
from Products.CMFCore import utils, CMFCorePermissions, DirectoryView
import sys
Then we declare the class constructor of our class and the class itself
contentConstructors = (CMFTutorial.addCMFTutorial,) contentClasses = (CMFTutorial.CMFTutorial,)
It's this first line that is going to cause addCMFTutorial will be called during the creation of a new instance in our CMF object.
The last phase consists of the declaration of the Zope product, indicating to it its constructor and associated permissions. We also declare here the skin path, which contains the DTML methods.
z_bases = utils.initializeBasesPhase1(contentClasses, this_module)
# Make the skins available as DirectoryViews
registerDirectory(skins, globals())
registerDirectory(skins/CMFTutorial, globals())
def initialize(context):
utils.initializeBasesPhase2(z_bases, context)
utils.ContentInit(CMF CMFTutorial,
content_types = contentClasses,
permission = CMFCorePermissions.AddPortalContent, \
extra_constructors = contentConstructors,
fti = CMFTutorial.factory_type_information,
).initialize(context)
This is where you choose the name your product will have in the Zope drop down menu of types to add; here it's CMFTutorial.
Installation Method
The TutorialInstall.py is found in CMFTutorial/Extensions.
This method allows for the automation of the steps which make the CMF recognize our object, know how to add the object type to our CMF instance, the modification of the skins...steps that you already recognize if you have manually installed CMF products.
DTML Methods
Now we have a new object type usable within the CMF, recognized first by the Zope server and by the CMF site instance that we've been working on, and now we are going to see how to add and edit one in our portal.
At the beginning of this tutorial, we saw that our object had properties, actions, security of DTML. It is these DTML documents that handle the posting and publishing of each instance of our new CMF object.
The associated DTML documents must be found in the skins/Tutorial/ directory before they can be recognized by the __init__ and Installation methods (and by conforming to the code of native CMF products).
This skins directory contains 3 files:
cmftutorial_edit.py
cmftutorial_edit_form.dtml
cmftutorial_view.dtml
The DTML document metadata_edit_form is not present here, it is acquired from other CMF objects
I don't think that it is necessary to review the validation and publication DTML files, since they are fairly basic.
A few remarks:
- The use of
enctype="multipart/form-data" - The use of type validation in this same form, like the authorization of DTML.
input type="text" size="30" name="release:date" value="" /> - and a test for the existance of the release variable on the View page, in order to avoid a formatting error for a non-specific date.
Of course, these DTML methods are modifiable as desired at the appearance level.
The script called by cmftutorial_edit_form.dtml is cmftutorial_edit, as indicated in the form. Its first lines are the specific initialization lines of the Python scripts in Zope.
Script (Python) "cmftutorial_edit" ##bind container=container ##bind context=context ##bind namespace= ##bind script=script ##bind subpath=traverse_subpath ##parameters=REQUEST, RESPONSE, type, author, contribs, release, file='', filetype=''
This bit of code consists of a simple call to the edit method of the CMFTutorial class, followed by a redirection to the same publish page in case of an error or to the View page in case of success.
Conclusion
Voila, you are now ready to create new CMF object types, though basic, but that will probably respond to the majority of your needs.
If you wish to add other functionality, don't hesitate to look at the source of the native CMF products in $ZOPEPATH/lib/python/Products/CMFDefault.
Links
Created by apassant
Copyright (c) 2002 - Makina Corpus - http://www.makina-corpus.org
Document written by Alexandre Passant (mailto: apa@makina-corpus.org) under license GNU FDL (see LICENSE or http://www.gnu.org/copyleft/fdl.html).
Translated by Russell Hires 2/15/03
You have permission to copy, distribute, and/or modify this page in as long as this message appears.
- mgarnsey (Jul 8, 2003 7:10 pm; Comment #1)
> <P>At the same time, the TutorialInstall.py file adds the directory > <code>$ZOPEPATH/Extensions/</code></P>
An alternative approach for the installer would be to place the TutorialInstall.py file into an
Extensionssubdirectory under the CMFTutorial folder; along with changing theModule Nameparameter of the external method to "CMFTutorial.TutorialInstall". Why? then the root-level of the .gz package could beCMFTutorial, and the product would easily unpack into theProductsfolder as is the case with many other products.
broken link
The link on
as explained in the ZWACK Book - Chapter 5is broken.