Personal tools
You are here: Home Plone Book 12. Ein Produkt in Python schreiben
Die maximale Bildbreite kann wie folgt gewählt werden:

12. Ein Produkt in Python schreiben

Document Actions

Ein Produkt in Python schreiben

Wenn Sie ein Produkt für Plone schreiben, dürfen Sie fast alles tun, was Sie jemals mit Plone machen wollten. Inhaltstypen oder Werkzeuge in Python zu schreiben ist der beste Weg, höchste Flexibilität zu erreichen. Falls Sie dringend etwas Bestimmtes in Plone benötigen, was noch nicht anderswo vorhanden ist, dann haben Sie die Gelegenheit, dieses Feature hinzuzufügen, indem Sie ein Produkt schreiben. Das könnte z.B. die Speicherung einer firmenspezifischen Art von Inhalt oder irgendeine Manipulation sein, die nur Sie benötigen. Im vorigen Kapitel habe ich gezeigt, wie Sie einen Inhaltstyp anpassen können. Diese Anpassung bringt Sie jedoch nur bis zu einem gewissen Punkt. Sie können z.B. keine neuen Attribute zu Ihrem Inhaltstyp hinzufügen. Daher werden Sie lieber Ihren eigenen Inhaltstyp schreiben wollen.

In diesem Kapitel werde ich zwei Beispiele beschreiben: einen Inhaltstyp und ein Werkzeug. Beide Beispiele sind relativ einfach gehalten, bereiten Sie aber auf das nächste Kapitel vor, in dem ich Ihnen zeigen werde, wie Sie Archetypes benutzen, ein Framework für Plone, mit dem Sie Inhaltstypen schnell und einfach mit nur einer minimalen Menge von Quellcode erzeugen können.

Insbesondere erzeuge ich einen eigenen Inhaltstyp in Plone und gehe durch den gesamten Code durch, der diesen Inhaltstyp erzeugt. Es handelt sich um einen recht interessanten Inhaltstyp, denn er benutzt mehrere Bausteine, die er aus einem C-Modul von dritter Seite herauszieht, und baut sie in Ihre Plone-Site ein. Ich zeige Ihnen dabei, wie Sie zuerst den Inhaltstyp erstellen und dann Rechte, eine Suchintegration, neue Elemente für die Benutzerschnittstelle sowie Installationskripten hinzufügen. Und schließlich behandle ich, wie man ein Plone-Werkzeug erstellen kann, d.h. eine Art, neue Werkzeuge zu einer Site hinzuzufügen. Beide Beispiele in diesem Kapitel können Sie online herunterladen, installieren und studieren. Außerdem finden Sie in Anhang B den gesamten Code.

Einen eigenen Inhaltstyp schreiben

Auf der Website zum Plone-Buch (http://plone-book.agmweb.ca) wollte ich den Code dieses Buchs online anzeigen. Dazu hätte ich den Code nehmen und ihn einfach in Dokumente setzen können, aber dann würde er ohne Syntax-Hervorhebung angezeigt werden. Außerdem würden die Leerzeichen für die Einrückung in Python verschwinden. Für ein so tolles Produkt wie Plone wollte ich etwas, was gut aussieht. Also brauchte ich einen Inhaltstyp, der die Syntax im Code hübsch hervorhebt und es den Benutzern ermöglicht, ihn online anzuschauen. Abbildung 12.1 zeigt das fertige Beispielprodukt.

img/12-01.png

Abbildung 12.1. Ein in Plone geladenes Python-Beispielskript

Aus diesem Design können Sie einige Anforderungen für dieses Produkt ableiten. Insbesondere wird dieses Produkt die folgenden Attribute haben:

  • ID: Jedes Stück Code hat eine eindeutige ID. Dieses Attribut ist notwendig.
  • Title: Jedes Stück Code sollte einen Titel haben. Dieses Attribut ist notwendig.
  • Description: Jedes Stück Code sollte eine Beschreibung dessen haben, was es tun sollte. Dieses Attribut ist optional.
  • Source code: Jedes Stück Code wird ein Quellcode-Attribut haben, das die Quelle zu diesem Inhaltstyp enthält. Das wird optional sein, aber es ist vernünftig, es notwendig zu machen.
  • Language: Dies ist die Sprache für den Quellcode, z.B. Perl, Python, HTML usw.

Der Inhaltstyp sollte natürlich mit Plone interagieren, damit Sie dessen Möglichkeiten nutzen können. Sie müssen sicherstellen, dass im Produkt gesucht werden kann, dass es mit den Sicherheitsmechanismen und dem Workflow zusammenarbeitet und korrekt persistent gespeichert wird. Außerdem wäre es nett, wenn die Benutzer Skripten direkt von ihrer Festplatte hochladen könnten, anstatt Code in irgendwelche Textbereiche einfügen zu müssen.

Beim Untersuchen dieses Codes musste ich einen einfachen Weg finden, Code in HTML umzuwandeln. Bei einer Sprache mit einer einfachen Syntax wie Python ist das ziemlich einfach (Python kann sogar seinen eigenen Code "lexen"), aber am liebsten hätte man das natürlich für mehrere Sprachen, z.B. HTML (Page Templates), JavaScript, Cascading Style Sheets (CSS) usw. Das SilverCity-Modul macht bereits genau das. Es ist auf SourceForge verfügbar (http://silvercity.sf.net/). Es verwendet C-Bibliotheken aus dem Scintilla-Texteditor, um den Code zu lexen. Ohne besonders auf die Implementierung einzugehen: Der Vorteil besteht darin, dass es zu fast einem Dutzend Programmiersprachen fröhlich HTML mit Syntax-Hervorhebung ausspuckt.

Bei einem Blick auf die Anforderungen werden Sie sehen, dass diese ziemlich leicht nachvollziehbar sind. Tatsächlich werden die ID, der Titel und die Beschreibung alle in der Dublin Core-Implementierung von Plone definiert. Sie brauchen sich also nur um den Quellcode und die Sprache zu kümmern. Plone verlangt eine ID und einen Titel, und eine Beschreibung ist wirklich sehr hilfreich.

Mit dem Inhaltstyp anfangen

Da Sie nun eine Vorstellung vom Inhaltstyp haben, den Sie in diesem Kapitel erstellen werden, können Sie anfangen, ihn zu erstellen, indem Sie Python-Code im Dateisystem schreiben. Dieser Inhaltstyp ist also auch ein Produkt, d.h., Sie erstellen ein neues Verzeichnis in Ihrem Produktverzeichnis. Der Name des zu erzeugenden Verzeichnisses ist der Name des Produkts, das Zope importieren wird. Treffen Sie also eine kluge Wahl für Ihren Namen. Ich habe mir spaßeshalber überlegt, das Produkt SourceCode oder PloneSourceCode zu nennen, fand diese Namen aber zu verwirrend, (man könnte denken, dass das Produkt der eigentliche Quellcode von Plone wäre). Aber PloneSilverCity schien ein netter Produktname zu sein, der auf seinen Ursprung hinweist und hinreichend obskur ist, dass ihn keiner mit etwas anderem verwechseln könnte.

Nachdem das Verzeichnis erstellt ist, füge ich normalerweise ein paar Dateien und Verzeichnisse hinzu, die ich benötige. Jedes Paket benötigt eine Datei namens __init__.py darin. Dieser Dateiname wird von Python so verlangt und bedeutet, dass dieses Verzeichnis ein Python-Paket ist, d.h., dass es importiert werden kann. Wenn das Paket importiert wird, führt Zope diese Datei aus. In dieser Datei fügen Sie den Code zur Registrierung des Produkts in Zope hinzu.

Da Sie benutzerfreundlich sein möchten, können Sie auch ein paar Textdateien wie readme.txt, install.txt usw. hinzufügen. Eine andere nützliche Textdatei ist refresh.txt. Damit können Sie sich in das Refresh-Modul von Zope einklinken und das Produkt dynamisch neu laden, während Sie es schreiben. Das ist gerade bei Ihren ersten Schritten unglaublich hilfreich, wenn Sie eine Klasse schreiben, und ich werde ihnen später zeigen, wie Sie das in Zope konfigurieren.

Im Moment haben Sie ein Verzeichnis namens PloneSilverCity im Produktverzeichnis, das folgende leere Dateien enthält: readme.txt, refresh.txt, install.txt und __init__.py. Das stellt nun ein gültiges Python-Paket dar, das absolut nichts tut (aber nicht mehr lange).

Integration von SilverCity

Bevor Sie sich zu tief in den Zope-Code verstricken, könnte es sinnvoll sein herauszufinden, wie man SilverCity benutzt. Bei jeder Softwareentwicklung ist ein schichtweises Vorgehen, das das Testen einzelner Schichten erlaubt, von größter Bedeutung. Aus diesem Grund sollten Sie damit anfangen, dass Sie sicherstellen, SilverCity direkt aus einem Python-Modul heraus benutzen zu können. Wenn das funktioniert, müssen Sie nur noch die Zope-Schicht hinzufügen.

Schauen Sie sich SilverCity also einmal an. Zuerst müssen Sie es installieren. Glücklicherweise folgt dieses Modul den in Kapitel 10 skizzierten Installationsanweisungen für Python-Module. Für eine Installation unter Windows laden Sie die Datei SilverCity-0.9.5.win32-py2.3.exe von http://silvercity.sf.net herunter und starten das grafische Installationsprogramm. Für Linux laden Sie die Datei SilverCity-0.9.5.tar.gz von der gleichen Website herunter und speichern sie auf der Platte. Dann packen Sie sie aus und starten das Programm setup.py. Beispiel:

$ tar -zxf SilverCity-0.9.2.tgz
$ cd SilverCity-0.9.2
$ python setup.py install
...

Danach können Sie unter dem Python-Prompt in Windows oder Linux schnell sehen, ob es funktioniert:

$ python
Python 2.3.2 (#1, Oct  6 2003, 10:07:16)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import SilverCity
>>>

Das bedeutet, dass SilverCity erfolgreich installiert worden ist. Falls Sie kein ähnliches Ergebnis erhalten und SilverCity nicht importieren können, unterbrechen Sie an dieser Stelle und lösen zuerst das Problem, denn sonst funktioniert überhaupt nichts.

Nun müssen Sie die API (Application Programming Interface) dieses Moduls herausfinden. Weil ich faul bin, habe ich mir ein Beispielskript in PySilverCity/Scripts namens source2html.py angeschaut. Dieses Skript tut genau, was Sie wollen: Es spuckt HTML zu einem gegebenen Stück Code aus. Eine wirklich freche Art, es in Aktion zu sehen, besteht darin, dieses Skript wie folgt auf sich selbst loszulassen:

$python source2html.py source2html.py --generator=python
 
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>source2html.py</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link
    rel="stylesheet"
    href="default.css" />
</head>
...

Das heißt, Sie müssen sich nur diese API anschauen und sie leicht abändern. Fügen Sie ein Modul namens source.py im Verzeichnis PloneSilverCity hinzu. Dort schreiben Sie den Code, der die Schnittstelle zur Bibliothek enthält. Dieses neue Modul enthält momentan keinen Zope- oder Plone-spezifischen Code. Es hat drei Hauptbestandteile: Es nennt Ihnen alle möglichen Sprachen, die Sie benutzen können, es nimmt einen Text an, und es gibt den korrekten Parser zurück, und schließlich führt es die eigentliche Umwandlung aus.

Fügen Sie zuerst die folgende Funktion create_generator hinzu, mit der Sie den korrekten Parser erhalten:

from SilverCity import LanguageInfo
from StringIO import StringIO
 
def create_generator(source_file_name=None, generator_name=None):
    """ Make a generator from the given information
    about the object, such as its source and type """
    if generator_name:
        return LanguageInfo.find_generator_by_name(generator_name)()
    else:
        if source_file_name:
            h = LanguageInfo.guess_language_for_file(source_file_name)
            return h.get_default_html_generator()()
        else:
            raise ValueError, "Unknown file type, cannot create lexer"

Zweitens, wenn Sie in Plone sind, müssen Sie genau herausfinden, welche Sprachen verfügbar sind, damit Sie diese den Benutzern anzeigen können. Schreiben Sie die folgende Funktion namens list_generators, die diese Liste zurückgibt:

def list_generators():
    """ This returns a list of generators, a generator
    is a valid language, so these are things like perl,
    python, xml etc..."""
    lexers = LanguageInfo.get_generator_names()
    return lexers

Die Funktion generate_html nimmt schließlich eine Quelldatei als String an, einen optionalen Generator und einen optionalen Dateinamen. SilverCity benötigt eine Datei, z.B. buffer, um den Inhalt irgendwohin zu schreiben, d.h., Sie können das Python-Modul StringIO zu diesem Zweck verwenden. Die Funktion generate_html lautet wie folgt:

def generate_html(source_file, generator=None, source_file_name=None):
    """ From the source make a generator
    and then make the html """
 
    # SilverCity requires a file like object
    target_file = StringIO()
    generator = create_generator(source_file_name, generator)
    generator.generate_html(target_file, source_file)
 
    # return the html back
    return target_file.getvalue(), generator.name

Sie werden bemerken, dass sie die Funktion create_generator aufruft, die Sie zuvor geschrieben haben, um den richtigen Generator für diese Sprache zu finden. Das ist der ganze Code, den Sie benötigen, um zu einer Datei das entsprechende HTML zu generieren. Ich bin nicht auf irgendwelche Besonderheiten des eigentlichen Lexens des Quellcodes oder der Erzeugung des HTML-Codes eingegangen. Die SilverCity-Bibliothek erledigt das alles für Sie. Um den vorigen Punkt zu wiederholen: In diesem Modul haben Sie keine Referenz auf Zope oder Plone, d.h., dieses Modul ist völlig selbstständig. Die eigentlichen Details dieses Moduls müssen Sie nicht kennen, solange Sie wissen, dass Sie eine Bibliothek von dritter Seite importieren.

In Pyhon-Skripten gibt es die Tradition, mindestens einen Test in den Code einzubauen. Sie könnten auch eine vollständige Unittest-Suite erstellen, aber das geht über das aktuelle Thema hinaus. Stattdessen werden Sie ein wenig Code hinzufügen, um zwei Dinge zu testen, nämlich zum einen, dass es funktioniert, und zum anderen, dass die Sprachen verfügbar sind:

if __name__ == "__main__":
    import sys
    myself = sys.argv[0]
    file = open(myself, 'r').read()
    print generate_html(file, generator="python")
    print list_generators()

Wenn Sie dieses Skript ausführen, öffnet es sich selbst und übergibt seinen Code an den Teil, der die HTML-Syntax hervorhebt. Dann wird eine Menge HTML-Code ausgespuckt. Das könnten Sie einfach ins Zope-spezifische Modul setzen, das Sie gleich schreiben werden. Wenn sich allerdings alles in getrennten Skripten befindet, wird es später einfacher zu testen und zu ändern.

Die Klasse schreiben

Ein Inhaltstyp in Plone ist lediglich ein Objekt, das einige bestimmte Attribute und einige bestimmte Basisklassen hat. Sie müssen sich nicht einmal um das Lesen und Schreiben in der Datenbank kümmern, das machen alles die Persistencebase-Klassen. Erstellen Sie nun ein Modul namens PloneSilverCity.py im Paket.

Importieren Sie zuerst das Modul source.py, das Sie vor wenigen Augenblicken geschrieben haben. Das geht mit einer einfachen Zeile, da sich das Modul im gleichen Paket befindet. Die Zeile, die die Funktion importiert, lautet wie folgt:

from source import generate_html, list_generators

Als Zweites benötigen Sie eine Klasse PloneSilverCity, in der Sie die benötigte Funktionalität kapseln können. Bei dieser Klasse müssen Sie auf die folgenden vier Attribute aufpassen:

  • id: Speichert die eindeutige ID dieser Instanz der Klasse PloneSilverCity.
  • _raw: Speichert den rohen Quellcode in der Klasse.
  • _raw_as_html: Speichert den Quellcode, nachdem er in HTML gelext wurde.
  • _raw_language: Speichert die Sprache dieses Quellcodes.

Für jedes dieser Attribute werden Sie einen Accessor schreiben, d.h. eine Methode, die den Wert dieses Attributs zurückgibt, damit Sie nicht das Attribut, sondern die Zugriffsmethode aufrufen. Ein Beispiel für eine Accessor-Methode ist getLanguage, die den Wert des language-Attributs zurückgibt. Einen Accessor zu schreiben ist normalerweise eine gute Idee, besonders deswegen, weil Sie später Sicherheitsmechanismen auf diese Accessor-Methoden anwenden werden. In Zope stehen alle Methoden oder Attribute, die mit einem Unterstrich beginnen, nicht für webbasierte Methoden wie Page Templates oder Script (Python)-Objekte zur Verfügung. Ein gute Vorgehensweise besteht darin, am Anfang für all Ihre Attribute einen Unterstrich zu verwenden und dann die Sicherheitsmechanismen auf die Zugriffsmethoden anzuwenden.

Listing 12.1 zeigt die Basisklasse.

Listing 12.1. Die Basisklasse in Python

class PloneSilverCity:
    def __init__(self, id):
        self.id = id
        self._raw = ""
        self._raw_as_html = ""
        self._raw_language = None
 
    def getLanguage(self):
        """ Returns the language this code has been lexed with """
        return self._raw_language
 
    def getRawCode(self):
        """ Returns the raw code """
        return self._raw
 
    def getHTMLCode(self):
        """ Returns the html code """
        return self._raw_as_html
 
    def getLanguages(self):
        """ Returns the list of languages available """
        langs = []
        for name, description in list_generators():
            langs.append( {'name':lang, 'value':language} )
        langs.sort()
        return langs

Eine weitere Methode müssen Sie noch hinzufügen, nämlich eine edit-Methode, mit der Sie eine Datei oder einen String hochladen können. Diese Methode liest die Datei und prüft, ob irgendetwas in der Datei steht. Wenn ja, wird sie gelesen und ein Dateiname wird bestimmt. Dann werden der Code, die Sprache und der Dateiname an die Erzeugungsfunktion übergeben. All das speichern Sie in den zuvor genannten Attributen, wie Sie in Listing 12.2 sehen können.

Listing 12.2. Die Methode zur Behandlung von Bearbeitungsschritten

def edit(self, language, raw_code, file=""):
    """ The edit function, that sets
    all our parameters, and turns the code
    into pretty HTML """
    filename = ""
    if file:
        file_code = file.read()

        # if there is a file and it's not blank...
        if file_code:
            raw_code = file_code
            if hasattr(file, "name"):
                filename = file.name
            else:
                filename = file.filename
            # set the language to None so set by SilverCity
            language = None

    self._raw = raw_code

    # our function, generate_html does the hard work here
    html, language = generate_html(raw_code, language, filename)
    self._raw_as_html = html
    self._raw_language = language

Hinweis

Python-Entwickler, die sich gut auskennen, sehen eventuell ein Problem darin, file.name und file.filename zu verwenden. Zope-Dateiobjekte verfügen über ein Attribut namens filename, das den Dateinamen repräsentiert, während in Python das Attribut name heißt. Dieser Code funktioniert also in normalem Python oder in Zope.

Nun haben Sie also eine Python-Klasse, die das Objekt kapselt. An dieser Stelle sollten Sie die Klasse unter einem Python-Prompt sehr leicht ausführen können, um zu testen, ob sie das macht, was Sie wünschen. Beispiel:

$ python
Python 2.3.2 (#1, Oct  6 2003, 10:07:16)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from PloneSilverCity import PloneSilverCity
>>> p = PloneSilverCity("test.py")
>>> p.edit("python", "print 'hello world'")
>>> p.getRawCode()
"print 'hello world'"
>>> p.getHTMLCode()
'<span class="p_word">print</span>
<span class="p_default">nbsp;</span>
<span class="p_character">\'hellonbsp;world\'</span>'
>>> p.getLanguage()
'python'

Aus einem Paket ein Produkt machen

Nun haben Sie ein einfaches Paket, aber das ist noch kein Plone-Produkt. Sie müssen es mit Plone initialisieren. Das heißt,Sie müssen zusätzliche Informationen zum Modul PloneSilverCity.py hinzufügen. Insbesondere müssen Sie eine Factory-Funktion hinzufügen. Die Verwendung einer Factory ist ein wohlbekanntes Muster im objektorientierten Design. Sie definiert, wie ein Objekt erzeugt wird. Fügen Sie im Modul PloneSilverCity.py den folgenden Konstruktor hinzu:

def addPloneSilverCity(self, id, REQUEST=None):
    """ This is our factory function and creates
    an empty PloneSilverCity object """
    obj = PloneSilverCity(id)
    self._setObject(id, obj)

Die Funktion addPloneSilverCity gehört nicht zur Klasse PloneSilverCity. Als Konstruktor für die Klasse wird sie in das Modul außerhalb der Klasse gesetzt. Diese Funktion ist die erste Plone-spezifische Funktion. Der Methode werden drei Parameter übergeben: das Objekt self, der ID-String für das Objekt und REQUEST. Das Objekt self ist eigentlich der context, den Sie zuvor schon gesehen haben, wenn auch unter einem anderen Namen. Da die Objekte immer innerhalb eines Ordners erzeugt werden, zeigt self auf den Ordner, in dem dieses Objekt erzeugt wird. Diese Funktion erzeugt eine Instanz von PloneSilverCity namens obj und übergibt sie an die Methode _setObject des Ordners. Die Methode _setObject ist in Zope etwas Besonderes: Sie instanziiert das Objekt in der Datenbank und registriert das Objekt im umgebenden Ordner.

Als Nächstes fügen Sie die Factory-Typinformation hinzu, die in Kapitel 11 behandelt wurde (das ist Ihre erste Chance, sie selbst zu erzeugen). Die Factory-Typinformation enthält in einem Dictionary alle Informationen über den Inhaltstyp. Diese Angaben werden in portal_types geladen, wenn das Produkt in Ihrer Plone-Instanz installiert wird. Sie stellen jene Information dar, die Sie vorher gesehen haben, als Sie die Factory-Typinformation über das Web geändert haben.

Bevor ich die Factory-Information erstelle, erstelle ich normalerweise eine Konfigurationsdatei, die alle wiederholten Variablen zu dem Produkt enthält. Diese Datei heißt config.py, und darin schreiben Sie wie folgt den Namen des Produkts, den Namen seiner Ebenen in den Skins und den Namen, den der Benutzer dafür sieht:

product_name = "PloneSilverCity"
plone_product_name = "Source Code"
layer_name = "silvercity"
layer_location = "PloneSilverCity/skins"

Dann können Sie die Factory-Typ-Information erstellen und diese Strings verwenden. Die ID z.B. wird Source Code sein, da das den Benutzern in Plone angezeigt wird. Der Aktionsabschnitt der Typinformation ist ein Tupel von Dictionaries aller Aktionen, die auf diesem Objekt vorkommen können. Wenn diese Factory in Plone geladen wird, wird der Actions-Reiter im Werkzeug portal_types mit diesem Inhalt gefüllt. All diese Aktionen haben eine entsprechende Methode, ein Template oder Skript, das aufgerufen wird. Die meisten davon haben eine direkte Entsprechung zu Page Templates, was ich später in diesem Abschnitt noch erörtern werde.

Wie Sie nun wissen, ist eine Aktion etwas, das Benutzer mit einem Element in der Plone-Datenbank machen können. Mit Blick auf diese Beispielanwendung können die Benutzer zwei offensichtliche Dinge mit dem Quellcode anstellen. Sie können ihn anzeigen und den hübsch hervorgehobenen Code sehen, und sie können das Element bearbeiten und einen Quellcode hochladen. Tatsächlich verlangt Plone, dass es eine Aktion namens view und eine Aktion namens edit gibt, d.h., diese beiden passen hier gut ins Konzept. Sie werden auch eine dritte Aktion haben wollen: Es ist nett, den Quellcode in seiner ursprünglichen Form herunterladen zu können. In Sprachen wie Python, in denen die Formatierung ein wesentliches Element ist, ist das wirklich nützlich. Diese Aktion zeigt direkt auf getRawCode, was die Methode für den Zugriff auf den rohen Code ist.

Jede Aktion hat ein zugehöriges Recht, wie in Listing 12.3 gezeigt wird (wo es genau herkommt, werde ich im weiteren Verlauf dieses Abschnitts zeigen).

Listing 12.3. Die Factory-Typinformationen und Aktionen

factory_type_information = {
     'id': plone_product_name,
     'meta_type': product_name,
     'description': ('Provides syntax highlighted HTML of source code.'),
     'product': product_name,
     'factory': 'addPloneSilverCity',
     'content_icon': 'silvercity.gif',
     'immediate_view': 'view',
     'actions': (
                 {'id': 'view',
                  'name': 'View',
                  'action': 'silvercity_view_form',
                  'permissions': (view_permission,)},
                 {'id': 'edit',
                  'name': 'Edit',
                  'action': 'silvercity_edit_form',
                  'permissions': (edit_permission,)},
                 {'id': 'source',
                  'name': 'Source',
                  'action': 'getRawCode',
                  'permissions': (view_permission,)},
                 ),
     }

Hinweis

An dieser Stelle kann das Produkt vom Python-Prompt aus nicht importiert werden, weil der Code noch unvollständig ist.

Die Rechte einstellen

Ein fundamentales Konzept bei Websites ist die Annahme, das man nichts und niemandem trauen darf. Bevor ein Zugriff auf eine Eigenschaft erfolgt oder irgendeine Methode aufgerufen wird, müssen Sie zuerst überprüfen, ob die Partei, die eine Aktion ausführen möchte, das auch tun darf. Bei den meisten Systemen gibt es drei Rechte: das Recht, ein Element hinzuzufügen, das Recht, ein Element zu löschen, und das Recht, ein Element zu bearbeiten. Plone kennt ein weiteres Recht: das Recht, ein Element über das Web (oder ein anderes Protokoll) anzuzeigen. In Plone hat der umgebende Ordner das Recht, etwas zu löschen. Wenn Sie etwas in diesem Ordner löschen können, können Sie auch den hier von Ihnen hinzugefügten Inhaltstyp löschen.

Das heißt, Sie müssen sich noch um drei Rechte kümmern. Es ist normal, jene zu benutzen, die im CMFCore-Paket enthalten sind: Add portal content, Modify portal content und View. Wieder zurück in der Konfigurationsdatei können Sie die benötigten Rechte wie folgt ändern:

from Products.CMFCore import CMFCorePermissions
 
add_permission = CMFCorePermissions.AddPortalContent
edit_permission = CMFCorePermissions.ModifyPortalContent
view_permission = CMFCorePermissions.View

Das heißt, die Variable add_permission steht für das aus CMFCorePermissions importierte Recht. Rechte haben nichts Magisches an sich, jedes einzelne Recht ist lediglich ein String. Die Verwendung des eingebauten Rechts ist bequem und für Ihre Benutzer leicht zu verstehen. Plone ist bereits so konfiguriert, dass es den richtigen Personen erlaubt, Inhalte mit Hilfe des Rechts Add portal content zu erstellen. Außerdem ist der Standard-Workflow so definiert, dass er diese Rechte benutzt und ändert. Diese Rechte waren es, die Sie zur Factory-Typinformation hinzugefügt haben.

Sollten Sie eigene Rechte erstellen wollen, könnten Sie das recht einfach tun. Angenommen, Sie möchten das Recht Add in das eigenständige Recht Add Source Code ändern. Dann würden Sie die Datei wie folgt ändern:

add_permission = "Add Source Code"

Nachdem das Produkt importiert worden wäre, hätten Sie im Security-Reiter ein neues Recht namens Add Source Code. Warum würden Sie das tun? Nun, es ist bequem, ein Recht zu benutzen, das jeder andere auch benutzt. Allerdings könnte es sein, dass Sie eine feinere Granularität oder andere Sicherheitseinstellungen benötigen. Aus diesem Grund können Sie Ihre eigenen Sicherheitseinstellungen einfach selbst anfertigen.

Die Initialisierung abschließen

Nun müssen Sie die Initialisierung des Produkts einrichten. Das machen Sie im Modul __init__.py, damit dann, wenn Zope diese Datei beim Hochfahren liest, es die Initialisierung des Produkts abschließt, wie in Listing 12.4 gezeigt wird.

Listing 12.4. Das Modul __init__.py

import PloneSilverCity
 
from Products.CMFCore import utils
from Products.CMFCore.DirectoryView import registerDirectory
 
from config import product_name, add_permission
 
contentConstructors = (PloneSilverCity.addPloneSilverCity,)
contentClasses = (PloneSilverCity.PloneSilverCity,)
contentFTI = (PloneSilverCity.factory_type_information,)
 
registerDirectory('skins', globals())
 
def initialize(context):
    product = utils.ContentInit(product_name,
        content_types = contentClasses,
        permission = add_permission,
        extra_constructors = contentConstructors,
        fti = contentFTI)
    product.initialize(context)

Was passiert in diesem Code? Nun, eigentlich nicht sehr viel, der Code ist nur ein wenig ausführlich. Zuerst legen Sie Referenzen auf Klassen und Konstruktoren an, die in contentClasses und contentConstructors verwendet werden. Diese legen die Factory-Funktion zur Erstellung der Objekte und die eigentliche Klasse fest. Diese werden dann an die Funktion ContentInit übergeben, und zwar in initialize, einer speziellen Funktion, die während der Produktinitialisierung aufgerufen wird. ContentInit verrichtet die ganze Arbeit, um das Produkt in Plone einzurichten. Die Parameter dieser Funktion sind die folgenden:

  • product_name: Der Name des Produkts, wie er in der Konfigurationsdatei definiert ist (in diesem Fall PloneSilverCity).
  • content_types: Das Tupel der Klassen, die dieses Produkt definieren. Normalerweise ist das nur eine Klasse, es können aber auch mehrere sein.
  • permission: Das Recht, das benötigt wird, um eine Instanz dieses Objekts zu erzeugen; in diesem Fall die Variable add_permission, die ich in config.py definiert habe.
  • fti: Steht für Factory-Typinformation und ist das Dictionary mit der Factory-Typinformation, die Sie im Modul PloneSilverCity.py für den Inhalt definiert haben.

Produkt-Module ändern

Nun können Sie zum Modul PloneSilverCity.py und zu der Aufgabe zurückkehren, es in ein Plone-Produkt zu verwandeln. Am Anfang des Moduls werden Sie die import-Anweisungen erstellen. Diese holen wie folgt aus unterschiedlichen Orten verschiedene Dinge, die für die Plone-Initialisierung notwendig sind:

from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent

Diese Import-Anweisungen stellen die Grundfunktionalität für das Produkt bereit und kommen bei den meisten Inhaltstypen vor. Die importierten Definitionen lauten wie folgt:

  • InitializeClass: Diese Funktion initialisiert die Klasse und wendet alle Sicherheitsdeklarationen an, die diese haben wird. Diese Sicherheitsdeklarationen geben Sie durch die Verwendung der Klasse ClassSecurityInfo an.
  • ClassSecurityInfo: Diese Klasse bietet eine Reihe von Sicherheitsmethoden, mit denen Sie den Zugriff auf die Methoden des Inhaltstyps beschränken können.
  • DefaultDublinCoreImpl: Diese Klasse enthält eine Implementation von Dublin Core-Metadaten. Der Dublin Core wurde in Kapitel 11 behandelt. Sie verleiht einem Objekt alle Dublin Core-Attribute und -Methoden für den Zugriff darauf, wie Title, Description, Creator usw.
  • PortalContent: Dies ist die Basisklasse für alle Inhalte in einer Plone-Site und einige der darin benötigten Schlüsselattribute. Durch die Verwendung dieser Basisklasse erhält das Objekt eine ganze Menge an Funktionalität, z.B. zur persistenten Speicherung des Objekts in der Datenbank, der Katalogisierung des Objekts für die Suche mit dem Objekt portal_catalog und seiner Registrierbarkeit mit dem Werkzeug portal_types.

Außerdem müssen Sie die Konfigurationsvariablen und Rechte importieren. Das erfolgt mit den beiden folgenden Zeilen:

from config import plone_product_name, product_name
from config import add_permission, edit_permission, view_permission

In der Klasse wiederum müssen Sie zwei Basisklassen hinzufügen, um sie vollständig Plone-kompatibel zu machen: PortalContent und DefaultDublinCoreImpl. Weiterhin müssen Sie der Klasse einen meta_type geben. In Zope hat jedes Produkt einen eindeutigen meta_type:

class PloneSilverCity(PortalContent, DefaultDublinCoreImpl):
    meta_type = product_name

Plone muss wissen, welche Basisklassen der Inhaltstyp implementiert. Andere Teile der Anwendung müssen wissen, welche Klassen implementiert werden. Geben Sie daher wie folgt explizit an, welche Klassen der Inhaltstyp implementiert:

__implements__ = (
    PortalContent.__implements__,
    DefaultDublinCoreImpl.__implements__
    )
Sicherheit zur Klasse hinzufügen

Wenn Sie bereits entschieden haben, die Aktionen abzusichern, müssen Sie diese Sicherheitsmechanismen auch auf die Klasse anwenden. In einer Umgebung wie Plone, in der Objekte veröffentlicht werden, kann jeder alle Methoden der Klasse über das Web aufrufen, es sei denn, sie beginnen mit einem Unterstrich. Das ist natürlich nicht so gut, und daher müssen Sie all Ihre Methoden schützen.

Um das in der Klasse selbst zu tun, erstellen Sie eine Instanz der Klasse ClassSecurityInfo mit der folgenden Zeile:

security = ClassSecurityInfo()

Dieses Sicherheitsobjekt bietet eine Schnittstelle zum Sicherheitsapparat von Zope. Dann können Sie Methoden auf das Objekt anwenden. Meine Lieblingsmethode dafür besteht darin, direkt über der Methode eine Zeile hinzuzufügen, in der die Sicherheit angebracht wird. Auf diese Weise kann man sich leicht merken, wo die Sicherheit angewendet wird, und später werden Sie nicht vergessen, sie zu aktualisieren, falls das nötig sein sollte. Die Methode declareProtected erwartet das Recht und den Methodennamen, um die edit-Methode zu schützen. Tun Sie also Folgendes, damit nur diejenigen sie aufrufen können, die das Recht zum Bearbeiten haben:

security.declareProtected(edit_permission, "edit")

Das wiederholen Sie für jede Methode, wobei Sie das passende Recht und den entsprechenden Methodennamen angeben. Die einzige Methode, die geschützt werden muss, ist __init__, weil sie mit einem Unterstrich beginnt. Um diese ganze Sicherheit anzuwenden, müssen Sie die Klasse initialisieren. Ohne diesen Schritt wird die gesamte anschließend deklarierte Sicherheit nicht angewendet, d.h., Ihr Objekt wird öffentlich zugänglich sein.

Mit anderen Worten, vergessen Sie die folgende Zeile nicht:

InitializeClass(PloneSilverCity)

Die API für ClassSecurityInfo enthält die folgenden Methoden für die Klasse:

  • declarePublic: Erwartet eine Liste von Namen. Alle Namen werden für alle Benutzer als öffentlich zugänglich über eingeschränkten Code wie auch über das Web deklariert.
  • declarePrivate: Erwartet eine Liste von Namen. Alle Namen sind privat und sind nicht über eingeschränkten Code zugänglich.
  • declareProtected: Erwartet ein Recht und eine beliebige Zahl von Namen. Alle Namen sind nur mit dem angegebenen Recht zugänglich.
  • declareObjectPublic: Setzt das gesamte Objekt als öffentlich zugänglich.
  • declareObjectPrivate: Setzt das gesamte Objekt als privat und unzugänglich für eingeschränkten Code.

Mit diesen Methoden können Sie fast jede gewünschte Sicherheit einstellen. Allerdings fand ich es fast immer ausreichend, den Schutz für alle Methoden mit einem bestimmten Recht explizit zu setzen.

Integration mit der Suche

Im vorigen Kapitel habe ich Ihnen gezeigt, wie die Suche funktioniert und welche Indizes existieren. Da die Indizes mit Dublin Core-Objekten arbeiten und Sie Dublin Core als Basisklasse verwendet haben, werden Titel, Beschreibung, Erzeuger, Änderungsdatum usw. Ihres Objekts alle für Sie indiziert, d.h., es ist keine weitere Arbeit dazu nötig. Durch das Ableiten von der Klasse PortalContent wird weiterhin bei jeder Änderung des Objekts der Katalog für Sie aktualisiert. Auch hierbei müssen Sie sich um nichts weiter kümmern.

Ein bestimmter Index jedoch benötigt ein wenig Unterstützung, nämlich SearchableText. Wie ich zuvor demonstriert habe, bietet der Index SearchableText einen Volltext-Index, den Plone verwendet, wenn eine Suche ausgeführt wird. Es wäre nett, wenn der Index auch im Quelltext suchen würde, damit beim Hochladen eines Codes mit einem import darin diese Anweisung von der Suche erfasst würde. Da der Katalog im Objekt nachschaut und versucht, ein Attribut oder eine Methode mit dem entsprechenden Namen des Indexes zu finden, müssen Sie lediglich eine Methode mit diesem Namen erstellen, die den gewünschten Wert zurückgibt.

Am einfachsten macht man das, indem man einen String aus den gewünschten Feldern erstellt, z.B. dem Titel, der Beschreibung und dem rohen Code. Mit dem Recht View lässt sich das schützen, denn jeder, der sich das Objekt anschaut, kann den Inhalt sowieso ohne weiteres sehen. Folgendes ist eine SearchableText-Methode, die diese Aufgabe erfüllt:

security.declareProtected(view_permission, "SearchableText")
def SearchableText(self):
    """ Used by the catalog for basic full text indexing """
    return "%s %s %s" % ( self.Title()
                        , self.Description()
                        , self._raw
                        )
Der Unterschied zwischen einer Python- und einer Plone-Klasse

Wie Sie sehen, besteht ein beträchtlicher Unterschied zwischen einem normalen Python-Produkt und einem, das in Plone registriert ist. Allerdings liegen die meisten dieser Unterschiede darin, wie das Produkt registriert und die Sicherheit gewährleistet wird. Die eigentliche Klasse ist sehr ähnlich. Listing 12.5 beschreibt alle Unterschiede zwischen der reinen Python- und der Plone-Implementierung.

Listing 12.5. Die Plone-Version der Klasse

from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent

from config import meta_type, product_name
from config import add_permission, edit_permission, view_permission
from source import generate_html, list_generators

factory_type_information = {
     'id': plone_product_name,
     'meta_type': product_name,
     'description': ('Provides syntax highlighted HTML of source code.'),
     'product': product_name,
     'factory': 'addPloneSilverCity',
     'content_icon': 'silvercity.gif',
     'immediate_view': 'view',
     'actions': (
                 {'id': 'view',
                  'name': 'View',
                  'action': 'silvercity_view_form',
                  'permissions': (view_permission,)},
                 {'id': 'source',
                  'name': 'View source',
                  'action': 'getRawCode',
                  'permissions': (view_permission,)},
                 {'id': 'edit',
                  'name': 'Edit',
                  'action': 'silvercity_edit_form',
                  'permissions': (edit_permission,)},
                 ),

     }

def addPloneSilverCity(self, id, REQUEST=None):
    """ This is our factory function and creates
    an empty PloneSilverCity object inside our Plone
    site """
    obj = PloneSilverCity(id)
    self._setObject(id, obj)

class PloneSilverCity(PortalContent, DefaultDublinCoreImpl):

    meta_type = product_name

    __implements__ = (
        PortalContent.__implements__,
        DefaultDublinCoreImpl.__implements__
        )

    security = ClassSecurityInfo()
    def __init__(self, id):
        DefaultDublinCoreImpl.__init__(self)
        self.id = id
        self._raw = ""
        self._raw_as_html = ""
        self._raw_language = None

    security.declareProtected(edit_permission, "edit")
    def edit(self, language, raw_code, file=""):
        """ The edit function, that sets
        all our parameters, and turns the code
        into pretty HTML """
        filename = ""
        if file:
            file_code = file.read()
 
            # if there is a file and its not blank...
            if file_code:
                raw_code = file_code
                if hasattr(file, "name"):
                    filename = file.name
                else:
                    filename = file.filename
                # set the language to None so set by SilverCity
                language = None
 
        self._raw = raw_code
 
        # our function, generate_html does the hard work here
        html, language = generate_html(raw_code, language, filename)
        self._raw_as_html = html
        self._raw_language = language

    security.declareProtected(view_permission, "getLanguage")
    def getLanguage(self):
        """ Returns the language that code has been lexed with """
        return self._raw_language

    security.declareProtected(view_permission, "getLanguages")
    def getLanguages(self):
        """ Returns the list of languages available """
        langs = []
 
        for name, description in list_generators():
            # these names are normally in uppercase
            langs.append( {'name':lang, 'value':language } )
 
        langs.sort()
        return langs

    security.declareProtected(view_permission, "getRawCode")
    def getRawCode(self):
        """ Returns the raw code """
        return self._raw

    security.declareProtected(view_permission, "getHTMLCode")
    def getHTMLCode(self):
        """ Returns the html code """
        return self._raw_as_html

    security.declareProtected(view_permission, "SearchableText")
    def SearchableText(self):

        """ Used by the catalog for basic full text indexing """

        return "%s %s %s" % ( self.Title()
                            , self.Description()
                            , self._raw
                            )

InitializeClass(PloneSilverCity)

Skins hinzufügen

Nun, da Sie den Hauptcode haben, bleiben noch zwei Dinge für Sie zu tun: die Skins und eine Installationsmethode erstellen. Die Skins gehören zu den einfacheren Teilen, weil ein Großteil der notwendigen Arbeit bereits vom Plone-Framework erledigt wird. Skins habe ich detailliert in vorangegangenen Kapiteln behandelt, in denen ich erörtert habe, wie man im Dateisystem eine Skin für eine Plone-Site erstellt. Jedes Produkt, das eine eigene Benutzerschnittstelle haben muss (UI, User Interface), macht das mit einem eigenen FSDV (File System Directory View), d.h., hier werdn Sie erneut das Gleiche machen.

Skins kommen in das Verzeichnis skins des Produkts. Dieser Verzeichnisname wird in der Datei __init__.py definiert, wo Sie das Verzeichnis mit der Funktion registerDirectory registrieren. Wenn Sie diesen Namen ändern möchten, sollten Sie ihn auf jeden Fall registrieren. Sie können so viele Verzeichnisse registrieren, wie Sie wollen, aber das ist rekursiv und dabei wird auch alles in und unter diesem registrierten Verzeichnis registriert.

Die einfachste all Ihrer Aufgaben bei diesem Produkt besteht darin, ein Icon für das Objekt hinzuzufügen, das in Plone erscheint. Der Name dieses Icons wird bereits in der Factory-Typinformation mit der Zeile 'content_icon': 'silvercity.gif' definiert, d.h., Sie müssen lediglich ein Icon namens silvercity.gif im Verzeichnis skins hinzufügen. Dieses Icon wird immer dann angezeigt, wenn Sie das Objekt in der Benutzerschnittstelle von Plone anzeigen. Wenn SilverCity eine Datei lext, gibt es HTML aus, in dem CSS-Tags benutzt werden, d.h., Sie müssen sicherstellen, dass die entsprechende CSS-Datei auch verfügbar ist. Bei diesem Produkt kopieren Sie einfach das CSS aus dem SilverCity-Produkt und setzen es in das Verzeichnis skins.

Diese zwei Dinge sind nun erledigt. Nun müssen Sie als Nächstes wirklich die Seiten zum Anzeigen und Bearbeiten der Seiten schreiben. Ich habe zuvor die Ähnlichkeit zu einem Dokument beschrieben, wenn Sie also nach Seiten zum Anzeigen und Bearbeiten suchen, dann sind die Seiten zu einem Dokument der beste Platz dafür. Dies sind document_view.pt und document_edit_form.cpt, die sich im Verzeichnis CMFPlone/skins/plone_content befinden.

Die Anzeigen-Seite erstellen

Um die Anzeigen-Seite zu ändern, nehmen Sie die Anzeigen-Seite zu einem Dokument, kopieren sie in das Verzeichnis skins Ihres Produkts und benennen sie in silvercity_view.pt um. Es gibt keinen Grund, die gesamte Seite neu zu erstellen, wenn diese Anzeigen-Seite so ähnlich ist und Sie lediglich zwei kleine Änderungen vornehmen müssen.

Wie schon erwähnt, spuckt SilverCity HTML aus, in dem der ganze Code mit Hilfe von CSS hervorgehoben wurde, zu dem Sie ein eigenes Stylesheet haben. Sie müssen dafür sorgen, dass die Anzeigen-Seite dieses CSS einfügt und das Haupt-Template einen Slot für CSS namens css_slot besitzt. Um die eigene CSS-Datei in diesen Slot einzufügen, müssen Sie dafür nur einen Wert angeben. Beispiel:

<metal:cssslot fill-slot="css_slot">
 
<link
   rel="stylesheet"
   href=""
   tal:attributes="href string:$portal_url/silvercity.css" />
</metal:cssslot>

Hierbei referenzieren Sie eine CSS-Datei namens silvercity.css. Diese befindet sich im Verzeichnis skins, und Sie werden aus der Skin darauf zugreifen, wenn diese dargestellt wird. Das Originaldokument hat eine Eigenschaft namens cookedBody, was ein Attribut eines Dokuments ist. Ich habe diesen Teil des Codes entfernt und stattdessen den Code eingefügt. Wie Sie bis hierher gesehen haben, gibt die Funktion getHTMLCode das HTML zurück, also müssen Sie nur noch Folgendes tun:

<div id="bodyContent">
    <div tal:replace="structure here/getHTMLCode" />
</div>

Wenn Sie irgendetwas anderes Bestimmtes in diesem Page Template ändern möchten, dann ist jetzt die Gelegenheit dazu. Es wäre eventuell nett, z.B. die Sprache anzuzeigen, in der es geschrieben wurde, oder ein Icon, oder seinen Verlauf zu ändern.

Die Bearbeiten-Seite erstellen

Wie bei der Anzeigen-Seite können Sie auch die Bearbeiten-Seite nehmen, in die Skin kopieren und dort in silvercity_edit_form.cpt umbenennen. Das größte Problem dabei ist, dass das Bearbeiten-Formular so entworfen ist, dass es mit einem WYSIWYG-Editor (What You See Is What You Get) wie Epoz benutzt werden kann. Solange kein guter WYSIWYG-Editor für Quellcode in Webbrowsern verfügbar ist, müssen Sie das ausschalten, weil Sie in einem HTML-Editor z.B. kein SQL schreiben können.

Dies ist eine ziemlich große Änderung des Page Templates. Erinnern Sie sich daran, dass Sie es von der Website zum Plone-Buch herunterladen können. Entfernen Sie in diesem Template alle Stellen, in denen der Editor erwähnt wird, und ersetzen Sie sie durch einen einfachen HTML-Textbereich. Lassen Sie den Namen des HTML-Felds unverändert, denn es gibt keinen guten Grund, ihn zu ändern. Außerdem verhält es sich später dann so, wie es soll mit dem Skript, das das Formular auswertet. Ein Dokument hat unten eine Reihe von Auswahlmöglichkeiten für das Format, was normalerweise Einträge wie Einfacher Text, HTML usw. sind. Das werden Sie durch ein Dropdown-Menü für alle Sprachen ersetzen, über die die SilverCity-Hauptbibliothek verfügt. Die zuvor geschriebene Methode getLanguages gibt eine Liste aller Sprachen zurück. Jedes Element ist ein Dictionary, das den Wert, z.B. CPP, und einen netten Namen dafür, z.B. C oder C++, enthält.

Listing 12.6 geht in einer Schleife über die zuvor geschriebene Methode getLanguages. Sie können auch eine Laufvariable für die aktuelle Sprache definieren, damit Sie die aktuelle Sprache in der Schleife über die Sprachen hervorheben können.

Listing 12.6. Hinzufügen eines Dropdown-Menüs für die Sprachauswahl

<div class="field">
  <label
   for="language"
   i18n:translate="label_silvercity_language">
Language</label>
 
    <div class="formHelp" i18n:translate="help_silvercity_language">
        Select the name of the language that you are adding
    </div>
    <select name="text_format"
            tal:define="l here/getLanguage">
        <option tal:repeat="item here/getLanguages"
            tal:content="item/name"
            tal:attributes="value item/value;
                            selected python:test(item['value'] == l, 1, 0)" />
    </select>
</div>

Wenn die Bearbeiten-Seite abgeschickt wird, müssen Sie die Validierer und Aktionen so einrichten, dass sie etwas mit dem Formular machen. Die Validierung sollte überprüfen, ob ein gültiger Titel und eine gültige ID angegeben wurden. Fügen Sie Folgendes zur Datei silvercity_edit.cpt.metadata hinzu:

[validators]
validators..Save = validate_id,validate_title
validators..Cancel =

Woher kommen diese Validierungen? Nun, ich habe ein wenig geschummelt und habe mir wieder die Validierungen eines Dokuments angeschaut. Dabei erfolgen drei Validierungen, von denen Sie aber nur zwei benötigen. Durch die Überprüfung dessen, was diese Validierung ergibt, können Sie sehen, welche benötigt werden und welche nicht. Sie finden alle Validierungen in plone_skins/plone_form_scripts, und deren Objektname beginnt mit validation.

Nun benötigen Sie die Aktion, also nehmen Sie das Bearbeiten-Skript eines Dokuments (document_edit.cpy) und kopieren es nach SilverCity. Zum größten Teil ist das Skript so in Ordnung, d.h., Sie können es mit nur einer Änderung beibehalten. Ändern Sie die Meldungen von Document in Source code. Listing 12.7 zeigt das Bearbeiten-Skript.

Listing 12.7. Das Bearbeiten-Skript

##parameters=text_format, text, file='', SafteyBelt='', 
  title='', description='', id=''
##title=Edit a document
 
filename=getattr(file,'filename', '')
if file and filename:
    # if there is no id, use the filename as the id
    if not id:
        id = filename[max( filename.rfind('/')
                       , filename.rfind('\\')
                       , filename.rfind(':') )+1:]
    file.seek(0)
 
# if there is no id specified, keep the current one
if not id:
    id = context.getId()
 
new_context = context.portal_factory.doCreate(context, id)
new_context.edit( text_format
                , text
                , file
                , safety_belt=SafetyBelt )
 
from Products.CMFPlone import transaction_note
transaction_note('Edited source code %s at %s' % 
  (new_context.title_or_id(), new_context.absolute_url()) 
  )
 
new_context.plone_utils.contentEdit( new_context
                                   , id=id
                                   , title=title
                                   , description=description )
 
return state.set(context=new_context,
  portal_status_message='Source code changes saved.')

Dieses Skript macht ein paar Dinge. Zuerst holt es den Dateinamen, falls einer existiert. Falls keine ID angegeben ist, wird die ID auf diesen Dateinamen gesetzt. Das bedeutet: Wenn ein Benutzer library.c hochlädt, wird die ID dieses Objekts library.c sein. Als Zweites weist es portal_factory an, ein Objekt zu erstellen (siehe den Kasten "Portal Factory" für weitere Informationen darüber, was das bedeutet). Dann ruft es die edit-Methode auf dem Objekt (die Sie zuvor geschrieben haben) und contentEdit im Werkzeug plone_utils auf. Ohne jetzt tiefer ins Werkzeug plone_utils zu schauen, nimmt contentEdit die angegebenen Schlüsselwörter und ändert diese Attribute, falls die Klasse Dublin Core implementiert. Da Sie das Attribut __implements__ vorher eingerichtet haben, wird die edit-Methode in Listing 12.7 diese Arbeit für Sie erledigen. Alle Änderungen an Titel, ID oder Beschreibung werden im Objekt geändert.

Installation des Produkts in Plone

Es gibt eine standardisierte Art, ein Produkt in Plone zu installieren: Sie gehen ins Plone-Control Panel und klicken auf das Produkt, um es zu installieren. Dieses Skript benutzt das Werkzeug portal_quickinstaller für die Installation. Damit dieses Produkt funktioniert, müssen Sie eine gewisse Funktionalität offen legen, die das Werkzeug lesen kann. Schließlich wollen Sie, dass so viele Leute wie möglich Ihr Produkt benutzen. Wenn Sie etwas nur für den internen Gebrauch schreiben und Sie es nicht an andere Leute verteilen, können Sie diesen Schritt auslassen. Aber Sie müssen diese Schritte sowieso von Hand ausführen, und es ist immer besser, man hat ein Skript für die Installation.

Hinweis

Der Quick Installer macht aus dieser Installationsfunktion eine externe Methode und führt sie hinter den Kulissen für Sie aus. Außerdem führt er auch noch einige weitere Aufgaben aus. Das heißt, wenn Sie möchten, könnten Sie eine externe Methode erstellen, um das zu machen. Deswegen steht in den Installationsanweisungen zu den meisten Produkten, dass Sie eine externe Methode erstellen sollen.

Für eine Integration mit dem Quick Installer müssen Sie ein spezielles Modul namens Install.py im Verzeichnis Extensions erstellen. Dieses Modul muss eine Funktion namens install enthalten. Das Quick Installer-Werkzeug führt die Funktion install aus, und die Ausgabe landet in einer Datei auf dem Server. Da die Methode install das Produkt in den Portal-Typen installieren muss, fügen Sie nun ein FSDV hinzu, das auf das Verzeichnis skins zeigt, und fügen dieses neue Verzeichnis zu den Skin-Ebenen hinzu.

Importieren Sie nun die Funktionen, und richten Sie die Variablen wie üblich ein. Die factory_type_information müssen Sie aus dem Produkt installieren, damit Sie es im Skript verwenden können, wie in Listing 12.8 zu sehen ist.

Listing 12.8. Der Anfang der Installationsfunktion

from Products.CMFCore.TypesTool import ContentFactoryMetadata
from Products.CMFCore.DirectoryView import createDirectoryView
from Products.CMFCore.utils import getToolByName
from Products.PloneSilverCity.PloneSilverCity import factory_type_information
 
from Products.PloneSilverCity.config import plone_product_name, product_name
from Products.PloneSilverCity.config import layer_name, layer_location
 
def install(self):
    """ Install this product """

Danach ist alles generisch und könnte auf jedem Produkt laufen, außer dann natürlich, wenn Sie möchten, dass es bei der Installation irgendwas Spezielles tut. Um Ihr Produkt zum Werkzeug portal_types hinzuzufügen, überprüfen Sie zuerst, ob Ihr Produkt schon registriert ist. Es könnte sein, dass ein anderer schon ein anderes Produkt mit dem gleichen Namen installiert hat. Dazu rufen Sie die Methode manage_addTypeInformation auf, wie in Listing 12.9 angegeben.

Listing 12-9. Rest der Installationsfunktion

out = []
typesTool = getToolByName(self, 'portal_types')
skinsTool = getToolByName(self, 'portal_skins')

if id not in typesTool.objectIds():
   typesTool.manage_addTypeInformation(
       add_meta_type =  factory_type_information['meta_type']
       id = factory_type_information['id']
       )
   out.append('Registered with the types tool')
else:
   out.append('Object "%s" already existed in the types tool' % (id))

Als Nächstes müssen Sie ein FSDV im skins-Verzeichnis hinzufügen. Wieder müssen Sie als Erstes prüfen, ob Sie nicht schon eines haben. Dann fügen Sie die Verzeichnisansicht wie folgt hinzu:

if skinname not in skinsTool.objectIds():
    createDirectoryView(skinsTool, skinlocation, skinname)
    out.append('Added "%s" directory view to portal_skins' % skinname)

Und schließlich iterieren Sie über alle Skins und fügen Ihr neues FSDV zu allen Skins hinzu. Dies ist eine generische Funktion. Jede Skin wird als String aufgeführt, in dem alle Ebenen mit Kommata voneinander getrennt sind. Nun müssen Sie den String nur noch aufteilen und Ihre neue Skin nach der Ebene namens custom einfügen wie in Listing 12.10 zu sehen ist.

Listing 12-10. Die Skin in der Installationsmethode setzen

skins = skinsTool.getSkinSelections()
for skin in skins:
    path = skinsTool.getSkinPath(skin)
    path = [ p.strip() for p in p.split(',') ]
    if skinname not in path:
        path.insert(path.index('custom')+1, skinname)

        path = ", ".join(path)
        skinsTool.addSkinSelection(skin, path)
        out.append('Added "%s" to "%s" skins' % (skinname, skin))
    else:
        out.append('Skipping "%s" skin' % skin)
return "\n".join(out)

Das wars. Nun ist Ihr Produkt bereit, ausgeführt zu werden.

Das Produkt testen

Um das Produkt zu testen, starten Sie Ihre Plone-Instanz neu, damit sie das Produktverzeichnis einliest. Wenn Sie Ihr Produkt noch nicht im entsprechenden Produktverzeichnis entwickelt haben, dann kopieren Sie es jetzt dorthin, damit es Teil Ihres normalen Installationsprozesses wird. Wenn es mit Ihrem Produkt irgendwelche Probleme gibt, dann startet Zope eventuell noch, aber das Produkt wird dann unter Umständen im Control Panel als defekt angezeigt.

Dann installieren Sie es in Plone mit Hilfe der Seite Produkte hinzufügen/löschen im Plone-Control Panel. Nun sollten Sie zu einem Ordner gehen und ein Quellcode-Objekt hinzufügen können. Das Icon wird Ihr Icon in der Skin sein, und der Name ist jener, den Sie im Dateisystem definiert haben. Danach erhalten Sie die Bearbeiten-Seite. Beachten Sie, dass die URL nun mit silvercity_edit_form endet und das passend geänderte Bearbeiten-Formular anzeigt.

Sie könnten weiteren Code hinzufügen, eine Sprache auswählen und auf Speichern klicken, oder Sie könnten eine Datei von Ihrem Computer hochladen. Nach einem Klick auf Speichern gelangen Sie zu der Anzeigefunktion zurück, und es wird hoffentlich der Code mit der hervorgehobenen Syntax angezeigt.

Dieses Produkt ist ein kleines Beispiel dafür, wie einfach man ein Produkt in Plone schreiben kann. Auch wenn es viele Seiten waren, wurden auf den meisten die Infrastruktur und die Skins eingerichtet. Es läge jetzt nahe, das mit anderen Web-Skriptsprachen wie PHP zu vergleichen. Aber Sie müssen daran denken, dass Sie dadurch, dass Ihr Code in Plone ist, eine ganze Menge Dinge erreicht haben, ohne dafür etwas neu schreiben zu müssen. Insbesondere haben Sie Folgendes erreicht:

  • Volltext-Suche im Inhalt
  • Integration mit dem Workflow
  • Integration mit Portal-Mitgliedschaft und -Authentifikation
  • Persistenz dank der Plone-Datenbank, ohne SQL schreiben oder andere Arbeit verrichten zu müssen

Außerdem kann später dadurch Ihr ganzes Produkt wirklich besser skalieren. Wenn Sie z.B. ein Bug-Tracking-System benötigen, nehmen Sie das Collector-Produkt hinzu, und wenn Sie ein Produkt zur Bilderverwaltung benötigen, nehmen Sie CMFPhoto. Indem Sie das Framework verwenden, verleihen Sie Ihrer ganzen Site eine Menge an Flexibilität und Skalierbarkeit.

Auch wenn dieses Produkt insofern ein wenig mogelt, als dass es eine Menge an vorhandenem Code wiederverwendet, so demonstriert es doch eine ganze Reihe von Schlüsselfunktionen beim Schreiben eines Produkts in Plone.

Fehlersuche bei der Entwicklung

Wenn Sie Ihr eigenes Produkt entwickeln, werden Sie früher oder später zwei Dinge feststellen (es sei denn, Sie verfügen über so viel Zen, dass Sie eigentlich am Code des Zope-Kerns mitschreiben sollten): Ihr Produkt versagt, und Sie müssen den Fehler finden.

Während der Entwicklung möchten Sie eventuell versuchen, das Produkt vom Python-Prompt aus zu importieren, um zu sehen, wie es funktioniert. Leider werden Sie dabei sehr wahrscheinlich einen Fehler bekommen. Das liegt daran, dass Sie bei diesem Import eine Lawine an Zope-bezogenen Imports auslösen. Mit einigen davon können Sie fertig werden, aber nicht mit allen. Eines der häufigen Probleme besteht darin, dass Sie den folgenden Fehler bekommen:

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "PloneSilverCity/__init__.py", line 1, in ?
    import PloneSilverCity
  File "PloneSilverCity/PloneSilverCity.py", line 4, in ?
    from Globals import InitializeClass
  File "/opt/Zope-2.7/lib/python/Globals.py", line 23, in ?
    import Acquisition, ComputedAttribute, App.PersistentExtra, os
  File "/opt/Zope-2.7/lib/python/App/PersistentExtra.py", line 15, in ?
    from Persistence import Persistent
ImportError: cannot import name Persistent

Das können Sie lösen, indem Sie sicherstellen, dass Sie vor dem Import von PloneSilverCity erst Zope importieren und die Startup-Methode ausführen. Um Zope importieren zu können, muss das Verzeichnis, das Zope enthält, in Ihrem Pfad enthalten sein. Auf meinem Computer ist das /opt/Zope-2.7/lib/python. Allerdings werden Sie dann beim Versuch, CMFCore zu importieren, Fehler erhalten, falls Sie Zope mit der Option "Instance Home" konfiguriert haben, was vermutlich der Fall ist.

Am leichtesten kann man PloneSilverCity importieren, indem man Zope mit zopectl von der Kommandozeile im Debug-Modus ausführt. Dabei wird ein Python-Prompt aufgemacht, unter dem Sie von Python direkt auf die Zope-Datenbank zugreifen können. Kapitel 14 behandelt das etwas detaillierter, aber Sie können es auch leicht jetzt tun (vorausgesetzt, dass Ihr Zope gerade nicht läuft). Das Skript zopectl finden Sie im Verzeichnis bin Ihrer Zope-Instanz. Auf meinem Computer z.B. ist das /var/zope/bin. Listing 12.11 zeigt ein Beispiel eines laufenden zopectl mit PloneSilverCity.

Hinweis

Beim Niederschreiben dieser Zeilen funktioniert zopectl nicht unter Windows. Auf Linux jedoch ist es eine bequeme Art, Ihren Code zu testen. Leider verlangt zopectl, dass man die Zope-Objektdatenbank (ZODB) sperrt, was man nicht machen kann, während Zope läuft, es sei denn, Sie setzen ZEO ein (was ich in Kapitel 14 erörtern werde).

Listing 12.11. Fehlersuche im Produkt mittels Zope

$ cd /var/zope/bin
$ ./zopectl debug
Starting debugger (the name "app" is bound to the top-level Zope object)
>>> from PloneSilverCity.PloneSilverCity import PloneSilverCity
>>> p = PloneSilverCity("test")
>>> p.edit("python", "import test")
>>> p.getRawCode()
'import test'
>>> p
<PloneSilverCity at test>

Falls Ihr Produkt defekt ist, bekommen Sie einen Traceback zu einer von zwei Stellen, entweder zur Fehlerprotokollseite oder zu einem der Ereignisprotokolle. Und wenn Sie es wirklich vermasselt haben, dann startet Plone erst gar nicht. Das passiert normalerweise, wenn ein Import fehlschlägt. Wenn das der Fall ist, startet Plone gar nicht erst. Ich empfehle dann, Plone von der Kommandozeile aus zu starten, egal ob unter Windows oder Linux. Mit der Ausgabe des Fehlers in der Konsole haben Sie eine sofortige Fehlermeldung. Listing 12.12 z.B. zeigt, was passiert, wenn Sie versuchen, Plone mit SilverCity zu starten, falls dabei ein beliebiger Import-Fehler auftritt.

Listing 12.12. Ein Beispielfehler beim Hochfahren

$ bin/runzope
------
2003-12-19T17:44:05 INFO(0) ZServer HTTP server started at Fri Dec 19 17:44:05 2003
        Hostname: laptop
        Port: 8080
------
2003-12-19T17:44:05 INFO(0) ZServer FTP server started at Fri Dec 19 17:44:05
2003
        Hostname: basil.agmweb.ca
        Port: 8021
------
2003-12-19T17:44:16 ERROR(200) Zope Could not import Products.PloneSilverCity
Traceback (most recent call last):
  File "/opt/Zope-2.7/lib/python/OFS/Application.py", line 533, in import_product
    product=__import__(pname, global_dict, global_dict, silly)
  File "/var/zope.zeo/Products/PloneSilverCity/__init__.py", line 1, in ?
    import PloneSilverCity
  File "/var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py", line 1
     import ThisModuleDoesNotExist
                                  ^
 ImportError: No module named ThismoduleDoesNotExist

An diesem Punkt stoppt Zope, und Sie müssen diesen Import reparieren, bevor Sie es erneut starten können. Wahrscheinlich ist das der am leichtesten zu reparierende Fehler. Er kommt aber wahrscheinlich nur dann vor, wenn Sie das neueste hippige Produkt aus dem Internet installieren, um festzustellen, dass es von einem Dutzend anderen Modulen abhängt.

Die nächste Art möglicher Fehler ist die von Fehlern innerhalb des Programms oder der Programmlogik. Angenommen, Ihr Produkt addiert zwei Zahlen, von denen eine aber ein String ist (was in Python ein Fehler ist). Es wird eine Fehlerausnahme aufgelöst, die Plone mit dem Fehlerwert und -typ an die Benutzerschnittstelle weitergibt. An dieser Stelle sollten Sie auf Plone-Konfiguration und Fehlerprotokoll klicken, um den Traceback anzuschauen, den Fehler zu finden und das Problem zu lösen.

Wenn Sie etwas im Produkt ändern, wird diese Änderung in Python nicht sofort umgesetzt. Um das zu erzwingen, müssen Sie ein Produkt namens Refresh verwenden. Das ist ein unglaublich nützliches Werkzeug für neue Entwickler, das Sie dadurch aktivieren, dass Sie eine Datei namens refresh.txt in Ihrem Verzeichnis Products haben. Sie werden bemerken, dass PloneSilverCity eine solche hat. Dann wählen Sie im ZMI das Control Panel, dann Products, gefolgt von PloneSilverCity (oder dem Namen Ihres Produkts), und klicken anschließend auf den Refresh-Reiter. Falls Ihr Produkt eine Datei namens refresh.txt enthält, können Sie auf den Refresh-Button klicken. Plone wird Ihr Produkt dann dynamisch mitsamt dem ganzen neuen Code neu laden. Falls Sie Zope im Debug-Modus betreiben, können Sie das Produkt so einstellen, dass es diese Überprüfung bei jedem Lauf dynamisch vornimmt, so dass Sie nicht jedes Mal zu diesem Bildschirm zurückkehren müssen.

Leider gibt es hierbei nicht nur Vorteile. Hinter den Kulissen passieren nämlich einige recht "interessante" Dinge mit Python, damit das funktioniert. Tatsächlich kann das Refresh-Produkt zu unerwarteten Ergebnissen in Ihrem Produkt führen, wenn auch zu nichts, was nicht durch einen Neustart von Zope zu reparieren wäre. Relativ einfache Produkte, die lediglich Manipulationen an Daten vornehmen, werden keine Probleme haben, andere hingegen schon. Zu Beginn werden Ihre Produkte jedoch eher einfacher Natur sein, und die Chancen stehen gut, dass Sie an dieser Stelle keine Probleme haben werden.

Und wenn schließlich doch etwas schief geht und Sie nicht herausfinden können, was es ist, dann müssen Sie zu einem Debugger greifen. Sie haben derart viele Möglichkeiten bei der Fehlersuche mit Zope, dass ich hier nur die eine beschreiben möchte, die ich selbst am meisten benutze, den Python-Debugger. Diesen Python-Debugger rufen Sie auf, indem Sie die folgende Zeile zu einem Stück Code hinzufügen:

import pdb; pdb.set_trace()

Tipp

In Python ist es unüblich, zwei Programmzeilen mit einem Semikolon hintereinander zu setzen. Hier ist das aber sehr praktisch, da später beim Löschen oder Auskommentieren nur eine Zeile betroffen ist.

Das bewirkt, dass ein Breakpoint in der Ausführung des Codes gesetzt wird, an dem Zope die Abarbeitung anhält und den Debugger aufruft. Das ist der Grund, warum man während der Entwicklung Plone wirklich besser von einer Konsole startet. Mit einem Dienst oder Daemon funktioniert das nicht, weil es keine Konsole gibt, mit der eine Verbindung möglich wäre. Wenn Sie Ihr Problem nun reproduzieren, gelangen Sie zum Python-Debugger, mit dem Sie den Fehler in Ihrem Produkt suchen können. In meinem nun reparierten und korrekt importierenden PloneSilverCity habe ich z.B. die folgende pdb-Trace-Funktion in die Methode getLanguages gesetzt:

def getLanguages(self):
     """ Returns the list of languages available """
     import pdb; pdb.set_trace()
     langs = []
     ...

Wenn Sie Zope nun starten und sich mit der Skin verbinden (was Sie bald hinzufügen werden), wird diese Funktion aufgerufen, und in der Konsole, in der Sie Zope gestartet haben, werden Sie Folgendes sehen können:

--Return--
> /var/tmp/python2.3-2.3.2-root/usr/lib/python2.3/pdb.py(992)set_trace()->None
-> Pdb().set_trace()
(Pdb)

Um eine Liste von Hilfestellungen zu erhalten, können Sie help eingeben. Die zwei wesentlichen Optionen sind n für nächstes und s für einen Schritt in ein Element hinein. Beispiel:

(Pdb) n
> /var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py(97)getLanguages()
-> langs = []
(Pdb) n
> /var/zope.zeo/Products/PloneSilverCity/PloneSilverCity.py(99)getLanguages()
-> for value, description in list_generators():
(Pdb) langs
[]

Für weitere Informationen zum Debugger empfehle ich die Online-Dokumentation auf der Python-Website (http://python.org/doc/current/lib/module-pdb.html). Sie haben auch andere Möglichkeiten, Fehler mit Zope zu suchen, z.B. können Sie ZEO verwenden, um einen Interpreter zu bekommen. ZEO wird in Kapitel 14 behandelt. Mit integrierten Entwicklerumgebungen wie Wing (http://wingide.com/wingide) oder Komodo (http://www.activestate.com/Products/Komodo) können Sie Fehler in Zope-Instanzen auch auf entfernten Rechnern suchen und haben dabei noch eine nette grafische Benutzerschnittstelle.

Eigene Werkzeuge schreiben

Ein Werkzeug ist vor allem deswegen einfacher zu schreiben als ein Inhaltstyp, weil man wenig tun muss, um das Produkt zu registrieren und weil die Benutzerschnittstelle einfach ist. Beispiel: ich benutze ein einfaches Statistikwerkzeug auf meiner ZopeZen-Website (http://www.zopezen.org), das mir Informationen über die Menge an Inhalten, die Anzahl der Benutzer usw. gibt. Dieses einfache Werkzeug gibt ein paar Zahlen aus, die mich als Manager der Site interessieren. Abbildung 12.2 zeigt meine ZopeZen-Statistik.

img/12-02.png

Figure 12-2. PloneStats auf ZopeZen

Das sind Statistiken über eine Website, die ich auch bekommen kann, indem ich die Web-Protokolldateien für meinen Plone-Server parse. Werkzeuge wie Analog, Webalizer, WebTrends usw. nehmen Ihnen gerne die Arbeit ab, Ihre Plone- oder Apache-Protokolldateien zu parsen. Auch hierbei gilt, dass Sie den gesamten Code zu diesem Projekt im Kollektiv unter http://sf.net/projects/collective im Paket PloneStats finden.

Das Werkzeug starten

Das Werkzeug sollten Sie auf die gleiche Weise in ein Produktverzeichnis setzen, wie Sie es beim Inhaltstyp gemacht haben, d.h., indem Sie ein Verzeichnis innerhalb des Produktverzeichnisses der Instanz erstellen. In diesen Ordner fügen Sie die Dateien refresh.txt, install.txt, readme.txt und __init__.py hinzu.

In diesem Verzeichnis lautet der Name des Hauptmoduls stats.py. Es enthält den gesamten Code zur Erstellung der Statistiken. Auch hier behandle ich das Aussehen des Moduls, ohne zusätzlichen Zope-Code zu betrachten. Da Sie es aber direkt mit den anderen Plone-Werkzeugen koppeln, macht es allerdings wenig Sinn, es außerhalb von Zope zu benutzen.

Listing 12.13 zeigt den Anfang des Werkzeugs. Dies ist eine einfache Version, die über zwei Methoden verfügt: eine, die die Anzahl der Inhaltstypen nach Typ und Workflow-Zustand zurückgibt, und eine andere für Benutzer, die die Gesamtzahl der Benutzer der Site zurückgibt.

Listing 12.13. Das grundlegende Statistik-Objekt

class Stats:
    def getContentTypes(self):
        """ Returns the number of documents by type """
        pc = getToolByName(self, "portal_catalog")
        # call the catalog and loop through the records
        results = pc()
        numbers = {"total":len(results),"bytype":{},"bystate":{}}
        for result in results:
            # set the number for the type
            ctype = str(result.Type)
            num = numbers["bytype"].get(ctype, 0)
            num += 1
            numbers["bytype"][ctype] = num
 
            # set the number for the state
            state = str(result.review_state)
            num = numbers["bystate"].get(state, 0)
            num += 1
            numbers["bystate"][state] = num
        return numbers
 
    def getUserCount(self):
        """ The number of users """
        pm = getToolByName(self, "portal_membership")
        count = len(pm.listMemberIds())
        return count

Das Paket in ein Werkzeug umwandeln

Um das Paket in ein Werkzeug umzuwandeln, müssen Sie den gleichen Prozess wie beim Inhaltstyp durchmachen. Mit anderen Worten, Sie müssen das Werkzeug im Modul __init__.py registrieren. Genau wie beim Beispiel mit dem Inhaltstyp erstellen Sie eine Datei namens config.py, die alle Konfigurationen enthält. Diese Datei sieht wie folgt aus:

from Products.CMFCore import CMFCorePermissions
 
view_permission = CMFCorePermissions.ManagePortal
 
product_name = "PloneStats"
unique_id = "plone_stats"

Die Sicherheit bei diesem Produkt ist einfacher, was daran liegt, dass das Produkt selbst recht einfach ist. Es interagiert lediglich mit anderen Werkzeugen und produziert einige Statistiken. Es gibt nichts, was Benutzer hinzufügen, bearbeiten oder löschen könnten, oder etwas, mit dem sie sonstwie interagieren könnten. Das heißt, Sie haben es nur mit einem einzigen Recht, ManagePortal, zu tun, dem Recht nämlich, die Konfiguration von Plone zu verwalten, das normalerweise nur an Manager vergeben wird. Also können nur Manager ins ZMI gehen und die Information sehen, die das Werkzeug bietet. Wenn Sie wollten, könnten Sie recht einfach eine hübsch ausschauende Skin für das Plone-Control Panel oder ein Portlet hinzufügen, das diese Information in Ihrer Site darstellt.

Was __init__.py anbelangt, fügen Sie nun den Initialisierungscode für das Werkzeug hinzu. Es gibt ein besonderes Initialisierungsskript für Werkzeuge namens ToolInit. In diesem Werkzeug sieht die Datei __init__.py wie folgt aus:

from Products.CMFCore import utils
from stats import Stats
from config import product_name
 
tools = (stats.Stats,)
 
def initialize(context):
    init = utils.ToolInit( product_name,
                    tools = tools,
                    product_name = product_name,
                    icon='tool.gif'
                    )
init.initialize(context)

Die Funktion ToolInit kann mehrere Werkzeuge annehmen. In diesem Fall haben Sie es aber nur mit einem zu tun. Bei mehreren Werkzugen können Sie nur einen Produktnamen und ein Produkt-Icon haben, das im ZMI angezeigt wird. Das ist alles, was man braucht, um das Werkzeug zu registrieren. Nun müssen Sie das Hauptmodul vervollständigen, um es in ein echtes Werkzeugobjekt zu verwandeln.

Den Werkzeug-Code ändern

Als Nächstes fügen Sie den Code zu der Klasse hinzu, um ihn in ein Werkzeug umzuwandeln. Wie beim Inhaltstyp besteht dieser Schritt nur aus dem Hinzufügen der Sicherheit durch Ableitung von den korrekten Basisklassen, z.B. so:

from Globals import InitializeClass
from OFS.SimpleItem import SimpleItem
from AccessControl import ClassSecurityInfo
 
from Products.CMFCore.utils import UniqueObject, getToolByName

Die Klasse SimpleItem ist die vorgegebene Basisklasse für ein einfaches Objekt in Zope (nicht für einen Ordner). Tatsächlich erben alle Inhaltstypen von einer Klasse, die irgendwo in der Klassenhierarchie von SimpleItem erbt. Es ist nur so, dass Sie die ganzen zusätzlichen Attribute dieser anderen Klassen nicht benötigen. UniqueObject garantiert, dass es genau eine Instanz dieses Objekts in Ihrer Plone-Site gibt und dass sie nicht umbenannt oder verschoben werden kann. Das heißt, Ihr Objekt wird immer verfügbar sein.

Als Nächstes importieren Sie wie üblich die Variablen aus der Konfigurationsdatei. Durch die Zuweisung an die ID Ihres Objekts garantieren Sie, dass das Werkzeug die ID dessen haben wird, was immer unique_id in der Konfigurationsdatei ist - in diesem Fall plone_stats. Die zwei Basisklassen für das Werkzeug sind UniqueObject und SimpleItem, die das Minimum dessen darstellen, was es benötigt. Beispiel:

from config import view_permission, product_name, unique_id
 
class Stats(UniqueObject,  SimpleItem):
    """ Prints out statistics for a Plone site """
    meta_type = product_name
    id = unique_id

Dann müssen Sie die Sicherheit einrichten, wobei Sie wiederum die Klasse ClassSecurityInfo benutzen werden, um explizit Rechte an den Methoden zu setzen. Beispiel:

security = ClassSecurityInfo()
security.declareProtected(view_permission, 'getContentTypes')
def getContentTypes(self):
    ...

Einige Elemente zur Benutzerschnittstelle hinzufügen

Der Hauptcode ist vollständig, also wäre es hübsch, dem Benutzer eine Antwort anzuzeigen, wenn Sie im ZMI auf das Werkzeug klicken, etwa in Form eines Beispiels dafür, wie das Produkt verwendet wird. Dazu werden Sie das ZMI so abändern, dass Sie etwas darstellen können.

Konkret heißt das, dass Sie ein Page Template schreiben, das tut, was Sie von ihm wollen. In diesem Beispiel ist es ein einfaches Page Template, das sich ins ZMI einklinkt. Das ZMI ist eine anspruchslose Benutzerschnittstelle, die lediglich Webseiten für den Benutzer ausspuckt, d.h., die Seite wird nicht durch aufwendige Makros oder Slots erstellt. Sie müssen nur ein wenig HTML schreiben und Folgendes hinzufügen:

<span tal:replace="structure here/manage_tabs" />

Diese eine tal:replace-Funktion bekommt die Management-Reiter und sorgt dafür, dass sie oben auf der Seite erscheinen. Meine ZMI-Seite iteriert über die zwei Methoden des Werkzeugs plone_stats und spuckt die Ergebnisse für den Benutzer aus, wie es in Listing 12.14 zu sehen ist.

Listing 12.14. Eine Seite zur Anzeige in der Management-Schnittstelle

<html>
<body>
<span tal:replace="structure here/manage_tabs" />
 
<p>Statistics for this Plone site.</p>
 
<h3>Content Types</h3>
<span tal:define="numbers here/getContentTypes">
    <p>
        Total count: <i tal:replace="numbers/total" /><br />
        Content types by type:
    </p>
 
    <span tal:repeat="type python:numbers['bytype'].keys()">
        <ul>
            <li>
              <span tal:replace="type" />:
              <i tal:replace="python: numbers['bytype'][type]" />
            </li>
        </ul>
    </span>
 
    <p>Content types by state:</p>
    <span tal:repeat="type python:numbers['bystate'].keys()">
        <ul>
            <li>
              <span tal:replace="type" />:
              <i tal:replace="python: numbers['bystate'][type]" />
            </li>
        </ul>
    </span>
</span>
 
<h3>Users</h3>
<p>
  User count: <i tal:replace="here/getUserCount" />
</p>
 
</body>
</html>

Sie heißt output.pt und wird ins Verzeichnis www platziert. Sie müssen kein separates Verzeichnis benutzen, aber wenn Sie es tun, ist es leichter zu merken.

Der letzte Schritt besteht darin, dieses Page Template für Ihr Produkt ins ZMI einzuklinken. Das machen Sie dadurch, dass Sie zur Klasse Stats zurückkehren und Folgendes hinzufügen (zuerst importieren Sie die Klasse PageTemplateFile, die mit dem Template aus dem Dateisystem umgehen kann):

from Products.PageTemplates.PageTemplateFile import PageTemplateFile

Dann registrieren Sie das Page Template als Methode für das Produkt, auf das zugegriffen werden kann. Im Folgenden kann die Methode outputPage nun über das Web aufgerufen werden, und es wird das entsprechende Page Template zurückgegeben:

outputPage = PageTemplateFile('www/output.pt', globals())
security.declareProtected(view_permission, 'outputPage')

Und schließlich werden die Reiter oben im ZMI von einem Tupel namens manage_options bestimmt, das eine Liste aller Reiter enthält, die auf einer Seite angezeigt werden sollen. Dort müssen Sie die neue Management-Seite einfügen, was Sie wie folgt tun:

manage_options = (
    {'label':'output', 'action':'outputPage'},
    ) + SimpleItem.manage_options

Das Werkzeug testen

Nun ist das Werkzeug fertig, d.h., Sie können testen, ob es funktioniert. Zuerst starten Sie Ihre Plone-Instanz neu, damit sie das Produktverzeichnis einliest und Ihr neues Werkzeug registriert. Als Zweites gehen Sie ins ZMI und dort in die obere rechte Ecke zum Dropdown-Menü namens Add. Sie werden feststellen, dass in der Liste nun PloneStats aufgeführt ist. Wählen Sie diese Option, und klicken Sie auf Add. Das nächste Formular listet die verfügbaren Werkzeuge im Produkt PloneStats auf, in diesem Fall erscheint nur eines, wie in Abbildung 12.3 zu sehen ist.

img/12-03.png

Abbildung 12.3. Hinzufügen des Werkzeugs

Wählen Sie das Werkzeug, und klicken Sie auf Add. Klicken Sie dann auf das Werkzeug, um zu testen, ob es funktioniert. Sie sollten eine Reihe von Statistiken sehen, wie Sie sie zuvor für ZopeZen gesehen haben.

Dieses Werkzeug ist einfach, weil ich nicht wirklich weiß, welche Art von Darstellung die Leute gerne hätten. Wenn ich ein standardisiertes Berichtwerkzeug erstelle, dann können Sie es benutzen, wie Sie wollen. Einige Ideen, die einem dafür einfallen, sind z.B. eine Seite im Control Panel, ein kleiner Portlet-Kasten, eine PDF-Datei (Portable Document Format) mit hübschen Grafiken, die via E-Mail an einen Manager verschickt wird, oder eine API, damit externe Berichtswerkzeuge wie Crystal Reports mein Werkzeug benutzen können. An dieser Stelle warte ich ab und beobachte, was in der Zukunft alles passiert.


Andy McKay: Plone. Addison-Wesley 2005
Diese online Version wurde mit Hilfe des 'PloneBookDE' Produkts von docs.neuroinf.de/products erstellt.
Es wurde zuletzt von
loesch am 2006-01-11 13:38 aus der fallback Quelle aktualisiert.

Powered by Plone CMS, the Open Source Content Management System

This site conforms to the following standards: