Personal tools
You are here: Home Aiuti ed Info The Definitive Guide to Plone 6. Introducing Advanced Plone Templating and Scripting
To change the maximal image width select one of the following:

6. Introducing Advanced Plone Templating and Scripting

Document Actions

Chapter 6

The previous chapter covered how the Zope Page Templates system works. To understand page templates, Chapter 5 also covered the object hierarchy, acquisition, and Template Attribute Language Expression Syntax (TALES). Using the code from the previous chapter, you were able to generate dynamic Web pages. The chapter also showed an example page template that plugged the code together, covered the building blocks of the templating system in Plone, and provided the key information you'll need in order to use Plone.

It's now time to move onto some of the more advanced features of page templates and templating in Plone in general. First, I'll introduce the Macro Expansion Template Attribute Language (METAL) and Internationalization (I18N) namespaces. Like the TAL namespace, these provide functionality to the site developer. For those itching to know exactly how a Plone page is plugged together, the 'Hooking Into Plone Using METAL” section provides many of the answers.

Up until now I've shown how you can use simple Python expressions in page templates. Of course, sometimes a one-line Python expression isn't enough. So in the 'Scripting Plone with Python” section, I'll show you can take Python to the next level and increase the power of your scripting.

Finally, I'll cover a common example, showing how to put together a form in Plone. This example demonstrates concepts learned in the previous chapters and ties it all together while showing you exactly how Plone handles forms.

Understanding Advanced Plone Templating

One of the nice elements of page templates is that different functions are clearly separated into different namespaces. In the previous chapter, you looked at the TAL namespace. That's not the only namespace that page templates provide; two other namespaces are key to Plone.

The first is METAL. As the rather long name suggests, it's similar to TAL in that it's an attribute language and inserts itself into element attributes. However, its primary aim is to ensure that you can reuse chunks of other page template code. It does this using the slot and macro functions.

The second is I18N, which allows you to translate the content of page templates. This is used in Plone to localize the interface of Plone into more than 30 languages and for many users is one of the key features of Plone. As you'll see, the ability to localize text is of interest to all users, even those building a monolingual site. You'll start with METAL.

Hooking Into Plone Using METAL

So far you've seen how to use TAL to dynamically create parts of pages. However, this really doesn't let you do a great deal of complex templating. There really isn't a mechanism to put a standard header on top of every page, other than using a TAL statement. METAL is a method of allowing preprocessing of the templates and provides some more powerful functions than TAL. All METAL functions start with the metal: prefix.

metal:define-macro

The metal:define-macro command allows you to define an element to reference from another template. The name of the referenced chunk is the name of the macro. The following is an example that defines boxA as a piece you want to use elsewhere:

<div metal:define-macro="boxA">
    ...
</div>

That div element is now a macro that can be referenced from other templates. The macro refers only to the part of the page referenced by the element, which, in this case, is the div tag. So, it's common to use multiple macro:defines in one page and for the page to be a valid Hypertext Markup Language (HTML) page, like so:

<html xmlns:tal="http://xml.zope.org/namespaces/tal"
    xmlns:metal="http://xml.zope.org/namespaces/metal"
    i18n:domain="plone">
    <body>
        <div metal:define-macro="boxA">
            ...
        </div>
        <div metal:define-macro="boxB">
            ...
        </div>
    </body>
</html>

Corresponding with the earlier goals of page templates, this page is a valid HTML page that can be edited by a designer. When the macro is called, the HTML outside the div tags will be discarded.

metal:use-macro

The metal:use-macro command uses a macro that has been defined using the define-macro. When a template defines a macro using the define-macro command, it's accessible to other templates through a macros property. For example, if you want to pull the portlet macro out of the portlet_login template, you can do the following:

<div metal:use-macro="context/portlet_login/macros/portlet">
    The about slot will go here
</div>

This will fetch the macro and insert the result in its place. As shown, the use-macro command takes a path expression that points to the template and then to the specific macro in the template.

Example: Using the use-macro and define-macro Macros

As an example of this, the following is a template called time_template. This template shows the date and time on the current Plone server. This is quite a useful function to have, so you can wrap this in a macro to be reused. This is the example page template containing the define-macro:

<html>
    <body>
        <div metal:define-macro="time">
            <div tal:content="context/ZopeTime">
                 The time
            </div>
        </div>
    </body>
</html>

If your template is called time_template, then you can reference this macro in another template. You can now reference this macro in multiple templates. This is an example template:

<html>
    <body>
    <div metal:use-macro="context/time_template/macros/time">
      If there is a message then the macro will display i here.
    </div>
    </body>
</html>

When this template is rendered, the HTML produced by Plone looks like this:

<html>
    <body>
    <div>
       <div>2004/04/15 17:18:18.312 GMT-7</div>
    </div>
    </body>
</html>
metal:define-slot

A slot is a section of a macro that the template author expects to be overridden by another template. You could think of it as a hole in your page template that you're expecting something else to fill in. All define-slot commands must be contained within a define-macro. For example:

<div metal:define-macro="master">
  <div metal:define-slot="main">
  ...
  </div>
</div>
metal:fill-slot

This completes a slot that has been defined with the define-slot command. A fill-slot must be defined with a use-macro command. When the define- macro part is called, the macro will attempt fill in all the define slots with the appropriate fill-slots. Here's an example fill-slot:

<div metal:use-macro="master">
  <div metal:fill-slot="main">
    The main slot will go here
  </div>
</div>
Example: Using Macros and Slots

Returning to the previous example, you'll now enhance it a little. If you wanted to put a custom message at the beginning of the time, then you'd add a slot at the beginning of the time_template, inside the define-macro. The slot is called time and is as follows:

<html>
    <body>
        <div metal:define-macro="time">
            <div metal:define-slot="msg">Time slot</div>
            <div tal:content="context/ZopeTime">
                 The time
            </div>
        </div>
    </body>
</html>

Now, in the calling page template, you can call the fill-slot:

<html>
    <body>
    <div metal:use-macro="context/time_template/macros/time">
      <div metal:fill-slot="msg">The time is:</div>
      If there is a message then the macro will display i here.
    </div>
    </body>
</html>

The end result is that you'll see the time-slot filled in as follows:

<html>
    <body>
    <div>
       <div>The time is: </div>
       <div>2004/04/15 17:18:18.312 GMT-7</div>
    </div>
    </body>
</html>
How Plone Uses Macros and Slots

Both macros and slots are similar inasmuch as they both extract content from another template and insert content, but they do this differently. The difference comes in how they're used: Macros are elements of a template that are explicitly called, but slots are like holes in a template that you expect other templates to fill in for you. For example, in Plone's case, the portlets such as the calendar, navigation, and so on are macros that are explicitly called.

In fact, if in the Zope Management Interface (ZMI) you look at the file by clicking portal_skins, clicking plone_templates, and then clicking main_template, you'll see that the entire page consists of macros and slots. At this stage, it's probably a little confusing, but when called, it runs through a series of macros and pulls everything together. This allows a user to easily change any part of a Plone site by overriding that macro, as you'll see in the next chapter. For example:

...
<div metal:use-macro="here/global_siteactions/macros/site_actions">
    Site-wide actions (Contact, Sitemap, Help, Style Switcher etc)
</div>
 
<div metal:use-macro="here/global_searchbox/macros/quick_search">
    The quicksearch box, normally placed at the top right
</div>
...

Continuing to scroll down through main_template, you'll encounter some define slots. Briefly I'll recap how a page in Plone is rendered. When an object is shown, a template for that view of that content is shown. When you view an image, the template image_view is shown, and that template controls how the image is shown. To do this task, the image template fills the main slot. If you look in the image_view template, you'll see the following code defined in that template:

<div metal:fill-slot="main">
...
</div>

If you jump back to the main_template, you'll see that it contains a define-slot definition for the slot main:

<metal:bodytext metal:define-slot="main" tal:content="nothing">
    Page body text
</metal:bodytext>

Each type of content has a different template, and each template defines how they'll use the main slot differently. So, then each content type has its own particular look and feel from the templates. Just one element is missing in the equation. Somehow when you called image_view, the template knew it should use main_template. In the image_view template, you use the macro from the main_template. This is defined in the following HTML:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
    lang="en-US"
    metal:use-macro="here/main_template/macros/master"
    i18n:domain="plone">

In this case, the main_template has the main slot filled in by the slot defined as main in the template being rendered. The following is a timeline of how the page is built for viewing an image:

image_view main_template image_view main_template define-slot="main" fill-slot image_view fill-slot main_template main_template This allows Plone to be flexible in terms of how each page is defined. For example, main_template defines just more than that one slot; there's also a slot for inserting Cascading Style Sheets (CSS) code:

<metal:cssslot fill-slot="css_slot">
    <metal:cssslot define-slot="css_slot" />
</metal:cssslot>

If a view needed a custom set of CSS, you could define this slot in the view, and it'd be filled in when rendering. Some of the macros in main_template define slots in them as well and then fill them back in the main_template so that if you really wanted, you could also fill in those slots. That's an advanced technique, however, so you'll want to ensure you have the basics down before going down that road.

Introducing Internationalization

Plone is always striving to maintain a large number of high-quality translations. The fact that Plone provides an accessible user interface in more than 30 languages is a key selling point for Plone. This also means that I18N is a key feature of the templates. To facilitate this, the I18N namespace is an extra namespace such as TAL or METAL that has specific statements.

This section details what users need to know in regard to the templates. In a template you can add an i18n tag to an element that will allow the translation of an attribute or its contents. Six statements exist: attributes, data, domain, source, target, and translate. The basic pattern is to wrap the piece of text you want to translate and add the appropriate i18n attributes. For example, if you want to translate the following:

<i>Some text</i>

it would become like so:

<i i18n:translate="some_text_label">Some text</i>

Each localization provides a translation of Some text, and the translation tool looks up a translation for a user. When performing the translation, each string to be translated must have a unique message ID that identifies the item to be translated. For example, a string such as Search may have a message ID of search_widget_label. The message ID allows the string to be identified uniquely and the translation to be repeated.

i18n:translate

This translates the contents of an element, with an optional message ID passed as a statement. For example, the following will create a message ID of title_string:

<h1 i18n:translate="title_string">This is a title</h1>

This example is for a piece of text that's static and doesn't change. However, in some situations, the piece of text could be taken from a database or an object and is dynamic. By leaving the translate statement blank, the message ID is composed of the value in the field. In the following example, if the title returned by the path expression here/title was Alice in Wonderland, then that title would be passed to the translation tool. If no translation exists, the original value will be inserted:

<h1
    tal:content="here/title"
    i18n:translate="">
      This is a title.
</h1>

The translation command is probably one of most common i18n tags you'll use, and you'll see it throughout the Plone templates. It not only enables you to translate static parts of your site, such as form labels, help messages, and descriptions, but also the more dynamic parts of your site that could change more often, such as page titles.

i18n:domain

This sets the domain for the translation. To prevent conflict, each site can have multiple domains or groups for translations; for example, there may be one domain for Plone and one for your custom application. Plone uses the domain plone, which is usually the default domain in Plone:

<body i18n:domain="plone">

You shouldn't have to use this tag much; however, if you're writing a custom application, you may find it useful to have a domain that doesn't conflict with other domains.

Ii8n:source

This sets the source language for the text about to be translated. It isn't used in Plone:

<p i18n:source="en" i18n:translate="">Some text</p>
i18n:name

This provides a way of preserving elements in a larger block of text so that the block of text can be reordered. In many languages, not only are the words changed but also the order. If you have to translate a whole paragraph or sentence that contains smaller chunks that shouldn't be translated, then they can be passed through:

<p i18n:translate="book_message">
    The
    <span
       tal:omit-tag=""
       tal:content="book/color"
       i18n:name="age">Blue</span>
    Book
</p>

This will produce the following message string:

The {color} Book

If the target language required these to be in a different order, these could then be moved around and still have the dynamic content inserted into the correct place. In French, this would need to be translated as so:

Le Livre {color}
i18n:target

This sets the target language for the text about to be translated. It isn't used in Plone.

i18n:attributes

This allows the translation of attributes within an element, rather than the content. For example, an image tag has the alt attribute, which shows an alternate representation of the image:

<img
    href="/someimage.jpg"
    alt="Some text"
    i18n:attributes="alt alternate_image_label" />

Multiple attributes should be separated with a semicolon, just like tal:attributes work.

i18n:data

This provides a way of translating something other than strings. An example is a DateTime object. An i18n:data statement requires a matching i18n:translate statement so that a valid message ID is available. For example:

<span i18n:data="here/currentTime"
    i18n:translate="timefmt"
    i18n:name="time">2:32 pm</span>... beep!
Translation Service

Now that I've covered the tags, I'll cover the mechanism for performing the translation. By default Plone comes with an I18N mechanism. This allows you to internationalize the user interface so that messages, tabs, and forms can all be translated. At this stage, this doesn't cover the actual content that users add. If you add a document in English and view the page asking for it in French, you'll get the English document with French text around the outside (see Figure 6-1).

img/3294f0601.png

Figure 6-1. Plone.org in French

Plone reads the HTTP headers that a browser sends to the client requesting a language. If your browser is in English, then you won't see much.

To change the language settings in Internet Explorer, do the following:

Once you've done this, pick your favorite Plone site and visit it in your browser.

The translations for Plone are handled through a tool called Placeless Translation Service (PTS). You can locate the PTS tool in the Zope control panel; at the bottom of the page you'll see an option for Placeless Translation Service. Click this, and it'll open all the translations that exist. These translations are read in from the file system; click a translation to see the information about the language, such as the translator, the encoding, and the path to the file. All the files are actually stored in the i18n directory of the CMFPlone directory.

Translations are handled using two files for a translation, a .po and a .mo file. For example, plone-de.po contains the translations for German (de is the code for German). The .mo file is the 'compiled” version of the .po file and is used by Plone for performance. You never need to look at the .mo file, so you can just ignore it. The .po is the file you can edit to change a translation. If you open that file in a text editor, you'll see a series of lines starting with the text msgid or msgstr. Above the msgid is actually the code where the i18n command occurs, so you can see which bit of a page you're translating. For example:

#: from plone_forms/content_status_history.pt
#.   <input attributes="tabindex tabindex/next;" value="Apply"
class="context" name="workflow_action_submit" type="submit" />
#.
#: from plone_forms/personalize_form.pt
#.   <input attributes="tabindex tabindex/next;" tabindex=""
value="Apply" class="context" type="submit" />
#.
msgid "Apply"
msgstr "Anwenden"

In the two parts of the previous page templates, the word Apply will be translated into Anwenden for German users. What gets translated is determined by the i18n tags that have been inserted into the page templates, as you saw earlier. If you want to change that translation or add your own variation, then merely change the .po file. If no msgstr is found, then the default English translation is found. Once you've made that change, restart Plone. When this happens, Plone will recompile that file into the .mo version, and your translation will be updated.

For Plone, the default translation is always to use the English translation file if no language is given or no translation is available. In fact, the plone-en.po file is blank, so no translation will be available. Therefore, Plone does the final fallback, does no translation, and shows the text in the page template. The text in all page templates is in English since most developers speak English. The long and short of this is that there's no English translation.

Therefore, you can make a new translation by copying the plone.pot file into a new file of the name plone-xx.po. The value of xx should match the country code of your translation. You can find a list of language codes at http://www.unicode.org/onlinedat/languages.html. Once you've started the translation, set the values at the top, including the language code, and start translating away. If you've done a new language file, then the Plone I18N team will happily accept it and help you complete it. The Plone team mailing list is at http://sourceforge.net/mailarchive/forum.php?forum_id=11647

Translating the content that people add is actually quite a tricky task and something that Plone is working toward, but currently it hasn't completely ironed out. The favorite approach at the moment is to use two products, PloneLanguageTool and i18nLayer, both of which can be found on SourceForge (http://sf.net/projects/collective). However, both of these are for more experienced developers to fully understand and integrate; I hope something like this will be in the next release of the book.

Example: Displaying Multiple User Information

In Chapter 5 you used simple TAL commands to show a user's information in more detail. That template has a few drawbacks; one of them is that it shows only one user at a time. You've seen that a simple tal:repeat can enable you to repeat content, but you'll now use a macro to make this page more modular.

You'll change the user_info page template so that it lists every page member in the site. Instead of looking for a username being passed in the request, you'll use the function listMembers, which returns a list of every member on the site:

<div metal:fill-slot="main">
  <tal:block
   tal:define="
   getPortrait nocall: here/portal_membership/getPersonalPortrait;
   getFolder nocall: here/portal_membership/getHomeFolder
   ">
   <table>
     <tr tal:repeat="userObj here/portal_membership/listMembers">
         <metal:block
          metal:use-macro="here/user_section/macros/userSection" />
     </tr>
   </table>
 </tal:block>
</div>

You'll note that the code for user_info is now a great deal shorter. The member returned by listMembers is passed in to tal:repeat. For each member, there will be a table row and then a macro to show information to the user. In that table row, the locally defined variable userObj now contains the user information. Of course, you now need to make a macro called userSection in a page template, so you'll create a page template called user_section as referenced in the macro. This template contains all the code that was between the table's row tags. Again, you can find a full listing for this page template in Appendix B:

<div metal:define-macro="userSection"
     tal:define="userName userObj/getUserName">
     ...

The only real change is that the use-macro in the main template has to be removed and a new macro defined so that this macro can be defined. Because the username is no longer explicitly passed, you need to get the username from the user object by using the getUserName method. To test the resulting page, go to http://yoursite/user_info, and you should see a list of users.

The page now is user-friendly, showing multiple users on one page. The code is more modular, rendering the user's information in a separate macro that can be altered independently. This page is still not perfect but will be improved in later chapters.

Example: Creating a New Portlet with Google Ads

In Chapter 4 you saw how to easily edit portlets in a Plone site; adding your own portlet isn't much harder. To write your own slot, you need to make a new page template with a macro inside it. Then a TALES expression that points to macro will be added to the list of portlet, rendering the portlet to the page.

The basic template for a portlet is as follows:

<div metal:define-macro="portlet">
    <div class="portlet">
      <!-- Enter code here -->
    </div>
</div>

All you need to do is insert some suitable code into the portlet. Google set up a text-based advertising system in 2003 that places text on your site. The ads are based upon what Google thinks your site is about, based on the search results for your site. The Google system is available at http://www.google.com/adsense. To display ads (and get paid for them), you'll have to register with Google. On the Google Web site, it'll ask you to pick some colors and style. Since you'll put this in a slot, I recommend the 'skyscraper” size—tall and thin. Make a copy of the JavaScript that the site produces.

Next, you have to create a portlet:

portal_skins/custom googleAds googleBox <!-- Enter code here --> The end result should be something like Listing 6-1; however, your version will have a valid value for google_ad_client, rather than yourUniqueValue. That value tells Google which site ordered this ad and who to pay. Curiously enough, if you don't have a valid value there, Google will still happily show the ads but not pay you!

Listing 6-1. Displaying Ads from Google

<div metal:define-macro="portlet">
    <div class="portlet">
<script type="text/javascript"><!--
google_ad_client = "yourUniqueValue";
google_ad_width = 120;
google_ad_height = 600;
google_ad_format = "120x600_as";
//--></script>
<script type="text/javascript"
  src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
 
   </div>
</div>

To then include this on your site, as detailed in Chapter 4, add the following portlet to your list of portlets:

here/googleAds/macros/portlet

Scripting Plone with Python

At least four different levels in Plone exist for creating logic. The simplest level for using Python in Plone is the Python TALES expression I discussed in the previous chapter. However, a Python expression allows you to do only one line of code; often you'll want to do something more complicated.

Even more common is the problem that you really don't want to cram all the logic into the template. Placing logic in your template is a bad idea in general; any time you can move anything that isn't explicitly presentation logic out of the template, you've saved yourself a headache. Separating logic and presentation allows you to easily allow different people to work on different parts of the project, and it improves code reuse. The other layers of adding scripting Plone happen roughly in the following order:

  • Template attribute expressions: These provide expressions and a way of inserting little snippets of logic or simple paths in many places.
  • Script (Python) objects: These are simple scripts that execute in Plone in a restricted environment.
  • External method objects: These are more complicated modules that don't execute in restricted environments.
  • Python products: This is the key source that the CMF and Plone is written in; this offers access to everything in Plone. Python products are an advanced subject and are covered in Chapter 14.

After an expression, the next level of complexity is Script (Python) object. This object allows for multiple lines of Python code, and you can call it from an expression. When you call a Script (Python) object, you're incurring a small amount of extra overhead as Plone makes a switch into that object. However, that overhead is minimal because there's a trade-off between clarity, separation, and performance. My advice is to put as much logic into Python as possible and keep page templates as simple and as clean as possible. It's easy to move it back later if there's a performance hit, but at least you'll understand what's happening later.

Using Script (Python) Objects

A Script (Python) object is may you might traditionally think of in Plone as a script. It's a snippet of Python that you can write and then call from other templates or through the Web directly. Plone actually has a large number of these scripts for performing various key functions. A Script (Python) is halfway between an expression and an external method in terms of power.

To add a Script (Python) object, go to the ZMI, select Script (Python) from the drop-down menu, and click Add, as shown in Figure 6-2.

img/3294f0602.png

Figure 6-2. Adding in a Script (Python) object

Give the script an ID such as test_py and then click Add and Edit. This will open the edit page for the Script (Python) object, which looks like Figure 6-3.

img/3294f0603.png

Figure 6-3. Editing a Script (Python) object

You can edit the script directly through the Web. If you make a syntax error, you'll be told about it after you've clicked Save Changes, as shown in Figure 6-4.

img/3294f0604.png

Figure 6-4. A deliberate indentation error in the Script (Python) object

If your Script (Python) has no errors, you can click the Test tab to see what the output is. In this case, the sample is rather boring; it prints the following text:

This is the Script (Python) "test_py" in
http://gloin:8080/Plone/portal_skins/custom

A script also has the following options:

Title: The edit form has a Title option, which is for you to give the script a title. This will show up in the ZMI, so it'll be easier to remember what it does.

Parameter List: This is a list of parameters that the script takes, such as variableA or variableB=None. In fact, this is a standard list of parameters you'd expect in a standard Python function. Some parameters are already defined for you in this object, however; you can see them by clicking the Bindings tab. In that tab, you'll see a list of the variables already bound into the object, which should have familiar names by now.

The following are the variables bound to the script that are accessible from a Script (Python) object:

  • context: This is the object on which the script is being called.
  • container: This is the containing object for this script.
  • script: This is the Script (Python) object itself; the equivalent in Zope Page Templates is template.
  • namespace: This is for when this script is called from Document Template Markup Language (DTML), which is something that doesn't happen in Plone.
  • traverse_subpath: This is the Uniform Resource Locator (URL) path after the script's name, which is an advanced feature.

I'll now show a simple example that ties these topics into the Zope Page Templates system, using the example I gave of a Python expression in the previous chapter that adds two numbers. As you saw, you could make a page template for this that looks like the following:

<p>1 + 2 = <em tal:content="python: 1 + 2" /></p>

The equivalent using a Script (Python) object looks like the following. Change the test_py script to the following line:

return 1+2

As you saw at the beginning of the previous chapter, you call an object by giving its path as an expression. So, in a page template, you can now do the following:

<p>1 + 2 = <em tal:content="here/test_py" /></p>

The object test_py is acquired in the path expression and called, and it then returns the Python back to the template and prints. You've now called a script from your template! This is obviously a rather simple example, but my point is that there's a great deal you can do in a Script (Python) object that you just can't do in a page template.

In a Script (Python) object, you can specify the title, parameters, and bindings setting by using the ## notation at the top of a script. When you save a script with that bit of text at the top, Plone will remove that line and instead change the appropriate value on the object. This syntax is used a lot in the Script (Python) object in this book to ensure that you have the right title and parameters. So, you could rewrite the previous script as follows:

##title=Returns 1+2
##parameters=
return 1+2
Scripting Plone

Scripting Plone is a rather complicated subject because as soon as you're able to script Plone, you have to take into account the Application Programming Interface (API) of all the objects and tools you may want to use. Explaining APIs is beyond the scope of this book; instead, I'll demonstrate how to do some simple tasks using Script (Python) objects. Once you're comfortable with them, I'll describe more API-specific functions.

Page templates can loop through Python dictionaries and lists quite nicely. But often you don't have data in one of these convenient formats, so you need to jump into a Script (Python) object, format the data nicely, and then pass it back to the page template.

The most convenient data format is a list of dictionaries, which lets you combine the power of a tal:repeat and a path expression in one function. As an example, you'll see a function that takes a list of objects. Each of these objects is actually an object in a folder. For each of those objects, you'll see the object if it has been updated in the last five days. Listing 6-2 shows a useful little portlet I put together for a site that wanted to locate this type of information and then highlight exactly those items.

Listing 6-2. Returning Objects Up to Five Days Old

##title=recentlyChanged
##parameters=objects
from DateTime import DateTime
 
now = DateTime()
difference = 5 # as in 5 days
result = []
 
for object in objects:
  diff = now - object.bobobase_modification_time()
  if diff < difference:
    dct = {"object":object,"diff":int(diff)}
    result.append(dct)
 
return result

In this Script (Python) object I've introduced a couple of new concepts. First, you import Zope's DateTime module using the import function. The DateTime module, covered in Appendix C, is a module to provide access to dates. It's pretty simple, but if you make a new DateTime object with no parameters, then you'll get the current date and time; this is the now variable. When you subtract two DateTime objects, you'll get the number of days. You can compare that to the difference a user wants to monitor and, if it's longer, add it to the result list. The result of this is a list of dictionary objects, which looks like Listing 6-3.

Listing 6-3. The Result of Listing 6-2

