LocCMFProduct HowTo |
Created by rthaden . Last modified 2003-11-20 01:03:27. |
HowTo build a CMF product which contains text and metadata in multiple languages with Localizer. |
This HowTo explains how to build a file system based product with several This howto was created in Aug. 2002, so there are many changes in the CMF until then. Also Plone came up and Archetypes, so this may be a bit outdated. But you still can learn something here. Some parts of the sourcecode wereinspired or directly used from CMFTutorial
by Alexandre Passant (apa@makina-corpus.org) and from PortalContentFolder by
Florent Guillaume (fg@nuxeo.com)
The product can be found here Contents
Some history: I was in the need of building a new website for a department
of the university where I am working. First I thought about the different items
needed. The central point are the data of the employees (staff, students, secretary,
etc.). Then there are research areas and projects categorized by these areas.
Each project and area has a contact person. Additionally there are diploma theses
and news items. For each of these parts a class is built. Our website is presented
in german and english so there must be a solution to have the content multilingual.
I will only present a simple version with two classes First we need some base classes from CMFCore and CMFDefault to get the basic CMF functionality: PortalContent to let our objects play with the types tool and CMF UI.
It subclasses CMFCatalogAware so our objects are catalogged and workflow aware. We want to have a multilingual website so we need a tool to make the attributes of our class multilingual. The Localizer product provides this. Unfortunately the DefaultDublinCoreImpl which provides title, description etc. is not multilingual. We will build a new class overriding these attributes by subclassing LocalPropertyManager and DefaultDublinCoreImpl. The new class is called LocDefaultDublinCoreImpl. Further we need some utility functions which can be used in PageTemplates and
Python scripts. We will put them in a Now the details: __version__ = "0.1.1"Provide information for the types tool. This information is used in __init__.py to tell the types tool which actions are supported, which string is displayed in the add new contents UI etc.
factory_type_information = (
{
Construct one object of the class and add it to the database. This method is outside
the class because it's called before the instance of the class is constructed.
It provides some default values for attributes, which are set in the __init__
method of the class. It is called automatically after construction.
def addEmployee(self,
id,
Firstname=
localizer_languages provides 2 languages, so right now the languages are set to fixed values. This can be changed to accept any languages that are set in the Localizer instance. I'll leave this to anyone with time and will to do it. Now we define the Employee class: When we're subclassing the order is important. The way Python looks
up attributes in the base classes that are not in the top class is from left
to right and from the top to the bottom. Let's say we want to access the Another important thing is the If we use PortalFolder instead of SkinnedFolder then we lose the ability of
being catalogged because the methods indexObject, reindexObject and unindexObject
are overridden in PortalFolder. They do nothing (pass) so PortalFolders can
not be catalogged or being subjected to a workflow since CMF1.3. If we derived
from PortalContent before PortalFolder then the indexing methods would behave
as desired, because they are found first in CMFCatalogAware which is a base
class of PortalContent. But the drawback is that a part of the folderish behaviour
is lost because the objectIds, objectItems and objectValues are overridden in
SimpleItem which PortalContent is derived from to return an empty tuple, so
the contents of our folderish object are not displayed. We could override these
methods with the defaults or just use SkinnedFolder which is catalogged and
derive from it before deriving from PortalContent. Another important attribute
is So finally our class definition looks like this: class Employee(LocDefaultDublinCoreImpl, SkinnedFolder, PortalContent):Since CMF1.3 we have to deliver the portal_type. CMF versions before 1.3 used to set the portal_type to the meta_type automatically
portal_type = meta_type =
Get an instance of ClassSecurityInfo to tell Zope, which parts of our class are
public or private, which means: are the parts accessible by through-the-web code
and which permissions are needed to access them.
security = ClassSecurityInfo()
security.declareObjectPublic()
One of our properties is multilingual. It is wrapped by the LocalProperty class.
Inside LocalProperty a dictionary is used like'en':'english text' 'de':'german text' and a call to this property will return the parameter in the language determined by one of the mechanisms Localizer uses (e.g. cookie: LOCALIZER_LANGUAGE= en or
path http://localhost/en/obj which adresses the english version of
http://localhost/obj if the Localizer resides in the root)
Shorttext = LocalProperty(
The __init__ method is called after constructing the object to initialize it with
some default values (which are defined in the addEmployee method above)
def __init__(self,
id,
Firstname='',
Lastname='',
eMail='',
Shorttext='',
localizer_languages=''
):
iterate through localizer_languages and1. add the language to the underlying LocalPropertyManager 2. assign the default values
for language in localizer_languages:
self.manage_addLanguage(language)
self._setLocalPropValue(
The _edit method changes the attributes of this class. It is not callable by
TTW code because it's declared private. Below is an edit method, which is
made public and calls _edit The arguments passed here are None by default. So
it is possible to pass e.g. only one of the arguments. Inside the method it is
checked if an argument is passed before the value is changed.
security.declarePrivate(
define an edit method, which is accessible by TTW-code but protected by
the Modify Portal Content permission
security.declareProtected( CMFCorePermissions.ModifyPortalContent,
SearchableText returns some property values to the portal_catalog to allow a text search inside these properties. If we want to let multilingual properties to be searched we have to pass a value for each language. LocalPropertyManager allows to access a property in a defined language by adding the language code with an underscore as seen below
def SearchableText(self):
""" Method used by search engine """
return "%s %s %s %s %s" \
%( self.id,
self.title_de,
self.title_en,
self.Firstname,
self.Lastname,
)
O.K. that was the first class. In the product there's a second class __version__ = "0.1.0"loc_utils.py contains some useful Python methods. We collect them in a single file instead of placing dozens of small Python scripts in the skins directory. These methods must be registered in the security machinery to be callable by TTW-code. We use setDefaultAccess( allow) to let all methods be callable.
ModuleSecurityInfo(We need to register the construction methods and the classes:
contentConstructors = (Employee.addEmployee,
Project.addProject,
)
contentClasses = (Employee.Employee,
Project.Project,
)
Register the skins directory:
DirectoryView.registerDirectory(We need to combine the factory type information of each class here:
factory_type_information = ( Employee.factory_type_information
+ Project.factory_type_information
)
def initialize(context):
utils.initializeBasesPhase2(z_bases, context)
utils.ContentInit(
We have to add a language selector in our pages. We will do this by modifying the main_template.
So we copy main_template.pt from
<p id="Breadcrumbs" style="padding-top: 5px">
<span tal:repeat="bc here/breadcrumbs"
><a href="."
tal:attributes="href bc/url" tal:content="bc/id"
>ID</a><span tal:condition="not: repeat/bc/end"> / </span>
</span>
</p>
with
<table id="Breadcrumbs" style="padding-top: 5px" width="100%">
<tr>
<td>
<span tal:repeat="bc here/breadcrumbs">
<a href="."
tal:attributes="href bc/url" tal:content="bc/id">ID
</a>
<span tal:condition="not: repeat/bc/end"> / </span>
</span>
</td>
<td align="right">
<tal:block define="Localizer nocall:root/Localizer"
content="structure Localizer/changeLanguageForm" />
</td>
</tr>
</table>
Now we need to implement a view and edit interface. We place two files called Employee_view.pt and Employee_edit_form.pt in a directory /skins/LocCMFProduct inside our product directory. First we edit the view for our class in Employee_view.pt: Define the namespaces for tal and metal and use the
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="here/main_template/macros/master">
We set the base in the response to our page. If we don't do this the base will
be differ if we call http://localhost:8080/folder/emp1/Employee_view or http://localhost:8080/folder/emp1.
In the latter case, the base is http:// ... /folder, so e.g. images inside our
folderish class will not be referenced correctly.
<metal:block fill-slot="base">
<base href=""
tal:attributes="href python: here.absolute_url() + '/'">
</metal:block>
We fill the header slot with nothing
<div metal:fill-slot="header"> </div><!-- header slot -->In the main slot we import our module with the utility Python methods and reference to an image called img. We can add an image with the id img to our folderish
object later. Additionally we place an image in the folder containing the Employee
instances, so that it will be loaded as default if no image is inside an Employee
instance.
<div metal:fill-slot="main">
<!-- import loc_utils-->
<div tal:define="loc_utils python:modules['Products.LocCMFProduct.loc_utils']">
<table cellpadding="2" cellspacing="10">
<tr>
<td valign="top"> <img src="#" width="200"
tal:attributes="src string:img"> </td>
<td valign="top">
<h1>
<span tal:replace="here/Firstname">Firstname</span>
<span tal:replace="here/Lastname">Lastname</span>
</h1>
<div id="DesktopDescription">
<span tal:replace="here/description"> Description </span>
</div>
<br>
<table width="100%" border="0" cellspacing="2" cellpadding="2">
<td>e-Mail:</td>
<td> <a href="#" tal:attributes="href string: mailto:${here/eMail}"
tal:content="here/eMail">pxe@spaninq.com</a>
</td>
</tr>
</table>
<hr>
The multilingual property Shorttext is displayed in the language set in the changeLanguageForm
which sets a cookie called LOCALIZER_LANGUAGE
<p tal:condition="exists:here/Shorttext">
<span tal:replace="here/Shorttext">
Shorttext, e.g. NOBODY expects the spanish inquisition&
lt;/span>
</p>
Next we define the Employee_edit_form:
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="here/main_template/macros/master">
The form will post its data to a method called Employee_edit, which is described below.
<form action="Employee_edit" method="post"
tal:attributes="action string:${here/absolute_url}/Employee_edit">
<table class="FormLayout">
We need to pass the language in an argument called localizer_language which represents
the contents of the cookie LOCALIZER_LANGUAGE which is set by the changeLanguageForm
<input type="hidden" name="localizer_language" value="de"
tal:attributes="value request/cookies/LOCALIZER_LANGUAGE">
Adding a language selector to the main_template
Here comes the edit method called Employee_edit.py in our skins/LocCMFProduct directory. We edit the
instance and metadata in a single form and call both edit methods.
## Script (Python) "Employee_edit"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=REQUEST, RESPONSE, title=None, Firstname=None, Lastname=None,
eMail=None, Shorttext=None, description=None, subject=None,
localizer_language=None
##title=
##
Provide a default value for the title and description. This is overridden in every
edit action so the user has no means of changing them via the edit form. If we
left this out here, the values could be edited via the edit form.
desc_string={'':''}
desc_string['de']=
Now I'll describe the LocDefaultDublinCoreImpl class: __version__ = "0.1"We override the attributes we want to have multilingual so that they are wrapped by the LocalPropertyManager:
title = LocalProperty(
The init method gets an additional argument localizer_language. The attributes are set via
_editMetadata, which we also need to override.
The methods returning the DublinCore elements which are multilangual must be overridden. The language argument is set to None by default, so a call e.g. to title without a language (the original Title method accepts no argument) will return the Title in the language set as default in Localizer.
security.declarePublic(
The methods setting the multilingual contents must also be overridden:
security.declareProtected( CMFCorePermissions.ModifyPortalContent
,
We override _editMetadata to accept the localizer_language argument:
security.declarePrivate(
We need a little install script, which registers our classes and skins to the types and skins tools: Create a directory
Id : LocCMFProductInstall
Title :
Module name : LocCMFProduct.Install
Function Name : install
and click the Test tab.
O.K. we're almost through with it. Only the configuration of the Localizer
is left. Install the Localizer product and add an instance of Localizer in the
root of your Zope installation. Go to the Languages tab and set the languages
to |