Thursday, November 18, 2010

How to replace colours with transparency with the Gimp

It sometimes happens you have an image which has been made partially transparent to be shown on a red background. You'd like to use this image on a blue background, but that looks very bad. What to do?

Here's how to change all occurences of red, including semi-transparents, half-tones and shades, to blue. You'll keep your bright/dark nuances.

  • Open the image with the Gimp.

  • Set mode to RGB: Image > Mode > RGB

  • Select the part where you want to change the color. Normally, you'd use the select-similar-colors tool. Point it to the offending (red) color.

  • Desaturate the selection: Colors > Desaturate > Luminosity. Your selection will turn grey, all shades of it (assuming your images had shades of the sames color, why else would you be doing this?).

  • Change the grey to alpha: Colors > Color to alpha. At the prompt box, use the color picker and point it to the same place you pointed to when creating the selection.

  • Add a new layer (Windows > Dockable dialogs > layers). Make its background color the target color (blue).

  • Invert the selection (Ctrl-i) and delete it (Del), so the new layer is transparent except on the places where you wanted to change the color.

  • Move this layer below the rest.

  • Save it, you're done.

Wednesday, November 3, 2010

Looking back on Bristol


  • Flying KLM instead of some DTA (Duck Tape Airlines)

  • Our apartment in Marsh St. from Alderman Apartments

  • Pub lunches and beer

  • Many many many interesting Plone talks

  • Richard Noble's guest keynote

  • Dinner with the guys from 4D

  • Many many many more interesting Plone talks

  • A run along the river with my colleague Peter

  • Pies, beer, cider and the music from Fellow Stranger

  • The Irish pub and the band

  • Many interesting Open Spaces

  • Sprinting on xdv, excuse me, Diazo documentation

  • The spin-off: putting all these great ideas to work!

IE8 bug causing first list item to be too high

I had an unordered list where the first list item was too high in IE8. At first i thought it was one of the many javascripts, then i thought it was something to do with IE8's rendering of lists, but when i stripped the HTML it turned out it was an anchor link on the top of the page.

It's a known bug ( The referenced site states no workaround, but not using a self-closing tag seems like a good workaround me, for example:

<a name="top"></a>

Sunday, October 3, 2010

Review: Plone 3 Intranets by Víctor Fernández de Alba

Plone has great potential for intranets, so it's a good thing there's a book dedicated to this application. PacktPub have a whole line of Plone books, and this is one of the latest on the family.

In this review, i'll try to give an idea of what the book is like. I also have a wishlist, I hope that the book does this:

* expand on the features which are required by an intranet;
* compare Plone to some of its rival systems in the context of these features;
* show how Plone can be used for an intranet: what settings to use, what add-on products to install;

There are some areas where your reviewer has taken the liberty to add a note of his own.
Plone 4 Intranets?

Reviewer note:

When building an intranet from scratch now, i'd recommend doing it in Plone 4.0 (or 4.1, even): Plone 4 is faster and has a prettier user interface, to name but a few advantages.

Fortunately, almost all of this book can be applied to Plone 4 as well. Notable exceptions are the add-on products mentioned, some of which haven't been updated to Plone 4 yet.

Introduction to Plone (ch. 1)

The book starts by introducing Plone, and discussing its place in the CMS market. Python and the ZODB are briefly introduced, as well as the Plone community.

Key intranet features are:

* Usability
* Security
* Collaboration tools
* Productivity
* Content life cycle

It is briefly sketched how Plone implements these features.

Reviewer note:

The comparison with other systems could be extended.

This chapter also points out that the difference between an intranet and a public website, which has traditionally been very pronounced, is fading.

The "Generic Plone" chapters

There are several chapters for introducing people to Plone. While these are indispensible for the beginning Plone users, i won't discuss them in depth here:

Getting started (ch. 2)
About installing Plone.
Managing our content (ch. 3)
Covers basic Plone content management. More advanced features are postponed (see chapters 8 and 11).
Configuring our site (ch. 4)
Covers the configuration options for power users.
Basic Product Development (ch. 10)
A brief introduction.
Theming our Intranet (ch. 12)
A brief introduction.

Managing Users, Groups, Roles and Permissions (ch. 5)

Managing users in an intranet might be different than in a situation where Plone is used as a Web CMS, so a chapter like this has my special attention.

In an intranet, there's a good chance that you have many users logged in simultaneously. The book says this about that:

"Dealing with more than fifty users increases the administration overhead exponentially (...), if we are going to administer more than fifty intranet users, then we might have to start thinking about using an external user repository database."

In practice, most intranets already have an authentication server (Active Directory, OpenLDAP) which you can use (discussed further in chapter 13), so it's not that big an issue.

Managing workflows (ch. 6)

Explains how default Plone workflows work. For an intranet, this is also a particularly interesting feature. This chapter has graphs of the standard Plone workflows (the most interesting being, of course, the intranet workflow). It also shows how you can use add-on products to help manage your workflow settings, and adds a "Best practises" list.

Securing our intranet (ch. 7)

This chapter discusses why you might want to use global roles or local roles, and how to implement both cases. It shows an example policy, and shows how that might be implemented. It touches lightly on the subject of creating private sections and workgroup areas (although this could use some more explanation).

Using Content Type effectively (ch. 8)

Using the right content type for the job. Also expands on Collections, the Table Of Content of a Page, Next/Previous navigation in a Folder, Presentation Mode, slide formatting. Choosing third-party content types.

Note: This chapter is available as a preview at

Intranet add-on products (ch. 9)

Reviewer note:

I'm missing the part where their differences are discussed. When would you prefer one add-on over another one?

The book discusses products in these categories:
Calendaring and extended Events

Plone4ArtistsCalendar and vs.event.

Reviewer note:

I myself recommend using Plone4Artists to beginners, as i've only heard hard luck stories about it.

Plone 4.1 will ship with, which is based on the work from vs.event and greatly improves Plone's calendaring capabilities.

Form generators

Discusses PloneFormGen.

Reviewer note:

PloneFormGen is a rock-solid product: Well maintained, and it's been updated to work on Plone 4 as well.


Quills (Products.QuillsEnabled), Scrawl, PloneBoard (and how to adjust it to intranet use).
Polls and surveys

Discusses PlonePoPoll, PloneSurvey.
Document files management

Quoting the book:

"Eventually, our intranet will contain a lot of files."

Reviewer note:

If this is the case, i recommend using Plone 4, which uses stores individual files on the filesystem instead of putting them all in the database file. This makes for faster startup, less memory usage, better scalability. (You can also get blobstorage to work in Plone 3.)

When people upload binary files, you can still search them using Plone's excellent search facility. Add-on products to do this are discussed.

Content Rules, Syndication, and Advanced Features (ch. 11)

Discusses these features which may be especially useful in an intranet. That's especially true of Versioning, which is also discussed here.

Versioning is especially powerful because you can see a line-by-line difference between two versions of a Page.

Reviewer note:

Storing documents in a binary format like Word, OpenOffice or PDF will make Versioning a lot less useful: All you can see is who uploaded what, and when. Storing documents as Pages will give you the full revision history. (This suggestion may not be feasible in all situations, for example when you want easy offline modification.)

Deploying our intranet (ch. 13)

There are lot of buildout tips and tricks here, quite useful. It also contains a link a prominent Plone developer's web log entry about the subject.

The thing that makes our intranet different from any other huge web site, is that we want to use external authentication. This chapter also shows how to install and configure


I had hoped for a more in-depth comparison with other systems. That said, this book does a good job at pointing out Plone's strengths in an intranet context, being:

* In-place editing (as opposed to "back end" interface)
* Hierarchical structure
* Workflow
* Fine-grained authorization control (Permissions system)
* Security (Python - Zope)
* Integration with existing authentication solutions (PAS)

The book successfully shows what to consider when setting up an intranet, and how it may be implemented.


I have made a point point of not reading others' reviews beforehand. Having done so now, i think i too should mention i've received a free e-book from PacktPub, and am likely to receive another title of my choice.

Wednesday, September 8, 2010

Adding a list of items to a content type without CompoundField

With all the Plone 4 goodness that surrounds us, you'd almost forget that there still are Plone 3 sites which need attention and extra features.

Our client wanted "be able to add a list of images to item X", for display in a slideshow on the item's view. I first thought of the CompoundField add-on for Archetypes content, which I'd used before. But because of technical difficulties with the current Archetypes release, I went looking for another way.

I found it in Plone's Collections feature: We add a reference to a Collection (code below), and let editors decide what images they want to show there. Advantages:
* It's easier to change a lot of images
* It allows for re-use of images
* It allows for dynamic content (if you add a criterion for a certain tag, for example, all new content with that tag will be "on item X" as well)
* Last but not least: It's a well-known, well supported Plone feature.

The last point is important: This approach fits the philosophy of letting standard Plone functionality take care of things, avoiding custom content as much as possible, or keeping them as minimal as possible when you do use them.

There might also be a disadvantage: it's less intuitive. But everybody should know about Collections, they're central to Plone and they can help you (and your customer) in many ways.

custom_content_type_schema = Schema ((
label="Extra images for slideshow",
description="Point to a Collection that contains Images",

Thursday, September 2, 2010

Creating an egg? Clean first!

I ran into an error on a production site which turned out to be due to an egg containing extra files. How did these old files get included in the egg? I'd been running python bdist_egg to create the egg, and then i uploaded them to a www server. (Not a "real" egg server.)

Before creating a new egg, the build/ directory is not cleaned by default. Therefore, files that were present in an earlier build but have now been removed from the package will still be included in the egg.

Running python clean remedies this.

Tuesday, August 24, 2010

Using fakemail during development

Update: this is far easier: python -m smtpd -n -c DebuggingServer localhost:1025

After a suggestion from my colleague Wietze, i decided to give fakemail a try. Fakemail will act as a mailserver, in that it accepts emails. It saves them to file, however. This eliminates the need for a real mailserver (be it on your local machine or via internet) during development.

To separate it from my default python installation, i create a virtualenv:

$ cd ~
$ virtualenv --no-site-packages venv_fakemail
$ . ./venv_fakemail/bin/activate
(venv_fakemail)$ cd /tmp

Go to, save file in ~/Downloads/.

(venv_fakemail)$ tar -xzf ~/Downloads/fakemail-python-1.0beta.tar.gz
(venv_fakemail)$ cd fakemail-python-1.0beta/
(venv_fakemail)$ python install
(venv_fakemail)$ deactivate
$ ~/venv_fakemail/bin/ --host=localhost --port=10025 --path=/tmp
Listening on port 10025

Now make your program (Plone site) connect to localhost:10025 and send mail:

Incoming mail
Capturing mail to
Mail to saved
Incoming mail dispatched

The e-mail will be saved as /tmp/

easy_installing fakemail-python from pypi didn't work, although it's on pypi.

More info:

Wednesday, July 7, 2010

Adding CSS that uses settings from content

Use case

A client wants to display a section of their site in a slightly different style. This different style currently consists of adding a background image, which should be changed by content editors who have no technical knowledge of CSS, and who we don't want to bother with things like the ZMI.


The object at the top of the section is a custom content type. We can add a background_image attribute to that, so content editors can change it. But can we get that into a CSS file?

Attempt 1: a skin template CSS file

First off, i tried to add a skin template to generate the CSS. This worked beautifully when calling section_object/my_skin_template.css. Unfortunately, CSS that is included by Plone's portal_css tool has the portal root as context, so my lookup of the background_image attribute never found anything.

Solution: A CSS viewlet

Adding a viewlet to the plone.htmlhead.links viewlet manager solved my problem.



This will render the viewlet in the <head> tag, so we can generate an internal style sheet here.


class SectionCSS(ViewletBase):

def getBackgroundImageUrl(self):
return_value = None
bg_image = self.context.getBackground_image()
return_value = bg_image.absolute_url()
return return_value

def render(self):
return_string = ''
image_url = self.getBackgroundImageUrl()
if image_url:
return_string = "<style type='text/css'>body { background-image: url('%s'); } </style>" % image_url
return return_string

What's next

There's much room for improvement here: The try/except will have to go, for example. I'm also curious what other people have done to solve this problem.

Saturday, July 3, 2010

iPython in Plone 4

[edit (2010-07-30): add link to full iPython extension script which doesn't require login]

The Plone 3 Products Development Cookbook shows a way to make your Plone 3 buildout create a Zope-aware iPython script. With a slight modification, the script can be used for Plone 4 as well.

parts +=

# an IPython Shell for interactive use with zope running.
# You also need to put
# in your $HOME/.ipython directory for the following to work.
# You may have to remove an existing if you 
# get errors when running ./bin/ipzope, check the stack trace for that 
# filename to make sure the correct version is used. 
recipe = zc.recipe.egg
eggs =
initialization =
    import sys, os
    os.environ["INSTANCE_HOME"] = "${instance:location}"
    sys.argv[1:1] = "-p zope".split()
scripts = ipython=ipzope

The modification makes the script check if a SOFTWARE_HOME environment variable existst. If not, it assumes we have an egg-based Zope (2.12 or higher). The full script is available from SVN.

The script still works with older Zope versions (Plone 3), so i think the branch can be merged. Note that in a Plone 3 buildout, you'd also have to define extra-paths = ${zope2:location}/lib/python in the [ipzope] part.

Thursday, July 1, 2010

Find my commits SVN script

This script finds all log messages for commits that you've done in (or below) the current SVN-controlled directory.

# Find all my commits _ever_ in this level of the repository (and below).

repo_url=`svn info | sed -n 's/^URL: *//p'`
# Multiple usernames can be given, separated by \|

for revision in `svn log -q $repo_url | sed -n "s/r\([0-9]*\) | \($username\) |.*/\1/p"`
svn log -vr $revision $repo_url

Thursday, June 24, 2010

Daily SVN report script

How can i get svn log information for my own commits? How can i see what i committed on a specific day? How do i list all svn repositories i'm working on?

# A script to extract SVN log info for all repositories you're working on,
# yielding only your own commits, on a particular day.
# The script does this:
# * find unique repository roots you're working on
# * get the revision numbers for your own commits in each repo
# * print the full log for each of these revisions
# Syntax: [DATE1] [DATE2]
# DATE1 is an optional argument in the format yyyy-mm-dd. If DATE1 is not
# present, today is assumed. DATE2 is an optional end date, the default is
# DATE1.
# As this may take a long time, you may want to write the output to a
# file: 2>&1 | tee -a /tmp/svn_daily_report.txt
# * Allow passing usernames and directories from the command line

# Set your SVN username. You can set multiple SVN usernames (handy if the
# same person has a different username for different repositories) by
# delimiting them with \|.

if [ ! -n "$1" ]
# No date set; use today
date1=`date +%F`
if [ ! -n "$2" ]
# No end date set; using start date
echo "Looking for $username's repository changes from $date1 00:00 to $date2 23:59"

# Find unique repository roots below current working directory
for repo in `find . -name .svn -type d | sed 's/\(.*\).svn$/\1/' | xargs svn info | sed -n 's/^Repository Root: *//p' | sort | uniq`;
echo "Repository root: $repo"
# Find all svn revision numbers made in this timespan by this svn user
for revision in `svn log -qr {"$date1 00:00"}:{"$date2 23:59"} $repo | sed -n "s/r\([0-9]*\) | \($username\) |.*/\1/p"`
# Print log info
svn log -vr $revision $repo

Monday, June 21, 2010

VirtualBox kernel module loading script

When starting VirtualBox (on Ubuntu Lucid), i have to load the kernel modules vboxdrv and vboxnetflt. I created a script to do this automatically. Call it virtualbox and place it somewhere on your $PATH, for example in ~/bin/, so the application launcher in the menu will use the script instead of the binary in /usr/bin/.


# Custom VirtualBox OSE startup script; loads kernel modules

echo "Starting kernel module loading script";
for module in vboxdrv vboxnetflt
echo "Testing kernel module $module";
if [ `lsmod | grep $module | wc -l` == '0' ]; then
echo "Module $module not yet installed, installing... "
gksudo modprobe $module
echo "done."
echo "Module $module already installed; skipping"

Thursday, June 17, 2010

SVN: How to get the changes for one revision

I was looking for a shortcut to svn diff -r N-1:N. Turns out it's -c:
svn diff -c N

Tuesday, June 15, 2010

Using an ssh tunnel to manage your production site

Update (2010-10-20): Script to shorten the command, use hostname instead of IP

Probably, your production servers run behind Apache or some other webserver stack. To manage these portals, ZMI access can be handy, but the Zope port is not exposed to the outside world. You could solve this with an Apache proxy or rewrite rule, but I find it easier to start an SSH tunnel.

Suppose your Zope runs on port 8080, on server on, where you have an account for username.

ssh -L 8765: -l username -N

This will make your Zope accessible locally on port 8765. Here you can add Plone instance, look into their custom skins folders, and do whatever else you need to do.

For detailed info on ssh -L, see the manual pages.

You can also use a hostname instead of the IP address.

And if you get tired of typing this long command, you might want to use this script:

# Shortcut to set up an SSH tunnel with concise syntax.
# Usage: username hostname portnumber
# This assumes you want set op a tunnel to a host that you know by its hostname.
# To keep it simple, the remote port number will also be used locally.

# TODO: test parameters present
# TODO: allow for optional LOCAL_PORT

# TODO: allow for optional LOCAL_PORT
# TODO: allow for any hostname, also if not defined in /etc/hosts

# Show what we're going to do
# Do it

Tuesday, May 4, 2010

Showing members' email addresses to other members

This skins folder script allows members to see other members' email addresses. The has_role() does a check to make sure not all users are allowed to do this.
## Script (Python) "getEmailById"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
from Products.CMFCore.utils import getToolByName
mtool = getToolByName(context, 'portal_membership')
plone_portal_state = context.restrictedTraverse('@@plone_portal_state')

email = 'unauthorized'
current_member = plone_portal_state.member()
if current_member.has_role(
author = mtool.getMemberById(author_id)
email = author.getProperty('email')
return email

In order to call getMemberById you have to have the Manage users permission, which i don't want to give out to all members. Therefore the script is run with a proxy role for Manager, as specified in
proxy = Manager

The script is called from TAL in a customized author.cpt, which has this extra code:
            <div tal:condition="
python: authorinfo['has_email'] and member.has_role(
<a tal:define="
email python: context.getEmailById(author_id=author);
tal:attributes="href string:mailto:${email}"
[e-mail address]

Sunday, May 2, 2010

Plone 3 on Ubuntu Lucid Lynx: Hand-compiling Python 2.4

update 2010-05-31: Warning for /usr/local/bin/python

After the upgrade to Ubuntu 10.04 (Lucid Lynx), i had no python2.4 binary anymore (causing a bash: ./bin/instance: /usr/bin/python2.4: bad interpreter: No such file or directory error), so i had to compile it myself. Hand-compiling is a big word here, because it was very, very easy:
tar -xzf Python-2.4.6.tgz
cd Python-2.4.6/
sudo make install

A typical case of CMMI (configure, make, make install)! Note: You may want to consider installing Python 2.4 in /opt, see the warning below.

(After this, you'll have to install PIL again, using easy_install which you'll have to install as well, see the post at

Note that python2.4 now lives in /usr/local/bin/ instead of /usr/bin/, so you'll also have to run bootstrap and buildout again on existing buildouts.

Warning: This will also add a symlink called python to your /usr/local/bin/, which may take precedence over your system python (2.6). This is a problem for Ubuntu's, which uses #!/bin/env python. This will result in a an import error:
$ system-config-printer
Traceback (most recent call last):
File "/usr/share/system-config-printer/", line 30, in
from timedops import *
File "/usr/share/system-config-printer/", line 20, in
import gobject
ImportError: No module named gobject

I think it's best to change the header line of that script to /usr/bin/python as recommended in the Launchpad issue, and additionally remove the python symlink in /usr/local/bin/. Maybe it's better to install Python 2.4 in /opt to avoid this kind of conflict.

Wednesday, April 14, 2010

Change creator programmatically

In order to change an object's Creator from a script or another piece of code, use setCreators(new_creator,). Mark the comma: apparently, the method takes a list as input.

I used this in a view class which mass-changes ownership:
    def changeOwnership(self, obj, userid):
""" Change ownership of obj to userid """
membership = getToolByName(self.context, 'portal_membership')
user = membership.getMemberById(userid)
roles = list(obj.get_local_roles_for_userid(userid))
if 'Owner' not in roles:
obj.manage_setLocalRoles(userid, roles)

I had a look at the internals of to find out what i had to do in order to make this work.

Friday, April 9, 2010

LaTeX: Including .sty files from the command line

$ export $TEXINPUTS='.:/home/kees/custom_latex_templates/:'
$ latex /path/to/file_that_uses_custom_latex_template.tex

Friday, April 2, 2010

Adding a ZCTextIndex from

Alternative title: Indexing an object's creator's full name on Plone 4 with a dexterity content type.

Adding a catalog index in profiles/default/catalog.xml is deprecated; see

I had some trouble getting this to work with a ZCTextIndex. I have a content type for which i want to be able to make the creator's full username searchable. In order to do this, i had to create a ZCTextIndex, as a simple FieldIndex didn't seem to work.

I got it to work by replacing the loop inside for name, meta_type in wanted: with this:
   if name not in indexes:
if meta_type == 'ZCTextIndex':
class Empty: pass
title_extras = Empty()
#title_extras.doc_attr = 'index attribute?'
title_extras.index_type = 'Okapi BM25 Rank'
title_extras.lexicon_id = 'plone_lexicon'
catalog.addIndex(name, meta_type, title_extras)
catalog.addIndex(name, meta_type)
indexables.append(name)"Added %s for field %s.", meta_type, name)

I'm using a dexterity-based content-type, the code contains:
def makerFullnameIndexer(obj):
creator = obj.Creator()
fullname = creator
membership = getToolByName(obj, 'portal_membership')
member = membership.getMemberById(creator)
if member:
fullname = '%s %s' % (member.getProperty('firstname'),
return fullname
grok.global_adapter(makerFullnameIndexer, name="MakerFullname")

Saturday, March 27, 2010


Some notes from the "WFNN" (Whisky Festival Noord Nederland, der Aa-Kerk, Groningen):

  • Have coffee. Note that other visitors appear to be generally over 40 and male. Make note to skip WFNN in favour of beach volleyball tournament next year.

  • First taste: a Linkwood 12 yrs. (Douglas Laing).

  • Compared a standard Glenlivet (12 years) with an 18 years old one. The 18 years old is fuller.

  • A Clynelish: fresh, salty. Worthy of a recommendation.

  • Had a taste of Ben Riachs: Rum cask vs. Madeira cask.

  • Compared a Highland Park with a MacAllen. Highland Park won, probably because the MacAllen's more subtle tones were lost on my diminishing senses.

  • Compared Dalmore 12 yr. with 15 yr. Both a bit too sweet for me at that time.

  • Finally, a Lagavulin. Superb, as always. (Barman made us eat a mint first?!)

  • Resisted urge to pay 15 euros for a glass of *extremely* rare Lagavulin.

  • Laphroiag tastes a bit thin after that, but still a great taste.

  • Failed to act upon recommendation of rare Aberlour by ex-colleague. ("You want the bottle that's almost empty.")

  • Compared Ardbeg 10 years old with an Ardbeg Corryvreckan. (Corryvreckan won.)

  • 23:00: Closing time. Down to the pub for a two shilling ale evaluation and a final round of beers.

Thursday, March 25, 2010

Manipulating PDF files

A customer wanted a product that adds a watermark or stamp to a PDF document. My colleague Kim Chee found out that stamping and watermarking can easily be done with pdftk. In our product, we call the OS's pdftk to manipulate the pdf files.

To generate a dynamic stamp or watermark which includes the username and time, we use reportlab.

One hurdle was to read the PDF file (in order to write that to a temporary file on the filesystem). The PDF files in our product are contained in a custom content type, in an array field of files. In order to correctly read the PDF from a field, i had to call str(field).

Below is some code from our product's This file and the code framework was generated by ArchGenXML, where we created a workflow transition script.

##code-section module-header #fill in your manual code here
from DateTime import DateTime
import os, sys
from Products.CMFCore.utils import getToolByName
import tempfile
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import cm
##/code-section module-header


def approveDocument(obj, event):
    """generated workflow subscriber."""
    # do only change the code section inside this function.
    if not event.transition \
       or not in ['approve'] \
       or obj != event.object:
    ##code-section approveDocument #fill in your manual code here

    # create stamp file
    tmp = tempfile.mkdtemp()
    stamppath = "%s/stamp.pdf" % tmp
    canvas = Canvas(stamppath)
    canvas.setFillColorRGB(1,0.75,0, alpha=0.75)
    user_id = event.status.get('actor')
    mtool = getToolByName(obj, 'portal_membership')
    member = mtool.getMemberById(user_id)
    fullname = member.getProperty('fullname')
    now = DateTime()
    message = "Approved by %s [%s] on %s" % (fullname, user_id,
            now.strftime('%Y/%m/%d %H:%M') )
    canvas.drawString(1*cm, 1*cm, message)

    # iterate over documents
    fields = obj.getField('documents').Schema().fields()
    file_fields = [field for field in fields if field.type == 'file']
    for i, field in enumerate(file_fields):
        # check if document is a pdf
        content_type = field.getContentType(obj)
        if content_type != 'application/pdf':
            obj.plone_log("File in field %s is not a PDF, skipping." % field)

        # get raw file
        document = obj.getDocuments()[i]
        assert document.filename == field.getFilename(obj)
        file = str(field.getRaw(obj))

        # write pdf document to tmp file
        infile_name = tmp + '/infile.pdf'
        infile = open(infile_name, 'wb')

        # create stamped version of PDF
        outfile_name = tmp + '/outfile.pdf'
        command_line = 'pdftk %s stamp %s output %s' % (infile_name, 
                stamppath, outfile_name)
        args = shlex.split(command_line)
        stampresult = subprocess.Popen(args)
        # wait for stamping to finish before going further
        sts = os.waitpid(, 0)[1]

        # read stamped pdf from file
        outfile = open(outfile_name,'r')
        stamped =

        # replace existing PDF with stamped version
    # clean up
    command_line = 'rm -rf %s' % tmp
    args = shlex.split(command_line)
    p = subprocess.Popen(args)


    ##/code-section approveDocument

Tuesday, March 23, 2010

Laptop battery remaining time vs. uptime

Question: Why is it that after 15 minutes' uptime, Ubuntu thinks my laptop battery will last another 3 hours, but all it ever manages in total is 2:55 (at most)?

Here's a shell script which writes the current uptime and the estimated remaining time in a file, every minute. Hopefully this can shed some light on the matter. At least we can draw a nice graph afterwards!

You may need to sudo apt-get install acpi first.

/bin/echo "up, remaining" > $filename

while true; do
# extract h:mm from uptime output:
# try to find (h)h:mm first, then try to find 'xx min' and print as 0:xx
up=`/usr/bin/uptime | sed 's/^.*up \+\([0-9]\+:[0-9]\{2\}\).*/\1/g' | \
sed 's/^.*up \+\([0-9]\+\) \+min.*/0:\1/g'`

# extract h:mm from acpi 'hh:mm:ss' output
remaining=`/usr/bin/acpi | sed 's/^.* \+[0-9]\([0-9]:[0-9]\{2\}\):[0-9]\{2\}.*/\1/'`

# write output to file and screen
echo $up, $remaining | tee -a $filename

# interval between polls (in seconds)
sleep 60


Update: Here's a graph:

Uptime on X axis, remaining time (blue) and total time (orange) on the Y axis. Polls every minute. You can see the prognosis is optimistic at the beginning.

Wednesday, March 10, 2010

Creating workflow transition scripts with ArchGenXML and ArgoUML

To add a script that should be executed on a workflow transition, do the following:
* On the transition's tab, click the "New call action" icon, which looks like this: a()
* Enter an id for the action
* Save UML, regenerate, a file will be created in your product's root folder, containing an empty script.
* Below ##code-section {action_id} #fill in your manual code here, insert your code

Creating worklists using ArchGenXML

Once you've created a workflow in a UML model, you can add a revision list (worklist) as follows:
* in the workflow diagram, select a state
* add a tagged value "worklist=finalize_queue"
* add a tagged value "review=Manager,Reviewer"
The finalize_queue is just a name for the list. I've named it after the transition that would be the logical next step, which is "Finalize". The "review" value defines which roles get the "Review portal content" permission on the object in this state, so which roles get to see the items in their review list.

How to remove a portlet

To delete Plone's default portlets in your product, just add an assignment (to profiles/default/portlets.xml) with the "remove" attribute:

You can find the names, types and managers of the portlets in Product.CMFPlone's portlets.xml. Moving a portlet from one column to the other is as simple as removing it in one place and deleting it in the other.

Monday, March 8, 2010

Dutch translations for Plone roles

This is how Plone's roles are currently translated (in Plone 3.3.4):
Contributor: Medewerker
Editor: Schrijver
Member: Gebruiker
Reader: Lezer
Reviewer: Redacteur
Manager: Beheerder

Friday, March 5, 2010

How to set a property programmatically

You read a property (from OFS.PropertyManager.PropertyManager) with getProperty, but setting it is done through _setProperty. Note the underscore.

Convert ArgoUML to XMI

If you want to use ArchGenXML on UML models generated with ArgoUML >= 0.28, you'll have to export them to .xmi first. This is a quick way to do that automatically before running agx.

A shell script:
# Creates a .xmi file from an ArgoUML-generated .uml file.
# Usage: ./ filename.uml
# A file called filename.xmi will be created.

if [ ! -n "$1" ]
echo "Usage: `basename $0` input_file"

XMI_FILE=`echo $1 | sed 's/\.uml/\.xmi/'`
echo '<?xml version = "1.0" encoding = "UTF-8" ?>' > $XMI_FILE
sed -n '/<XMI*/,/<\/XMI>/p' $1 >> $XMI_FILE

Let the Makefile in your uml/ directory read:
./ MyProject.uml
archgenxml MyProject.xmi ../

and you'll only have to do is run make -C uml/

Tuesday, March 2, 2010

Why run ArchGenXML on a UML file instead of zargo?

Short answer: there's no reason.

The original version of this post comes from an error i got from ArchGenXML when running it on a UML file, instead of the generally recommended zipped (zargo) version. Why did i want to use UML instead of zargo?

The reason was version control: As a binary (zipped) file, zargo files will be replaced whole, while UML files can be diff'ed. This results in smaller diffs, and diffs that can be scanned for useful information.

However, the difference between two UML files, as output by the diff command, is hardly human-readable: Adding one field to a content class yields a 146 line diff that is almost completely unreadable. Also, the difference in size for changes is small: the patch is about 6k, while the zargo file is 36k. (UML size is 388k.)

So my arguments for running AGX on UML are invalid, i'll use zargo from now on.

(update 2010-07-06)

Original post: ArchGenXML: xml.parsers.expat.ExpatError: unbound prefix

I was working on a workflow in ArgoUML. After i'd added a transition, I got this error:
Traceback (most recent call last):
File "/usr/lib/python2.6/xml/dom/", line 1918, in parse
return expatbuilder.parse(file)
File "/usr/lib/python2.6/xml/dom/", line 924, in parse
result = builder.parseFile(fp)
File "/usr/lib/python2.6/xml/dom/", line 207, in parseFile
parser.Parse(buffer, 0)
xml.parsers.expat.ExpatError: unbound prefix: line 3843, column 6
make: *** [default] Error 1
make: Leaving directory `...'

The offending XML is this:

<argouml:pathitem figname="Fig2.1"
offset="10" />

As it turns out, this only happens with ArgoUML 0.28.1, with 0.26.2 all goes well.

The answer is that ArgoUML adds the "<argouml:... />" tag since 0.28, and archgenxml doesn't (yet?) know how to deal with it. However, you can export the model as an xmi profile, and let archgenxml work with that.

See also this part about generating the .xmi file automatically from the .uml.

Friday, February 26, 2010

Monday, February 22, 2010

plone.recipe.pound Error: Bad version 1.7

While installing pound in a Plone buildout, an error occurred:
The version, 1.7, is not consistent with the requirement, 'Markdown>=2.0.1'. 
Getting section poundbuild.
Initializing section poundbuild.
Installing recipe plone.recipe.pound.
Error: Bad version 1.7

Markdown = 2.0.3
to the buildout remedies this. Funny it can't do it by itself.

Friday, February 19, 2010

A Plone 4 buildout setup

A set of buildout files for Plone 4. My goal is to cover the entire project. from a development setup to production.


Define here everything you want to have in your portal that is not in standard Plone.

extends =
# ...

eggs +=
# ...
# eggs for your site


A default Plone buildout. This one was created with paster create -t plone3_buildout and uses Plone 4.0a4.

parts =

# Change the number here to change the version of Plone being used
extends =
versions = versions

# Add additional egg download sources here. contains archives
# of Plone packages.
find-links =

# Add additional eggs here
eggs =

# Reference any eggs you are developing here, one per line
# e.g.: develop = src/my.package
develop =

# Use this section to download additional old-style products.
# List any number of URLs for product tarballs under URLs (separate
# with whitespace, or break over several lines, with subsequent lines
# indented). If any archives contain several products inside a top-level
# directory, list the archive file name (i.e. the last part of the URL,
# normally with a .tar.gz suffix or similar) under 'nested-packages'.
# If any archives extract to a product directory with a version suffix, list
# the archive name under 'version-suffix-packages'.
# For more information on this step and configuration options see:
recipe = plone.recipe.distros
urls =
nested-packages =
version-suffix-packages =

# For more information on this step and configuration options see:
recipe = plone.recipe.zope2instance
user = admin:admin
http-address = 8080
#debug-mode = on
#verbose-security = on
blob-storage = var/blobstorage
# If you want Zope to know about any additional eggs, list them here.
# This should include any development eggs you listed in develop-eggs above,
# e.g. eggs = Plone my.package
eggs =

# If you want to register ZCML slugs for any packages, list them here.
# e.g. zcml = my.package my.other.package
zcml =

products =

# For more information on this step and configuration options see:
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
scripts = zopepy


A buildout for local development. This uses mr.developer to develop eggs, and adds dev products (such as plone.reload), testrunner and omelette.

extends =

extensions = mr.developer
sources = sources
auto-checkout +=
# ...
parts +=
unzip = true

recipe = collective.recipe.omelette
eggs = ${instance:eggs}
packages = ${instance:location}/lib/python ./

# ...

debug-mode = on
verbose-security = on
eggs +=

recipe = zc.recipe.testrunner
eggs =


The test server (as in testing / acceptance / production). This adds zeo config to buildout-dvl.cfg. (My tst/acc/prd setup consists of two machines, each of them runs one client.)

The zeo settings are read from another file, to keep passwords out of version control.

extends =


Turns a single instance buildout into a zeo server-client setup.

extends = buildout-base.cfg

parts +=

recipe = plone.recipe.zeoserver

recipe = plone.recipe.zope2instance
debug-mode = ${instance:debug-mode}
verbose-security = ${instance:verbose-security}
eggs = ${instance:eggs}
zcml = ${instance:zcml}
products = ${instance:products}
zeo-client = on
# Note: Do not set shared-blob = on with multiple machines.


An example for the settings required by buildout-tst.cfg.

zeo-address = 18105
file-storage = /ha-data/APPS/viva/tst/filestorage/Data.fs
# use full path for blob-storage
blob-storage = /ha-data/APPS/viva/tst/blobstorage

user = admin:secret
http-address = 8085
zeo-address = x.x.x.x:18085


A local development buildout which uses zeo setup with two clients.

extends = buildout-dvl.cfg

parts +=

recipe = plone.recipe.zeoserver
zeo-address = 8100

recipe = plone.recipe.zope2instance
user = ${instance:user}
debug-mode = ${instance:debug-mode}
verbose-security = ${instance:verbose-security}
eggs = ${instance:eggs}
zcml = ${instance:zcml}
products = ${instance:products}
zeo-client = on
zeo-address = ${zeoserver:zeo-address}
http-address = 8080

<= client1
http-address = 8081

Monday, February 15, 2010

Copying input from one terminal another

There's a couple of solutions for copying input from one terminal to several others. I used to use Konsole, which has two drawbacks:
* You have to setup which terminal you copy to every time you start it;
* It pulls in all kinds of KDE dependencies, which bothers me because i have Gnome.

I now use ClusterSSH as an alternative to Konsole. Its advantages are:
* Being able to define a setup, so all you have to type is cssh {setup}
* Having a separate window for the stuff that gets put in all terminals, so it's clear immediately what goes where, immensely reducing the amount of confusion and errorst
* It's lightweight

Its main disadvantage is that it always starts an ssh session, so now i'm ssh-ing to my localhost sometimes. Another thing is that the setup file is in /etc/ by default, requiring root permissions to modify it. But it's still my tool of choice.

Sunday, February 14, 2010

French phonetic characters

As a student of the French language, which i am not, one is sometimes required to analyze French phonology. In the course of this, one might want to write about one's work, and inserting symbols in documents on a computer becomes a necessity.

To cut this short, the question is: How do you type a "backwards c with a tilde on it"?

A paragraph or two about OpenOffice first. OpenOffice comes with a huge list of special characters, which you can insert (by clicking "Insert" > "Special character", funnily enough). A list of characters and symbols appears, which you can just click to insert. There's a "subset" of characters called IPA Extensions, IPA stands for International Phonetic Alphabet.

Having this wealth to choose from is nice, but to speed up typing it would be nice not have to use the mouse each time. Luckiliy, there are shortcut keys. In the lower right corner, you will see something like "U+00A9 (169)" (example for the copyright symbol). The first string, the one after the 'U+', is the Unicode "code point", an internationally valid code which represents the character. The second one, between the brackets, is the Latin-1 code, another code representation for the character. Remember that in the old days, we used to type Alt+{number} to insert special characters. Latin-1 is a simple character encoding which contains enough characters for most Western languages, but cannot be used for something as complex as phonetic characters.

On Ubuntu, you can create a character from its Unicode code point with Ctrl+Shift+u {codepoint}. (Windows?) This will work not only in, but in many programs where you can enter text. For example, if i hold down Ctrl and Shift and press 'u', an underlined 'u' appears. I then enter '0254' and press Enter, and what do i get? A beautiful reversed c!


The question remains how to create the tilde on my "backward c". It turns out there's also character subsets called "Spacing modifying letters" and "Combining diacritical marks". The characters from the latter subset will add themselves to the previous character when clicked. You can look up these characters in OpenOffice, but they're available on lots of places. Unicode Phonetic Symbols is a good starting point. After some searching around, it turns out the code for the tilde is 0303. So first i create my reversed c (Ctrl+U 0254 Enter), and then the tilde (Ctrl+U 0303 Enter), and the tilde places itself above the reversed c.


Friday, February 12, 2010

Turning my Plone 4 buildout into a ZEO server/client setup

This post has been replaced with this one

Using zc.recipe.testrunner with Plone 4

To test the contentratings package on a Plone 4.0a4 buildout, i added this to my buildout:
recipe = zc.recipe.testrunner
eggs =

Wednesday, February 10, 2010

Making SVN ignore certain files

To ignore the files *.po.backup and .registration.cache in my i18n folder, i ran: svn propedit svn:ignore i18n/. This opened an editor where i added these files, one per line:

Makefile for generating translation files in i18n folder

# Makefile for creating translation files (.pot and .po) in Plone products.
# Kees Hink, 2009-2010
# Place this inside the 'i18n' directory in your package.
# Adjust the definitions.
# When you run 'make', a .pot file will be created.
# Run 'make xx-translation' to create a translation file for language xx.

# Definitions
DOMAIN = 'Products.MyProduct'
# The definitions below are derived from the definitions above.
# You could also override them.
GENERATED := ${DOMAIN}-generated.pot
MANUAL := ${DOMAIN}-manual.pot

default: generate merge

@echo "Extracting strings from templates, putting them in generated potfile"
i18ndude rebuild-pot --pot ${GENERATED} --create ${DOMAIN} ../

@echo "Merging generated and manual string files into potfile"
i18ndude merge --pot ${POTFILE} --merge ${MANUAL} --merge2 ${GENERATED}

@echo "Making backup of translation file"
if [ ! -e ${DOMAIN}-$*.po ]; \
then touch ${DOMAIN}-$*.po;\
else cp ${DOMAIN}-$*.po ${DOMAIN}-$*.po.backup;\
@echo "Syncing translation file"
i18ndude sync --pot ${POTFILE} ${DOMAIN}-$*.po

* Makefiles are always indented with tabs
* You may require a space at the end of a line after a backslash

Wednesday, February 3, 2010

Using the locate command with --regex (or --regexp)

locate is much faster than find...

# Find images in my homedir and its descendants (children, thier children etc.)
locate --regex \/home\/kees\/.*\(.png\|.jpg\|.gif\)
# Find images only in direct children of my homedir
locate --regex \/home\/kees\/[^\/]*\/[^\/]*\(.png\|.jpg\|.gif\)

Tuesday, February 2, 2010

Favoriete Nederlandse scheldwoorden

Aangestoken door een lijstje met iemands favoriete Franse scheldwoorden, dat
echt te makkelijk en te grof was, hier mijn top-N:

  • Boefje

  • Druif

  • Muppet

  • Schelm

  • Schobbejak

  • Schavuit

  • Zoon van een coyote met een gele lever

  • Boerenlul

  • Idioot

  • Schertsfiguur

Bij "Boerenlul" moet ik altijd denken aan het verhaal van een voetballer in Zuid-Oost Drenthe die van het veld gestuurd werd omdat hij "boetnspul" (buitenspel) naar de scheidsrechter riep. De scheids was helaas minder bekend met het dialect en verstond het verkeerd.

"Boefje" is leuk om vooral niet te gebruiken voor kinderen (brugklassers die te laat komen en zo), maar voor witteboordencriminelen die voor miljoenen oplichten. Een duchtige schrobbering voor hen!

"Idioot" brengt die 80'er jaren-melancholie met zich mee van Murdoch, der richtige Vollidiot.

Schelm, Schobbejak en Schavuit is een heilige 3-eenheid. Max und Moritz in de Nederlandse vertaling.

Exemplarisch voor het Creatieve Schelden is "Zoon van een coyote met een gele lever". De collega die mij hiervoor uitmaakte kan er zo nog 10 uit zijn mouw schudden. Doet me denken aan de vloek: "May your daughter marry a jazz musician" (Zappa?).

Ook bij "Schertsfiguur" is een voetbalverhaal: Een speler die bij een Europese wedstrijd voor een Russische (meen ik) club uitkwam, was kermend van de pijn op de grond blijven liggen, zodat de commentator zich afvroeg hoe lang het zou duren voordat de bal over de zijlijn zou worden gespeeld. Toen er even later een bal de kant van de speler op kwam, stond hij ineens op en rende als een kievit. De commentator had zich in de luren had laten leggen en alleen dit woord, vol walging uitgesproken, leek zijn afkeuring te kunnen bevatten.

Ohja, in de categorie eervolle vermelding (met dank aan het internet): Addergebroed, Butje (Gr.), Fabricagefout, Gek, Hansworst, Minkukel, Mispunt, Oliebol, Quasimodo, Slampamper, Slapjanus, Stuk verdriet, Uilskuiken, Worm, Zak hooi.

Monday, February 1, 2010

Fixing PosKey Errors in Zope

Update: For a possibly easier way to _find_ PosKey errors, look here:

Some objects in my Plone site were broken: when clearing and rebuilding the catalog, i saw "PosKey" errors in my Zope logfile (*). Which objects are broken, and what to do about it?

I put a pdb statement in the file which caused errors, which was CMFCore/, line 390:

+ try:
+ dummy = base_hasattr(obj, 'indexObject')
+ except:
+ import pdb; pdb.set_trace()

I re-ran the catalog rebuild and was dropped into a pdb shell:

> from Acquisition import aq_parent
> parent = aq_parent(obj)

This showed which folderish thing contained the broken object.

Stopping the pdb session (using c, continue with the program) and looking in the ZMI, i was able to see that these objects were indeed broken: They could not be viewed in the ZMI (the same PosKey error).

Luckily, you can start Zope in debug mode for this, so it's Zope debug to the rescue:

$ ./client0/bin/zopectl debug # yep, a Plone 2.5 site
> site = getattr(app, 'my-sites-id-in-zope')
> folder = site.somefolder
> delattr(folder,'my-broken-object')
> import transaction; transaction.commit()

Broken object is thrown away: Problem solved. You can now clear and rebuild the catalog, and there will be much rejoicing.

If something in your site depends in the object being there, most likely with a special id, you could re-create the object from the Zope debug console like this:

> from Products.SomeProduct.content.SomeContentType import SomeContentType
> id = 'special-id' # use an id that's unique in the folder
> recreated_object = SomeContentType(id)
> folder.special-id = recreated_object
> import transaction; transaction.commit()

(*) The question remains, how object get "broken" in this manner. Anyone willing to shed light on this is very welcome to reply.

Thursday, January 28, 2010

Translations in i18n folder not picked up

I had translations in an 'i18n' folder, but they didn't work. No '.mo' file was created on Zope startup, and the products wasn't listed in the ZMI's product listing.

In order to get the 'i18n' folder to be picked up, i put a <five:registerPackage package="." /> statement in configure.zcml.

Tuesday, January 26, 2010

Indexing of content created by Generic Setup

Just adding a 'profiles/default/structure' to your site will add content, but it won't be indexed properly. You'll have to manually upgrade the portal catalog. To fix this, add this to 'configure.zcml':

title="Update catalog"
description="After creating content (from profiles/default/structure), the catalog needs to be updated."
<depends name="content"/>

This is the preferred way to define dependencies for import profiles: The import step declares its dependency on the content import step. content is the name for the step which creates content from profiles/default/structure.

You could then add a method which updates the catalog in the product's
def updateCatalog(context, clear=True):
portal = context.getSite()
logger = context.getLogger('my.policy updateCatalog')'Updating catalog (with clear=%s) so items in profiles/default/structure are indexed...' % clear )
catalog = portal.portal_catalog
err = catalog.refreshCatalog(clear=clear)
if not err:'...done.')
logger.warn('Could not update catalog.')

Friday, January 22, 2010

using a policy product to install (not just get) add-ons

We all know how you can use a policy product to automatically get add-on
products and do basic site configuration. If you don't look here: and

But how to actually install these products and configure them when the policy
product is installed?

Use case: I'd like to create a policy egg that will not only download but also
install a custom theme, Products.LinguaPlone and Products.Collage. (Afterwards i want to set up LinguaPlone's available languages, and perhaps I'd also like to create translations for content that was added in the policy product's 'profiles/default/structure', but that's a different story.)

In your profiles/default/metadata, add dependencies:

Note that the name of the GS profile is defined in the product, so it could be anything. Look for the genericsetup:registerProfile's name attribute.

Alternatively, you could call the quickinstaller's installProducts method from
    quickinstaller = portal.portal_quickinstaller
installable_products = [
for product_name in installable_products:

, but that will also make products show up twice in Plone's add-on products panel. For mere products installation i would recommend the above method.

Error: "Couldn't find index page for"

When running a Plone buildout, i got: "Couldn't find index page for {package}". Turns out i'd "deactivated" it via mr.developer. Use ./bin/develop activate {package} to turn it on again.

Monday, January 18, 2010

Noorderslag 2010

I'm late with this. After skipping the second day of Eurosonic, i saw:

  • Tim Knol (singer/songwriter, too sweet but with the band it was okay)

  • Awkward I (yes again, same as last year. better atmosphere, more sound, more talking, which is good.)

  • Lola Kite (electro-punk-thingy)

  • Wende (Evening's winner. Perhaps too many English songs from the new English album.)

  • Charlene (Singer. Didn't impress me, but then again, the whole Beyoncé thing isn't up my alley.)

And much more, because there'e music everywhere, there's no escape.

Friday, January 15, 2010

Eurosonic 2010 day 1

I don't personal-blog much anymore, but i want to remember that last night, i saw:

  • Wadruna (Norwegian Role Playing Gamers with deep voices and sound effects)

  • Leisure Society (Too soft, people complained we talked too loud. In the Spieghel!)

  • Pony the Pirate (Energetic folk rock circus, evening's winner)

  • Cosmic Carnival (70's rock/bluesrock and reggae)

  • Dret & Krulle (hiphop, what were they doing?)

  • Le Roi mort et les Lentilles Rouges (Gogol Bordello goes Aznavour)

  • Heike has the giggles (Trio with girl on vocals+guitar)

Sunday, January 10, 2010

mr.developer SyntaxError "finally"

Got this error:
  File "/home/kees/.buildout/eggs/mr.developer-1.8-py2.4.egg/mr/developer/", line 109
SyntaxError: invalid syntax

Turns out 1.8 is currently unreleased, perhaps that's the reason.
Fixed it by specifying mr.developer = 1.7 in my buildout.cfg.

Friday, January 8, 2010

Testing the layout for python egg's README.txt file

Before uploading an egg, i like to test the layout of the egg to see if it is correct ReST, so it shows up nicely on the page and

$ python --long-description | rst2html > description.html

This is assuming that you create the long-description in your by reading the README.txt. My usually contains something like this:
version = '1.0'

description="Some description about what this does, who might use it etc.",
open(os.path.join("docs", "NEWS.txt")).read(),
open(os.path.join("docs", "TODO.txt")).read(),

The "\n\n".join() bit is used to make sure there's enough separation between chapters (from the various files), because otherwise you could remove a line which would result in invalid ReST.

Thursday, January 7, 2010

What is a good WYSIWYG HTML editor?

Tried vi(m), gedit, bluefish, and the winner is:!

To disable inline style statements, modify Tools > Options > Load/Save > HTML Compatibilty > set "Export" to "HTML 3.2".