[
  {
      'diff': 1,
      'object': <PloneFolder instance at 02C0C110>
  },
  {
l      'diff': 4,
      'object': <PloneFolder instance at 02FE3321>
  },
  ...

So now that you have the results in the right order, you need a page template that will pass in the list of objects and process the results. An example of this is as follows:

<ul>
  <li tal:repeat="updated python: context.updateScript(context.contentValues())">

This template has a tal:repeat call at the top that calls the script (in this case, called updateScript). Into that function it passes one value, a list of contentValues from the current context. Previously you called the Script (Python) object using a path expression; you could do that here as context/updateScript. However, you can't pass parameters through to the script being called in that syntax, so you make a Python expression instead, which is python: context.updateScript(). The contentValues function returns a list of all content objects in a folder. Next, look at the code for each iteration:

  <a href="#"
     tal:attributes="href updated/object/absolute_url"
     tal:content="updated/object/title_or_id">
     The title of the item</a>
  <em tal:content="updated/diff" /> days ago
  </li>
</ul>

As shown, you can loop through this list of values, and you can then use path expressions to access first the repeated value (updated), then the object (object), and then a method of that object (title_or_id). This is an example of taking complicated logic processing and passing it off to a Script (Python) object.

Restricted Python

I've mentioned several times that Script (Python) objects and Python TAL expressions all run in restricted Python mode. Restricted Python is an environment that has some functions removed. These functions may potentially be dangerous in a Web environment such as Plone. The original reasoning is that you may have untrusted (but authenticated) users writing Python on your site. If you open an account at one of the many free Web hosts for Zope, you'll find you can do this. However, if you have given people the right to do that, you don't want them to get access to certain things such as the file system.

In restricted Python, some common Python functions have been removed for security reasons—most notably, dir and open aren't available. This means that, as with Script (Python) objects, they can't be introspected, and access is limited to the file system. A few Python modules are available to the user. Most of these are for experienced developers; for more information, see the relevant documentation or module code:

If you want to import a module that isn't in the previous list, then you can find excellent instructions in the PythonScript module. You'll find them at Zope/lib/python/Products/PythonScripts/module_access_examples.py. However, a more simple method is available to you—using an external method.

Using External Method Objects

An external method is a Python module written on the file system and then accessed in Plone. Because it's written on the file system, it doesn't run in restricted Python mode, and therefore it conforms to the standard Plone security settings.

This means you can write a script that does anything you want and then call it from a page template. Common tasks include opening and closing files, accessing other processes or executables, and performing tasks in Plone or Zope that you simply can't perform in any other way. For obvious reasons, when you're writing a script that can do this, you need to be sure you aren't doing anything dangerous, such as reading the password file to your server or deleting a file you don't want to delete.

To add an external method, you go to the file system of your instance home and find the Extensions directory. In that directory, add a new Python script file; for example, Figure 6-5 shows that I added test.py to a directory on my Windows computer.

img/3294f0605.png

Figure 6-5. A new external method, test.py

You can now open test.py and edit it to your heart's content, writing any Python code you want. The only catch is that you must have an entry function that takes at least one argument, self. This argument is the external method object in Plone that you'll be adding shortly. The following is an example entry function that reads the README.txt file out of the same Extensions directory and spits it back to the user (you'll have to change the path to point to your file):

def readFile(self):
   fh = open(r'c:\Program Files\Plone\Data\Extensions\README.txt', 'rb')
   data = fh.read()
   return data

Now that you've done that, you need to map an external method to this script. This is a Zope object, so return to the ZMI, click portal_skins,* and then click custom*. Finally, select External Method from the Add New Items drop-down list. When you add an external method, you need to give the name of the module (without the .py) and the entry function, so in this case the add form looks like Figure 6-6.

img/3294f0606.png

Figure 6-6. The newly added external method

After clicking Save Changes, you can hit the Test tab to see what happens when it runs. In this case, you should get a line or two of text. Since you have the External Method module in Plone, you can access it from a page template in the same way as any other object. A path expression to here/test_external would do the trick in this case. For example:

<h1>README.txt</h1>
<p tal:content="here/test_external" />

The real power is that you can pass code off to the unrestricted Python mode and from there to any function you want, without having to worry about security. Although this may seem like a cool function, external methods aren't used a great deal in Plone because complicated logic is usually moved into a Product object, and simple logic is kept in a Script (Python) object. If you find yourself using External Method objects a lot, consider one of the tools discussed in Chapter 12.

Useful Tips

Because page templates are valid Extensible Markup Language (XML) and can be used independently of Zope or Plone, you have several useful scripts for cleaning up page template code and performing syntax checks. These are additional tools and checks; Zope actually performs all the necessary checks when you upload a page template. For a project such as Plone, it can be useful to run automatic checks on your code or verify it locally before committing changes.

To run these checks, you'll need to be able to edit these tools locally and have Python installed on your computer. For more information on External Editor, a method for editing remote code locally, see Chapter 10.

Introducing XML Namespaces

Page templates use XML namespaces to generate code. Programmers can use the rules of XML namespaces to make life easier. At the top of a page template, you'll see a declaration of the namespace in the starting tag:

<html xmlns="http://www.w3.org/1999/xhtml"...

This sets the default namespace to Extensible HTML (XHTML). For any containing element, if no namespace is defined, it uses that default namespace. For example, you know the next element is XHTML because it has no prefix:

<body>

Normally for TAL and METAL elements and attributes, you have been adding the prefix tal: and metal: to define the namespace. The following code is something that should be familiar by now:

<span tal:omit-tag="" tal:content="python: 1+2" />

This will render 3. However, the following is an alternative:

<tal:number content="python: 1+2" />

By using the tal: prefix on the element, you've defined the default namespace for this whole element as tal. If no other prefix is given, the tal namespace is used. In the example, using span tags, the default namespace is XHTML, so you have to specifically define the tal: prefix when using the Content tab.

Note that the element name is descriptive and can be anything not already defined the tal namespace (for example, content or replace). Because tal:number isn't a valid XHTML element, the actual tag won't display, but the content will—making the omit-tag unnecessary. This technique is used a lot in Plone to make code that's smaller, simpler to debug, and more semantic.

Introducing Tidying Code

HTML Tidy is an excellent tool for testing and cleaning up HTML code that can perform a few useful tasks. Versions of HTML Tidy exist for all operating systems; you can download it from http://tidy.sourceforge.net. For Windows users, find the appropriate download for your version of Windows, unzip the tidy.zip file, and place the tidy.exe in your PATH (usually your Windows directory, such as C:up WINNT).

HTML Tidy can tell you if there are any XHTML errors in your page template. For these purposes, one flag can make a difference: -xml. This tells HTML Tidy to process the file as XML and report any XML errors. Given the example 'bad” template shown in Listing 6-4, you can see a few errors. Not only is the code not indented, but it's missing closing elements and has invalid nesting.

Listing 6-4. An Example Broken Page Template: bad_template.pt

<!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></title>
</head>
<body>
<p>
<div>
This is bad HTML,
XHTML or XML...<a tal:contents="string: someUrl"></a>
</p>
<img>
Further it isnt indented!
</body>
</html>

If you run Listing 6-4 through HTML Tidy, you'll see the errors in the template and get nicely indented code, as shown in Listing 6-5.

Listing 6-5. The Output from HTML Tidy

$ tidy -q -i bad_template.pt
line 11 column 1 - Warning: <img> element not empty or not closed
line 10 column 1 - Warning: missing </div>
line 10 column 39 - Warning: <a> proprietary attribute "tal:contents"
line 11 column 1 - Warning: <img> lacks "alt" attribute
line 11 column 1 - Warning: <img> lacks "src" attribute
line 9 column 1 - Warning: trimming empty <p>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta name="generator" content=
  "HTML Tidy for Linux/x86 (vers 1st August 2003), see http://www.w3.org" />
 
  <title></title>
</head>
 
<body>
  <div>
    This is bad HTML, XHTML or XML...<a tal:contents=
    "string: someUrl"></a> <img />Further it isnt indented!
  </div>
</body>
</html>

The complaints about proprietary attributes can be a little annoying. To check that your page template is valid XML, pass the -xml flag. The output is less verbose and just points out the missing tags:

$ tidy -q -xml bad_template.pt
line 15 column 1 - Error: unexpected </body> in <img>
line 16 column 1 - Error: unexpected </html> in <img>

Conducting Syntax Checks

When you edit a page template in the ZMI, Zope performs a syntax check on the document for things such as invalid tags. If a tag is invalid, an error will be shown on the template while you're editing it through the Web. If, like me (and as I demonstrate in Chapter 7), you write most of your page templates on the file system, then a simple syntax check for a page template is really useful. Listing 6-6 is a Python script that resides on your file system and runs independently from Zope.

To run this, you must have a Python interpreter, and the Python module PageTemplate must be importable. To make PageTemplate importable to your Python interpreter, you must add the Products directory of your Zope installation to your Python path. You have several ways to do this (covered in Appendix B).

Listing 6-6. Error Checking Page Templates

#!/usr/bin/python
from Products.PageTemplates.PageTemplate import PageTemplate
import sys
 
def test(file):
    raw_data = open(file, 'r').read()
    pt = PageTemplate()
    pt.write(raw_data)
    if pt._v_errors:
        print "*** Error in:", file
        for error in pt._v_errors[1:]:
            print error
 
if __name__=='__main__':
    if len(sys.argv) < 2:
        print "python check.py file [files...]"
        sys.exit(1)
    else:
        for arg in sys.argv[1:]:
            test(arg)

For every file passed through to the script, the ZMI will compile the page template and see if there are any TAL errors. Taking the bad_template.pt file from Listing 6-4, you'll get an error:

$ python zpt.py /tmp/bad_template.pt
*** Error in: /tmp/bad_template.pt
TAL.TALDefs.TALError: bad TAL attribute: 'contents', at line 10, column 39

In this case, it has picked up on the incorrect spelling of tal:content as tal:contents. This error is something HTML Tidy doesn't catch. Unfortunately, the processing stops at the first syntax error. If there are multiple errors, only the first is picked up, meaning sometimes you have to check the syntax several times.

Using Forms

Forms are an integral part of any site, and almost everyone needs to create a method for creating and altering forms in your Plone site. With the form framework in Plone, you can change the validation that process forms have, where they take the user to, and so on. This framework isn't just specifically designed for stand-alone forms that perform a simple task, such as request a password, login, and so on. The framework also works for all content types for tasks such as editing a content type, which I'll cover later in this book in Chapters 11–13.

All basic forms have at least two components that you've already seen so far: a Page Template object to show the form to the user, and a Script (Python) object to parse the results and perform some action on the results.

The form controller framework in Plone introduces a few new object types that are equivalent to the types you've seen in this chapter. These are the Controller Page Template object, the Controller Script (Python) object, and the Controller Validator object. These new objects have their equivalent objects, as shown in Table 6-1. These new objects have more properties and act in slightly different ways than the equivalent objects.

Table 6-1. New Object Types That the Controller Provides

Object Type Equivalent Zope Object
Controller Filesystem Page Template Page Template
Controller Python Script Python Script
Controller Validator Python Script

To add one of these objects using the ZMI, go to the drop-down box, and select the name.

The form controller framework creates a sequence of events for a form that a user can then define. The following is the sequence of events when executing a form:

When this sequence of events occurs, a state object is passed around, which contains information about the status of the object, the success of any validations, and any message that are to be passed.

The following sections run through these steps to show how a form can be validated, and then I'll show a full example in the 'E-Mail Example: Sending E-Mail to a Webmaster” section.

Creating a Sample Form and Associated Scripts

The beginning of this process is a form. Although this is actually a Controller Page Template object, it's written using standard TAL code. To add one, select Controller Page Template from the now-familiar drop-down box and give it an ID of test_cpt.

A form in Plone is actually a rather lengthy piece of code if you want to utilize all the options available to you. This piece of code is reproduced in full in Appendix B and is the code used in the later example:

<form method="post"
      tal:define="errors options/state/getErrors"
      tal:attributes="action template/id;">
    ...
    <input type="hidden" name="form.submitted" value="1" />
</form>

Looking at this code, you should note that to work in the framework, a few minor differences exist between this and what you may consider a standard form. First, the form is set up to submit to itself; this isn't *optional. Second, a special hidden variable exists called *form.submitted.

The Controller Page Template object checks the request variable for the value form.submitted to see if the form has been submitted. If, instead, it has just been accessed—for example, via a link—this isn't *optional. At the beginning of the form, you set the variable errors. The *errors dictionary comes from the state object that's passed into the templates. The state object is a common object to all the templates and scripts in this system.

Creating Validators

Once the user clicks the Submit button on your form, the data will be run through the validators and be validated. Validators are optional. Data doesn't need to be validated, but of course any application should do that as appropriate. The Validator tab for a Controller Page Template object gives you a link to the possible validators.

A validation script is the same as a normal Script (Python) object that has one extra variable, state. The state variable is how you can pass results of the validation. Listing 6-7 shows a simple validation script for checking to see if you've been given a number.

Listing 6-7. Validating That a Number Has Been Provided

##title=A validation script to check we have a number
##parameters=
num = context.REQUEST.get('num', None)
try:
    int(num)
except ValueError:
    state.setError("num", "Not a number", new_status="failure")
except TypeError:
    state.setError("num", "No number given.", new_status="failure")
if state.getErrors():
    state.set(portal_status_message="Please correct the errors.")
return state

This state object contains basic information about what has happened during the validation chain. The state object stores the errors for each field, the status, and any other values. For example, if the number given can't be turned into an integer, you set the status to failure and give an error message for the field using the setError method. Later this error message will be shown for the field. At the end of the script, any errors returned so far are retrieved via the getErrors method.

To add the previous script, click portal_skins, click custom, and select Controller Validator from the drop-down box. Give it an ID of test_validator. You can now return to the Validation tab of your Controller Page Template object and add a pointer to this validation script, as shown in Figure 6-7.

img/3294f0607.png

Figure 6-7. Adding the test_validator to the Controller Page Template object

You have a couple of choices for a validation. In the example I've ignored them since they aren't relevant, but the following is a list of the options:

contextType: This is the type of the context object, if any, that template is executed in. This is a shortcut to the content type of the context object. If you wanted only this validation to occur on a link, then you could set this value to Link.

button: This is the button, if any, that's clicked to submit the form. You could have different buttons on a form (for example, a Submit and a Cancel button). Each of these buttons could then map to a different action; clicking Cancel would take you to one place, and clicking Submit would take you to another.

validators: This is a comma-separated list of validators, which are Controller Validator objects that the template will acquire. In the previous example, you used the validator ID of test_validator.

NOTE When writing validation scripts, use Controller Validator objects instead of Script (Python) objects. Controller Validator objects are just like ordinary Script (Python) objects with the addition of a ZMI Actions tab.

Specifying Actions

Actions are the ending actions after the validators have been run, and they depend upon the status that's returned by the validators. The Actions tab for a Controller Page Template object shows all the actions for the page template in question. You can specify actions with the same kind of specialization options as described previously via a Web form, as shown in Figure 6-8.

img/3294f0608.png

Figure 6-8. Adding an action

You have the following four choices for the actual resulting action:

redirect_to: This redirects to the URL specified in the argument (a TALES expression). The URL can be either absolute or relative.

redirect_to_action: This redirect to the action specified in the argument (a TALES expression) for the current content object (for example, string:view). At this stage I haven't covered actions yet, but each content object has actions such as view and edit. Chapter 11 covers actions for an object.

traverse_to: This traverses to the URL specified in the argument (a TALES expression). The URL can be either absolute or be relative.

traverse_to_action: This traverses to the action specified in the argument (a TALES expression) for the current content object (for example, string:view).

One example of this is if the completion of the form is a success, you traverse to a Controller Python Script object that you've written that processes the result of the form. If the page is a failure, you traverse back to the template and show them the error.

The difference between a redirect and a traversal is that the redirect is an HTTP redirect sent to the user's browser. The browser processes it and then sends the user off to the next page. Thus, the redirect actions lose all the values passed in the original request. If you need to examine the contents of the original form, then this isn't the best approach. Instead, I recommend the traversal to options. The result is the same; it's just that the traverse option does this all on the server. Doing this preserves the request variables and allows you to examine this in scripts.

E-Mail Example: Sending E-Mail to the Webmaster

You'll now see a real example and spend the rest of this chapter building it. A common requirement is a custom form that sends e-mail to the Webmaster. You'll build this type of form in the following sections. The complete scripts, page template, and assorted code are available in Appendix B. If you really don't want to type all this in, you can see this example online at the book's Web site; it's also downloadable as a compressed file from the Plone book Web site (http://plone-book.agmweb.ca) and the Apress Web site (http://www.apress.com), so you can just install it and try it. This example has just two fields in the form: the e-mail of the person submitting the form and some comments from that person. For this form, the e-mail of the person will be required so you can respond to their comments.

Building the Form

The form is the largest and most complicated part of this procedure, mostly because there's so much work that has to be done to support error handling. This form is a Controller Page Template object called feedbackForm. To ensure that it's wrapped in the main template, I'll start the form in the standard method:

<html
    xmlns="http://www.w3.org/1999/xhtml"
    xml:lang="en-US"
    lang="en-US"
    i18n:domain="plone"
    metal:use-macro="here/main_template/macros/master">
  <body>
    <div metal:fill-slot="main"
         tal:define="errors options/state/getErrors;">

One addition here is errors options/state/getErrors, which will place any and all errors into the errors local variable for later use.

Because of the requirement for the form to post back to itself, you set this action in TAL, with the expression template/id. This path will pull out the ID of the template and insert it into the action, so this path will always work, even if you rename the template. Note that you're also adding the i18n tags you saw earlier to ensure that this form can be localized:

<form method="post"
      tal:attributes="action template/id;">
 
<legend i18n:translate="legend_feedback_form">
    Website Feedback
</legend>

The following is the start of the row for the e-mail address. You'll define a variable here called error_email_address that's set to an error string if there's a suitable string in the errors dictionaries. That error value will be generated by the validator should there be an error:

<div class="field"
     tal:attributes="class python:test(error_email_address,
                                       'field error', 'field')">
     tal:define="error_email_address errors/email_address|nothing;">

The following is the label for the e-mail address field. In this label you'll include a div for the help text. The span element will become the now-familiar red dot next to the label so that the user knows it's required:

<label i18n:translate="label_email_address">Your email address</label>
<span class="fieldRequired" title="Required">(Required)</span>
<div class="formHelp"
     i18n:translate="label_email_address_help">
     Enter your email address.
</div>

Next you'll add the actual element:

    <div tal:condition="error_email_address">
        <tal:block i18n:translate=""
                   content="error_email_address">Error
        </tal:block>
    </div>
    <input type="text" name="email_address"
           tal:attributes="tabindex tabindex/next;
                           value request/email_address|nothing" />
</div>

At the top of this block, you test to see if there's an error. If there is, the class for the element changed to be the field error* class; this class will show a nice orange box around the field. Next, if an error has occurred for this field (as you've already tested for), the corresponding message will be displayed. Finally, you'll show the form element, and if there's a value for *email_address already in the request, you'll populate the form element with that value.

The tabindex is a useful tool in Plone. It contains a sequential number that's incremented for each element, and each time it sets a new HTML tabindex value for each element in a form. This is a nice user interface feature; it means each form element can be safely moved around without having to worry about remembering the tabindex numbers because that'll happen automatically.

That's a lot of work for one element, but it's mostly boilerplate code; you can easily copy or change it. You can find the remainder of the form in Appendix B.

Creating a Validator

In the example you have only one required element (the e-mail), so it's a simple piece of Python called validEmail.vpy that does the work. The contents of this script are as follows:

email = context.REQUEST.get('email_address', None)
if not email:
    state.setError('email_address', 'Email is required',
                    new_status='failure')
if state.getErrors():
    state.set(portal_status_message='Please correct the errors.')
return state

If no e-mail address can be found, this script adds an error to the dictionary of errors with the key of email_address and a message. This key is used in the page template to see if an error occurred on that particular field.

Processing the Script

This example has a simple e-mail script that gets the values (which are already validated) and forms an e-mail out of them. This is a Controller Python Script object; it's just like a standard Script (Python) object except that it has a state variable, and, like the Controller Page Template, you can give it actions for when it succeeds:

mhost = context.MailHost
emailAddress = context.REQUEST.get('email_address')
administratorEmailAddress = context.email_from_address
comments = context.REQUEST.get('comments')
 
# the message format, %s will be filled in from data
message = """
From: %s
To: %s
Subject: Website Feedback
 
%s
URL: %s """
 
# format the message
message = message % (
    emailAddress,
    administratorEmailAddress,
    comments,
    context.absolute_url())
 
mhost.send(message)

You've now seen a simple script for sending e-mail. This is a common script that you'll see again and again. Basically, the MailHost object in Plone will take an e-mail as a string, as long as it conforms to the Request for Comment (RFC) specification for e-mail that has From and To addresses.

In this e-mail, you take the administrator address you specified in the portal setup and send the e-mail to that person. The only extra part in this script is the addition of setting the state. This will set a message that provides some feedback to the user:

screenMsg = "Comments sent, thank you."
state.setKwargs( {'portal_status_message':screenMsg} )
return state
Binding the Three Parts Together

At the moment, however, three separate entities exist: a form, a validator, and an action script. These need to be tied together to form the chain, so you'll return to the Controller Template object. Click the Validator tab, and enter a new validator that points to the validEmail script. You'll also add a success action if the processing is correct to traverse to the sendEmail script (Expression: 'string:sendEmail')n the sendEmail script, you can now add another traversal back to feedbackForm (Expression: 'string:feedBackForm') so that after sendEmail happens correctly, the user will be sent back to the original page.

NOTE A much more complete e-mail validation script appears in Plone called validate_emailaddr, which checks that the e-mail is in the right format. If you want to use this script instead, you can point the validator to this script.

That's it you're done! You should now be able to test the form on the book's Web site. To make it even easier, I made a Feedback tab, which points to the feedbackForm template, and from there you can now give feedback to me about this book!


Andy McKay: The Definitive Guide to Plone. Apress 2004
This online version was generated using the 'PloneBook' product from docs.neuroinf.de/products.
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.

Powered by Plone CMS, the Open Source Content Management System

This site conforms to the following standards: