Das Plone-Entwicklerhandbuch möchte anhand eines konkreten Projekts aufzeigen, wie Plone den eigenen Bedürfnissen angepasst werden kann. Gleichzeitig kommt dabei die Überzeugung des Autors von agiler Software-Entwicklung, vor allem testgetriebener Entwicklung, zum Ausdruck.
Plone ermöglicht die schnelle Erstellung von funktionsreichen und performanten Websites, da es bereits mächtige Werkzeuge zur Verwaltung von Rechten und Inhalten bereitstellt. So ist es einfach, öffentliche Websites, Intra- und Extranets sowie branchenspezifische Web-Anwendungen schnell und zuverlässig zu erstellen.
Dabei wird Plone von einer großen internationalen Community getragen. Zudem hält die Plone-Foundation als eine nonprofit-Organisation die geistigen Eigentums- und Markenrechte, sodass der weitere Entwicklungsprozess von Plone stabil und kontrolliert möglich ist.
Plone basiert auf dem objektorientierten Web-Application-Framework Zope und steht für viele Betriebssysteme wie Windows, Mac OS X und Linux/Unix zur Verfügung.
Plone ist in Python geschrieben, einer mächtigen und dennoch einfach zu nutzenden Programmiersprache.
Dieses Plone-Entwicklerhandbuch wird anhand einer Fallstudie aufzeigen, wie ein prototypischer Verlauf eines Software-Entwicklungsprozesses mit Plone aussehen kann.
Das Buch richtet sich in erster Linie an Entwickler, die ihr webbasiertes Content Management System mit Plone realisieren möchten. Zumindest einige Kenntnis von Python, HTML und CSS werden erwartet, auch eigene frühere Erfahrungen mit Zope und Plone können hilfreich sein.
In diesem Buch werden unterschiedliche Schriftstile zur Differenzierung der Inhalte verwendet. So sieht die Darstellung von Codeabschnitten so aus:
[buildout]
parts =
zope2
productdistros
instance
zopepy
während Angaben im Terminal so dargestellt werden:
$ paster create -t plone3_buildout
bzw als root:
# curl -O http://peak.telecommunity.com/dist/ez_setup.py
erlaubt, identische Entwicklungsumgebungen einfach aufzusetzen. Hierzu nutzt
buildout
die Fähigkeit der setuptools, automatisch
Abhängigkeiten aufzulösen und Aktualisierungen durchzuführen (s.a.: Jim
Fulton: Buildout Tutorial).
sind ein Deploymentformat für Python-Packages. Sie enthalten ein
setup.py
-Skript mit Metainformationen (Lizenz, Abhängigkeiten, etc.) Mit
der Python-Bibliothek Setuptools können solche Abhängigkeiten automatisch
nachgeladen werden, wobei in Eggs spezifische Versionen angegeben werden
können.
Index mit tausenden von Python-Paketen. Setuptools, easy_install
und
Buildout nutzen diesen Index, um Eggs automatisch zu installieren.
Python-Modul mit dem der Python Package Index durchsucht werden kann und das die Pakete in die globale Python-Umgebung installiert. Wir werden nur Buildout mit EasyInstall installieren, alle weiteren Eggs werden von Buildout in das lokale Buildout-Projekt heruntergeladen, unter anderem um Versionskonflikte zu vermeiden.
Bevor zc.buildout
installiert werden kann, sind folgende Schritte
erforderlich:
Installation von Python.
Anforderungen
bash oder eine andere Shell
ein C- und C++-Compiler
GNU make
Zope setzt das Python-zlib-Modul voraus, das
Python beim Kompilieren erstellt sofern zlib.h
in /usr/include
installiert ist.
Für diese Anforderungen müssten auf Debian- und Ubuntu-Systemen folgende Pakete installiert werden:
$ sudo apt-get install build-essential zlib1g-dev
oder:
$ sudo yum install make gcc-c++ zlib-devel
Falls Sie die SSL-Bibliotheken benötigen, z.B. zum Verschicken von Mails mit TLS, sollten Sie auf Debian- und Ubuntu-Systemen das OpenSSL- Paket installieren:
$ sudo apt-get install libssl-dev
oder:
$ sudo yum install openssl-devel
Mir der GNU Readline-Bibliothek können Sie im Python-Prompt die zuletzt eingegebenen Befehle erneut aufrufen:
$ sudo apt-get install libreadline6-dev
Für die Indizierung von Word- und PDF-Dokumenten werden darüberhinaus noch die folgenden Pakete benötigt:
$ sudo apt-get install wv poppler-utils
oder:
$ sudo yum install wv poppler-utils
Erstellen und Installieren
Bemerkung
Im Folgenden wird Python 2.7 für Plone 4.3 installiert. Für Plone 4.1 und 4.0 benötigen Sie jedoch Python 2.6 und für Plone 3 Python 2.4. Die Installationsschritte für moderne Linux-Installationen unterscheiden sich jedoch, sodass sie besonders behandelt werden.
$ sudo apt-get install python-dev python-libxml2 python-libxslt1 python-virtualenv
oder:
$ sudo yum install python27-devel python-lxml python-virtualenv
Anschließend können Sie ein Virtual Environment erstellen und darin
zc.buildout
installieren:
$ virtualenv --system-site-packages venv
$ cd venv
$ ./bin/pip install zc.buildout
Alternativ können Sie selbst Python aus den Sourcen kompilieren.
Gehen Sie hierzu in das Verzeichnis, in dem Sie die aktuelle Python-
Version installieren möchten, z.B. in /opt/
. Anschließend
laden Sie das Python-Paket herunter und entpacken es:
# curl -O http://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
# tar -xvzf Python-2.7.9.tgz
Installieren Sie das Python-2.7.9-Paket:
# cd Python-2.7.9
# ./configure --prefix=/opt/python/2.7.9
--prefix
Python-Installationspfad. Ohne Angabe wird
Python in /usr/local
erstellt.
Anschließend fahren Sie mit der Installation fort:
# make
# make install
Der Python-Interpreter kann nun in PATH
eingetragen werden. Hierzu wird folgendes in die ~/.bashrc
(oder auf dem Mac in ~/.bash_profile
) eingetragen:
export PATH=/opt/python/2.7.9/bin:$PATH
Danach kann die Konfiguration neu eingelesen werden mit:
$ source ~/.bashrc
Buildout benötigt zusätzlich auch noch easy_install
:
# mkdir /opt/python/2.7.9/Extensions
# cd $_
# curl -O http://peak.telecommunity.com/dist/ez_setup.py
# python ez_setup.py
Welche Versionen dieser Python-Pakete mit easy_install
installiert wurde,
erfahren Sie indem Sie das jeweilige Skript aufrufen mit der Option
--version
.
Wollen Sie zu einem späteren Zeitpunkt eines dieser Python-Pakete
aktualisieren, so können Sie dies mit der Option -U
, also z.B.:
# easy_install -U setuptools
Sie können auch spezifische Versionen angeben, z.B.:
# easy_install setuptools==0.9.8
Bemerkung
Verwenden Sie z.B. Subversion 1.5 zusammen mit Buildout und erhalten folgende Fehlermeldung:
NameError: global name 'log' is not defined
dann benötigen Sie mindestens die dev06
-Version der setuptools. Dies erhalten Sie mit:
# easy_install setuptools>=dev06
Installieren der OSX development tools (XCode).
Installieren von Macports.
Um bootstrap.py
aufzurufen, sollte folgender Befehl verwendet werden um zu
gewährleisten, dass der Python-Interpreter von Macports verwendet wird:
$ python2.7 bootstrap.py
Häufig kann Buildout nicht direkt auf die Quellen zugreifen um Python Eggs o.ä.herunterzuladen. In diesem Fall sollte der Proxy zunächst als Environment-Variable z.B. in der ~/.bashrc
-Datei angegeben werden:
export http_proxy = http://localhost:8123/
export https_proxy = http://localhost:8123/
Alternativ kann auch über einen ssh-Tunnel auf den entfernten Server zugegriffen werden:
$ ssh -L 8123:localhost:8123 yourserver.com
LD_LIBRARY_PATH
LD_LIBRARY_PATH
ist eine Unix-Environment-Variable, die angibt, aus welchem Verzeichnis dynamisch verlinkte Bibliotheken (*.so
-Dateien) geladen werden sollen. Falls die systemweit verfügbaren Bibliotheken überschrieben werden sollen, kann dies mit environment-vars
aus dem zope2instance
-Rezept geschehen:
[instance]
…
# Use statically compiled libxml2
environment-vars =
LD_LIBRARY_PATH ${buildout:directory}/parts/lxml/libxml2/lib:${buildout:directory}/parts/lxml/libxslt/lib
s.a. Issue 11715: Building Python on multiarch Debian and Ubuntu - Python tracker.
Für Python 2.6 müssen zunächst einige Dateien geändert werden bevor sie auf
moderneren Linux-Distributionen mit multiarch
-Features, die manche
Bibliotheken in architekturspezifischen Verzeichnissen speichern, so z.B. in
/usr/lib/x86_64-linux-gnu/libz.so
.
Zunächst wird die Python-Distribution heruntergeladen und entpackt:
# wget https://www.python.org/ftp/python/2.6.9/Python-2.6.9.tar.xz
# tar xvzf Python-2.6.9.tgz
Anschließend wechseln wir in dieses Verzeichnis und ergänzen in der Datei
setup.py
die lib_dirs
um /usr/lib/x86_64-linux-gnu
, sodass der
Abschnitt anschließend folgendermaßen aussieht:
lib_dirs = self.compiler.library_dirs + [
'/lib64', '/usr/lib64',
'/lib', '/usr/lib',
'/usr/lib/x86_64-linux-gnu'
]
Anschließend ändern wir in der Datei Modules/_ssl.c
den Abschnitt mit PySSL_BEGIN_ALLOW_THREADS
:
PySSL_BEGIN_ALLOW_THREADS
if (proto_version == PY_SSL_VERSION_TLS1)
self->ctx = SSL_CTX_new(TLSv1_method()); /* Set up context */
else if (proto_version == PY_SSL_VERSION_SSL3)
self->ctx = SSL_CTX_new(SSLv3_method()); /* Set up context */
else if (proto_version == PY_SSL_VERSION_SSL23)
self->ctx = SSL_CTX_new(SSLv23_method()); /* Set up context */
PySSL_END_ALLOW_THREADS
In derselben Datei sind auch noch die Protokoll-Versionen anzupassen:
/* protocol versions */
PyModule_AddIntConstant(m, "PROTOCOL_SSLv3",
PY_SSL_VERSION_SSL3);
PyModule_AddIntConstant(m, "PROTOCOL_SSLv23",
PY_SSL_VERSION_SSL23);
PyModule_AddIntConstant(m, "PROTOCOL_TLSv1",
PY_SSL_VERSION_TLS1);
Auch in Lib/ssl.py
sind die Protokoll-Versionen noch anzupassen:
Die Zeile
from _ssl import PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1
sollte ersetzt werden durch
from _ssl import PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1
Nun kann Python 2.6 konfiguriert und erstellt werden mit
# env CPPFLAGS="-I/usr/lib/x86_64-linux-gnu" LDFLAGS="-L/usr/include/x86_64-linux-gnu" ./configure --prefix=/opt/python/2.6.9
# make
# make install
Schließlich kann noch EasyInstall installiert werden mit:
# cd /opt/python/2.4.6/
# mkdir Extensions
# cd $_
# wget https://pypi.python.org/packages/2.4/s/setuptools/setuptools-0.6c11-py2.4.egg#md5=bd639f9b0eac4c42497034dec2ec0c2b
# export PATH=/opt/python/2.4.6/bin:$PATH
# sh setuptools-0.6c11-py2.4.egg
Auch für Python 2.4 müssen zunächst einige Änderungen vorgenommen werden bevor
es auf moderneren Linux- oder Debian-Distributionen mit sog. multiarch
-
Architektur lauffühig ist.
Zunächst wird die Python-Distribution heruntergeladen und entpackt:
# wget https://www.python.org/ftp/python/2.4.6/Python-2.4.6.tgz
# tar xvzf Python-2.4.6.tgz
Anschließend wechseln wir in dieses Verzeichnis und ergänzen in der Datei
setup.py
die lib_dirs
um '/usr/lib/x86_64-linux-gnu'
, sodass der
Abschnitt anschließend folgendermaßen aussieht:
lib_dirs = self.compiler.library_dirs + [
'/lib64', '/usr/lib64',
'/lib', '/usr/lib',
'/usr/lib/x86_64-linux-gnu'
]
Weiter unten in derselben Datei sind die Pfade nochmals anzupassen in
ssl_libs
:
ssl_libs = find_library_file(self.compiler, 'ssl',lib_dirs,
['/usr/local/ssl/lib',
'/usr/contrib/ssl/lib/',
'x86_64-linux-gnu'
] )
Nun wird Python 2.4 konfiguriert und erstellt mit:
# env CPPFLAGS="-I/usr/lib/x86_64-linux-gnu" LDFLAGS="-L/usr/include/x86_64-linux-gnu" ./configure --prefix=/opt/python/2.4.6
# make
# make install
Schließlich kann noch EasyInstall installiert werden mit:
# cd /opt/python/2.4.6/
# mkdir Extensions
# cd $_
# wget https://pypi.python.org/packages/2.4/s/setuptools/setuptools-0.6c11-py2.4.egg#md5=bd639f9b0eac4c42497034dec2ec0c2b
# export PATH=/opt/python/2.4.6/bin:$PATH
# sh setuptools-0.6c11-py2.4.egg
Ein Buildout-Projekt für Plone lässt sich am einfachsten aus meiner vs_buildout-Vorlage bei github erstellen:
$ curl -o master.zip https://codeload.github.com/veit/vs_buildout/zip/master
$ unzip master.zip
$ cd vs_buildout-master
$ python bootstrap.py -c devel.cfg
…
Got distribute 0.6.28.
Generated script '/Users/plone/vs_buildout-master/bin/buildout'.
-d
verwendet Distribute anstatt der Setuptools.
-c
erlaubt die Angabe eines Pfades zu einer Buildout-Konfigurationsdatei,
in unserem Fall devel.cfg
.
Bemerkung
Falls das buildout
-Skript für den Nutzer, z.B. plone
,
noch nicht in PATH
eingetragen wurde, ändern wir die ~/.bashrc
(oder auf dem Mac in ~/.bash_profile
) folgendermaßen:
export PATH=/opt/python/Python-2.7.5/bin/:$PATH
Danach kann die Konfiguration neu eingelesen werden mit:
$ source ~/.bashrc
Bevor nun Plone mit Buildout installiert werden kann, sollten die für die Python Imaging Library (PIL) benötigten Bibliotheken installiert werden.
Anforderungen:
Für JPEG-Unterstützung benötigen Sie die IJG JPEG library, Version 6a oder 6b:
Für PNG- und ZIP-Unterstützung benötigen Sie die ZLIB library.
Für TrueType/OpenType-Unterstützung benötigen Sie die FreeType 2.0 library:
Unter Debian und Ubuntu können Sie die Pakete installieren mit:
$ sudo apt-get install libjpeg62-dev libfreetype6
Unter Fedora und CentOS können Sie die Pakete installieren mit:
$ sudo yum install libjpeg-turbo-devel freetype-devel
Unter Mac OS X können Sie die Pakete z.B. mit Homebrew installieren:
$ brew install freetype jpeg libtiff
Sofern diese Anforderungen erfüllt sind, wird die PIL mit folgendem Eintrag
in der base.cfg
-Datei installiert:
[buildout]
…
versions = versions
…
eggs =
Pillow
[versions]
…
Pillow = 1.7.8
Schließlich wird ab Plone 4.2 auch lxml benötigt.
Unter Debian und Ubuntu können Sie die Pakete installieren mit:
$ sudo apt-get install libxml2-dev libxslt1-dev
Unter Fedora und CentOS können Sie die Pakete installieren mit:
$ sudo yum install libxml2-devel libxslt-devel
Nun kann Buildout aufgerufen werden:
$ ../venv/bin/buildout -c devel.cfg
…
--------------------------------------------------------------------
SETUP SUMMARY (Pillow 1.7.8 / PIL 1.1.7)
--------------------------------------------------------------------
version 1.7.8
platform darwin 2.7.3 (default, Feb 8 2013, 10:02:27)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)]
--------------------------------------------------------------------
--- TKINTER support available
--- JPEG support available
--- ZLIB (PNG/ZIP) support available
--- FREETYPE2 support available
*** LITTLECMS support not available
--------------------------------------------------------------------
Dieser Prozess kann längere Zeit dauern, da Zope, Plone und alle Zusatzprodukte heruntergeladen und installiert werden.
Ist der Prozess abgeschlossen, kann der Zope-Server gestartet werden mit:
§ ./bin/instance start
Und das Stoppen des Zope-Servers geht mit:
§ ./bin/instance stop
Schlägt das Starten des Zope-Servers fehl, können Sie den Zope-Server im Vordergrund starten und bekommen dann auf der Konsole ausgegeben, an welcher Stelle Zope den Startvorgang abbricht:
$ ./bin/instance fg
Mit STRG-c
kann dieser Prozess wieder beendet werden.
Schließlich sollten Sie noch den admin
-Zugang ersetzen. Hierzu starten
Sie zunächst die Instanz und gehen dann in den User Folder des Zope
Management Interface (ZMI): http://localhost:8080/acl_users/manage
.
Hier können Sie unter http://localhost:8080/acl_users/manage_users
einen
neuen Nutzer anlegen und diesem die Rolle Manager zuweisen.
Anschließend können im ZMI Logout
auswählen und sich gleich anschließend
wieder mit den neuen Zugangsdaten anmelden.
Nun sollten Sie noch den admin
-Nutzer löschen.
Installation und Konfiguration von Python 2.6.6 und 2.4.4, MinGW, Libxml- und Libxslt-Python-Bindings.
Herunterladen und Installieren von Python 2.6.6 mit python-2.6.6.msi.
Wählen Sie Install for all users.
Der Standard-Installationsort ist C:\Python26
.
Herunterladen und Installieren der pywin32-Extension von http://sourceforge.net/projects/pywin32/files/pywin32/Build%20214/pywin32-214.win32-py2.6.exe/download.
Eintragen von Python in die Systemvariable PATH
, sodass nicht jedesmal der gesamte Pfad angegeben werden muss.
Öffnen Sie die Systemeigenschaften und klicken anschließend zunächst auf den Erweitert-Reiter, dann auf Umgebungsvariablen.
Fügen Sie anschließend die Pfade zu Ihrer Python-Installation ein, z.B.:
C:\Python26;C:\Python26\Scripts;
Beachten Sie bitte, dass die verschiedenen Pfade durch Semikolon voneinander getrennt sind:
Öffnen Sie nun eine neue Shell mit Windows
-r
und geben cmd
in das Popup-Fenster ein.
Mit python -V
wird Ihnen die Versionsnummer des verwendeten Python ausgegeben – dies sollte Python 2.6.6
sein.
Herunterladen und Installieren von Python 2.4.4 mit python-2.4.4.msi;
Wählen Sie Install for all users.
Der Standard-Installationsort ist C:\Python24
.
Herunterladen und Installieren der pywin32-Extension von http://downloads.sourceforge.net/pywin32/pywin32-210.win32-py2.4.exe.
Eintragen von Python in die Systemvariable PATH
, sodass nicht jedesmal der gesamte Pfad angegeben werden muss.
Öffnen Sie die Systemeigenschaften und klicken anschließend zunächst auf den Erweitert-Reiter, dann auf Umgebungsvariablen.
Fügen Sie anschließend den Pfad zu Ihrer Python-Installation ein, z.B.:
C:\Python24;C:\Python24\Scripts;
Beachten Sie bitte, dass die verschiedenen Pfade durch Semikolon voneinander getrennt sind.
Öffnen Sie nun eine neue Shell mit Windows
-r
und geben cmd
in das Popup-Fenster ein.
Mit python -V
wird Ihnen die Versionsnummer des verwendeten Python ausgegeben – dies sollte Python 2.4.4
sein.
Dies ist ein gcc-Compiler für Windows, womit C-Komponenten von Zope auf Windows kompiliert werden können.
Herunterladen des MinGW-Installationsprogramms von http://downloads.sourceforge.net/mingw/MinGW-5.1.4.exe.
Geben Sie beim Ausführen des Installationsprogramms als Optionen MinGW base tools und MinGW Make an.
Das Programm wird üblicherweise nach C:\MinGW
installiert.
Das Installationsprogramm holt sich die benötigten Dateien von sourceforge.net, wobei es gegebenenfalls mehrfach aufgerufen werden muss bis alle Dateien heruntergeladen wurden.
Tragen Sie nun C:\MinGW\bin
in PATH
ein.
Testen Sie die Installation in einer Shell mit:
gcc --version
Die Ausgabe sollte gcc (GCC) 3.4.5
oder neuer sein.
Anschließend wird Distutils für MinGW konfiguriert. Hierzu erstellen Sie die Datei distutils.cfg
in C:\Python24\Lib\distutils
mit folgendem Inhalt:
[build]
compiler=mingw32
Die Bindings können heruntergeladen werden von http://users.skynet.be/sbi/libxml-python/binaries/libxml2-python-2.7.7.win32-py2.6.exe.
Tragen Sie in der Buildout-Konfigurationsdatei statt PIL
bitte Pillow ein, also:
[instance]
…
eggs =
Pillow
Plone
…
Nun können Sie ein Buildout-Projekt erstellen, wobei jedoch im Pfad keine Leer- oder Sonderzeichen enthalten sein dürfen.
Anschließend können Sie die Zope-Instanz als Windows-Service installieren, indem Sie in der Shell folgendes angeben:
> bin\instance.exe install
Anschließend lässt sich die Instanz starten mit:
> bin\instance.exe start
Soll der Service wieder entfernt werden, geben Sie einfach folgendes an:
> bin\instance.exe remove
Die base.cfg
-Datei gliedert sich in die folgenden Abschnitte:
[buildout]
Hier werden die globalen Einstellungen für diesen Buildout angegeben.
parts
Die in dieser Konfiguration angegebenen Abschnitte, die in der angegebenen Reihenfolge durchlaufen werden:
parts =
instance-base
zopepy
i18ndude
zopeskel
extends
Eine bestehende Buildout-Konfiguration wird erweitert, nämlich
http://dist.plone.org/release/4.3/versions.cfg
und
versions.cfg
.
find-links
URLs, Datei- oder Verzeichnisnamen, in denen Buildout nach Links zu Distributionen suchen soll.
extensions
Erweiterungen, die den Funktionsumfang von Buildout vergrößern:
allow-picked-versions
Mit dem Wert false
kann gewährleistet werden, dass alle Versionen
festgeschrieben wurden.
versions
Es wird auf einen Abschnitt verwiesen, in dem die Versionen der
Python-Packages festgeschrieben werden können, in unserem Fall ist
der Abschnitt versions
genannt worden. Weiter Hinweise zur
Verwaltung von Versionen erhalten Sie in Aktualisierung und Versionierung.
unzip
Üblicherweise packt Buildout keine gezippten Python Eggs aus.
[instance-base]
Dieser Abschnitt erstellt und konfiguriert eine Zope-Instanz unter Verwendung von plone.recipe.zope2instance:
[instance-base]
recipe = plone.recipe.zope2instance
user = admin:admin
http-address = 8080
debug-mode = on
verbose-security = on
blob-storage = var/blobstorage
eggs =
Plone
Pillow
zcml =
environment-vars =
PTS_LANGUAGES en de
zope_i18n_allowed_languages en de
zope_i18n_compile_mo_files true
eggs
Hier können zusätzliche Python-Eggs angegeben werden, wobei elementtree
von Plone benötigt wird. Auf diese Eggs wird später in [instance]
verwiesen.
Es können auch spezifische Versionen angegeben werden. Soll z.B. SQLAlchemy 0.3
installiert werden, sieht der Eintrag so aus:
eggs =
...
SQLAlchemy>=0.3,<0.4dev
Auch die in diesem Projekt entwickelten Eggs werden hier angegeben:
eggs =
...
my.package
develop =
src/my.package
verbose-security
Damit lässt sich angeben,
Für welche Objekte wurde der Zugriff verweigert?
Welche Rechte sind notwendig?
Welche Rollen und Rechte sind den Objekten und Eigentümern zugeteilt?
Welches sind die wirksamen Proxy-Rollen?
Damit wir alle unauthorized
-Ereignisse auch im Fehlerprotokoll angezeigt bekommen, müssen wir später im Site Error Log im Zope Management Interface (ZMI) der Plone-Site Unauthorized
aus der Liste Ignored exception types entfernen.
eggs
Hier geben wir die der Instanz zur Verfügung stehenden Eggs an. In unserer
Konfiguration werden die in den buildout
- und plone
-Abschnitten
angegebenen Eggs verwendet.
zcml
Da die Konfigurationsdateien nicht automatisch für ältere Eggs oder Pakete
geladen werden, kann Buildout angewiesen werden, einen sog. ZCML slug in
parts/instance/etc/package-includes
zu erstellen, indem die entsprechenden
Pakete unter dieser Option aufgelistet werden:
zcml =
my.package
Es kann auch explizit angegeben werden, welche Art von ZCML slug erstellt werden soll, z.B.:
zcml =
my.package-overrides
my.package-meta
overrides
Dies erstellt eine *-overrides.zcml
-Datei in
myproject/parts/instance/etc/package-includes/
, mit der sich eine per
zcml
angegebene Konfiguration wieder überschrieben wird.
Anschließend wird in der configure.zcml
-Datei von my.package
eine
overrides
-Konfigurationsdatei eingefügt:
<includeOverrides file="overrides.zcml" />
Diese overrides.zcml
-Datei enthält dann die Ersetzung einer bestehenden
Konfiguration.
meta
Dies erstellt eine *-meta.zcml
-Datei in
myproject/parts/instance/etc/package-includes/
, die gewährleistet, dass
die gesamte Konfiguration dieses Pakets zur Verfügung steht bevor die
weiteren zcml
-Anweisungen abgearbeitet werden.
Weitere Konfigurationsoptionen von plone.recipe.zope2instance
sind:
default-zpublisher-encoding
Liefert ein Request eine Unicode-Antwort und ist für
ZPublisher.HTTPResponse
kein spezifischer Zeichensatz angegeben, dann wird
der Unicode-String mit dem default-zpublisher-encoding
kodiert.
Der Standardwert ist utf-8
.
zope-conf
Ein relativer oder absoluter Pfad zu einer Zope-Konfigurationsdatei. Eine zope.conf
-Datei wird dann mit den Angaben in diesem Abschnitt generiert in parts/instance/etc/zope.conf
.
zope-conf-additional
Sollen nur die Werte einiger Attribute der zope.conf
-Datei geändert werden, können diese in zope-conf-additional
angegeben werden. Dabei müssen die nachfolgenden Zeilen eingerückt sein.
environment-vars
definiert Umgebungsvariablen zur Laufzeit von Zope, z.B.:
environment-vars =
zope_i18n_compile_mo_files = true
Einen vollständigen Überblick über alle Optionen des [instance]
-Abschnitts erhalten Sie in plone.recipe.zope2instance.
[zopepy]
In diesem Abschnitt wird ein Python-Interpreter definiert, der alle Eggs und Pakete, aber keine Zope2-Produkte enthält und sich daher gut zum Debuggen und Testen eignet:
[zopepy]
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy
Mit dem Rezept wird das ./bin/zopepy
-Skript erstellt und sowohl die Eggs aus dem [instance]
-Abschnitt als auch die Zope-Module aus parts/zope2/lib/python
der Zope-Installation eingeschlossen. Es muss also nicht mit jedem neuen Buildout-Projekt auch die PYTHONPATH
-Umgebungsvariable neu gesetzt werden. Mit zopepy
sollte sich z.B. auch einfach das Modul PageTemplates
aus Products
importieren lassen:
$ ./bin/zopepy
>>> from Products import PageTemplates
Da kein Fehler für den Import angegeben wurde, wird das Modul geladen, und der Python-Interpreter kann mit Strg-D
(unter Windows Strg-Z
) wieder verlassen werden.
annotate
¶Mit der Buildout-Option annotate
werden alle Abschnitte alphabetisch
sortiert angezeigt. Innerhalb jedes Abschnitts werden alle Schlüssel-Wert-Paare
zusammen mit der Quelle angezeigt. Eine solche Quelle kann entweder ein
Dateiname oder die Variablen COMPUTED_VALUE
, DEFAULT_VALUE
oder
COMMAND_LINE_VALUE
sein. Die Ausgabe kann z.B. folgendermaßen aussehen:
$ ./bin/buildout -c deploy.cfg annotate
Setting socket time out to 3 seconds.
Annotated sections
==================
[backup]
enable_snapshotrestore= false
/home/veit/sandboxes/vs_buildout/deploy.cfg
...
[buildout]
...
develop-eggs-directory= develop-eggs
DEFAULT_VALUE
directory= /home/veit/vs_buildout
COMPUTED_VALUE
...
annotate
kann auch genutzt werden um herauszufinden, welche Version aufgrund
welcher Konfiguration verwendet wird, z.B.:
[versions]
...
six= 1.2.0
/home/veit/vs_buildout/plone-versions.cfg
...
Für Plone 3.2 sieht die Buildout-Konfigurationsdatei etwas anders aus. Sie gliedert sich in die folgenden Abschnitte:
parts =
zope2
productdistros
instance
zopepy
[zope2]
Dieser Abschnitt lädt und erstellt Zope 2 aus der im Abschnitt plone
angegebenen URL:
[zope2]
recipe = plone.recipe.zope2install
fake-zope-eggs = true
additional-fake-eggs =
ZODB3
url = ${versions:zope2-url}
Mit dem Rezept wird Zope 2 in parts/zope2
installiert, die Variable ZOPE_HOME
ist also parts/zope2
und die Variable SOFTWARE_HOME
parts/zope2/lib/python
.
fake-zope-eggs
Falls der Wert auf true
gesetzt wird, werden Links auf die Zope-3-Bibliotheken gesetzt. Wenn nun ein Egg in seiner setup.py
-Datei auf ein zope.*
-Egg verweist, finden die setuptools diese in /parts/zope2/lib/python/zope/
und installieren nicht erneut Versionen dieser Eggs in womöglich inkompatiblen Versionen. Ab Version 3 ist der Standardwert auf true
gesetzt.
additional-fake-eggs
Hiermit lasst sich eine Liste zusätzlicher fake eggs angeben, wobei nur Python-Packages angegeben werden sollten, die sich auch in PYTHONPATH
befinden. Der Standardwert schließt Acquisition
, ClientForm
, DateTime
, docutils
, ExtensionClass
, mechanize
, Persistence
, pytz
, RestrictedPython
, tempstorage
, ZConfig
, zLOG
, zodbcode
, ZODB3
, zdaemon
und Zope2
ein.
Die Versionen von additional-fake-eggs
lassen sich einfach angeben, z.B.:
additional-fake-eggs =
ZODB3 = 3.7.1
zope.annotation = 3.3.2
Wird keine Version für additional-fake-eggs
angegeben, haben die faked eggs immer die Version 0.0
.
skip-fake-eggs
Hier kann eine Liste von Packages angegeben werden, für die keine Fake eggs erstellt werden sollen. Somit können neuere Versionen spezifischer Zope-Packages installiert werden auch wenn fake-zope-eggs = true
gesetzt ist, z.B.:
[buildout]
versions = versions
[versions]
zope.app.catalog = 3.5.2
zope.component = 3.5.1
zope.i18n = 3.6.0
zope.sendmail = 3.5.1
zope.testing = 3.7.1
five.intid = 0.3.0
[zope2]
fake-zope-eggs = true
additional-fake-eggs =
ZConfig
ZODB3
pytz
skip-fake-eggs =
zope.component
zope.i18n
zope.sendmail
zope.testing
url
Die URL, unter der Zope heruntergeladen werden kann, in unserem Fall wird auf die versions.cfg
-Datei verwiesen und den dort angegebenen Wert für zope2-url
.
[productdistros]
Der Abschnitt kann verwendet werden, um Archive von Produkten herunterzuladen und zu installieren, z.B.:
urls =
http://www.zope.org/Members/shh/DocFinderTab/1.0.2/DocFinderTab-1.0.2.tar.gz
nested-packages
Archive, die mehrere Zope2-Produkte enthalten.
Im folgenden Beispiel soll PloneLDAP 1.0 installiert werden:
[productdistros]
recipe = plone.recipe.distros
urls =
http://plone.org/products/ploneldap/releases/1.0/PloneLDAP-bundle-1.0.tar.gz
nested-packages =
PloneLDAP-bundle-1.0.tar.gz
version-suffix-packages =
Nach dem Aufruf von ./bin/buildout
finden sich die Produkte LDAPMultiPlugins
, LDAPUserFolder
und PloneLDAP
in parts/productdistros
.
version-suffix-packages
Produkte, deren Verzeichnisnamen die Version enthält und die daher vor ihrer Verwendung umbenannt werden müssen.
In den Abschnitten products
in [instance]
wird dann auf den Installationsort von productdistros
verwiesen:
${productdistros:location}
[instance]
Dieser Abschnitt erstellt und konfiguriert eine Zope-Instanz unter Verwendung von plone.recipe.zope2instance
:
[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
...
products =
${buildout:directory}/products
${productdistros:location}
zope2-location
Es wird das im zope2
-Abschnitt angegebene Verzeichnis für die Zope2-Installation verwendet.
Für Plone 3.1 sieht die buildout.cfg
-Datei etwas anders aus:
[buildout]
parts =
...
plone
index
URL eines Index-Servers. In diesem Index sucht Buildout sofern in den unter find-links
angegebenen Distributionen nichts gefunden wurde.
Ohne spezifische Angabe für index
wird der Python Package Index verwendet. Aus Gründen der Stabilität und Performance kann sich jedoch ein anderer Index empfehlen:
index = http://download.zope.org/ppix
[plone]
verwendet plone.recipe.plone um die Plone Produkte und Eggs herunteruladen:
[plone]
recipe = plone.recipe.plone
Dabei ist zu beachten, dass immer die aktuellste Version verwendet wird. Soll immer nur ein Plone-3.1.x-Release verwendet wird, wird beim Erstellen des Buildout-Projekts zunächst auf die Frage Enter plone_version
mit 3.1
geantwortet, und anschließend kann man sich zunutze machen, dass die Versionsnummern des plone.recipe.plone
immer mit denen von Plone übereinstimmen:
recipe = plone.recipe.plone>=3.1,<3.2dev
Und für ein bestimmtes Plone-Release sieht die Angabe so aus:
recipe = plone.recipe.plone==3.1.7
Das plone
-Rezept gibt jeweils passende Zope-Versionen, Produkte und Eggs an, die in den Abschnitten [zope2]
und [instance]
mit den Buildout-Variablen ${plone:zope2-url}
, ${plone:eggs}
und ${plone:products}
referenziert werden.
Damit Änderungen der buildout.cfg wirksam werden, müssen wir ./bin/buildout
erneut aufrufen. Diese Aktualisierung kann häufig beschleunigt werden, indem
buildout im non-updating-Modus aufgerufen wird, also:
$ ./bin/buildout -N
Eine Übersicht über alle für buildout
verfügbaren Optionen erhalten Sie mit:
$ ./bin/buildout -h
zc.buildout < 2.0 verwendet üblicherweise immer die neueste Version eines Eggs. Sollen jedoch nur finale Versionen verwendet werden, kann im buildout-Abschnitt folgendes angegeben werden:
prefer-final = true
zc.buildout < 2.0 aktualisiert üblicherweise immer auf die neueste Version. Dies unterbleibt jedoch, wenn im buildout-Abschnitt folgendes angegeben wird:
newest = false
Darüberhinaus kann buildout
eine Fehlermeldung ausgeben, wenn Versionen noch
nicht festgeschrieben wurden mit:
allow-picked-versions = false
In den oben angegebenen Beispielen wurde gezeigt, wie sich die Versionen für Eggs, Recipes und Products fest vorgeben lassen.
Eine Liste der noch nicht festgeschriebenen Versionen erhalten wir in buildout ≥ 2.0 mit:
[buildout]
…
show-picked-versions = true
In früheren Buildout-Versionen können wir diese Angabe erhalten mit:
$ ./bin/buildout -Nv | sed -ne 's/^Picked: //p' | sort | uniq
Ab Plone 4.1 werden die benötigten Versionen festgeschrieben indem in der
buildout.cfg
-Datei auf eine externe versions.cfg
-Datei angegeben wird:
[buildout]
…
extends =
http://dist.plone.org/release/4.1/versions.cfg
versions = versions
In der Datei http://dist.plone.org/release/4.1/versions.cfg
werden dann im
[versions]
-Abschnitt die von Plone benötigten Eggs in definierten Versionen
angegeben:
[buildout]
extends = http://download.zope.org/zopetoolkit/index/1.0.3/zopeapp-versions.cfg
http://download.zope.org/Zope2/index/2.13.8/versions.cfg
[versions]
# Buildout infrastructure
mr.developer = 1.17
…
# External dependencies
…
# Plone release
Plone = 4.1
Products.ATContentTypes = 2.1.3
…
Falls weitere Eggs festgeschrieben werden sollen, kann nicht in der
buildout.cfg
-Datei ein weiterer versions
-Abschnitt angegeben werden, es
kann jedoch in extends
eine eigene versions.cfg
-Datei angegeben werden,
die ebenfalls wieder einen [versions]
-Abschnitt enthält. So kann z.B. die
buildout.cfg
-Datei folgendermaßen aussehen:
[buildout]
…
extends =
http://dist.plone.org/release/4.1/versions.cfg
versions.cfg
versions = versions
Und in der versions.cfg
-Datei werden dann weitere Versionen festgeschrieben:
[versions]
…
Sollen spezifische, in einem Rezept angegebene Versionen überschrieben werden, wird zunächst im buildout
-Abschnitt mit der Option versions
auf einen Abschnitt verwiesen, der die zu verwendenden Versionen enthält:
[buildout] … versions = release1
[release1] plone.locking = 1.0.5
Anschließend muss das Egg noch im [plone]
-Abschnitt angegeben werden, um die
Festlegung der Version für dieses Produkt in plone.recipe.plone
aufzuheben:
[plone]
recipe = plone.recipe.plone
eggs =
plone.locking
Siehe auch: Repeatable buildouts: controlling eggs used
buildout.dumppickedversions
¶buildout.dumppickedversions ist eine
buildout-extension
für zc.buildout < 2.0, die alle Eggs, deren Versionen
bisher nicht festgeschrieben wurden, in einer versions.cfg
-Datei
festschreiben kann. Eine Beispielkonfiguration kann z.B. so aussehen:
[buildout]
…
extensions =
buildout.dumppickedversions
dump-picked-versions-file = versions.cfg
overwrite-picked-versions-file = false
Sofern noch nicht vorhanden, wird nun beim Aufruf von ./bin/buildout
eine versions.cfg
-Datei erzeugt, die z.B. so aussehen kann:
[versions]
Cheetah = 2.2.1
Paste = 1.7.5.1
PasteScript = 1.7.3
ZopeSkel = 2.19
i18ndude = 3.2.2
#Required by:
#PasteScript 1.7.3
PasteDeploy = 1.3.4
#Required by:
#i18ndude 3.2.2
ordereddict = 1.1
z3c.checkversions findet neuere Versionen der im Buildout-Projekt verwendeten Python-Pakete.
Es kann folgendermaßen installiert werden:
[buildout]
parts =
…
checkversions
…
[checkversions]
recipe = zc.recipe.egg
eggs = z3c.checkversions [buildout]
extends
¶Im Buildout-Abschnitt können mit extends
mehrere Konfigurationsdateien eingebunden werden. Auf diese Weise können dann auch umfangreiche Konfigurationen, wie z.B. die Installation der libxml2- und libxslt-Bibliotheken in eine eigene Konfigurationsdatei lxml.cfg
mit folgendem Inhalt ausgelagert werden:
[lxml]
parts =
staticlxml
pylxml
[pylxml]
recipe=zc.recipe.egg
interpreter=pylxml
eggs=
lxml
[staticlxml]
recipe = z3c.recipe.staticlxml
egg = lxml
Anschließend kann diese Konfigurationsdatei mit all ihren Abschnitten in die buildout.cfg
-Datei eingebunden werden mit:
[buildout]
extends =
lxml.cfg
parts =
${lxml:parts}
…
Es kann auch eine URL angegeben werden, also z.B.:
[buildout]
extends =
http://www.plone-entwicklerhandbuch.de/plone-entwicklerhandbuch/entwicklungsumgebung/lxml.cfg
Umgekehrt kann auch die buildout.cfg
-Datei in eine andere Konfiguration übernommen werden, siehe hierzu Buildout für Produktivserver.
Mit jarn.setuptoolsfixer wird ein Bug in den Setuptools behoben, der auftritt sofern die Homepage oder Download-URL eines Pakets nicht erreichbar ist, das Paket jedoch in PyPI zur Verfügung steht.
Mit plone.recipe.command können Sie eigene Shell-Befehle während der Installation oder des Updates durchführen. Somit können Sie zum Beispiel der Zope-Instanz externe Methoden im Verzeichnis parts/instance/Extension
zur Verfügung stellen:
[extensions]
recipe = plone.recipe.command
command =
ln -sf ${buildout:directory}/Extensions/* ${instance:location}/Extensions/
update-command =
${extensions:command}
buildout.extensionscripts erlaubt die Verwendung von Python-Skripten als Buildout-Erweiterungen.
Die Buildout-Konfiguration kann dann z.B. so aussehen:
[buildout]
extensions =
…
buildout.extensionscripts
…
extension-scripts =
${buildout:directory}/buildout-utils.py:patchScriptGeneration
Und buildout-utils.py
kann dann z.B. so aussehen:
# Workaround for https://bugs.launchpad.net/zc.buildout/+bug/164629
def patchScriptGeneration(buildout):
from zc.buildout import easy_install
if not 'sys.exit(' in easy_install.script_template:
easy_install.script_template = easy_install.script_template.replace(
"%(module_name)s.%(attrs)s(%(arguments)s)",
"sys.exit(%(module_name)s.%(attrs)s(%(arguments)s))")
Das Rezept z3c.recipe.usercrontab
ändert die crontab-Einträge des Nutzers. So kann z.B. für den @reboot
-Eintrag folgendes in der buildout.cfg
-Datei angegeben werden:
[buildout]
…
parts =
…
crontab
[crontab]
recipe = z3c.recipe.usercrontab
times = @reboot
command = ${buildout:directory}/bin/instance start
Dabei kann eine Buildout-Konfigurationsdatei auch mehrere crontab-Abschnitte enthalten.
Mit collective.recipe.template lassen sich Textdateien aus Vorlagen erstellen wobei die buildout
-Variablen verwendet werden können. Hierzu wird in der buildout.cfg
-Datei z.B. folgender neuer Abschnitt definiert:
[buildout]
parts =
…
logrotate
…
[logrotate]
recipe = collective.recipe.template
input = templates/logrotate.conf
output = ${buildout:directory}/etc/logrotate.conf
Und wenn ein Auszug aus templates/logrotate.conf
-Datei so aussieht:
…
${buildout:directory}/var/log/instance.log {
postrotate
${buildout:bin-directory}/instance logreopen
endscript
}
sieht dieser Auszug in der generierten Datei myproject/etc/logrotate.conf
so aus:
…
/home/veit/myproject/var/log/instance.log {
postrotate
/home/veit/myproject/instance logreopen
endscript
}
vs_buildout
├── README.rst
├── base.cfg
├── bootstrap.py
├── deploy.cfg
├── devel.cfg
├── develop-eggs
│ └── vs.policy.egg-link
├── docs
│ └── HISTORY.rst
├── eggs
│ ├── AccessControl-2.13.10-py2.7-macosx-10.4-x86_64.egg
│ ├── Acquisition-2.13.8-py2.7-macosx-10.4-x86_64.egg
│ ├── …
├── etc
│ ├── logrotate.conf
│ └── plone.vcl
├── rsync.cfg
├── src
│ ├── README.txt
│ └── vs.policy
├── templates
│ ├── haproxy.conf.in
│ ├── logrotate.conf.in
│ └── plone.vcl.in
├── var
│ ├── blobbackup-blobstorages
│ ├── blobbackup-blobstoragesnapshots
│ ├── blobstorage
│ │ └── tmp
│ ├── filebackup-snapshots
│ ├── filebackups
│ ├── filestorage
│ │ ├── Data.fs
│ │ ├── Data.fs.index
│ │ ├── Data.fs.lock
│ │ └── Data.fs.tmp
│ ├── instance
│ │ └── import
│ ├── instance-base
│ │ └── import
│ └── log
│ └── zeoserver.log
├── versions.cfg
└── vs_buildout.txt
bin/
enthält ausführbare Dateien, u.a. buildout
und das Zope-Kontrollskript instance
.
bootstrap.py
Bootstrap-Skript des Buildout-Projekts.
*.cfg
Konfigurationsdateien von Buildout, siehe Buildout-Konfiguration.
develop-eggs/
Verweise auf Eggs, die in diesem Buildout-Projekt entwickelt werden sollen und die in der buildout.cfg
angegeben wurden.
downloads/
Rezepte wie plone.recipe.plone
und plone.recipe.distros
laden ihre Archive von Produkten und Paketen in diesem Verzeichnis herunter.
eggs/
Eggs, die Buildout automatisch heruntergeladen hat. Aktiviert werden die Eggs explizit in den Kontrollskripten im bin
-Verzeichnis.
fake-eggs/
In Buildout-Projekten für Plone ≤ 3.1 enthält dieses Verzeichnis die sog. fake-zope-eggs
, wobei in der *.egg-info
-Datei die Version angegeben werden kann.
.installed.cfg
Buildout speichert die aktuellen Konfigurationsdaten in dieser Datei.
parts/
Dateien, die für die jeweiligen Abschnitte der Buildout-Konfiguration verwendet werden.
products/
Für ältere Plone-Versionen lassen sich hier die Zope-Produkte erstellen, die in diesem Buildout-Projekt entwickelt werden.
src/
Eggs, die in diesem Buildout-Projekt entwickelt werden.
var/
var-Dateien der Zope-Instanz: z.B. ZODBs in filestorage/
Log-Dateien in logs/
und zopectlsock
und kompilierte Übersetzungsdateien in instance-base
.
Ein solches Buildout-Projekt kann anderen Entwicklern in einem Repository zur Verfügung gestellt werden, wobei die Verzeichnisse bin
, eggs
, download
, var
und parts
ignoriert werden können, da sie sich mit bootstrap.py
wiederherstellen lassen.
Da wir mehrere Projekte mit verschiedenen Buildout-Konfigurationen betreuen, wollen wir die Sourcen zwischen den Projekten teilen.
So ist es möglich, Eggs verschiedener Versionen in einem Verzeichnis
bereitzustellen. Ein solches Verzeichnis kann in der
Buildout-Konfigurationsdatei buildout.cfg
im Abschnitt [buildout]
eingetragen werden, z.B.:
[buildout]
eggs-directory = /home/veit/.buildout/eggs
Analog teilen wir das downloads
-Verzeichnis zwischen verschiedenen
Buildout-Projekten:
download-cache = /home/veit/.buildout/downloads
Ab Version 1.4.1 von zc.buildout
lässt sich auch ein geteiltes Verzeichnis
für die heruntergeladenen Dateien angeben:
extends-cache = /home/veit/.buildout/cache
Um Fehlermeldungen zu vermeiden sollten die zu verwendenden Verzeichnisse vor
dem ersten Durchlauf von Buildout erstellt werden, z.B. mit mkdir -p
/home/veit/.buildout/{eggs,downloads,cache}
.
Üblicherweise sucht Buildout im Python Package Index nach Distributionen. Es können jedoch auch auch weitere Indizes angegeben werden mit:
[buildout]
…
index = https://pypi.org/simple/
Dieser Index oder falls kein Index angegeben ist
https://pypi.python.org/simple/
wird zur Suche nach der passenden
Distribution verwendet. Dabei wird immer die letzte passende Version einer
angeforderten Distribution heruntergeladen.
Neben Indizes kann auch die find-links
-Option angegeben werden um
Distributionen zu finden.
Dabei können sowohl URLs als auch Pfadangaben verwendet werden:
[buildout]
…
find-links =
http://download.zope.org/distribution/
/some/path
/other/path/someegg-1.0.0-py2.4.egg
Um diese Konfiguration nicht für jedes Buildout-Projekt erneut angeben zu
müssen, kann eine default.cfg
-Datei im Verzeichnis ~/.buildout
angelegt
werden:
[buildout]
eggs-directory = /home/veit/.buildout/eggs
download-cache = /home/veit/.buildout/downloads
extends-cache = /home/veit/.buildout/cache
index = https://pypi.python.org/simple
socket-timeout = 3
Wollen wir weitere Entwicklungswerkzeuge in einem Buildout-Projekt installieren, geben wir diese einfach in der buildout.cfg
-Datei an. Folgende Entwicklungswerkzeuge können die Arbeit deutlich vereinfachen:
Produkt, das alle Klassen und Methoden eines Objekts im Zope Management Interface (ZMI) auflistet.
DocFinderTab kann direkt als Egg in der Instanz angegeben werden:
[instance]
...
eggs =
Products.DocFinderTab
Pyflakes analysiert Python-Programme und entdeckt verschiedene Fehlerarten. Es ist sehr viel schneller als das Ausführen der Programme.
pyflakes lässt sich einfach mit Buildout installieren:
parts =
...
pyflakes
[pyflakes]
recipe = zc.recipe.egg:scripts
eggs = pyflakes
scripts = pyflakes
entry-points = pyflakes=pyflakes.scripts.pyflakes:main
Debug-Toolbar, die einfach für eine Plone-Site aktiviert werden kann. Die Installation kann einfach mit Buildout erfolgen:
[instance]
...
eggs =
plone.app.debugtoolbar
Pylint analysiert Python-Code in Bezug auf Bugs und geringe Code-Qualität.
Pylint lässt sich einfach mit Buildout installieren:
parts =
pylint
...
[pylint]
recipe = zc.recipe.egg
eggs =
${instance:eggs}
pylint
entry-points = pylint=pylint.lint:Run
arguments = sys.argv[1:]
Für entsprechende Prozesse wird Debugging möglich, indem ein Traceback aller laufenden Pythonprozesse sowohl zum Eventlog als auch zum Browser geschickt wird.
PDBDebugMode erlaubt sog. post-mortem-Debugging für exceptions im Debug-Modus, d.h., bei einem Fehler wird der Debugger aufgerufen, der den Traceback ausgibt. Sofern vorhanden, nutzt PDBDebugMode
ipdb
statt pdb
.
Diese Entwicklungswerkzeuge lassen sich einfach angeben mit:
[instance]
...
debug-mode = on
eggs =
Products.PDBDebugMode
z3c.deadlockdebugger
Monkey Patch, der MailHost-Nachrichten nicht verschickt, sondern auf der Konsole ausgibt, d.h., Zope versendet damit keine Mails mehr.
Testrunner, der die testgetriebene Entwicklung deutlich beschleunigen kann.
roadrunner
läd vorab das Standard-Zope- und Plone-Environment für PloneTestCase. zur Installation wird einfach folgendes in die buildout.cfg
-Datei eingetragen:
[buildout]
parts =
...
roadrunner
[roadrunner]
recipe = roadrunner:plone
packages-under-test = vs.policy
Anschließend kann es wie der reguläre Zope-Testrunner aufgerufen werden:
$ ./bin/roadrunner -s vs.policy
Rezept, mit dem in Buildout auf ${grp:GROUP}
referenziert werden kann um
die Gruppe des aktuellen Nutzers herauszubekommen.
Zusammen mit gocept.recipe.env, das Environment-Variablen in einem Buildout-Abschnitt zur Verfügung stellt, lassen sich hiermit die Eigentümer (Owner) der Buildout-Inhalte setzen lassen, z.B. mit:
chown -R ${env:USER}:${grp:GROUP} ${buildout:directory}
Installieren lassen sich die Pakete mit:
[env]
recipe = gocept.recipe.env
[grp]
recipe = collective.recipe.grp
Python-Shell, die Ihnen u.a. folgende Vorteile bietet:
Objekt-Introspektion
Code- Introspektion
Dokumentation-Introspektion (mit %pdoc
)
Eingabehistorie, persistent auch über Sessions hinweg.
Zur Installation fügen Sie bitte folgendes in Ihrer devel.cfg
-Datei hinzu:
[buildout]
_
parts =
...
ipzope
...
[ipzope]
# An IPython Shell for interactive use with Zope running.
#
# It requires the `ipy_profile_zope.py` configuration script. Get this from
# git@github.com:collective/dotipython.git and put it in your profile
# directory. Depending on your setup, this may be at
# `$HOME/.ipython/profile_zope/startup`,
# `$HOME/.config/ipython/profile_zope/startup` (Ubuntu 12.04), or see
# http://ipython.org/ipython-doc/dev/config/overview.html#configuration-file-location
# for more details.
#
recipe = zc.recipe.egg
eggs =
ipython
${instance}
initialization =
import sys, os
os.environ["INSTANCE_HOME"] = "${instance:location}"
sys.argv[1:1] = "--profile=zope".split()
scripts = ipython=ipzope
Rufen Sie dann zunächst das buildout-Skript auf. Anschließend können Sie dann die IPython-Sell aufrufen:
$ ./bin/buildout
$ ./bin/ipzope
Beim ersten Aufruf von ipzope
wird ein neues IPython
-Profil in Ihrem
Home-Verzeichnis erstellt. In *ix-Betriebssystemen finden Sie das
entsprechende Verzeichnis unter $HOME/.ipython/
, in Windows unter
%userprofile%\_ipython
. In dieses Verzeichnis sollten das Profil aus
https://github.com/collective/dotipython/blob/master/ipy_profile_zope.py
legen. Anschließend sollten Sie die IPython-Session mit Ctrl-d
beenden und
erneut starten.
Anschließend lässt sich z.B. portal.error_log.get
eingeben und durch
Drücken der Tab-Taste erhalten Sie alle verfügbaren Methoden des
èrror_log`, die mit get
beginnen.
Falls Sie Änderungen an Ihrer Plone-Site vorgenommen haben, können Sie diese speichern mit:
utils.commit()
Und falls auch andere auf der Zope-Instanz arbeiten, sollten Sie gelegentlich die Änderungen übernehmen mit:
utils.sync()
Weitere Informationen zu iPython erhalten Sie im iPython-Tutorial.
ipdb
ist Python-Debugger, der viele Vorteile von IPython nutzt, z.B.
automatische Vervollständigung. iw.debug
erlaubt Ihnen, den ipdb
-
Debugger über jedem veröffentlichten Objekt einer Zope2-Anwendung aufzurufen.
Zum Installieren fügen Sie in Ihrer devel.cfg
-Datei folgendes hinzu:
[buildout]
_
[instance]
eggs +=
...
iw.debug
_
[instance]
_
zcml +=
iw.debug
Anschließend wird das Buildout-Skript aufgerufen und die Instanz im Vordergrund gestartet:
$ ./bin/buildout
$ ./bin/instance fg
Bemerkung
Wenn in Ihrem Code an irgendeiner Stelle ein ipdb
oder pdb
Code enthalten ist, erhalten Sie die Exception BdbQuit
.
Nun kann der URL eines jeden Objekts der Plone-Site /ipdb
angehängt werden
um eine IPython-Shell für diese Plone-Site zu erhalten:
...
--Return--
None
> /Users/veit/.buildout/eggs/iw.debug-0.3-py2.7.egg/iw/debug/pdbview.py(92)pdb()
91 else:
---> 92 set_trace()
93
Um die lokalen Variablen zu erhalten, können Sie nun zunächst ll
eingeben:
ipdb> ll
{'request': <HTTPRequest, URL=http://localhost:8080/Plone/ipdb>, 'portal': <PloneSite at /Plone>, 'context': <PloneSite at /Plone>, 'meth': None, 'view': None}
ipdb> context
<PloneSite at /Plone>
ipdb> context == portal
True
ipdb> portal.Title()
'Website'
ipdb> portal.portal_quickinstaller.listInstallableProducts()
[{'status': 'new', 'hasError': False, 'id': 'plone.app.dexterity', 'title': u'Dexterity Content Types'}, {'status': 'new', 'hasError': False, 'id': 'plone.app.theming', 'title': u'Diazo theme support'}, {'status': 'new', 'hasError': False, 'id': 'plone.app.caching', 'title': u'HTTP caching support'}, {'status': 'new', 'hasError': False, 'id': 'Marshall', 'title': 'Marshall'}, {'status': 'new', 'hasError': False, 'id': 'plone.app.openid', 'title': u'OpenID Authentication Support'}, {'status': 'new', 'hasError': False, 'id': 'plone.app.debugtoolbar', 'title': u'Plone debug toolbar'}, {'status': 'new', 'hasError': False, 'id': 'plone.session', 'title': u'Session refresh support'}, {'status': 'new', 'hasError': False, 'id': 'plone.resource', 'title': u'Static resource storage'}, {'status': 'new', 'hasError': False, 'id': 'CMFPlacefulWorkflow', 'title': u'Workflow Policy Support (CMFPlacefulWorkflow)'}, {'status': 'new', 'hasError': False, 'id': 'plone.app.iterate', 'title': u'Working Copy Support (Iterate)'}, {'status': 'new', 'hasError': False, 'id': 'collective.z3cform.datetimewidget', 'title': u'collective.z3cform.datetimewidget'}]
ipdb>
Früher wurden Änderungen in Templates oder Python-Skripten im Debug-Modus oder mit refresh.txt einfach übernommen, und ein Neustart von Zope war nur selten nötig.
Heute mag es häufig so erscheinen, als ob Zope bei jeder kleinen Änderung neu gestartet werden muss, damit die Änderungen auch übernommen werden. Ich gebe hier nun mal einen Überblick, welche Änderungen wie übernommen werden:
Wird die Instanz im Debug-Modus gestartet, werden für Änderungen an Page-Templates und Zope-3-Browser-Ressourcen keine Neustarts benötigt.
Die Generic-Setup-XML-Dateien werden bei jedem Import durch das Generic-Setup-Tool ausgelesen.
Auch die Install.py
-Dateien in einem Extensions
-Verzeichnis, die vom Portal-Quickinstaller verwendet werden, können ohne Neustart verändert werden.
Und auch alle Dateien im skins
-Ordner, einschließlich der Python-Skripte, werden ohne Neustart aktualisiert.
Andere Python-Skripte, also z.B. setuphandlers.py
, werden jedoch nur bei einem Neustart aktualisiert. Hier schafft sauna.reload Abhilfe, indem es Code und ZCML-Dateien nachlädt. Um sauna.reload
zu installieren, geben Sie folgendes in der buildout.cfg
-Datei an:
[buildout]
…
eggs =
…
sauna.reload
[instance]
…
zope-conf-additional =
%import sauna.reload
Nach dem Aufruf des buildout-Skripts sollte die Instanz gestartet werden mit:
$ RELOAD_PATH=src/ ./bin/instance fg
Anschließend können Sie sich im ZMI anmelden und folgende URL aufrufen:
http://localhost:8080/@@saunareload
Darüberhinaus helfen pyflakes und PDBDebugMode schon vor einem Neustart die Fehler zu finden.
Und schließlich wird bei Test Driven Development nicht der Zope-Server selbst sondern nur der Testrunner aufgerufen ;-)
Bemerkung
Achten Sie auch darauf, dass die Resource Registries CSS-, JavaScripts- und KSS-Registry im Debug/development-Modus betrieben werden, damit die zugehörigen Dateien nicht gecachet und entsprechende HTTP-Header ausgeliefert werden.
Bemerkung
Unter Windows muss PyYAML unter Verwendung der Binärdateien installiert werden.
mr.developer ist eine Buildout-Erweiterung, mit der sich Projekte, die über mehrere Repositories verteilt sind, verwalten lassen.
mr.developer
wird in der extensions
-Option im [buildout]
-Abschnitt
hinzugefügt. Anschließend können folgende weitere Optionen festgelegt werden:
sources-dir
Das Verzeichnis, in das die Pakete heruntergeladen werden.
Der Standardwert ist src
.
sources
Liste der Repository-Informationen der Pakete.
Das Format ist <kind> <url> [key=value]
.
kind
svn
, hg
oder git
url
Die URL des Repository
key=value
Hier können Optionen für jedes einzelne Paket angegeben werden.
Es können weder Leerzeichen in key
noch in value
noch um das Gleichheitszeichen herum verwendet werden.
Im Folgenden einige der gebräuchlichsten Optionen:
path
Optionale Angabe des Pfads, in den das Paket ausgecheckt wird, wobei der Name des Pakets dem Pfad angehängt wird.
Wird keine Angabe für path
getroffen, wird stattdessen sources-dir
verwendet.
full-path
Angabe des Pfades, in das ein Paket ausgecheckt wird ohne dass der Paketname angehängt wird.
update
spezifiziert, ob ein Paket beim Durchlaufen von Buildout aktualisiert
werden soll oder nicht. Die Angabe überschreibt die globale
always-checkout
-Anweisung.
egg
Diese Option erlaubt, die Verwaltung von Paketen, die keine Python-Eggs sind
mit egg=false
. Dann wird das Paket nicht der develop
-Option von
Buildout hinzugefügt.
auto-checkout
Pakete, die beim initialen Aufruf des buildout
-Skripts automatisch ausgecheckt werden. *
kann angegeben werden falls alle Pakete in sources
ausgecheckt werden sollen.
always-checkout
Der Standardwert ist false
.
true
Alle in auto-checkout
angegebenen Pakete, die im develop mode sind, werden aktualisiert sobald das buildout
-Skript aufgerufen wird.
force
Wie bei true
, jedoch werden auch die als dirty
markierten Pakete ohne Rückfrage aktualisiert.
Hier ein Beispiel für einen solchen Eintrag in die buildout.cfg
-Datei:
[buildout]
…
extensions = mr.developer
sources = sources
auto-checkout =
vs.policy
some.package
bootstrap
always-checkout = true
[sources]
vs.policy = svn https://svn.veit-schiele.de/svn/vs.policy/trunk
some.package = git git://example.com/git/some.package.git
bootstrap = git git://github.com/twitter/bootstrap.git rev=d9b502dfb876c40b0735008bac18049c7ee7b6d2 path=${buildout:directory} egg=false
Buildout erzeugt nun ein Skript bin/develop
, das verschiedene Aktionen zu den einzelnen Paketen erlaubt, wie z.B. das Auschecken des Quellcodes ohne den Ort des Repositories kennen zu müssen. Für weitere Aktionen geben Sie einfach folgendes ein:
$ ./bin/develop help
Wird der Quellcode aus einem Paket ausgecheckt, muss buildout
erneut durchlaufen werden. Das Paket wird dann automatisch als develop egg markiert, und falls es in der Liste der versions
-Option festgeschrieben wurde wird dieser Eintrag gelöscht und das develop egg verwendet.
Die Liste der develop eggs kann mit den activate
- und deactivate
-Kommandos von bin/develop
gesteuert werden.
Zum Erstellen eines Python-Eggs, das ein Zope2-Produkt enthält, verwenden wir
das ZopeSkel-Template plone
:
$ cd src
$ $ ../bin/zopeskel plone_basic
…
Enter project name (or q to quit): vs.policy
Expert Mode? (What question mode would you like? (easy/expert/all)?) ['easy']: all
Namespace Package Name (Name of outer namespace package) ['vs']:
Package Name (Name of the inner namespace package) ['policy']:
Version (Version number for project) ['1.0']:
Description (One-line description of the project) ['']: Policy package for demonstration purposes
Register Profile (Should this package register a GS Profile) [False]: True
Long Description (Multi-line description (in ReST)) ['']: Policy package for demonstration purposes
Author (Name of author for project) ['']: Veit Schiele
Author Email (Email of author for project) ['']: kontakt@veit-schiele.de
Keywords (List of keywords, space-separated) ['']: Zope Plone
Project URL (URL of the homepage for this project) ['http://svn.plone.org/svn/collective/']: https://github.com/veit/vs.policy
Project License (Name of license for the project) ['GPL']:
Zip-Safe? (Can this project be used as a zipped egg? (true/false)) [False]:
Zope2 Product? (Are you creating a product for Zope2) [True]:
Creating directory ./vs.policy
…
Hiermit werden folgende Dateien erzeugt:
vs_buildout/src/vs.policy
├── CHANGES.rst
├── CONTRIBUTORS.rst
├── README.rst
├── bootstrap.py
├── buildout.cfg
├── docs
│ ├── LICENSE.GPL
│ └── LICENSE.txt
├── setup.cfg
├── setup.py
└── src
└── vs
├── __init__.py
└── policy
├── __init__.py
├── configure.zcml
├── profiles
│ ├── default
│ │ └── metadata.xml
│ └── testing
│ └── metadata.xml
├── testing.py
└── tests
├── __init__.py
└── test_example.py
setup.py
enthält Anweisungen für Setuptools, Distribute oder Buildout, wie die Paketdistribution verwaltet werden soll.
setup.cfg
enthält zusätzliche Konfigurationsinformationen, in diesem Fall über das verwendete ZopeSkel-Template.
README.rst
Dokumentation des Pakets.
Soll das Paket im PyPI veröffentlicht werden, wird der Inhalt zusammen mit
dem Wert für long_description
in der setup.py
-Datei als HTML gerendert.
docs/
Enthält zusätzliche Dokumentation einschließlich der Software-Lizenz
CHANGES.rst
wird verwendet für die Change Log-Einträge im PyPI.
src/vs
das Namespace-Package, das in der __init__.py
-Datei eine Methode für
Setuptools und Distribute bereitstellt.
src/vs/policy
Das Wurzelverzeichnis des Paketes selbst.
src/vs/policy/__init__.py
Datei, die dieses Paket als Zope2-Produkt initiiert.
Ggf. wird diese Datei auch benötigt um Archetypes-Artikeltypen zu erstellen. Siehe hierzu Initialisierung und Hinzufügen-Rechte.
src/vs/policy/configure.zcml
Die wesentliche Zope-Konfigurationsdatei für unser Paket. Diese wird automatisch von Plone beim Starten der Instanz geladen.
src/vs/policy/testing.py
enthält ein Gerüst für Integrationstests. Wir werden dieses später durch unsere eigenen Tests ersetzen.
Nachdem das Python-Egg im src
-Verzeichnis erstellt worden ist, sollten
wir es noch in der devel.cfg
-Datei eintragen. Der instance-base
-Abschnitt
übernimmt die Eggs:
[buildout]
…
develop =
src/vs.policy
…
[instance-base]
eggs +=
…
vs.policy
Für Eggs, die in Plone-Sites ≤ 3.3 verwendet werden sollen, kann
z3c.autoinclude nicht
verwendet werden. Dieses Paket erstellt automatisch zwei neue ZCML-
Anweisungen: includeDependencies
und includePlugins
. Es ist mit dem
ZopeSkel-Template bereits in der Datei src/vs.policy/setup.py
verwendet
worden:
setup(name='vs.theme',
…
entry_points="""
# -*- entry_points -*-
[z3c.autoinclude.plugin]
target = plone
""",
…
)
In Plone ≤ 3.3 muss hingegen noch folgendes in Die Buildout-Konfiguration eingetragen werden:
[instance]
…
zcml =
vs.policy
Damit erstellt Buildout in der Instance einen sog. zcml-slug z.B. im
Verzeichnis parts/instance/etc/package-includes/
die Datei
001-vs.policy-configure.zcml
mit folgendem Einzeiler:
<include package="vs.policy" file="configure.zcml" />
Um zu Testen, ob das Python-Egg in der Instanz zur Verfügung steht, rufen wir
den Python-Interpreter zopepy
auf:
$ ./bin/zopepy
>>> from vs import policy
Da kein Fehler für den Import ausgegeben wurde, scheint das Egg geladen zu
werden, und der Python-Interpreter kann mit Strg-D
(unter Windows
Strg-Z
) wieder verlassen werden.
Alternativ kann auch die Instanz gestartet werden mit ./bin/instance
;
anschließend sollten sich im Zope Management Interface → Control_Panel →
Products das vs.policy
-Produkt finden.
In unserem Beispiel ändern wir Titel und Beschreibung der Site. Im ZMI könnten beide geändert werden unter localhost:8080/mysite/manage_propertiesForm
. Für die programmatische Änderung solcher Einstellungen gibt es seit Plone 2.5 das Generic Setup Tool, das unter localhost:8080/mysite/portal_setup/manage_workspace
verfügbar ist. Wenn Sie hier auf den Export-Reiter klicken, können solche Konfigurationen auch als XML-Dateien exportiert werden.
EXTENSION
-Profils¶Um ein solches Profil zu erstellen wird src/vs.policy/vs/policy/configure.zcml
folgendermaßen geändert:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="vs.policy">
<five:registerPackage package="." initialize=".initialize" />
<genericsetup:registerProfile
name="default"
title="vs.policy"
directory="profiles/default"
description="Policies for www.veit-schiele.de"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
</configure>
Anschließend sind noch die angegebenen Verzeichnisse zu erstellen:
$ mkdir src/vs.policy/vs/policy/profiles src/vs.policy/vs/policy/profiles/default
Schließlich wird in src/vs.policy/vs/policy/profiles/default
das Profil properties.xml
erstellt mit:
<?xml version="1.0"?>
<site>
<property name="title">Veit Schiele</property>
<property name="description">Welcome to Veit Schiele</property>
</site>
Leider lässt sich bisher nicht die gesamte Site durch Profile konfigurieren. Es lassen sich jedoch Methoden hinzufügen, die eine solche umfassende Konfiguration erlauben.
Hierzu wird zunächst in vs.policy/vs/policy/configure.zcml
folgendes angegeben:
<genericsetup:importStep
name="vs.policy.various"
title="vs.policy: miscellaneous import steps"
description="Various import steps that are not handled by GS import/export handlers."
handler="vs.policy.setuphandlers.setupVarious">
</genericsetup:importStep>
Anschließend wird die Datei setuphandlers.py
angelegt in vs.policy/vs/policy/
.
Diese Datei enthält zumindest die Methode setupVarious
, die nur ausgeführt wird, sofern in im Kontext eine Datei vs.policy_various.txt
vorhanden ist:
def setupVarious(context):
if context.readDataFile('vs.policy_various.txt') is None:
return
Schließlich wird noch die Datei vs.policy_various.txt
in vs.policy/vs/policy/profiles/default
angelegt.
Die HTML-Filterregeln lassen sich aktuell nicht mit Generic Setup-Profilen konfigurieren. Soll dies mit der setuphandlers.py
-Datei geschehen, kann diese z.B. so aussehen:
import logging
from plone.app.controlpanel.filter import IFilterSchema
logger = logging.getLogger('vs.policy')
def allowTags(site):
"""
Allows embed, object, param and iframe tags
"""
adapter = IFilterSchema(site)
nasty_tags = adapter.nasty_tags
if 'object' in nasty_tags:
nasty_tags.remove('object')
if 'embed' in nasty_tags:
nasty_tags.remove('embed')
stripped_tags = adapter.stripped_tags
if 'object' in stripped_tags:
stripped_tags.remove('object')
if 'param' in stripped_tags:
stripped_tags.remove('param')
custom_tags = adapter.custom_tags
if not 'embed' in custom_tags:
custom_tags.append('embed')
if not 'iframe' in custom_tags:
custom_tags.append('iframe')
adapter.nasty_tags = nasty_tags
adapter.stripped_tags = stripped_tags
adapter.custom_tags = custom_tags
logger.info("Allowing embed, object, param and iframe tags.")
def setupVarious(context):
if context.readDataFile('vs.policy_various.txt') is None:
return
site = context.getSite()
allowIframeTags(site)
Statt der bereits angelegten Datei src/vs.policy/vs/policy/tests.py
erstellen wir ein eigenes tests
-Modul:
$ rm -rf src/vs.policy/vs/policy/tests.py
$ mkdir src/vs.policy/vs/policy/tests
$ touch src/vs.policy/vs/policy/tests/__init__.py
Anschließend definieren wir im neu erstellten tests
-Ordner zunächst ein Test-Fixture, eine gleichbleibende Testumgebung mit der Basisklasse TestCase
, die an den Layer VS_POLICY_INTEGRATION
gebunden wird. Hierzu erstellen wir im tests
-Verzeichnis die Datei base.py
mit folgendem Inhalt:
import unittest2 as unittest
from plone.testing import z2
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD
from vs.policy.tests import layer
def get_browser(app, loggedIn=True):
browser = z2.Browser(app)
if loggedIn:
auth = 'Basic %s:%s' % (TEST_USER_NAME, TEST_USER_PASSWORD)
browser.addHeader('Authorization', auth)
return browser
class TestCase(unittest.TestCase):
layer = layer.VS_POLICY_INTEGRATION
class FunctionalTestCase(unittest.TestCase):
layer = layer.VS_POLICY_FUNCTIONAL
In layer.py
werden anschließend die Test-Layer VS_POLICY_INTEGRATION
und VS_POLICY_FUNCTIONAL
definiert, die beide auf VS_POLICY_LAYER
basieren:
from plone.app.testing import applyProfile
from plone.app.testing import PloneFixture
from plone.app.testing import PloneSandboxLayer
from plone.app.testing import PloneTestLifecycle
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.testing import z2
from zope.configuration import xmlconfig
class VsPolicyFixture(PloneFixture):
# No sunburst please
extensionProfiles = ()
VS_POLICY_FIXTURE = VsPolicyFixture()
class VsPolicyTestLifecycle(PloneTestLifecycle):
defaultBases = (VS_POLICY_FIXTURE, )
class IntegrationTesting(VsPolicyTestLifecycle, z2.IntegrationTesting):
pass
class FunctionalTesting(VsPolicyTestLifecycle, z2.FunctionalTesting):
pass
class VsPolicyLayer(PloneSandboxLayer):
defaultBases = (VS_POLICY_FIXTURE, )
def setUpZope(self, app, configurationContext):
import vs.policy
xmlconfig.file("configure.zcml", vs.policy,
context=configurationContext)
z2.installProduct(app, 'vs.policy')
def tearDownZope(self, app):
z2.uninstallProduct(app, 'vs.policy')
def setUpPloneSite(self, portal):
applyProfile(portal, 'vs.policy:default')
setRoles(portal, TEST_USER_ID, ['Manager'])
portal.invokeFactory('Folder', 'test-folder')
setRoles(portal, TEST_USER_ID, ['Member'])
VS_POLICY_LAYER = VsPolicyLayer()
VS_POLICY_INTEGRATION = IntegrationTesting(
bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Integration")
VS_POLICY_FUNCTIONAL = FunctionalTesting(
bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Functional")
Die eigentlichen Tests werden in der Datei test_test.py
definiert:
from vs.policy.tests.base import FunctionalTestCase
class TestTest(FunctionalTestCase):
def test_test(self):
self.assertTrue(True)
Unit Tests, die auf dem Python unittest-Modul, ZopeTestCase und PloneTestCase basieren, müssen sich an einige Namenskonventionen halten:
Alle Testdateien müssen mit test
beginnen, z.B. test_setup.py
.
In den Testdateien werden Klassen für Testfälle definiert, die ein oder mehrere Testmethoden enthalten können, die ebenfalls mit test
beginnen müssen, z.B. test_portal_title
.
Zunächst wird die Basisklasse importiert, dann die Klassen für die Testfälle und schließlich die Test Suite selbst definiert.
Jede Testsuite kann aus mehreren Testklassen bestehen. Wird die Testsuite ausgeführt, werden alle Testmethoden aller Testklassen der Test-Suite ausgeführt.
Innerhalb einer Testklasse kann die afterSetUp()
-Methode unmittelbar vor jedem Test aufgerufen werden um Testdaten für diesen Test anzugeben. Nachdem der Test durchgeführt wurde, werden die Transaktionen zurückgenommen, so dass normalerweise keine Artefakte zurückbleiben.
Werden jedoch Änderungen außerhalb von Zope vorgenommen, müssen diese mit der Methode beforeTearDown()
aufgeräumt werden.
Die in einer Testklasse verwendeten Methoden wie self.assertEqual()
oder self.failUnless()
sind Assertion-Methoden, und wenn eine von ihnen fehlschlägt, gilt der ganze Test als fehlgeschlagen.
Testmethoden überprüfen, ob etwas wahr oder falsch ist. Daher kann aus den Tests auch herausgelesen werden, wie sich Ihr Produkt verhalten soll, welche Fähigkeiten es enthält. Die Liste der Testmethoden ist ausführlich in der Python-Dokumentation für unittest.TestCaseObjects enthalten. Die häufigsten sind:
failUnless(expr)
stellt sicher, dass der Ausdruck expr
wahr ist.
assertEqual(expr1, expr2)
stellt sicher,dass expr1
gleich expr2
ist.
assertRaises(exception, callable, ...)
stellt sicher, dass beim Aufruf von callable
die Fehlermeldung
exception
ausgegeben wird.
Hinweis: callable
sollte der Name einer Methode oder ein
aufrufbares Objekt sein, nicht ein aktueller Aufruf, z.B.:
self.assertRaises(AttributeError, myObject.myMethod, someParameter)
fail()
Dies ist sinnvoll, wenn ein Test noch nicht fertiggestellt ist oder in
einem if
-Statement, das deutlich macht, dass der Test fehlgeschlagen ist.
ZopeTestCase und PloneTestCase fügen zu den Assertion-Methoden noch weitere hilfreiche Methoden und Variablen hinzu, die mit Zope interagieren. Hier nur kurz die wesentlichen Variablen:
self.portal
Die PloneSite, in der der Test ausgeführt wird.
self.folder
Der member
-Ordner des Mitglieds, als der die Tests ausgeführt werden.
Und hier die wesentlichen Hilfsmethoden:
self.logout()
abmelden, d.i. die Rolle anonymous
bekommen;
self.login()
sich erneut anmelden; wird ein Nutzername mit übergeben, erfolgt die Anmeldung als dieser Nutzer.
self.setRoles(roles)
durchläuft eine Liste von Rollen, die angenommen werden sollen.
self.setRoles((Manager,))
lässt Sie beispielsweise die Rolle des
Managers für eine bestimmte Zeit annehmen.
self.setPermissions(permissions)
analog können auch Berechtigungen für den Testnutzer in self.folder
angegeben werden;
self.setGroups(groups)
eine Liste von Gruppen, der der aktuelle Nutzer angehören soll.
Mehr über Unit Tests in Python erfahren Sie in der unittest-Python-Dokumentation.
Der Testrunner kann nun gestartet werden mit:
$ ./bin/test -s vs.policy
Wären die Tests geschrieben worden, bevor die Profile erstellt wurden, hätten beide Tests fehlschlagen müssen und der Testrunner folgendes ausgeben:
AssertionError:"Welcome to Veit Schiele != ''
…
AssertionError:'Veit Schiele != 'Plone site'
Ran 2 tests with 2 failures and 0 errors
Nachdem die Profile angelegt wurden, sollte jedoch keiner der Tests fehlschlagen:
Ran 2 tests with 0 failures and 0 errors.
-s my.package
, --package my.package
, --dir my.package
durchsucht die angegebenen Verzeichnisse nach Tests.
-m test_setup
, --module test_setup
spezifiziert ein Testmodul als regulären Ausdruck, z.B.:
$ ./bin/test -s my.package -m 'test_setup'
-t '.*installed.*'
, --test test_theme_installed
spezifiziert einen Testfilter als regulären Ausdruck, z.B.:
$ ./bin/test -s vs.policy -m '.*setup.*' -t '.*installed.*'
Hiermit werden im Paket vs.policy
alle, mit installed
endenden,
Methoden in allen Testmodulen, die auf setup
enden, durchlaufen.
-u
, --unit
durchläuft ausschließlich Unit tests und ignoriert andere layer
-Optionen.
-f
, --non-unit
durchläuft alle Tests, die keine Unit Tests sind
-v
, --verbose
führt zu ausführlicherer Ausgabe
--ndiff
falls ein Doctest fehlschlägt, wird ndiff.py
zur Darstellung der
Unterschiede verwendet
--udiff
falls ein Doctest fehlschlägt, wird Unified Diff zur Darstellung der Unterschiede verwendet
--cdiff
falls ein Doctest fehlschlägt, wird Context Diff zur Darstellung der Unterschiede verwendet
-d
, post-mortem
stoppt die Ausführung nach dem ersten nicht-bestandenen Test und ermöglicht post-mortem-Debugging, d.h. die Debug-Session wird nur gestartet, wenn ein Test fehlschlägt.
--path src/my.package
fügt einen Pfad zu Pythons Suchpfad hinzu, wobei die Option mehrfach angegeben werden kann.
Diese erhalten Sie mit:
$ ./bin/test --help
Wenn die relevanten Tests erfolgreich verliefen, sollten schließlich noch alle Tests durchgeführt werden um sicherzustellen, dass nicht an anderer Stelle etwas gebrochen ist. Wenn alle Tests erfolgreich durchlaufen wurden, erscheint eine Meldung:
Ran 10 tests with 0 failures and 0 errors in 4.830 seconds.
Falls nicht alle Tests erfolgreich durchlaufen wurden, ändert sich die Meldung:
Ran 10 tests with 2 failures and 3 errors in 9.688 seconds.
Dabei wurden dann zwei Tests nicht bestanden und drei Tests enthielten Fehler.
roadrunner ist ein Testrunner für Plone 2.5 bis 3.1, der die testgetriebene
Entwicklung deutlich beschleunigen kann, da er vorab das Standard-Zope- und
Plone-Environment für PloneTestCase läd. zur Installation wird einfach folgendes
in die devel.cfg
-Datei eingetragen:
[buildout]
parts =
…
roadrunner
[roadrunner]
recipe = roadrunner:plone
packages-under-test = vs.policy
Anschließend kann es wie der reguläre Zope-Testrunner aufgerufen werden:
$ ./bin/roadrunner -s vs.policy
Übernehmen Sie Tests z.B. aus Plone wenn diese Ihren eigenen Absichten entsprechen.
Dummy-Implementierungen sind häufig der einzige Weg um bestimmte Funktionen zu testen. Siehe auch CMFPlone/tests/dummy.py für einige Dummy-Objekt- Beispiele.
Tests können auch verwendet werden um Dinge auszuprobieren – sie sind eine sichere Umgebung.
Während des Debugging können print
-Statements in den Test eingefügt
werden um nachvollziehbare Hinweise im Terminal zu erhalten.
Es kann jedoch auch gleich der Python-Debugger in die Testmethoden importiert werden mit:
import pdb; pdb.set_trace()
Anschließend können Sie mit r
schrittweise durch den Testkode gehen.
Mehr zum Python-Debugger erfahren Sie in Debugging und in der Python-Dokumentation.
Zur Installation des Produkts in der Plone-Site gehen wir in Konfiguration → Zusatzprodukte, markieren vs.policy und klicken auf Installieren.
Anschließend sollten Sie in Ihrem Webbrowser unter der URL localhost:8080/mysite/folder_contents
Titel und Beschreibung sehen können.
Finden, evaluieren und installieren von Zusatzprodukten.
Die meisten Produkte befinden sich mittlerweile auf Github: https://github.com/collective.
Um nun zu überprüfen, ob das Produkt wirklich passend ist, können Sie verschiedene Schritte durchführen:
Zunächst sollten Sie überprüfen, wie sich das Produkt selbst präsentiert:
Ist es hinreichend gut dokumentiert?
Gibt es einen Bug Tracker?
Wie viele offene und geschlossene Bugs gibt es?
Wie schnell wurden die Bugs behoben?
In welcher Version liegt das Produkt vor?
Eine 0.* oder alpha-Version ist voraussichtlich weniger stabil als ein final release.
Gibt es eine Roadmap?
Sie gibt Ihnen Hinweise, wie die Planung für die weitere Entwicklung aussieht und wie zukunftssicher das Produkt ist.
Wie umfangreich und mit welchen Erfahrungen wird das Produkt eingesetzt?
Fragen Sie in einer Mailingliste nach, welche Erfahrungen mit dem Produkt gemacht wurden.
Dann sollten Sie das Produkt ausführlich in einer Testumgebung testen.
Kopieren Sie gegebenenfalls die ZODB aus Ihrem Produktivsystem in Ihr Testsystem.
Achten Sie darauf, dass die Buildout-Konfigurationen beider Systeme identisch sind.
Wie hoch ist die Testabdeckung des Produkts?
In Testabdeckung (Code Coverage) erhalten Sie weitergehende Informationen.
Durchlaufen Sie die automatisierten Tests sowohl Ihrer eigenen als auch des neuen Produkts.
Schließlich sollten Sie die Funktionalität und das User Interface auch in Ihrem Browser testen.
Allgemeinere und umfassendere Informationen zur Evaluation von OpenSource- Software erhalten Sie im Artikel Software-Evaluation.
Zusatzprodukte können unter install_requires
in der setup.py``-Datei
angegeben werden, also z.B.:
install_requires=[
'setuptools',
'Plone',
'vs.event',
],
Anschließend wird das Zusatzprodukt noch in der Instanz registriert in der
configure.zcml
-Datei:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="vs.policy">
<includeDependencies package="." />
…
</configure>
<includeDependencies package="." />
fügt alle Pakete der install_requires
-Liste einer Site hinzu.
Sollen die Pakete explizit angegeben werden, kann dies z.B. erfolgen mit:
<include package="vs.event" />
Damit unser Zusatzprodukt beim Aufsetzen einer neuen Plone-Site mit unserem
vs.policy
-Produkt automatisch aktiviert wird, sollten wir es noch in der
metadata.xml
-Datei eintragen:
<?xml version="1.0"?>
<metadata>
<version>1.0</version>
<dependencies>
<dependency>profile-vs.event:default</dependency>
</dependencies>
</metadata>
Schließlich sollten diese Änderungen noch in unserem Buildout-Projekt übernommen werden. Hierzu wird das Buildout-Skript erneut aufgerufen:
$ bin/buildout
Zunächst müssen wir sicherstellen, dass ``vs.event´´ in unseren Tests zur Verfügung steht:
class VsPolicy(PloneSandboxLayer):
defaultBases = (PLONE_FIXTURE,)
def setUpZope(self, app, configurationContext):
# Load ZCML
import vs.policy
xmlconfig.file(
'configure.zcml',
vs.policy,
context=configurationContext
)
# Install products that use an old-style initialize()
# function
z2.installProduct(app, 'vs.event')
def tearDownZope(self, app):
# Uninstall products installed above
z2.uninstallProduct(app, 'vs.event')
def setUpPloneSite(self, portal):
applyProfile(portal, 'vs.policy:default')
Nun fügen wir den eigentlichen Test hinzu:
def test_vs_event_installed(self):
portal = self.layer['portal']
portal_types = getToolByName(portal, 'portal_types')
self.assertTrue("VSEvent" in portal_types)
Schließlich führen wir diesen Test aus:
$ ./bin/test
Zusatzprodukte für Plone 5 finden Sie im Python Package Index (PyPI): Framework: Plone: 5.0.
erlaubt das web-basierte Erstellen von Formularen.
Mit uwosh.pfg.d2c gibt es auch eine Erweiterung für PloneFormGen, mit der sich aus PloneFormGen-Formularen Artikel erstellen lassen.
ist ein einfach zu verwendendes Web-Forum, das gut in die Plone-Oberfläche integriert ist.
Blogging-Tool für Plone.
integriert das jQuery-Plugin bookmark.
erlaubt das einfache Erstellen von Newslettern aus Inhalten einer Plone- Site.
für Captcha Spam-Schutz
für ReCaptcha-Spam-Schutz
für Akismet-Spam-Schutz
bietet ein Widget basierend auf einer Liste von Fragen und Antworten
ist ein Termin-Artikeltyp mit Unterstützung für sich wiederholende Termine und Zeitzonen, RFC5545-Kalender-Export etc.
ändert einen Ordner in einen Kalender mit verschiedenen Ansichten: Monat, Woche, Tag.
erlaubt das einfache Ändern von Layouts einer Plone-Site.
Gallerie und Slideshow, die auch Bilder aus Picase und Flickr anzeigen können.
erlaubt das Hinzufügen eines Sliders zu jeder Plone-Seite. Darüberhinaus gibt es auch eine Slider-Ansicht für Ordner und Kollektionen.
OCR und Ansicht von PDF-, Word- und Excel-Dokumenten
docsplit kann ggf. im Userspace installiert werden mit:
$ gem install docsplit --user-install
Anschließend kann der Pfad in die ~.bashrc
eingetragen werden, z.B.:
export PATH=$HOME/.gem/ruby/1.9.1/bin:$PATH
und anschließend das Terminal aktualisert werden mit:
$ source ~/.bashrc
erlaubt das Erstellen mehrerer Glossare in einer Plone-Site.
ermöglicht das Erstellen mehrerer Vokabularien in Plone, die entweder flach, hierarchisch oder VDEX-basiert sein können.
erlaubt mehrsprachige Plone-Sites.
bietet eine bessere Ansicht von Ordnerinhalten, das sowohl die Sortierung erleichtert als auch das Hochladen mehrerer Dateien.
ermöglicht das Hoch- und Herunterladen von Zip-Dateien.
stellt Teile des Dateisystems in einer Plone-Site dar.
bietet persönliche Verzeichnisse, Arbeitsräume etc. wobei es einerseits gut in Plones Nutzer- und Gruppenverwaltung integriert ist und andererseits leicht für spezifische Anforderungen erweitert werden kann.
ist ein Link-Artikeltyp mit Bild und veränderbarem Icon.
erstellt einen Presseraum mit Presseveröffentlichungen, Mitteilungen und Pressekontakten.
wurde entwickelt für die Dokumentation von Plone, kann jedoch auch gut für die Dokumentation anderer Open-Source-Produkte verwendet werden.
Um LinguaPlone ordnungsgemäß in einer Plone-Site aktivieren zu können, muss folgende Reihenfolge eingehalten werden:
Zunächst muss die Sprachauswahl im Plone Language Tool angegeben werden.
Erst im Anschluss daran kann LinguaPlone installiert werden.
Und schließlich wird der @@language-setup-folders
-View von LinguaPlone aufgerufen um auch das Site-Root-Objekt mehrsprachig darzustellen.
Programmatisch lässt sich dies realisieren, indem in vs.policy/vs/policy/setuphandlers.py
zunächst die Spracheinstellungen gesetzt werden, anschließend LinguaPlone aktiviert und der View @@language-setup-folders
aufgerufen wird:
def installLinguaPlone(site):
# LP must be installed a last step in order to deal with
# several strange annoyances and expecations that LP relies on.
pl = getToolByName(site, 'portal_languages')
pl.supported_langs = ('de', 'en')
qi = getToolByName(site, 'portal_quickinstaller')
qi.installProducts(['Products.LinguaPlone'])
transaction.savepoint(1)#
site.restrictedTraverse('@@language-setup-folders')()
…
def setupVarious(context):
if context.readDataFile('ise.policy_various.txt') is None:
return
…
installLinguaPlone(site)
In test_language_settings.py
können folgende Tests geschrieben werden:
import unittest2
from base import TestBase
class LanguageTests(TestBase):
def testInstalledProducts(self):
…
self.assertEqual('LinguaPlone' in installed, True)
def testLanguageSettings(self):
lang_tool = self.portal.portal_languages
default_language = self.portal.portal_languages.getDefaultLanguage()
self.assertEqual(default_language == 'de', True)
# return [(country code, countryname), ...]
supported_languages = [r[0] for r in self.portal.portal_languages.listSupportedLanguages()]
self.assertEqual('en' in supported_languages, True)
self.assertEqual('de' in supported_languages, True)
self.assertEqual(lang_tool.use_cookie_negotiation, True)
self.assertEqual(lang_tool.use_request_negotiation, True)
self.assertEqual(lang_tool.use_content_negotiation, True)
def test_suite():
from unittest2 import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(LanguageTests))
return suite
Sollen sprachneutrale Inhalte wie z.B. Bilder erstellt werden, sollte TinyMCE so gepatcht werden, dass er über die sprachspezifischen Ordner hinaus referenzieren kann. Dies lässt sich am einfachsten realisieren mit collective.monkeypatcher.
Anschließend erweitern wir unsere configure.zcml
-Datei um patches.zcml
:
<include file="patches.zcml" />
Nun legen wir patches.zcml
mit folgendem Inhalt an:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:monkey="http://namespaces.plone.org/monkey"
i18n_domain="vs.policy">
<include package="collective.monkeypatcher" />
<monkey:patch
description="TinyMCE JSON Folder listing should ignore INavigationRoot"
class="Products.TinyMCE.adapters.JSONFolderListing.JSONFolderListing"
original="getListing"
replacement=".patches.getListing"
/>
<monkey:patch
description="Navigation support RefBrowserWidget across INavigationRoot"
class="archetypes.referencebrowserwidget.browser.view.ReferenceBrowserPopup"
original="breadcrumbs"
replacement=".patches.breadcrumbs"
/>
<monkey:patch
description="Unrestrict TinyMCE image search"
class="Products.TinyMCE.adapters.JSONSearch.JSONSearch"
original="getSearchResults"
replacement=".patches.getSearchResults"
/>
</configure>
Schließlich schreiben wir noch die Datei patches.py
, die die Originalklassen mit den entsprechenden Änderungen enthält.
$ cd src
$ paster create -t plone3_theme
Enter namespace_package (Namespace package (like plonetheme)) ['plonetheme']: vs
Enter package (The package contained namespace package (like example)) ['example']: theme
Enter skinname (The skin selection to be added to 'portal_skins' (like 'My Theme')) ['']: vs.theme
Enter skinbase (Name of the skin selection from which the new one will be copied) ['Plone Default']:
Enter empty_styles (Override default public stylesheets with empty ones?) [True]: False
Enter include_doc (Include in-line documentation in generated code?) [False]:
Enter zope2product (Are you creating a Zope 2 Product?) [True]:
…
Enter zip_safe (True/False: if the package can be distributed as a .zip file) [False]:
Enter empty_styles
Wenn die Skin-Anpassungen sehr umfangreich sind oder es sich um eine
performance-kritische Anwendung handelt empfehle ich, den Skin vollständig
neu aufzusetzen und Enter empty_styles
mit True
anzugeben.
Enter skinbase
In Plone 4 kann hier zwischen folgenden beiden Skins gewählt werden:
Ein neuer Skin aus dem plonetheme.sunburst-Egg.
Sunburst ist der Standard-Skin für neu erstellte Plone-4-Sites.
Der aus Plone 3 bekannte Plone Default-Skin. Er ist nun im
plonetheme.classic
-Egg zu finden.
Der Plone Default-Skin ist in Plone 4 nur noch ein minimalistischer Skin, der ideal geeignet ist für die nachgelagerte Gestaltung einer Plone-Site mit XDV oder Deliverance.
In Plone 3 besteht üblicherweise die Wahl zwischen zwei Skins:
Der Standard-Skin.
Ein modernerer Skin für Plone 3.
Verzeichnisübersicht des vs.theme
-Produkts
Um nun das Plone Skin Tools für unsere Bedürfnisse anzupassen, wurde die Datei src/vs.theme/vs/theme/profiles/default/skins.xml
folgendermaßen erstellt:
<?xml version="1.0"?>
<object name="portal_skins" allow_any="False" cookie_persistence="False"
default_skin="vs.theme">
<object name="vs_theme_custom_images"
meta_type="Filesystem Directory View"
directory="vs.theme:skins/vs_theme_custom_images"/>
<object name="vs_theme_custom_templates"
meta_type="Filesystem Directory View"
directory="vs.theme:skins/vs_theme_custom_templates"/>
<object name="vs_theme_styles"
meta_type="Filesystem Directory View"
directory="vs.theme:skins/vs_theme_styles"/>
<skin-path name="vs.theme" based-on="Plone Default">
<layer name="vs_theme_custom_images"
insert-after="custom"/>
<layer name="vs_theme_custom_templates"
insert-after="vs_theme_custom_images"/>
<layer name="vs_theme_styles"
insert-after="vs_theme_custom_templates"/>
</skin-path>
</object>
Damit werden die drei Verzeichnisse vs_theme_custom_images
, vs_theme_custom_templates
und vs_theme_styles
registriert und ein neuer Skin vstheme, der auf Plone Default basiert und zudem die drei oben genannten Layer enthält, als Standard-Skin angegeben.
Bemerkung
Sollen die Layer allen Skins zugewiesen werden, kann dies einfach so angegeben werden:
<skin-path name="*">
...
</skin-path>
Layer können auch einfach wieder entfernt werden mit:
<object name="vs_theme_custom_templates" remove="True "/>
Dies kann z.B. für ein uninstall
-Profil verwendet werden.
Wollen Sie z.B. das Logo durch ein eigenes im gif-Format ersetzen, sollten Sie zunächst in der Datei src/vs.theme/vs/theme/skins/vs_theme_styles/base_properties.props
die Angabe für logoName
ändern:
logoName:string=logo.gif
Anschließend können Sie Ihr Logo in src/vs.theme/vs/theme/skins/vs_theme_custom_images/
einfügen.
Bemerkung
Um die DTML-Variable fontFamily
in einer CSS-Datei verwenden zu können, darf sie nicht mit &dtml-fontFamily;
eingebunden werden sondern mit <dtml-var fontFamily>;
, da ansonsten das Zeichen "
als "
interpretiert würde.
Bemerkung
Sollen PageTemplates überschrieben werden, die auch Meta-Angaben in einer .metadata
-Datei enthalten, dann sollte auch diese Datei mitkopiert werden.
Um die CSS-Datei src/vs.theme/vs/theme/browser/stylesheets/main.css
am CSS Registry Tool zu registrieren, wird die Datei src/vs.theme/vs/theme/profiles/default/cssregistry.xml
folgendermaßen geändert:
<?xml version="1.0"?>
<object name="portal_css">
<stylesheet title=""
id="++resource++vs.theme.stylesheets/main.css"
media="screen"
rel="stylesheet"
rendering="import"
cacheable="True"
compression="safe"
cookable="True"
enabled="1"
expression=""/>
</object>
id
ergibt sich daraus, dass das Verzeichnis src/vs.theme/vs/theme/browser/stylesheets
, das die main.css
-Datei enthält, als resourceDirectory
in src/vs.theme/vs/theme/browser/configure.zcml
registriert ist.
Title
Durch die Angabe eines Titels zusammen mit rel=stylesheet
wird ein Stylesheet-Dokument vor anderen bevorzugt.
expression
Die Bedingung, unter der das Stylesheet ausgeführt werden soll, als TALES-Ausdruck.
Im Folgenden einige der häufigsten Bedingungen:
nach Artikeltyp:
expression = "python:object.meta_type == 'ATFolder'"
nach View:
expression="object/@@registration_view/enabled | nothing"
nach globalen Views:
expression="python:portal.restrictedTraverse ('@@plone_portal_state').is_rtl()"
nach Rollen:
expression="not: portal/portal_membership/isAnonymousUser"
oder:
expression="python: not here.restrictedTraverse
('@@plone_portal_state').anonymous()"
media
Das Medium, für das das Stylesheet gilt:
all
, aural
, braille
, embossed
, handheld
, print
, projection
, screen
, tty
und tv
.
rel
mögliche Werte sind stylesheet
und alternate stylesheet
. Der Standardwert ist stylesheet
.
rendering
Angabe, wie das Stylesheet in die HTML-Seiten eingebunden werden soll. Mögliche Werte sind import
, link
und inline
.
compression
Angabe, ob und wie das Stylesheet komprimiert werden darf. Mögliche Werte sind none
, safe
oder full
.
enabled
Angabe, ob das Stylesheet aktiv ist.
cookable
Angabe, ob das Zusammenfügen mit anderen Stylesheets erlaubt wird.
cacheable
Angabe, ob das Caching des Stylesheets erlaubt wird.
conditionalcomment
Ab der Plone-Version 3.3 kann mit der CSS-Registry das Einbinden der CSS-Datei auch in sog. Conditional Comments erfolgen, also z.B.:
<!--[if IE]>
<style type="text/css" media="all">@import url(http://localhost:8080/mysite/portal_css/vs.theme/iefixes-cachekey7904.css);</style>
<![endif]-->
Hierzu wird in der cssregistry.xml
-Datei folgendes angegeben:
<stylesheet title=""
…
conditionalcomment="IE"
id="iefixes.css"/>
Weitere Hinweise zu Conditional Comments erhalten Sie in About Conditional Comments.
Bemerkung
Zum Entwickeln von Stylesheets empfiehlt sich, entweder die Instanz im Debug-Modus zu starten oder im ZMI der CSS Registry Debug/development mode zu aktivieren und damit das Caching von CSS-Dateien im Browser zu verhindern.
CSS-Dateien können entfernt werden mit:
<stylesheet id="++resource++vs.theme.stylesheets/main.css" remove="True"/>
Dies kann z.B. für ein uninstall
-Profil verwendet werden.
Zope 3 Browser Views werden üblicherweise als Page Templates definiert. Meist liegen diese Page Templates in einem Unterpaket namens browser
. Um ein solches Page Template ändern zu können, wird zunächst in browser/configure.zcml
der Skin an unser vs.theme gebunden:
<interface
interface=".interfaces.IThemeSpecific"
type="zope.publisher.interfaces.browser.IBrowserSkinType"
name="vs.theme"
/>
Um herauszufinden, wo sich das gesuchte Page Template im Dateisystem befindet, kann im ZMI Plone View Customizations verwendet werden. Sobald sich der Cursor über dem Namen des entsprechenden Templates befindet, wird der Pfad im Dateisystem angezeigt. Alternativ kann auch nach .zcml
-Dateien gesucht werden, die den Namen der Ansicht enthalten.
Suchen wir z.B. nach Dashboard, so finden wir zwei Ergebnisse:
plone.app.layout.dashboard
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="dashboard"
permission="plone.app.portlets.ManageOwnPortlets"
class=".dashboard.DashboardView"
template="dashboard.pt"
/>
plone.app.portlets.browser
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
class=".manage.ManageDashboardPortlets"
name="manage-dashboard"
template="templates/manage-dashboard.pt"
permission="plone.app.portlets.ManageOwnPortlets"
/>
Um diese beiden Views zu ändern, tragen wir folgendes in vs.theme/vs/theme/browser/configure.zcml
ein:
<include package="plone.app.portlets" />
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="dashboard"
permission="plone.app.portlets.ManageOwnPortlets"
class="plone.app.layout.dashboard.dashboard.DashboardView"
template="templates/dashboard.pt"
layer=".interfaces.IThemeSpecific"
/>
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="manage-dashboard"
permission="plone.app.portlets.ManageOwnPortlets"
class="plone.app.portlets.browser.manage.ManageDashboardPortlets"
template="templates/manage-dashboard.pt"
layer=".interfaces.IThemeSpecific"
/>
Um die Templates überschreiben zu können, ist die Angabe für layer
wesentlich.
Anschließend wird in browser
der Ordner templates
erstellt und dann die beiden Templates dashboard.pt
und manage-dashboard.pt
dahin kopiert und geändert.
Sollen z.B. die Portlets der linken und rechten Spalte angezeigt werden, müssen nur die entsprechenden Zeilen auskommentiert werden, also:
<!-- <metal:left fill-slot="column_one_slot" /> -->
<!-- <metal:right fill-slot="column_two_slot" /> -->
Ab Plone 4.0 lassen sich die Portlet-Manager besser kontrollieren. So finden Sie nun z.B. im Page Template dashboard.pt
folgende Anweisung:
<head>
<metal:block fill-slot="top_slot"
tal:define="dummy python:request.set('disable_border',1);
disable_column_one python:request.set('disable_plone.leftcolumn',1);
disable_column_two python:request.set('disable_plone.rightcolumn',1);" />
</head>
Dies sorgt dafür, dass die linke und rechte Spalte üblicherweise nicht im Dashboard angezeigt werden.
Setzen wir nun die Anweisung auf 0
, so werden der linke und rechte Portlet-Manager wieder angezeigt.
Werden die Anweisungen im Template weggelassen, so greift die übliche Logik, bei der unterschieden wird, ob Portlets zugewiesen sind oder nicht.
Bemerkung
Eine Anleitung zu Page Templates erhalten Sie in Zope Page Templates (ZPT)
In Plone 3.x mussten einige Variablen nicht explizit in einem Template definiert werden, da sie bereits als global_defines
in das main_template
eingefügt wurden. Ab Plone 4 müssen auch diese Variablen wieder explizit definiert werden. Hier ein Überblick über die gebräuchlichsten dieser globalen Variablen:
portal
Das Plone-Site-Root-Objekt
portal_url
Die URL des Plone-site root-Objekts
member
Der aktuell angemeldete Nutzer
checkPermission
Funktion, die überprüft ob der aktuell angemeldete Nutzer im aktuellen Kontext eine bestimmte Berechtigung hat. Hier ein Beispiel aus parts/plone/CMFPlone/skins/plone_forms/folder_rename_form.cpt
:
tal:define="canModifyItem python:checkPermission('Modify portal content', obj);"
isAnon
True
wenn der aktuelle Nutzer nicht angemeldet ist
is_editable
True
wenn der aktuelle Nutzer den aktuellen Kontext editieren darf
isLocked
True
wenn das aktuelle Objekt für das Editieren gesperrt ist
Einen vollständigen Überblick über die verfügbaren Variablen erhalten Sie im Docstring der globalize()
-Methode in Products.CMFPlone.browser.interfaces.IPlone
.
In plone.app.layout.globals
sind Hilfsansichten definiert, mit denen Sie sich häufig genutzte Informationen anzeigen lassen können:
@@plone_tools
Zugang zu den gebräuchlichsten CMF Tools
@@plone_context_state
Informationen des aktuellen Kontexts, wie URL, Pfad, Status und Editierbarkeit
@@plone_portal_state
Informationen über die aktuelle Plone-Site, wie URL der Site, aktueller Nutzer und ob er anonym ist
Ein Vorteil dieser Hilfsansichten ist, dass ihre Methoden gecached werden, sodass sie nur bei der ersten Anfrage berechnet werden müssen.
Wie diese Hilfsansichten in Viewlets verwendet werden, können Sie z.B. in plone/app/layout/viewlets/content.py
sehen:
self.portal_state = getMultiAdapter((self.context, self.request),
name=u'plone_portal_state')
self.portal_url = self.portal_state.portal_url()
Als Beispiel, wie diese Hilfsansichten in Page Templates verwendet werden können, hier ein Auszug aus plone/app/portlets/portlets/login.pt
:
tal:attributes="value context/@@plone_context_state/current_page_url"
Eine vollständige Übersicht über die verfügbaren Interfaces finden Sie in plone.app.layout.globals.interfaces
.
Um sich einen Überblick über die verschiedenen Viewlet-Manager und Viewlets zu verschaffen, empfiehlt sich im Webbrowser der Aufruf der URL http://localhost:8080/mysite/@@manage-viewlets
.
Wie die Anzeige und Reihenfolge der Viewlets programmatisch verändert werden kann, können Sie sich in src/vs.theme/vs/theme/profiles/default/viewlets.xml
anschauen:
<?xml version="1.0"?>
<object>
<order manager="plone.portaltop" skinname="vs.theme" based-on="Plone Default">
<viewlet name="plone.header" />
</order>
<hidden manager="plone.portaltop" skinname="vs.theme">
<viewlet name="plone.app.i18n.locales.languageselector" />
<viewlet name="plone.path_bar" />
<viewlet name="plone.personal_bar" />
</hidden>
<order manager="plone.portalheader" skinname="vs.theme" based-on="Plone Default">
<viewlet name="plone.skip_links" />
<viewlet name="plone.site_actions" />
<viewlet name="plone.logo" />
<viewlet name="plone.global_sections" />
</order>
<hidden manager="plone.portalheader" skinname="vs.theme">
<viewlet name="plone.searchbox" />
</hidden>
<order manager="plone.contentviews" skinname="vs.theme" based-on="Plone Default">
<viewlet name="vs.path_bar" />
<viewlet name="vs.personal_bar" />
<viewlet name="plone.contentviews" />
<viewlet name="plone.contentactions" />
</order>
<hidden manager="plone.belowcontenttitle" skinname="vs.theme">
<viewlet name="plone.belowcontenttitle.documentbyline" />
</hidden>
<hidden manager="plone.abovecontentbody" skinname="vs.theme">
<viewlet name="plone.presentation" />
</hidden>
<order manager="plone.portalfooter" skinname="vs.theme" based-on="Plone Default">
<viewlet name="vs.footer" />
<viewlet name="plone.colophon" />
</order>
<hidden manager="plone.portalfooter" skinname="vs.theme">
<viewlet name="plone.footer" />
</hidden>
</object>
<order ...>
gibt die Reihenfolge der Viewlets an:
skinname
beschränkt die angegebene Reihenfolge auf einen spezifischen Skin.
Soll der entsprechende Manager für alle Skins verwendet werden, kann auch folgendes angegeben werden:
skinname="*"
based on
gibt an, auf welchem Skin die Reihenfolge basiert.
Für die einzelnen Viewlets stehen auch noch die folgenden drei Angaben zur Verfügung:
<viewlet name="my.viewlet" insert-before="another.viewlet" />
Das Viewlet wird unmittelbar vor einem spezifischen Viewlet eingefügt.
<viewlet name="my.viewlet" insert-before="*" />
Das Viewlet wird vor allen anderen Viewlets eingefügt.
<viewlet name="my.viewlet" insert-after="another.viewlet" />
Das Viewlet wird unmittelbar nach einem spezifischen Viewlet eingefügt.
<hidden ...>
die hier angegebenen Views werden in diesem Viewlet-Manager nicht angezeigt.
Viewlet-Manager werden in zcml
-Dateien angegeben. Wie dies geschieht, lässt sich leicht in plone/app/layout/viewlets/configure.zcml
nachvollziehen, z.B. für plone.portaltop
:
<browser:viewletManager
name="plone.portaltop"
provides=".interfaces.IPortalTop"
permission="zope2.View"
class="plone.app.viewletmanager.manager.OrderedViewletManager"
/>
Dabei ist das Interface IPortalTop
definiert in plone/app/layout/viewlets/interfaces.py
. Allgemein dienen solche Interfaces dazu, Viewlets bestimmten Viewlet-Managern zuzuweisen.
In PageTemplates lassen sich Viewlet-Manager aufrufen z.B. mit:
<div tal:replace="structure provider:plone.portaltop" />
In unserem Beispiel soll die personal bar vom plone.portaltop
- zum plone.contentviews
-Manager verschoben werden. Hierzu wird in src/vs.theme/vs/theme/browser/configure.zcml
folgendes angegeben:
<browser:viewlet
name="vs.personal_bar"
manager="plone.app.layout.viewlets.interfaces.IContentViews"
layer=".interfaces.IThemeSpecific"
class="plone.app.layout.viewlets.common.PersonalBarViewlet"
permission="zope2.View"
/>
personal_bar
erhält einen anderen Namen, einen neuen Viewlet-Manager und wird mit layer
an unser spezifisches Theme gebunden. Dieses IThemeSpecific
-Interface wurde bereits vom plone3_theme
-Template in browser/configure.zcml
konfiguriert:
<interface
interface=".interfaces.IThemeSpecific"
type="zope.publisher.interfaces.browser.IBrowserSkinType"
name="vs.theme"
/>
Schließlich wird noch in src/vs.theme/vs/theme/profiles/default/viewlets.xml
angegeben, dass das alte Viewlet nicht mehr und das neue in plone.contentviews
angezeigt werden sollen:
<hidden manager="plone.portaltop" skinname="vs.theme">
…
<viewlet name="plone.personal_bar" />
…
</hidden>
…
<order manager="plone.contentviews" skinname="vs.theme" based-on="Plone Default">
<viewlet name="vs.personal_bar" />
…
</order>
Neue Viewlets werden in browser.configure.zcml
registriert, z.B. mit:
<browser:viewlet
name="vs.footer"
manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
layer=".interfaces.IThemeSpecific"
template="templates/footer.pt"
permission="zope2.View"
/>
Mit der Angabe für layer
wird der View an unser spezifisches Theme gebunden und mit template
lässt sich auch ein eigenes Page Template angeben.
Die Pfadleiste (breadcrumbs) ist z.B. ein Viewlet. Um es ändern zu können, muss es zunächst in der Datei browser/configure.zcml
registriert werden:
<browser:viewlet
name="vs.path_bar"
manager="plone.app.layout.viewlets.interfaces.IContentViews"
layer=".interfaces.IThemeSpecific"
class=".viewlets.PathBarViewlet"
permission="zope2.View"
/>
Der Name des Viewlets wird angegeben mit vs.path_bar
und der Layer bindet das Viewlet an unser spezifisches Theme. Nun definieren wir noch die angegebene Klasse PathBarViewlet
, indem wir die Datei browser/viewlets.py
mit folgendem Inhalt anlegen:
from zope.component import getMultiAdapter
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from plone.app.layout.viewlets import common
class PathBarViewlet(common.PathBarViewlet):
render = ViewPageTemplateFile('templates/path_bar.pt')
Hier wird verwiesen auf ein PageTemplate browser/templates/path_bar.pt
.
Plone 4 kommt mit den folgenden Slots:
content-title
Slot mit dem View des Titels
Der Inhalt wird in main_template.pt
generiert.
content-description
Slot mit dem View der Beschreibung.
Der Inhalt wird in main_template.pt
generiert.
content-core
Slot, der in main_template.pt
unterhalb von content-description
angezeigt wird. Er zeigt den Inhalt einer Seite, die Liste eines Ordners o.ä. an.
main
Dieser Slot ist weiterhin in Plone 4 vorhanden um rückwärtskompatibel zu bleiben und Views zu rendern, die ohne Viewlet-Manager oder modifizierte Titel und Beschreibungen auskommen.
Alle Viewlet-Manager für content
werden nicht mehr wie in Plone 3 in eigenen Templates verwaltet sondern immer im main_template.pt
. Hierdurch werden die Templates für Artikeltypen nochmals deutlich vereinfacht, sehen Sie z.B. document_view
:
<metal:content-core fill-slot="content-core">
<metal:content-core define-macro="content-core">
<metal:field use-macro="python:context.widget('text', mode='view')">
Body text
</metal:field>
</metal:content-core>
</metal:content-core>
Um eine angepasste SearchBox in der linken Spalte anzeigen, wird zunächst in browser/templates/
die Datei search_portlet.pt
mit den gewünschten Anpassungen angelegt. Anschließend wird in browser/configure.zcml
dieses Template angegeben:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="vs.theme">
<include package="plone.app.portlets" />
…
<plone:portletRenderer
portlet="plone.app.portlets.portlets.search.ISearchPortlet"
layer=".interfaces.IThemeSpecific"
template="templates/search_portlet.pt"
/>
…
</configure>
In setuphandlers.py
lässt sich das Search-Portlet der linken Spalte zuweisen:
from zope.component import getMultiAdapter
from zope.component import getUtility
from plone.portlets.interfaces import IPortletAssignmentMapping
from plone.portlets.interfaces import IPortletManager
from plone.app.portlets import portlets
class Generator:
def setupPortlets(self, portal):
leftColumn = getUtility(IPortletManager, name=u'plone.leftcolumn', context=portal)
rightColumn = getUtility(IPortletManager, name=u'plone.rightcolumn', context=portal)
left = getMultiAdapter((portal, leftColumn,), IPortletAssignmentMapping, context=portal)
right = getMultiAdapter((portal, rightColumn,), IPortletAssignmentMapping, context=portal)
if u'search' not in left:
left[u'search'] = portlets.search.Assignment(enableLivesearch=True,)
def setupVarious(context):
if context.readDataFile('vs.theme_various.txt') is None:
return
site = context.getSite()
gen = Generator()
gen.setupPortlets(site)
Hier erfolgt die Zuweisung in profiles/default/portlets.xml
:
<?xml version="1.0"?>
<portlets
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
<assignment
manager="plone.leftcolumn"
category="context"
key="/"
type="portlets.Search"
name="search"
insert-before="*"
>
<property name="enableLivesearch">True</property>
</assignment>
</portlets>
type
(erforderlich)Der Name des Portlets entsprechend dem name-Atribut in der <portlet />
-
Anweisung in portlets.xml
.
manager
(erforderlich)Der Name des Portlet-Manager, der verwendet werden soll. Portlet-Manager werden
registriert in portlets.xml
, z.B.:
<portletmanager
name="plone.leftcolumn"
type="plone.app.portlets.interfaces.ILeftColumn"
/>
In Plone verfügbare Portlet-Manager sind:
für die linke und rechte Spalte
für die vier Spalten des Dashboard.
category
(erforderlich)Die zu verwendende Kategorie. Mögliche Kategorien sind:
context
content_type
group
user
(Diese Angabe ist vmtl. nur für die Dashboard-Portlet-Manager
sinnvoll.)
key
(erforderlich)Der Schlüssel, mit dem das Portlet zugewiesen wird.
Für die context-Kategorie ist der Schlüssel die Angabe des Pfades relativ zur Site-Root, z.B. /
.
Für die content_type
-Kategorie ist der Schlüssel die Angabe des Artikeltyps.
Für die group
-Kategorie ist der Schlüssel die Angabe der ID einer bestimmten Gruppe.
Für die user
-Kategorie ist der Schlüssel die Angabe der user-ID.
name
(optional)Der Name der Zuweisung. Wird keine Name angegeben, wird ein eindeutiger Name erzeugt. Wird ein bereits bestehender Name verwendet, der denselben key, dieselbe Kategorie und denselben Portlet-Manager verwendet, so wird dieser überschrieben.
insert-before
(optional)Dieser Parameter kann verwendet werden um die Reihenfolge der Portlets festzulegen.
Ist der Wert *
, wird das Porlet an oberster Stelle platziert.
Ist der Wert der Name eines anderen Portlets, wird das einzufügende Portlet direkt über diesem angezeigt.
Wird kein Wert angegeben, wird das Porlet zuunterst angezeigt.
Die Portlets werden dabei in der Reihenfolge verarbeitet und eingefügt, wie sie in der portlets.xml
-Datei angegeben sind.
Hier noch ein Beispiel für das Zuweisen eines Portlets im Dashboard eines Nutzers:
<assignment name="quick-links" category="user" key="veit"
manager="plone.dashboard1" type="plone.portlet.collection.Collection">
<property name="show_more">True</property>
<property name="header">Quick links</property>
<property name="limit">10</property>
<property
name="target_collection">/quick-links/quick-links</property>
<property name="random">False</property>
<property name="show_dates">False</property>
</assignment>
Von übergeordneten Objekten übernommene Portlets lassen sich in setuphandlers.py
blockieren, z.B. mit:
from plone.portlets.constants import CONTEXT_CATEGORY as CONTEXT_PORTLETS
class Generator:
def setupPortlets(self, portal):
rightColumn = getUtility(IPortletManager, name=u'plone.rightcolumn', context=portal)
portletAssignments = getMultiAdapter((members, rightColumn,), ILocalPortletAssignmentManager)
portletAssignments.setBlacklistStatus(CONTEXT_PORTLETS, True)
Das Blockieren lässt sich hier in profiles/default/portlets.xml
konfigurieren, z.B.:
<blacklist
manager="plone.rightcolumn"
location="/Members"
category="context"
status="block"
/>
manager
(erforderlich)Der Name des Portlet-Managers (Spalte), für die die Portlets nicht übernommen werden sollen.
category
(erforderlich)Die Kategorie, die geblockt werden soll: context, group oder content_type.
location
(erforderlich)Ein relativer Pfad, der den Ordner angibt, in dem die Portlets geblockt werden sollen.
status
(erforderlich)Der Status für übernommene Portlets einer Kategorie in einem bestimmten Ordner:
block
Portlets des übergeordneten Objekts werden übernommen.
show
Portlets der angegebenen Kategorie werden angezeigt.
acquire
Portlets des übergeordneten Objekts werden übernommen.
Portlets lassen sich in setuphandlers.py ausblenden, z.B. mit:
from zope.component import getUtility
from zope.component import getMultiAdapter
from plone.portlets.interfaces import IPortletManager
from plone.portlets.interfaces import IPortletAssignmentMapping
class Generator:
def setupPortlets(self, portal):
rightColumn = getUtility(IPortletManager, name=u'plone.rightcolumn', context=portal)
right = getMultiAdapter((portal, rightColumn,), IPortletAssignmentMapping, context=portal)
if u'calendar' in right:
del right["calendar"]
def setupVarious(context):
if context.readDataFile('vs.theme_various.txt') is None:
return
site = context.getSite()
gen = Generator()
gen.setupPortlets(site)
Portlets lassen sich in setuphandlers.py entfernen, z.B. mit:
from zope.component import getSiteManager
from zope.component import getUtilitiesFor
from plone.portlets.interfaces import IPortletType
from Products.CMFCore.utils import getToolByName
def removeRegistrantsPortlet(self):
sm = getSiteManager()
for name, portletType in getUtilitiesFor(IPortletType):
if name == "portlets.Registrants":
sm.unregisterUtility(provided=IPortletType, name=name)
Hier erfolgt das entfernen von Portlets in profiles/default/portlets.xml
:
<assignment
remove="true"
name="calendar"
category="context"
key="/"
manager="plone.rightcolumn"
type="portlets.Calendar" />
In Plone 4 wurde das visible-Attribut zum Ein- oder Ausblenden der Portlets eingeführt:
<assignment
visible="0"
name="calendar"
category="context"
key="/"
manager="plone.rightcolumn"
type="portlets.Calendar" />
Das Ausblenden aller Portlets in einem bestimmten Kontext erfolgt mit dem purge-Attribut:
<assignment
purge="True"
manager="plone.rightcolumn"
category="context"
key="/Plone/news"
/>
Für dieses Portlet erstellen wir ein Unterpaket entsprechend den Konventionen in plone.app.portlets
. Für unser Site-Actions
-Portlet wird das Page Template siteactions.pt
im Ordner portlets
erstellt:
<dl class="portlet portletSiteActions"
i18n:domain="vs.theme">
<dt class="portletHeader">
<span class="portletTopLeft"></span>
Site Actions
<span class="portletTopRight"></span>
</dt>
<tal:actions tal:define="accesskeys python: {'sitemap' : '3', 'accessibility' : '0', 'contact' : '9'};"
tal:condition="view/site_actions">
<dd class="portletItem"
tal:repeat="saction view/site_actions"
tal:attributes="id string:siteaction-${saction/id}">
<a href=""
tal:define="title saction/title;
id saction/id;
accesskey python: accesskeys.get(id, '');"
i18n:attributes="title"
i18n:translate=""
tal:content="title"
tal:attributes="href saction/url;
target saction/link_target|nothing;
title title;
accesskey accesskey;"
>Site action</a>
</dd>
</tal:actions>
</dl>
Dies entspricht dem Aufbau der meisten Plone-Portlets. Dabei ist die
Darstellungslogik, welche siteactions
angezeigt werden, in den View
siteactions.py
ausgelagert worden.
Auch der Aufbau von siteactions.py
entspricht der üblichen Konvention.
Zunächst wird einiges importiert, darunter auch das base
-Modul von
plone.app.portlets
, das verschiedene Basisklassen wie Assignment
und
Renderer
zum Erstellen eines neuen Portlets bereitstellt:
from zope import schema
from zope.component import getMultiAdapter
from zope.formlib import form
from zope.interface import implements
from plone.app.portlets.portlets import base
from plone.portlets.interfaces import IPortletDataProvider
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from vs.theme import VsThemeMessageFactory as _
class ISiteActionsPortlet(IPortletDataProvider):
"""A portlet which shows the available site actions.
"""
class Assignment(base.Assignment):
implements(ISiteActionsPortlet)
@property
def title(self):
return _(u"Site actions")
class Renderer(base.Renderer):
render = ViewPageTemplateFile('siteactions.pt')
title = _('box_siteactions', default=u"Site actions")
def site_actions(self):
context_state = getMultiAdapter((self.context, self.request),
name=u'plone_context_state')
self.siteactions = context_state.actions('site_actions')
return self.siteactions
Schließlich werden noch Klassen für AddForm
definiert, die Nutzern das Erstellen des Site-Action-Portlets erlauben:
class AddForm(base.NullAddForm):
form_fields = form.Fields(ISiteActionsPortlet)
label = _(u"Add Site Actions Portlet")
description = _(u"This portlet lists the available site actions.")
def create(self):
return Assignment()
Um neue Portlet-Typen zu konfigurieren, wird die Datei portlets/configure.zcml
mit folgendem Inhalt erstellt:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone">
<include package="plone.app.portlets" />
<plone:portlet
name="vs.theme.SiteActionsPortlet"
interface=".siteactions.ISiteActionsPortlet"
assignment=".siteactions.Assignment"
renderer=".siteactions.Renderer"
addview=".siteactions.AddForm"
/>
</configure>
Damit werden einige Hilfsmethoden, Adapter und Views registriert, Und falls Sie ein editierbares Portlet erstellen wollen, können Sie das editview
-Attribut hinzufügen und statt NullAddForm
die AddForm
-Klasse angeben. Ein solches
Portlet ist beschrieben in Portlet erstellen.
Ab Plone 3.1 muss das Portlet zusätzlich in src/vs.theme/vs/theme/profiles/default/portlets.xml
angegeben werden:
<?xml version="1.0"?>
<portlets>
<portlet
addview="vs.theme.SiteActionsPortlet"
title="Site Actions"
description="A portlet which can show the available site actions."
/>
</portlets>
Dabei entspricht die Angabe für addview
dem Namen des Portlets, der in portlets/configure.zcml
angegeben wurde.
Zunächst wird ein Viewlet erstellt, das auf einen Portlet-Manager verweist – in unserem Fall vs.abovecontentportlets
– und für den Viewlet-Manager IContentViews
registriert wird. Anschließend wird für diesen Viewlet-Manager noch ein Management-View erstellt.
Fügen Sie in browser/templates
die Datei abovecontentportlets.pt
mit folgendem Inhalt hinzu:
<tal:block replace="structure provider:vs.abovecontentportlets" />
Dieses Viewlet wird nun registriert in browser/configure.zcml
:
<browser:viewlet
name="vs.abovecontentportlets"
manager="plone.app.layout.viewlets.interfaces.IContentViews"
layer=".interfaces.IThemeSpecific"
template="templates/abovecontentportlets.pt"
permission="zope2.View"
/>
Das Viewlet steht nur zur Verfügung sofern das Interface IThemeSpecific
in der Site zur Verfügung steht. Durch das plone3_theme
-Template sollte dieses Marker-Interface bereits in browser/interfaces.py
erstellt worden sein:
from plone.theme.interfaces import IDefaultPloneLayer
class IThemeSpecific(IDefaultPloneLayer):
"""Marker interface that defines a Zope 3 browser layer.
If you need to register a viewlet only for the
"vs.theme" theme, this interface must be its layer.
"""
Nun wird dieses Interface als paketspezifischer Browserlayer registriert in profiles/default/browserlayer.xml
:
<?xml version="1.0"?>
<layers>
<layer name="vs.theme.layer"
interface="vs.theme.browser.interfaces.IThemeSpecific" />
</layers>
Dieser Browserlayer steht nun jenen Sites zur Verfügung, in denen dieses Profil importiert wurde.
Zunächst wird ein Marker-Interface in browser/interfaces.py
erstellt:
from plone.portlets.interfaces import IPortletManager
class IVsAboveContent(IPortletManager):
"""Portlet manager above the content area.
"""
Anschließend wird in der Datei profiles/default/portlets.xml
der neue Portlet-Manager registriert:
<?xml version="1.0"?>
<portlets>
<portletmanager
name="vs.abovecontentportlets"
type="vs.theme.browser.interfaces.IVsAboveContent"
/>
</portlets>
Um die Portlets des vs.abovecontentportlets
zu verwalten, wird ein neuer View erstellt und hierfür zunächst folgende Zeilen in browser/configure.zcml
eingetragen:
<browser:page
for="plone.portlets.interfaces.ILocalPortletAssignable"
class="plone.app.portlets.browser.manage.ManageContextualPortlets"
name="manage-vsabove"
template="templates/managevsabove.pt"
permission="plone.app.portlets.ManagePortlets"
/>
Und falls nicht bereits zu einem früheren Zeitpunkt geschehen, sollte noch das Paket plone.app.portlets
eingeschlossen werden:
<include package="plone.app.portlets" />
Schließlich wird dann noch das Page-Template browser/templates/managevsabove.pt
erstellt:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="context/main_template/macros/master"
i18n:domain="plone">
<head>
<div metal:fill-slot="javascript_head_slot" tal:omit-tag="">
<link type="text/css" rel="kinetic-stylesheet"
tal:attributes="href string:${context/absolute_url}/++resource++manage-portlets.kss"/>
</div>
</head>
<body>
<div metal:fill-slot="main">
<h1 class="documentFirstHeading">Manage above content portlets</h1>
<span tal:replace="structure provider:vs.abovecontentportlets" />
</div>
</body>
</html>
Nach einem Neustart des Zope-Servers sollten sich nun die Portlets verwalten lassen wenn folgende URL aufgerufen wird:
http://localhost/mysite/@@manage-vsabove
Zope Page Templates verwenden TAL (Template Attribute Language), z.B.:
<title tal:content="context/title">Page Title</title>
Dabei ist tal:content
das TAL-Attribut wobei tal
den XML-Namensraum angibt und content
darauf hinweist, dass der Inhalt des title
-Tags gesetzt werden soll. Der Wert context/title
schließlich ist ein Ausdruck, der den in den Tag einzufügenden Text liefert und dabei Page Title
ersetzt.
Der Text context/title
ist ein einfacher Pfadausdruck der TAL Expression Syntax (TALES), der die Titel-Eigenschaft des Kontexts aufruft. Andere häufig verwendete Pfadausdrücke sind:
request/URL
Die URL des aktuellen Web-Requests.
user/getUserName
Der Login-Name des aktuell angemeldeten Nutzers.
container/objectIds
Eine Liste aller IDs von Objekten im selben Ordner wie das Template.
Jeder Pfadausdruck startet mit einem Variablenname und kann, getrennt durch einen Schrägstrich (/
), um den Namen eines Unterobjekts oder eine Eigenschaft spezifiziert werden.
Die Menge der verfügbaren Variablen, wie request
oder user
, ist relativ gering und wird später noch vollständig beschrieben werden. Zudem werde ich zeigen, wie eigene Variablen definiert werden können.
Angenommen unser Template hätte die ID my_page
, und wir wollten Text dynamisch einfügen, so könnten wir dies innerhalb eines span
-Tags machen:
Die URL ist <span tal:replace="request/URL">URL</span>.
Beachten Sie, dass der gesamte Tag ersetzt wird durch das Ergebnis der TAL-Anweisung, also:
Die URL ist http://localhost:8080/mysite/my_page.
Soll ein Tag erhalten bleiben, wird tal:content
verwendet:
<title tal:content="template/title">The Title</title>
Soll eine ganze Liste von Werten automatisch eingefügt werden, so kann dies mit tal:repeat
erfolgen, z.B.:
<table>
<tr>
<th>#</th><th>Id</th><th>Meta-Type</th><th>Title</th>
</tr>
<tr tal:repeat="item container/objectValues">
<td tal:content="repeat/item/number">#</td>
<td tal:content="item/id">Id</td>
<td tal:content="item/meta_type">Meta-Type</td>
<td tal:content="item/title">Title</td>
</tr>
</table>
Der tal:repeat
-Ausdruck auf einer Tabellenzeile bedeutet, dass diese Zeile für jeden Artikel in diese Container erstellt wird. Dabei wird für jede Zeile ein Artikel der Liste als item
-Variable verwendet und dessen Werte ausgelesen, wobei statt item
auch jeder andere Name verwendet werden kann.
repeat/item/number
ist die fortlaufende Nummerierung der Artikel innerhalb der Aufzählung. Soll die Nummerierung mit 0
beginnen, muss statt number
index
verwendet werden, soll die fortlaufende Nummerierung alphabetisch sein, ist das Attribut letter
.
Selbstverständlich können auch mehrere Aufzählungen ineinander verschachtelt werden.
Um jeder zweiten Zeile der obigen Tabelle eine andere Hintergrundfarbe zu geben, können entsprechende Bedingungen angegeben werden:
<table>
<tr>
<th>#</th><th>Id</th><th>Meta-Type</th><th>Title</th>
</tr>
<tbody tal:repeat="item container/objectValues">
<tr bgcolor="#DDE0E8" tal:condition="repeat/item/even">
<td tal:content="repeat/item/number">#</td>
<td tal:content="item/id">Id</td>
<td tal:content="item/meta_type">Meta-Type</td>
<td tal:content="item/title">Title</td>
</tr>
<tr tal:condition="repeat/item/odd">
<td tal:content="repeat/item/number">#</td>
<td tal:content="item/id">Id</td>
<td tal:content="item/meta_type">Meta-Type</td>
<td tal:content="item/title">Title</td>
</tr>
</tbody>
</table>
Chameleon ist eine HTML/XML-Template-Engine, die mit der Standardinstallation von Plone 5 mitkommt, jedoch auch als Zusatzprodukt für Plone 4 installiert werden kann.
Chameleon vereinfacht das Einfügen von Variablen erheblich, z.B. kann nun anstat:
<a tal:attributes="href href" tal:content="text" />
folgendes verwendet werden:
<a href="${href}">${text}</a>
Zudem lassen sich mit der __html__
-Methode einfach Strings einfügen ohne
dass diese escaped werden müssten, z.B.:
class Markup(object):
def __init__(self, s):
self.s = s
def __html__(self):
return s
Anschließend kann eine Instanz dieser Klasse verwendet werden um einen String einzufügen:
from chameleon.utils import Markup
form.status = Markup('<div class="note">Note</div>')
Beachten Sie jedoch, dass Sie nun selbst verantwortlich sind für das Escaping unerwünschter Nutzereingaben.
Siehe auch
In Plone funktionieren alle Anwendungen ohne JavaScript, und JavaScript wird ausschließlich zur Verbesserung des User Interfaces verwendet. Darüberhinaus wird vermieden, JavaScript direkt in eine Seite einzufügen und viele Aufgaben lassen sich ohne selbst JavaScript schreiben zu müssen, erledigen.
Beim Aufruf einer Plone-Seite mit KSS läuft folgende Sequenz ab:
KSS-Dateien, die in die Seite mit einem <link />
-Tag eingebunden sind, werden analysiert.
Dabei werden die im KSS definierten Aktionen an nutzerseitige Ereignisse gebunden, z.B. das Klicken auf einen Schalter.
Tritt ein solches Ereigns ein, wird die verknüpfte Aktion ausgeführt. Dies kann ein einfacher Effekt auf Nutzerseite sein, häufig jedoch wird eine asynchrone Anfrage an den Server gestellt werden.
Eine serverseitige Aktion führt anwendungsspezifische Operationen durch.
Die serverseitige Anwendung verknüpft die Antwort mit einem oder mehreren Befehlen und sendet alles gemeinsam an den Client. Befehle werden mit PlugIns verfügbar gemacht, wobei diverse PlugIns bereits mit Plone mitkommen, so z.B. das Ändern von Text oder das Aktualisieren von Portlets.
Der Client führt die Befehle aus.
Bemerkung
Mit firekiss.xpi steht eine Erweiterung fpr firebug zur Verfügung, mit der sich KSS-Dateien untersuchen lassen.
Bemerkung
Wie eigene KSS-PlugIns geschrieben werden, wird hier nicht Thema sein. Sie erhalten jedoch weitere Informationen zu KSS unter http://kssproject.org.
Zunächst erstellen wir die KSS-Datei registration.kss
:
#confirm-registrant input:click {
evt-click-preventdefault: true;
action-server:confirmRegistrant;
confirm-registrant: kssAttr('confirm');
}
Die erste Zeile identifiziert ein input
-Feld in einem Knoten mit der ID confirm-registrant
. Um zu gewährleisten, dass das Formular nicht wie üblich abgeschickt wird, setzen wir evt-click-preventdefault
auf true
. Stattdessen soll die serverseitige Aktion confirmRegistrant
ausgeführt werden wobei der Parameter confirm
übergeben wird.
Anschließend wird die Datei registration.kss
als Browser-Ressource in browser/configure.zcml
registriert:
<browser:resource
name="registration.kss"
file="registration.kss"
/>
Die KSS-Datei wird nun in die Seite mit folgendem Tag eingebunden:
<link rel="kinetic-stylesheet"
type="text/css"
href="http://localhost:8080/mysite/++resource++registration.kss" />
Häufig empfiehlt es sich jedoch, die KSS-Datei in der KSS-Registry anzumelden. Die Anmeldung erfolgt dann im Profil src/vs.registration/vs/theme/profiles/default/kssregistry.xml
mit:
<?xml version="1.0"?>
<object name="portal_kss" meta_type="KSS Registry">
<kineticstylesheet
cacheable="True"
compression="safe"
cookable="True"
enabled="1"
expression=""
id="registration.kss"/>
</object>
Wird nun der import step in portal_setup
durchlaufen oder das vs.registration
-Produkt neu installiert, sollte der Eintrag mit ++resource++registration.kss
in portal_kss
eingetragen sein.
Im Page Template src/vs.registration/vs/registration/browser/registration.pt
wird nun ein Formular mit der ID confirm-registrant
und den entsprechenden input
-Feldern eingetragen:
<tal:registrants condition="view/have_registrants">
<h2 i18n:translate="title_registration_contents">Registrants</h2>
<form action="confirmRegistrant">
<tal:block repeat="registrant view/registrants">
<dt>
<a tal:attributes="href registrant/url"
tal:content="registrant/title" />
</dt>
<dd tal:content="registrant/address" />
<dd>
<input class="kssattr-confirm-yes"
type="submit"
name="vs.registration.confirm.confirm"
value="yes"
i18n:attributes="value"
i18n:attributes="confirm-button"
/>
<input class="kssattr-confirm-no"
type="submit"
name="vs.registration.confirm.reject"
value="no"
i18n:attributes="value"
i18n:attributes="reject-button"
/>
</dd>
</tal:block>
</form>
</tal:registrants>
In src/vs.registration/vs/registration/browser/registration.kss
wird die Serveraktion referenziert mit:
action-server:confirmRegistrant;
confirm-registrant: kssAttr('confirm');
Beim Aufruf der Aktion wird confirmRegistran
im aktuellen Kontext mit den entsprechenden Parametern aufgerufen. So wird z.B. eine HTTP POST-Anfrage zu der URL http://localhost:8080/mysite/registration/confirmRegistrant
mit dem Parameter confirm
gesendet.
Die serverseitige Aktion wird meist als View implementiert. Entsprechend geben wir in browser/configure.zcml
folgendes an:
<browser:page
for="vs.registration.interfaces.IRegistration"
name="confirmRegistrant"
class=".confirmations.confirmedRegistrants"
attribute="confirm_registrant"
permission="zope2.RequestReview"
/>
Und die confirmedRegistrants
-Klasse in browser/confirmations
sieht dann so aus:
from zope.interface import alsoProvides
from kss.core import kssaction
from plone.app.kss.plonekssview import PloneKSSView
from plone.app.layout.globals.interfaces import IViewView
from Akquisition import aq_inner
from Products.Five.browser import BrowserView
from vs.Registration.interfaces import IConfirmations
class confirmedRegistrants
@kssaction
def confirm_registrant
confirm = confirm.lower()
if confirm not in ("confirm", "reject"):
return
Zentriertes Design und unterschiedlich gestaltete Bereiche der Website lassen sich in Plone einfach realisieren.
#visual-portal-wrapper {
width: 62em;
margin-left: auto;
margin-right: auto;
}
In parts/plone/CMFPlone/skins/plone_templates/main_template.pt:
wird für den body
-Tag eine css-Klasse defniniert, die es erlaubt, unterschiedliche Gestaltungen für einzelne Bereiche der Wwbsite anzugeben:
<body tal:attributes="class string:${here/getSectionFromURL} .">
Dies führt dann z.B. zu folgenden body
-Tag, je nachdem in welchem Bereich sich die gerade aufgerufene Seite befindet:
<body class="section-news">
<body class="section-events">
So lassen sich die Bereiche auch gestalterisch unterscheiden:
body.section-news {
...
}
body.section-events {
...
}
Analog lassen sich auch die Gestaltungen für einzelne Templates unterscheiden:
<body tal:attributes="class ... template-${template/id}; ...">
body.template-frontpage_view {
...
}
Plone bringt einige Javscripts mit, die sich einfach in Templates und Inhalten verwenden lassen.
form_tabbing.js
Um Reiter in Formularen zu erhalten, erwartet dieses Skript folgendes Markup:
<form class="enableFormTabbing">
<fieldset id="fieldset-id1">
<legend id="fieldsetlegend-id1">Tab one</legend>
</fieldset>
<fieldset id="fieldset-id2">
<legend id="fieldsetlegend-id2">Tab two</legend>
</fieldset>
</form>
Alternativ kann auch folgendes Markup verwendet werden:
<dl class="enableFormTabbing">
<dt id="fieldsetlegend-id1">tab one</dt>
<dd id="fieldset-id1">content one</dd>
<dt id="fieldsetlegend-id2">tab two</dt>
<dd id="fieldset-id2">content two</dd>
</dl>
table_sorter.js
Um Tabellen zu sortieren kann dieses Javascript einfach in folgendem Markup verwendet werden:
<table class="listing" id="my-table">
<thead>
<tr>
<th>First Name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Veit
</td>
<td>
Schiele
</td>
</tr>
<tr>
<td>
Sönke
</td>
<td>
Löffler
</td>
</tr>
</tbody>
</table>
Beachten Sie bitte, dass das table_sorter.js
-Javascript normalerweise nur angemeldeten Nutzern zur Verfügung steht. Wollen Sie es auch anonymen Nutzern zur Verfügung stellen, sollte Ihr jsregistry.xml
-Profil so aussehen:
<?xml version="1.0"?>
<object name="portal_javascripts" meta_type="JavaScripts Registry">
...
<javascript
cacheable="True"
compression="safe"
cookable="True"
enabled="True"
expression=""
id="table_sorter.js"
inline="False"/>
</object>
Das Skript erwartet die Klasse listing
und eine eindeutige ID für die Tabelle sowie <th>
-Tags innerhalb von <thead>
-Tags.
Soll eine Tabelle der Klasse listing
keine sortierbaren Spalten enthlaten, kann der Tabelle eine Klasse nosort
hinzugefügt werden.
Soll nur eine Spalte innerhalb einer Tabelle nicht sortiert werden können, so kann dem entsprechenden <th>
-Tag die Klasse nosort
zugewiesen werden.
collapsiblesections.js
Dieses Javascript kannn bei folgendem Markup verwendet werden:
<dl class="collapsible">
<dt class="collapsibleHeader">
Title
</dt>
<dd class="collapsibleContent">
Content
</dd>
</dl>
Sobald collapsible
umgeschaltet wurde, erhalt das dl
-Tag eine zusätzliche Klasse, die zwischen collapsedBlockCollapsible
und expandedBlockCollapsible
hin- und herschaltet. Hierfür können Sie dann z.B. folgende CSS-Anweisungen angeben:
.expandedBlockCollapsible .collapsibleContent {
display: block;
}
.collapsedBlockCollapsible .collapsibleContent {
display: none;
}
Wird die collapsedOnLoad
-Klasse dem dl
-Tag hinzugefügt, wird die Definitionsliste bereits beim Laden der Seite ausgeklappt.
Wird die inline
-Klasse für das dl
-Tag angegeben, wird zwischen collapsedInlineCollapsible
und expandedInlineCollapsible
umgeschaltet anstatt zwischen collapsedBlockCollapsible
und expandedBlockCollapsible
.
jQuery
JavaScript-Bibliothek, die die Traversierung und das Event-Handling von HTML-Dokumenten vereinfacht. So lässt sich z.B. in einem Einzeiler angeben, dass alle PDFs in einem neuen Fenster geöffnet werden sollen:
jQuery("#content a[ @href $= '.pdf']").attr('target', '_blank');
Weitere Informationen zu jQuery erhalten Sie unter:
Und mit FireQuery gibt es eine Firefox-Extension, die in Firebug integriert ist.
Der neue Artikeltyp soll als neues Paket erstellt werden. Entsprechend unserer Anforderung nennen wir es vs.registration
. Um dieses Paket zu erstellen verwenden wir wieder PasteScript:
$ cd src
$ ../bin/zopeskel archetype vs.registration
Antworten Sie dabei auf die Frage Are you creating a Zope 2 Product? mit True.
Anschließend informieren wir die Buildout-Umgebung von unserem neuen Paket. Hierzu ändern wir buildout.cfg
:
[buildout]
...
develop
src/vs.policy
src/vs.theme
src/vs.registration
...
eggs =
elementtree
vs.policy
vs.theme
vs.registration
...
Nun wird das Buildout-Skript erneut aufgerufen:
$ ./bin/buildout -o
Entgegen dem Policy-Produkt fügen wir keinen neuen zcml-slug hinzu, sondern definieren es als Abhängigkeit in vs.policy
. Deshalb fügen wir in vs.policy/configure.zcml
folgendes hinzu:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="vs.policy">
<include package="vs.registration" />
...
</configure>
Interfaces können als formale Dokumentation der Fähigkeiten eines Artikeltyps betrachtet werden.
Folgende Interfaces beschreiben z.B. Registrierungen und die darin enthaltenen registrierten Personen in myproject/src/vs.registration/vs/registration/interfaces.py
:
from zope.interface import Interface
from zope import schema
from zope.app.container.constraints import contains
class IRegistration(Interface):
"""A folder containing registrants
"""
contains('vs.registration.interfaces.IRegistrants',)
text = schema.SourceText(title=_(u"Descriptive text"),
description=_(u"Descriptive text about this registration"),
required=True)
class IRegistrant(Interface):
"""A registrant
"""
name = schema.TextLine(title=_(u"Registrant name"),
required=True)
address = schema.Text(title=_(u"Address"),
description=_(u""),
required=True)
email = schema.TextLine(title=_(u"Email"),
description=_(u""),
required=True)
Im Gegensatz zu zope.formlib
nutzt Archetypes die angegebenen Titel und Beschreibungen nicht, dennoch sind die Angaben meines Erachtens nützlich um leicht den Funktionsumfang der Artikeltypen erkennen zu können.
Einen Überblick über die verschiedenen Feldtypen erhalten Sie in zope.schema.interfaces.
Damit die Feldattribute übersetzt werden können, wird folgendes in src/vs.registration/vs/registration/__init__.py
eingetragen:
from zope.i18nmessageid import MessageFactory
RegistrationMessageFactory = MessageFactory('vs.registration')
Und in src/vs.registration/vs/registration/interfaces.py
:
from vs.registration import RegistrationMessageFactory as _
Anmerkung: Eine vollständige Referenz zu Archetypes finden Sie in http://plone.org/documentation/manual/archetypes-developer-manual oder in Products.Archetypes.atapi
. Für Beispiele empfehlen sich die Quellen von Products.ATContentTypes
.
Üblicherweise sind die Content-Klassen von einer der folgenden Klassen abgeleitet:
BaseContent
ein einfacher, non-folderish Artikeltyp, der die Dublin Core-Metaangaben (über die ExtensibleMetadata
-Mixin-Klasse) enthält.
BaseFolder
eine folderish
-Version von BaseContent
.
OrderedBaseFolder
Version von BaseFolder
, der die Sortierung der enthaltenen Objekte erlaubt.
BaseBTreeFolder
Version von BaseFolder
, der die Inhalte in einem binary tree speichert und damit gut geeignet ist, mehrere tausend Objekte zu enthalten.
Alle vier Klassen sind importierbar von Products.Archetypes.atapi
.
In unserem Fall sollen Registration und Registrant jedoch den Plone-Artikeltypen ähnlich sein, z.B. beim Editieren sollen die Felder in verschiedene Reiter kategorisiert werden und auch ein Feld für Verweise soll enthalten sein. Daher werden die Artikeltypen von Plone erweitert, die in Products.ATContentTypes.content
implementiert sind. Registrant erweitert dabei base.ATCTContent
, in dem das übliche Verhalten aller einfachen (non-folderish) Artikeltypen von Plone bereits definiert ist:
class Registrant(base.ATCTContent):
implements(IRegistrant)
portal_type = "Registrant"
_at_rename_after_creation = True
schema = RegistrantSchema
Registrant
erweitert base.ATCTContent
und implementiert IRegistrant
.
portal_type
ist der eindeutige Name des Artikeltyps.
at_rename_after_creation
benennt Objekte in die normalisierte Version ihres Titels um.
Für den obigen Artikeltyp wird das Schema direkt oberhalb der Klasse definiert:
RegistrantSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('email',
required=True,
searchable=True,
storage=atapi.AnnotationStorage(),
widget=atapi.StringWidget(label=_(u"Email"),
description=_(u""))
),
))
RegistrantSchema['title'].storage = atapi.AnnotationStorage()
RegistrantSchema['title'].widget.label = _(u"Registrant name")
RegistrantSchema['title'].widget.description = _(u"")
RegistrantSchema['description'].storage = atapi.AnnotationStorage()
RegistrantSchema['description'].widget.label = _(u"Address")
RegistrantSchema['description'].widget.description = _("")
RegistrantSchema['email'].storage = atapi.AnnotationStorage()
RegistrantSchema['email'].widget.label = _(u"Email")
RegistrantSchema['email'].widget.description = _(u"")
finalizeATCTSchema(RegistrantSchema, folderish=False, moveDiscussion=False)
class Registrant(base.ATCTContent):
"""Describe a registrant.
"""
implements(IRegistrant)
portal_type = "Registrant"
_at_rename_after_creation = True
schema = RegistrantSchema
name = atapi.ATFieldProperty('title')
address = atapi.ATFieldProperty('description')
email = atapi.ATFieldProperty('email')
atapi.registerType(Registrant, PROJECTNAME)
Zunächst wird das Schema von einem Basistypen, in diesem Fall ATContentTypeSchema
, kopiert und anschließend das eigene Schema angehängt.
Im Folgenden eine Liste der gebräuchlichsten Felder:
Feld |
dazugehörige Widgets |
Beschreibung |
---|---|---|
StringField |
StringWidget, SelectionWidget, PasswordWidget |
Eine einzelne Textzeile |
TextField |
TextAreaWidget, RichWidget |
Mehrzeiliges Textfeld, wobei
das |
LinesField |
LinesWidget, MultiSelectionWidget, InAndOutWidget |
Liste von Zeichenketten, mehrzeilig |
IntegerField |
IntegerWidget |
Ganze Zahl |
FixedPointField |
DecimalWidget |
Dezimalzahl |
BooleanField |
BooleanWidget |
Wahr/falsch-Checkbox |
FileField |
FileWidget |
Feld um Dateien hochzuladen |
ImageField |
ImageWidget |
Feld um Bilder hochzuladen |
DateTimeField |
CalendarWidget |
Feld um ein Datum auszuwählen |
ReferenceField |
ReferenceWidget, InAndOutWidget |
Referenz auf ein anderes Archetypes-Objekt |
Werden für SelectionWidgets Vokabularien (Wertelisten) verwendet hängt die Umsetzung des Widgets von der Größe der Werteliste ab. Bei bis zu drei Werten werden Radiobuttons verwendet, ab vier Werten erhält man eine Select-Box.
Die Felder lassen sich mit beliebig vielen Eigenschaften versehen. Im Folgenden nur eine Übersicht über die gebräuchlichsten Eigenschaften:
Feldeigenschaft |
Beschreibung |
---|---|
required |
Erforderlich, die möglichen Werte sind
|
searchable |
Der Wert |
default |
Bietet einen Standardwert für dieses Feld. |
default_method |
Name einer Methode (als Zeichenkette), die aufgerufen wird, um den Standardwert zu liefern. |
schemata |
Der Name eines Reiters in der Editieransicht.
Das Standardschema ist |
read_permission, write_permission |
Die Namen der Berechtigungen, die zum Lesen
bzw. Schreiben des Feldes erforderlich sind.
Die Standardwerte sind |
vocabulary, vocabulary_factory, enforceVocabulary |
Definieren eines Vokabulars für das Auswahlfeld, s.u. |
validators |
Eine Liste von Feldvalidatoren, s.u. |
accessor, edit_accessor, mutator |
Überschreibt die Namen der Accessor-, Edit-Accessor- oder Mutator-Methode. |
widget |
Widget, mit dem das Feld dargestellt werden soll |
storage |
Speicherabstraktion, die für dieses Feld
verwendet werden soll
Der Standardwert ist
|
Widgets werden in Products.Archetypes.Widget
definiert. Und ähnlich wie für Felder gibt es auch für Widgets eine Reihe von Eigenschaften, wovon die Häufigsten unten aufgeführt sind:
Widget-Eigenschaft |
Beschreibung |
---|---|
|
Eine Zeichenkette oder übersetzbare Nachricht, die als Etikett des Widgets verwendet wird |
|
Eine Zeichenkette oder übersetzbare Nachricht, die als Hilfe-Text verwendet wird |
|
Ein TALES-Ausfruck, die bestimmt, ob ein
Widget angezeigt wird. Die Variablen
|
|
Die Länge einer Textbox oder die Höhe einer Auswahlbox |
|
Höhe einer Textbox |
|
Wird von
|
vocabulary
spezifisches Vokabular für ein Feld.
enforceVocabulary
wenn der Wert True
ist und der eingegebene Wert nicht im Vokabular vorhanden ist, gibt Archetypes einen validation error aus.
vocabulary_factory
erwartet den Namen einer Zope3-IVocabularyFactory
-Hilfsmethode. Damit kann Archetypes Zope3-Vokabularien nutzen und sie z.B. mit formlib-Formularen teilen.
Das einfachste Vokabular ist eine statische Liste von akzeptierten Werten (ganze Zahlen für das IntegerField
und Zeichenketten für das StringField
).
Vokabularien werden im allgemeinen zusammen mit einem SelectionWidget
, MultiSelectionWidget
oder InAndOutWidget
verwendet, wobei nur die Werte des Vokabulars für die Auswahl verfügbar sind. Alternativ kann das AddRemoveWidget
, das eine flexiblere Handhabung des Vokabulars ermöglicht und über ein separates Egg importiert werden muss. Um das Vokabular zur Verfügung zu haben importieren wir die config.py
in der __init__.py
:
...
from zope.i18nmessageid import MessageFactory
import config
...
Im Beispiel wollen wir für Gebrauchtwaren festhalten, wie es um die Funktionsfähigkeit bestellt ist. Für das Produkt definieren wir eine statische Liste von Tupeln, mit der die Werte „ja, nein, eingeschränkt“ zur Wahl stehen. Die Angaben lauten beispielsweise wie folgt:
ARTICLE_USABLE=DisplayList((
('yes', _(u'Yes')),
('no', _(u'No')),
('some', _(u'somewhat usable')),
))
Für die Internationalisierung des Produkts inklusive des Vokabulars ist die MessageFactory zu importieren und jeder String mit _(u'')
zu definieren. Siehe auch Erstellen der Übersetzungsdateien.
Dieses Vokabular wird in der content/vsresale.py
verwendet:
...
TypeSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('usable',
vocabulary=config.ARTICLE_USABLE,
default='yes',
widget=atapi.SelectionWidget(label=_(u"usable"),
description=(u"is the article usable?"))
),
Es kann auch gegen eine Liste von (value, label)
-Tupeln validiert werden, wobei die Etiketten andere Angaben als die Werte des Vokabulars annehmen können. Archetypes transformiert diese Liste in DisplayList
(s.a. Products.Archetypes.utils
).
Für dynamische Vokabularien kann für vocabulary
die Methode eines Objekts, eines übergeordneten Objekts oder ein Skript in einem Skin-Layer angegeben werden. Beim Aufruf erwartet Archetypes eine einfache Werteliste, eine Liste von Tupeln oder eine DisplayList
. Hier ein Beispiel für eine solche Implementierung in ATTopic (Products/ATContentTypes/content/topic.py
):
LinesField('customViewFields',
required=False,
mode="rw",
default=('Title',),
vocabulary='listMetaDataFields',
enforceVocabulary=True,
write_permission = ChangeTopics,
widget=InAndOutWidget(
label=_(u'label_custom_view_fields', default=u'Table Columns'),
description=_(u'help_custom_view_fields',
default=u"Select which fields to display when "
"'Display as Table' is checked.")
),
),
...
security.declareProtected(View, 'listMetaDataFields')
def listMetaDataFields(self, exclude=True):
"""Return a list of metadata fields from portal_catalog.
"""
tool = getToolByName(self, TOOLNAME)
return tool.getMetadataDisplay(exclude)
Die Methode gibt eine DisplayList
mit Werten und Etiketten zurück.
Will man die für einen bestimmten Content-Typen vergebenen Schlagworte für alle gleichartigen Objekte desselben Typs verfügbar machen, stellt man den Vokabelbestand über eine Abfrage des portal_catalog
bereit. Als Auswahlfeld verwenden wir das AddRemoveWidget, das über die buildout.cfg
hinzugefügt wird:
eggs =
...
Products.AddRemoveWidget
Diese Liste wird dann in der content/vsresale.py
zusammengestellt:
atapi.LinesField('category',
required=True,
searchable=True,
vocabulary='getTagsVocab',
enforceVocabulary=False,
accessor="Category"
widget=atapi.AddRemoveWidget(label=_(u"tags"),
description=_(u"")),
),
Das Vokabular entnehmen wir über über die Methode getTagsVocab
aus dem Bestand, der in den vorhandenen Objekten desselben Content-Typs angelegt wurde. Die Methoden werden wie folgt definiert:
class VsResale(base.ATCTContent):
...
def getTagsVocab(self):
"""
Get the available tags as a DisplayList.
"""
tags = self.getTagsInUse()
vocab = atapi.DisplayList()
for t in tags:
vocab.add(t, t)
return vocab
def getTagsInUse(self):
"""
Get a list of the resale tags in use in this contenttype.
"""
catalog = getToolByName(self, 'portal_catalog')
issues = catalog.searchResults(portal_type = 'Resale Goods Type',)
tags = {}
result = set()
for i in issues:
issue = i.getObject()
result.update(issue.Category())
return sorted(result)
Darüberhinaus bringt Plone 3 in plone.app.vocabularies
bereits eine Reihe von häufig verwendeten Vokabularien mit.
Wenn im Editierformular eines Archtetypes-Artikeltyps auf Speichern geklickt wird, wird die Methode validate()
von BaseObject
aufgerufen. Alle Felder bieten einfache Validierungen wie diejenigen, ob auch ein Eintrag in einem als erforderlich deklarierten Feld gemacht wurde oder ein Nummernfeld keine Buchstaben enthält.
Es lassen sich jedoch auch eigene Validatoren für spezifische Felder schreiben, z.B.:
atapi.TextField('text',
....
validators=("isTidyHtmlWithCleanup",),
....
),
Dies weist einem Feld einen oder mehrere Validatoren zu, die in der validator registry registriert sein müssen.
Wie solche Validatoren registriert werden können, sehen Sie in Products.ATContentTypes.lib.validators
:
validatorList.append(TidyHtmlWithCleanupValidator('isTidyHtmlWithCleanup', title='', description=''))
Dies ist jedoch nur notwendig, wenn der Validator für mehrere Felder und Artikeltypen Verwendung finden soll. Wird nur ein feldspezifischer Validator benötigt, so lässt sich dieser einfach in einer Methode validate_fieldname()
der Klasse dieses Artikeltyps hinzufügen wobei fieldname
der Name des zu überprüfenden Feldes ist:
def validate_text (self, value):
if "maybe" in value:
return _(u"You shouldn’t be so vague.")
return None
atapi.registerType(Registrant, PROJECTNAME)
registriert den Artikeltyp Registrant, indem der Klassengenerator aufgerufen wird und jedem Feld der Registrant-Klasse drei Methoden hinzufügt:
getter-Methode
falls der Accessor eine Transformation vornimmt und das Editierfeld anders eingelesen werden muss.
setter-Methode
So werden z.B. für das Feld email
die Methoden getEmail()
, getRawEmail()
und setEmail()
als Accessor, edit accessor und mutator erzeugt.
Manchmal kann es auch notwendig werden, eigene getter- und setter-Methoden zu schreiben. Entspricht der Name der Methode derjenigen eines Accessors oder Mutators, nimmt Archetypes an, dass diese Methode anstatt einer generierten verwendet werden soll.
Der Name einer solchen Methode kann in den Feldeigenschaften angegeben werden, z.B. in Products/Archetypes/ExtensibleMetadata.py
:
BooleanField(
'allowDiscussion',
accessor="isDiscussable",
mutator="allowDiscussion",
edit_accessor="editIsDiscussable",
...
),
Nachdem die Artikeltypen mit ihrem jeweiligen Schema erstellt wurden, gehen wir
nun zum User-Interface über, dessen Code sich im browser
-Paket befindet.
Für jeden Artikeltyp wird ein eigenes Icon definiert in
browser/configure.zcml
, in unserem Fall:
<browser:resource
name="registration_icon.gif"
image="registration_icon.gif"
/>
Dieses Icon kann referenziert werden mit ++resource++registration_icon.gif
.
Um das Icon innerhalb eines Page Templates aufzurufen, können Sie folgendes
angeben:
<img tal:attributes="src context/++resource++registration_icon.gif" />
Dem Icon analog lässt sich auch ein Stylesheet-Dokument hinzufügen mit:
<browser:resource
name="registration.css"
file="registration.css"
/>
Diese Datei können Sie mit folgendem Code in ein Page Template einfügen:
<metal:css fill-slot="css_slot">
<style type="text/css" media="all"
tal:content="string: @import url(${context/++resource++registration.css});"></style>
</metal:css>
Auch die Views werden in browser/configure.zcml
registriert:
<browser:page
for="..interfaces.IRegistration"
name="view"
class=".registration.RegistrationView"
permission="zope2.View"
/>
Üblicherweise wird die Standardansicht eines Artikeltyps mit @@view
aufgerufen.
Sollen Autoren zwischen verschiedenen Ansichten eines Artikeltyps in Plones
Ansicht-Menü wählen können, müssen diese Ansichten einerseits in einer Liste im
GenericSetup-Profil angegeben werden, andererseits jedoch auch in
browser/configure.zcml
registriert werden:
<include package="plone.app.contentmenu" />
...
<browser:menuItem
for="..interfaces.IRegistration"
menu="plone_displayviews"
title="Registration view"
action="@@view"
description="Default view of a registration"
/>
action
verweist auf den Namen der Ansicht, wobei der Menüeintrag nur für das
IRegistration
-Interface angezeigt wird.
Die View-Klasse selbst enthält die Methoden name
und details
:
from Products.Five import BrowserView
class RegistrationView(BrowserView):
"""A view of a Registration object"""
def name(self):
return self.context.Title()
def details(self):
return self.context.Description()
Der Decorator @memoize
stellt sicher, dass der Aufruf in einer Instanz nur einmal ausgeführt wird – und der zurückgegebene Wert gespeichert wird. Wenn Templates eine Methode mehrfach aufrufen, kann so die Performance deutlich gesteigert werden (s.a. Memoize). Das Template registration.pt
sieht dann so aus:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="vs.registration">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main"
tal:define="text context/text;">
<div tal:replace="structure provider:plone.abovecontenttitle" />
<h1 class="documentFirstHeading">
<span metal:use-macro="python:context.widget('title', mode='view')" />
</h1>
<div tal:replace="structure provider:plone.belowcontenttitle" />
<div class="documentDescription">
<span metal:use-macro="python:context.widget('description', mode='view')" />
</div>
<div tal:replace="structure provider:plone.abovecontentbody" />
<p tal:condition="python: not text and is_editable"
i18n:translate="no_body_text"
class="discreet">
This item does not have any body text, click the edit tab to change it.
</p>
<div tal:condition="text" metal:use-macro="python:context.widget('text', mode='view')" />
<form action="createObject">
<input name="type_name"
type="hidden"
value="Registrant"
/>
<input class="standalone"
value="Registration"
type="submit"
i18n:attributes="value"
/>
</form>
<tal:registrants condition="view/have_registrants">
<h2 i18n:translate="title_registration_contents">Registrants</h2>
<dl>
<tal:block repeat="registrant view/registrants">
<dt>
<a tal:attributes="href registrant/url"
tal:content="registrant/title" />
</dt>
<dd tal:content="registrant/address" />
</tal:block>
</dl>
</tal:registrants>
<div metal:use-macro="context/document_relateditems/macros/relatedItems">
show related items if they exist
</div>
<div tal:replace="structure provider:plone.belowcontentbody" />
</tal:main-macro>
</metal:main>
</body>
</html>
Das Template entspricht weitgehend Plones document_view.pt
.
Beachten Sie, dass verschiedene Viewlet-Manager angegeben wurden, wie z.B.:
<div tal:replace="structure provider:plone.abovecontenttitle" />
Im Kapitel Viewlets wird ausführlich auf diese Zope3-Komponenten eingegangen.
In diesem Template wurde auch das Inline-Editing mit Kinetic Style Sheets ermöglicht, z.B. mit:
<span metal:use-macro="python:context.widget('title', mode='view')" />
Soll ein Feld nicht direkt editiert werden können, genügt ein einfacheres Konstrukt:
<span tal:content="context/title" />
Mehr über das JavaScript-Framework erfahren Sie im Kapitel Kinetic Style Sheets.
Nachdem die Artikeltypen und die zugehörigen Views erstellt sind, müssen diese Artikeltypen noch installiert werden. Dies geschieht mit einem Generic Setup Extension-Profil in vs/registration/profiles/default/types.xml
:
<object name="portal_types" meta_type="Plone Types Tool">
<object name="Registration"
meta_type="Factory-based Type Information with dynamic views"/>
<object name="Registrant"
meta_type="Factory-based Type Information with dynamic views"/>
</object>
Damit werden zwei verschiedene factory-based type information (FTI)-Objekte im portal_types
-Tool erstellt, wobei die Dynamic Views Editoren das Auswählen verschiedener Ansichten im Darstellung-Menü von Plone erlauben.
Jede FTI ist detaillierter in der entsprechenden Datei in profiles/default/types/
definiert. Dabei muss der Dateiname dem Namen des portal type entsprechen, wobei Leerzeichen als Unterstriche angegeben werden müssen. Schauen wir uns nun profiles/default/types/Registration.xml
genauer an:
<?xml version="1.0"?>
<object name="Registration"
meta_type="Factory-based Type Information with dynamic views"
i18n:domain="vs.registration"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title"
i18n:translate="">Registration</property>
<property name="description"
i18n:translate="">A folder which can contain registrants.</property>
<property name="content_icon">++resource++registration_icon.gif</property>
In diesen Zeilen wird dem Artikeltyp Name, Beschreibung und Icon zugewiesen, die Plone auch zur Darstellung nutzt.
<property name="content_meta_type">Registration</property>
<property name="product">vs.registration</property>
<property name="factory">addRegistration</property>
<property name="immediate_view">atct_edit</property>
Hier wird der Meta-Typ des Artikeltyps angegeben, der meist dem Namen des portal type entspricht. Anschließend wird die Methode angegeben, die das neue Artikelobjekt erstellt und initialisiert. immediate_view
gibt die Ansicht, die unmittelbar nach dem Erstellen des Objekts gezeigt wird, an (auch wenn diese momentan nicht durch Plone unterstützt wird).
<property name="global_allow">True</property>
<property name="filter_content_types">True</property>
<property name="allowed_content_types">
<element value="Registrant" />
</property>
Diese Eigenschaften bestimmen das Verhältnis von übergeordneten zu untergeordneten Objekten. Registration
darf in allen Ordner-Objekten, für die filter_content_types
auf False
gesetzt wurde, hinzugefügt werden. Darüberhinaus darf in Registration
mit filter_content_types
und allowed_content_types
nur der Artikeltyp Registrant
hinzugefügt werden.
<property name="allow_discussion">False</property>
Diese Eigenschaft bestimmt, ob für diesen Artikeltyp üblicherweise Diskussionen erlaubt sind oder nicht.
<property name="default_view">view</property>
<property name="view_methods">
<element value="view"/>
<element value="folder_summary_view"/>
<element value="folder_tabular_view"/>
<element value="folder_listing"/>
</property>
Diese Eigenschaften geben die Standardansicht und die im Darstellung-Menü auswählbaren Ansichten (dynamic views) an.
<alias from="(Default)" to="(dynamic view)"/>
<alias from="edit" to="atct_edit"/>
<alias from="sharing" to="@@sharing"/>
<alias from="view" to="(selected layout)"/>
Üblicherweise nutzen die meisten Plone-Artikeltypen diese vier Aliase. Werden keine dynamic views FTIs verwendet, müssen die Namen der Views oder Templates für die (Default)
- und view
-Aliase angegeben werden.
<action title="View"
action_id="view"
category="object"
condition_expr=""
url_expr="string:${object_url}/"
visible="True">
<permission value="View"/>
</action>
<action title="Edit"
action_id="edit"
category="object"
condition_expr=""
url_expr="string:${object_url}/edit"
visible="True">
<permission value="Modify portal content"/>
</action>
Schließlich werden noch zwei typspezifische Aktionen definiert, die der Kategorie object
zugeordnet werden und die oben definierten Aliase nutzen. Beachten Sie, dass bei folderish-Artikeltypen für die view
-Aktion string:${folder_url}/
, hingegen für non-folderish-Artikeltypen string:${object_url}
verwendet wird.
Die oben bereits erwähnte Methode addRegistration
zum Erstellen eines neuen Objekts des Artikeltyps Registration wird automatisch durch Archetypes erstellt, wenn das Produkt initiiert wird. Darüberhinaus werden in der Datei __init__.py
auch die Rechte zum Hinzufügen der neuen Artikeltypen gesetzt.
from vs.registration import config
from Products.Archetypes import atapi
from Products.CMFCore import utils
def initialize(context):
Damit wird das Zope-2-Produkt initialisiert.
from content import registration, registrant
content_types, constructors, ftis = atapi.process_types(
atapi.listTypes(config.PROJECTNAME),
config.PROJECTNAME)
Die Artikeltypen aus content
werden importiert und mit Archetypes registerType()
-Aufruf registriert.
for atype, constructor in zip(content_types, constructors):
utils.ContentInit("%s: %s" % (config.PROJECTNAME, atype.portal_type),
content_types = (atype,),
permission = config.ADD_PERMISSIONS[atype.portal_type],
extra_constructors = (constructor,),
).initialize(context)
Nun werden die Artikeltypen mit den angegebenen Rechten zum Hinzufügen registriert. Diese Rechte sind in der config.py
-Datei angegeben:
ADD_PERMISSIONS = {
"Registration" : "vs: Add Registration",
"Registrant" : "vs: Add Registrant",
}
Damit werden die zwei Artikeltypen entsprechenden Rechten zum Hinzufügen zugeordnet. Zudem sind diese Rechte auch noch definiert in content/configure.zcml
. Die Rechte erhalten mit vs
ein Präfix, damit Sie im Security-Reiter des ZMI zusammen dargestellt werden.
Die den Hinzufügen-Rechten zugehörigen Rollen sind in profiles/default/rolemap.xml
definiert:
<rolemap>
<permissions>
<permission name="vs: Add Registration" acquire="False">
<role name="Manager" />
</permission>
<permission name="vs: Add Registrant" acquire="False">
<role name="Manager" />
<role name="Owner" />
<role name="Contributor" />
</permission>
</permissions>
</rolemap>
Für die meisten Artikeltypen empfiehlt sich die Registrierung am Factory Tool, um halbfertige Objekte beim Erstellen zu vermeiden. Die Registrierung geschieht auch hier mit einem Generic-Setup-Profil, nämlich factorytool.xml
:
<object name="portal_factory"
meta_type="Plone Factory Tool">
<factorytypes>
<type portal_type="Registration"/>
<type portal_type="Registrant"/>
</factorytypes>
</object>
Die ATCT-Artikeltypen, mit denen Plone ausgeliefert wird, werden alle automatisch am CMFEditions Repository angemeldet, sodass für die entsprechenden Objekte auch die früheren Versionen angezeigt werden können. Um nun unsere beiden Artikeltypen am CMFEditions Repository zu registrieren, schreiben wir die Methode setupEditions
:
from StringIO import StringIO
from logging import getLogger
from Products.CMFCore.utils import getToolByName
from Products.Archetypes import atapi
from config import PROJECTNAME
class Generator:
def setupEditions(self, p, out):
content_types, constructors, ftis = atapi.process_types(
atapi.listTypes(PROJECTNAME),
PROJECTNAME)
portal_repository = getToolByName(p, 'portal_repository')
types = portal_repository.getVersionableContentTypes()
for type in content_types:
if type.portal_type not in types:
types.append("%s" %type.portal_type)
portal_repository.setVersionableContentTypes(types)
print >> out, " Editions enabled %s \n" % types
Diese Methode wird in der setuphandlers.py
-Datei erstellt und mit setupVarious
aufgerufen:
def setupVarious(context):
if context.readDataFile('vs.registration_various.txt') is None:
return
# Add additional setup code here
out = StringIO()
site = context.getSite()
gen = Generator()
gen.setupEditions(site, out)
logger = context.getLogger(PROJECTNAME)
logger.info(out.getvalue())
Entsprechend erstellen wir die Konfigurationsdatei vs.registration/vs/registration/profiles/default/import_steps.xml
mit folgendem Inhalt:
<?xml version="1.0"?>
<import-steps>
<import-step id="idg.org.various"
version="20080617-01"
handler="vs.registration.setuphandlers.setupVarious"
title="vs.registration: miscellaneous import steps">
<dependency step="typeinfo" />
Various import steps that are not handled by GS import/export
handlers.
</import-step>
</import-steps>
Und schließlich benötigen wir noch die Datei vs.registration/vs/registration/profiles/default/vs.registration_various.txt
, damit unsere setupVarious
-Methode auch ausgeführt wird.
Schließlich soll das vs.policy
-Produkt noch so geändert werden, dass es automatisch vs.registration
bei der Installation in der Plone-Site mitinstalliert. Hierzu wird zunächst in src/vs.policy/vs/policy/configure.zcml
folgende Zeile eingefügt:
<include package="vs.registration" />
Für Plone 3.0 wird anschließend vs.registration
auch noch in die benötigten Produkte in src/vs.policy/vs/policy/Extensions/Install.py
eingetragen:
PRODUCT_DEPENDENCIES = ('vs.theme',
'vs.registration')
Für Plone 3.1 wird src/vs.policy/vs/policy/profiles/default/metadata.xml
um folgende Zeile ergänzt:
<dependency>profile-vs.registration:default</dependency>
Die ATContenttypes liefern in Products/ATContentTypes/etc/atcontenttypes.conf.in
die Vorlage für eine Konfigurationsdatei. Um diese nun anpassen zu können, kann die Buildout-Konfiguration folgendermaßen geändert werden:
[buildout]
...
parts =
...
atct_conf
...
[atct_conf]
recipe = plone.recipe.command
target = ${instance:location}/etc/atcontenttypes.conf
command = ln -s ${buildout:directory}/etc/atcontenttypes.conf ${:target}
Schauen wir uns nun etc/atcontenttypes.conf
genauer an.
mxtidy
HTML-Filter-Optionen:
drop_font_tags
Sofern auf yes
gesetzt, verwirft Tidy alle font
- und center
-Tags.
drop_empty_paras
Sofern auf yes
gesetzt, werden leere Paragraphen verworfen. Bei no
werden leere Absätze durch zwei br
-Elemente ersetzt.
input_xml
Sofern auf yes
gesetzt, verwendet Tidy den XML-Parser und nicht den HTML-Parser.
output_xhtml
Sofern auf yes
gesetzt, generiert Tidy wohlgeformtes HTMLund der Doctpe wird in XHTML geändert.
quiet
Sofern auf yes
gesetzt, gibt Tidy nicht die Anzahl der Fehler und Warnungen aus.
show_warnings
Sofern auf ``no``gesetzt, werden Warnungen unterdrückt.
indent_spaces
Setzt die Anzahl der Leerzeichen zum Einrücken der Inhalte.
word_2000
Sofern auf yes
gesetzt, filtert Tidy zusätzliche Anweisungen aus, die Microsoft Word 2000 beim Speichern als Web pages einfügt.
wrap
Setzt die Anzahl der Zeichen, nach denen spätestens umbrochen wird.
Falls kein automatischer Zeilenumbruch gewünscht ist, kann hier einfach 0
angegeben werden.
tab_size
Setzt die Anzahl der Leerzeichen bei der Eingabe eines Tabulators.
char_encoding
Bestimmt die Interpretation von Zeichen.
Bei ascii
akzeptiert Tidy Latin-1-Zeichen und Entitäten für alle Zeichen > 127
.
Eine vollständige Liste der Optionen finden Sie in HTML Tidy Options.
feature swallowImageResizeException
Sofern enable yes
gesetzt ist, werden Fehlermeldungen beim Ändern der Bildgröße nicht ausgegeben.
pil_config
Konfiguration der Python Imaging Library (PIL).
quality
Qualität, mit der die Bilder berechnet werden. Die Skala reicht von 1
bis 100
. Bei 100
wird jedoch keine JPEG Quantisierung mehr verwendet.
resize_algo
Algorithmus, mit dem die Größenänderungen von Bildern berechnet werden.
Mögliche Angaben sind:
nearest
bilinear
bicubic
antialias
archetype
Konfiguration der Archetypes-Artikeltypen
contenttypes
MIME-Type des ATContenttype.
default
Standardwert des MIME-Type
allowed
Zulässige MIME-Types
max_file_size
Maximale Dateigröße in byte
, kb
oder mb
.
0
begrenzt die Dateigröße nicht.
max_image_dimension
Maximale Breite und Höhe von Bildern.
0,0
begrenzt weder die Breite noch die Höhe der Originalbilder.
allow_document_upload
yes
erlaubt das Hochladen von Artikeln.
metadate
Ein einzelnes Metadata-Element.
Folgende Attribute sind möglich:
name
friendlyName
description
enabled
index
Ein einzelnes Metadata-Element.
Folgende Attribute sind möglich:
name
friendlyName
description
enabled
criterion
topic_tool
Standardkonfiguration des Topic-Tools.
atct_tool
Standardkonfiguration des ATCT-Tools.
Mit dem archetype-Template und dessen local commands lassen sich Artikeltypen noch einfacher erstellen.
Statt des Templates plone
kann auch das Template archetype
mit local commands verwendet werden um neue Artikeltypen zu erstellen. Hierzu geben wir zunächst folgendes an:
paster create -t archetype vs.registration
Anschließend wechseln wir in das gerade eben erstelle Verzeichnis und lassen uns die hier verfügbaren local commands anzeigen:
$ cd vs.registration
$ paster addcontent --list
Available templates:
atschema: A handy AT schema builder
contenttype: A content type skeleton
form: A form skeleton
formfield: Schema field for a form
i18nlocale: An i18n locale directory structure
portlet: A Plone 3 portlet
view: A browser view skeleton
zcmlmeta: A ZCML meta directive skeleton
Weitere local commands sind für das plone_pas
-Projekt verfügbar. Eine Übersicht über alle verfügbaren local commands erhält man mit:
$ paster addcontent -a
N anonymous_user_factory_plugin: A Plone PAS AnonymousUserFactory Plugin
atschema: A handy AT schema builder
N authentication_plugin: A Plone PAS Authentication Plugin
N challenge_plugin: A Plone PAS Challenge Plugin
contenttype: A content type skeleton
N credentials_reset_plugin: A Plone PAS CredentialsReset Plugin
N extraction_plugin: A Plone PAS Extraction Plugin
form: A form skeleton
formfield: Schema field for a form
N group_enumeration_plugin: A Plone PAS GroupEnumeration Plugin
N groups_plugin: A Plone PAS Groups Plugin
i18nlocale: An i18n locale directory structure
portlet: A Plone 3 portlet
N properties_plugin: A Plone PAS Properties Plugin
N role_assigner_plugin: A Plone PAS RoleAssigner Plugin
N role_enumeration_plugin: A Plone PAS RoleEnumeration Plugin
N roles_plugin: A Plone PAS Roles Plugin
N update_plugin: A Plone PAS Update Plugin
N user_adder_plugin: A Plone PAS UserAdder Plugin
N user_enumeration_plugin: A Plone PAS UserEnumeration Plugin
N user_factory_plugin: A Plone PAS UserFactory Plugin
N validation_plugin: A Plone PAS Validation Plugin
view: A browser view skeleton
zcmlmeta: A ZCML meta directive skeleton
Die beim Anlegen des Projekts gemachten Angaben finden mit Erläuterungen in der setup.py
.
Mit den Angaben in der Datei vs.registration/vs/registration/configure.zcml
wird das Produkt für das Generic Setup-Tool registriert, wenn hierfür der Default-Wert auf „True“ belassen wurde. Titel und Beschreibungstext werden für die Anzeige im Generic Setup-Tool verwendet:
<configure
...
i18n_domain="vs.registration">
<genericsetup:registerProfile
name="default"
title="Registration Content-types"
directory="profiles/default"
description="Short description of registration content-types"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
Um einen Content-Type zu unserem Projekt hinzuzufügen kann folgendes ausgeführt werden:
$ cd vs.registration
$ paster addcontent contenttype
Enter contenttype_name (Content type name ) ['Example Type']: Registration
Enter contenttype_description (Content type description ) ['Description of the Example Type']: Container for registrants
Enter folderish (True/False: Content type is Folderish ) [False]: True
Enter global_allow (True/False: Globally addable ) [True]:
Enter allow_discussion (True/False: Allow discussion ) [False]:
Inserting from README.txt_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/README.txt
Inserting from config.py_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/config.py
Recursing into content
Copying +content_class_filename+.py_tmpl to /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/content/registration.py
Inserting from configure.zcml_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/content/configure.zcml
Recursing into interfaces
Copying +content_class_filename+.py_tmpl to /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/interfaces/registration.py
Inserting from __init__.py_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/interfaces/__init__.py
Recursing into profiles
Recursing into default
Inserting from factorytool.xml_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/profiles/default/factorytool.xml
Copying rolemap.xml_insert to /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/profiles/default/rolemap.xml
Recursing into types
Creating /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/profiles/default/types/
Copying +types_xml_filename+.xml_tmpl to /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/profiles/default/types/Registration.xml
Inserting from types.xml_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/profiles/default/types.xml
Damit der Content-Type vollständig internationalisierbar angelegt wird fügen wir noch die Angaben für die Internationalisierung i18nlocale
hinzu:
$ cd vs.registration
$ paster addcontent i18nlocale
Enter language_code (The iso-code of the language) ['']: de
Inserting from configure.zcml_insert into /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/configure.zcml
Recursing into locales
Creating /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/locales/
Recursing into +language_iso_code+
Creating /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/locales/de/
Recursing into LC_MESSAGES
Creating /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/locales/de/LC_MESSAGES/
Copying README.txt to /home/veit/Projects/IDG/idg_buildout/src/vs.registration/vs/registration/locales/de/LC_MESSAGES/README.txt
Jetzt sollte die Instanz ohne Fehlermeldung gestartet werden können. Unser Content-Type ist beim Hinzufügen einer Plone-Site als „Extension Profile“ mit dem vergebenen Titel „Registration Content-types“ auswählbar.
Die Datei vs/registration/profiles/default/types.xml
registriert neue Content-Typen am Types-Tool:
<object name="portal_types" meta_type="Plone Types Tool">
<object name="Registration"
meta_type="Factory-based Type Information with dynamic views"/>
<object name="Registrant"
meta_type="Factory-based Type Information with dynamic views"/>
</object>
In der neu erstellten Plone-Site lässt sich der Content-Typ global hinzufügen, soweit bei der Erstellung nichts anderes angegeben wurde.
Bei Content-Typen, die auf Archetypes oder CMF beruhen, wird das Objekt bereits angelegt bevor das zugehörige Formular ausgefüllt wird. Mit den Angaben in vs/registration/profiles/default/factorytool.xml
wird das Produkt am Factory-Tool angemeldet. Dieses verwaltet neue Objekte nur solange, bis das Formular tatsächlich gespeichert wird und verschiebt das temporäre Objekt beim Speichern an die vorgesehene Stelle. Damit der Mechanismus für unseren Objekte greift sind diese Angaben hinzuzufügen:
<factorytypes>
...
<type portal_type="Registration"/>
<type portal_type="Registrant"/>
</factorytypes>
Bisher stehen beiden Content-Typen lediglich die Strukturen zur Verfügung, die dem Default von ATContentType
entsprechen, nämlich die Felder title
und description
. Wir ergänzen im folgenden die übernommenen Felder um spezifische Informationen.
Um z.B. die Registrierung mit einen formatierbaren Textfeld zu versehen fügen wir eine entsprechende Definition in content/registration.py
hinzu. Zunächst wird das Schema von einem Basistypen, in diesem Fall ATFolderTypeSchema, kopiert und anschließend das eigene Schema angehängt:
RegistrationSchema = folder.ATFolderSchema.copy() + atapi.Schema((
atapi.TextField('text',
required=True,
searchable=True,
storage=atapi.AnnotationStorage(),
validators=('isTidyHtmlWithCleanup',),
default_output_type='text/x-html-safe',
widget=atapi.RichWidget(label=_(u"Body Text"),
description=_(u"Text for front page of registration"),
rows=25,
allow_file_upload=False),
),
))
RegistrationSchema['title'].storage = atapi.AnnotationStorage()
RegistrationSchema['description'].storage = atapi.AnnotationStorage()
RegistrationSchema['text'].storage = atapi.AnnotationStorage()
schemata.finalizeATCTSchema(
RegistrationSchema,
folderish=True,
moveDiscussion=False
)
...
title = atapi.ATFieldProperty('title')
description = atapi.ATFieldProperty('description')
text = atapi.ATFieldProperty('text')
Das zusätzliche Feld erhält erhält den Status Pflichtfeld, wird durchsuchbar und wird auf valides HTML validiert. Der Text kann formatiert werden. Alle Felder verwenden explizit den Speichertyp AnnotationStorage
.
Um z.B. das Teilnehmer-Objekt um ein Feld für eine Email-Adresse zu ergänzen fügen wir die Angaben für das Feld in content/registrant.py
hinzu. Zunächst wird wieder das Schema von einem Basistypen, in diesem Fall ATContentTypeSchema, kopiert und anschließend das eigene Schema angehängt:
RegistrantSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('email',
required=True,
searchable=True,
storage=atapi.AnnotationStorage(),
widget=atapi.StringWidget(label=_(u"Email"),
description=_(u""))
),
))
RegistrantSchema['title'].storage = atapi.AnnotationStorage()
RegistrantSchema['title'].widget.label = _(u"Registrant name")
RegistrantSchema['title'].widget.description = _(u"")
RegistrantSchema['description'].storage = atapi.AnnotationStorage()
RegistrantSchema['description'].widget.label = _(u"Address")
RegistrantSchema['description'].widget.description = _("")
RegistrantSchema['email'].storage = atapi.AnnotationStorage()
RegistrantSchema['email'].widget.label = _(u"Email")
RegistrantSchema['email'].widget.description = _(u"")
schemata.finalizeATCTSchema(RegistrantSchema, moveDiscussion=False)
class Registrant(base.ATCTContent):
"""Describe a registrant.
"""
implements(IRegistrant)
meta_type = "Registrant"
_at_rename_after_creation = True
schema = RegistrantSchema
name = atapi.ATFieldProperty('title')
address = atapi.ATFieldProperty('description')
email = atapi.ATFieldProperty('email')
atapi.registerType(Registrant, PROJECTNAME)
Dieses Schema implementiert ein Interface, IRegistrant
. Die Definition steht im Unterordner interfaces
. Die Klasse Registrant
erweitert base.ATCTContent
. Die Angabe at_rename_after_creation
benennt Objekte in die normalisierte Version ihres Titels um.
meta_type
setzt den eindeutigen Namen des Artikeltyps. Alternativ kann dieser auch mit portal_type
gesetzt werden. Zur Unterscheidung siehe unten.
Um nun z.B. ein Portlet unserem Projekt hinzuzufügen, kann einfach folgendes angegeben werden:
$ paster addcontent portlet
Enter portlet_name (Portlet name (human readable)) ['Example portlet']: My portlet
Enter portlet_type_name (Portlet type name (should not contain spaces)) ['ExamplePortlet']: registrants
Enter description (Portlet description) ['']: My portlet
Recursing into portlets
Copying +portlet_filename+.pt_tmpl to /home/veit/vs.mytype/vs/mytype/portlets/registrants.pt
Copying +portlet_filename+.py_tmpl to /home/veit/vs.mytype/vs/mytype/portlets/registrants.py
File '__init__.py' already exists: skipped
Inserting from configure.zcml_insert into /home/veit/vs.mytype/vs/mytype/portlets/configure.zcml
Recursing into profiles
Recursing into default
Inserting from portlets.xml_insert into /home/veit/vs.mytype/vs/mytype/profiles/default/portlets.xml
Recursing into tests
File '__init__.py' already exists: skipped
Copying base_+portlet_filename+.py_tmpl to /home/veit/vs.mytype/vs/mytype/tests/base_registrants.py
Copying test_+portlet_filename+.py_tmpl to /home/veit/vs.mytype/vs/mytype/tests/test_registrants.py
Neben den neu erstellten Dateien portlets/registrants.py
und portlets/registrants.pt
wurden auch die Dateien portlets/configure.zcml
und profiles/default/portlets.xml
aktualisiert.
Um z.B. den Ordnern für Registrierungen ein eigenes Icon zu geben kann man einfach diese Angaben in browser/configure.zcml
hinzufügen:
<browser:resource
name="registration_icon.gif"
image="registration_icon.gif"
/>
Die zugehörige Klasse class RegistrantView(BrowserView)
wird in browser/registrant.py
und browser/registration.py
definiert.
Mit Hilfe des View-Templates lässt sich über Paster auf einfache Art ein neuer Browser View hinzufügen:
paster addcontent view
Sofern noch nicht vorhanden werden die Dateien browser/registrantview.py
und browser/registrationview.py
sowie browser/registrantview.pt
und browser/registrationview.pt
erstellt. Die Templates können angepasst werden.
Damit das Icon vom Factory Tool verwendet wird ändert man das default-Icon in profiles/default/types/Registration.xml
:
<object name="Registration"
...
<property name="content_icon">++resource++registration_icon.gif</property>
Der Registrierungs-Ordner ist als Containerobjekt für Teilnehmer vorgesehen. Außerhalb dieser Ordner sollen sich auf der Site keine Teilnehmer hinfügen lassen. Diese Einschränkung wird in der profiles/default/types/Registration.xml
getroffen, und die Eigenschaft filter_content_types
auf True
gesetzt:
<object name="Registration"
...
<property name="filter_content_types">True</property>
<property name="allowed_content_types">
<element value="Registrant" />
</property>
Damit die Einschränkung übernommen wird und nur Teilnehmer einem Ordner „Registration“ zugeordnet werden können muss bei einer bestehenden Site das Generic Setup-Profil aktualisiert werden. Gehen Sie hierzu in das ZMI der Site und dort ins Types-Tool, Reiter „Import“, und wählen sie das Profil „vs.registration“, und setzen das Häkchen bei „Types Tool - Import types tool’s type information objects“. Vor dem tatsächlich Import sollte noch „include dependencies“ abgewählt werden. Nach dem Import sollten sich in Registration-Ordnern nur noch Teilnehmer hinzufügen lassen.
Werden keine Änderungen an den voreingestellten Berechtigungen vorgenommen dürfen Manager und Redakteure Registrierungs-Container und Teilnehmer anlegen. Wir ändern die profiles/default/rolemap.xml
, so dass nur Manager neue Container anlegen können:
<?xml version="1.0"?>
<rolemap>
<permissions>
<permission name="vs.registration: Add Registration" acquire="False">
<role name="Manager" />
</permission>
<permission name="vs.registration: Add Registrant" acquire="False">
<role name="Manager" />
<role name="Owner" />
<role name="Contributor" />
</permission>
</permissions>
</rolemap>
Um das Hinzufügen von Content über den ZMI-Reiter security
festlegen zu können wird content/configure.zcml
mit folgenden Angaben versehen:
<configure
...
i18n_domain="vs.registration">
<class class=".registration.Registration">
<require
permission="zope2.View"
interface="..interfaces.IRegistration"
/>
</class>
<class class=".registrant.Registrant">
<require
permission="zope2.View"
interface="..interfaces.IRegistrant"
/>
</class>
...
</configure>
Nähere Erläuterungen der Berechtigungen finden Sie im Abschnitt Sicherheit und Arbeitsabläufe.
In der Datei profiles/default/Registration.xml
z.B. werden Eigenschaften des Objekts definiert, die sich auf die Sichtbarkeit beziehen. Die Aufrufe für Ansehen und Bearbeiten erfordern unterschiedliche Rechte:
<action title="View"
action_id="view"
category="object"
condition_expr=""
url_expr="string:${folder_url}/"
visible="True">
<permission value="View"/>
</action>
<action title="Edit"
action_id="edit"
category="object"
condition_expr=""
url_expr="string:${object_url}/edit"
visible="True">
<permission value="Modify portal content"/>
</action>
Das fertige Produkt sollte im Dateisystem in etwa so aussehen:
|-- README.txt
|-- __init__.py
|-- config.py
|-- configure.zcml
|-- browser
| |-- __init__.py
| |-- configure.zcml
| |-- registrant_icon.gif
| |-- registrantview.pt
| |-- registrantview.py
| |-- registration_icon.gif
| |-- registrationview.pt
| |-- registrationview.py
|-- content
| |-- __init__.py
| |-- configure.zcml
| |-- registrant.py
| |-- registration.py
|-- interfaces
| |-- __init__.py
| |-- registrant.py
| |-- registration.py
|-- locales
| `-- de
| `-- LC_MESSAGES
|-- portlets
| |-- __init__.py
| `-- configure.zcml
|-- profiles
| `-- default
| |-- factorytool.xml
| |-- metadata.xml
| |-- portlets.xml
| |-- rolemap.xml
| |-- types
| |-- types.xml
| |-- workflows
| `-- workflows.xml
`-- tests
|-- __init__.py
|-- base.py
`-- test_doctest.py
Die Dateien und Ordner werden im folgenden kurz erläutert, die Angaben sind relativ zu /src/vs.registration/vs/registration
, außer wo abweichend angegeben:
- ``/__init__.py``: Das Initialisierungsmodul.
- ``/configure.zcml``: beschreibt Konfigurationsangaben, meldet das Produkt am Generic Setup-Tool an und enthält weitere Verweise auf Sub-packages (=Unterordner), die ihrerseits Konfigurationsangaben enthalten. Hier werden auch die Übersetzungen des i18n-Layers registriert.
- ``/config.py``: Fügt den Content-Typen die Berechtigungen für das Hinzufügen von Registrierungen und Teilnehmern zu hinzu. Die Berechtigungen werden durch ``__init__.py`` aufgerufen.
- ``/browser/``: kann zusätzliche Templates für die Gestaltung enthalten.
- ``/browser/configure.zcml`` registriert die Komponenten und legt fest, welche Pagetemplates für welchen Content-Typ verwendet werden, und definiert die angepassten Icons.
- ``/browser/registrantview.py``, ``browser/registrationview.py`` weisen dem View spezifische Templates zu. Die Angaben werden durch FTI (factory type information) in ``profiles/default/types/*.xml`` aufgerufen.
- ``browser/registrantview.pt``, ``browser/registrationview.pt`` sind Page-Templates in TAL/METAL.
- ``content``: enthält die Implementierungen der Content-types.
- ``content/configure.zcml`` legt die Berechtigungen für die Content-types fest, die mindestens erfüllt sein müssen.
- ``content/registrant.py``, ``content/registration.py`` enthalten die eigentliche Definition der Datenstrukturen, und melden die Content-typen bei Archetypes an
- ``interfaces/``: Ordner enthält Beschreibungen der Interfaces für unsere definierten Klassen, ``IRegistrant``und ``IRegistration``.
- ``interfaces/__init__.py`` importiert die Interface-Definitionen aus den Content-Types. Datei wird vom Template ``contenttype`` angelegt.
- ``interfaces/registrant.py``, ``interfaces/registration.py`` legt Pflichtfelder und die Benennungen der Felder fest. In ``registration.py`` contains('vs.registration.interfaces.IRegistrant',)
- ``locales/de/LC_MESSAGES/`` kann einmal die sprachspezifischen Übersetzungsdateien enthalten, in der Form ``vsregistration-de.po``. Genauere Informationen erhält man in `i18n-locales und Plone 3.0`_
- ``profiles/default``: die Dateien definieren das Extension Profile gegenüber Generic Setup; dieser Pfad wird in ``/configure.zcml`` festgelegt.
- ``profiles/default/factorytool.xml`` macht die Content-typen dem Factory-Tool bekannt. Datei wird vom Template ``contenttype`` angelegt.
- ``profiles/default/metadata.xml`` enthält eine Versionsnummer. Datei wird vom Template ``contenttype`` angelegt/modifiziert.
- ``profiles/default/portlets.xml`` sofern Portlets mit ``paster addcontent portlet`` erzeugt wurden.
- ``profiles/default/rolemap.xml`` Datei wird vom Template ``contenttype`` angelegt.
- ``profiles/default/types.xml`` Datei wird vom Template ``contenttype`` angelegt.
- ``profiles/default/workflows.xml`` Datei wird vom Template ``contenttype`` angelegt.
- ``profiles/default/types``
- ``profiles/default/types``
- ``tests/``: Unit-Tests für das Produkt.
The types are configured with the corresponding files in types/*.xml
. Note that spaces are allowed in type names, but the corresponding XML file uses an underscore instead.
Die types/*.xml
-Dateien werden mit Unterstrichen benannt werden
The „Factory-based Type Information with dynamic views“ refers to an FTI from Products.CMFDynamicViewFTI, which supports Plone’s „display“ menu.
Die Kontaktdaten des Autors (Name, Emailadresse, Homepage) sowie die Adresse des SVN-Repository werden mit einigen kommentierenden Angaben in vs.registration.egg-info/PKG-INFO
geschrieben. Vor der Weitergabe des Produkts sollten einige Angaben in dieser Datei sowie in CHANGES.txt
präzisiert werden.
Kollektionen sollen auch Felder unserer Artikeltypen suchen können. Hierzu sind doe folgenden beiden Profile hinzuzufügen:
Diese werden im catalog.xml
-Profil hinzugefügt:
<?xml version="1.0"?>
<object name="portal_catalog" meta_type="Plone Catalog Tool">
<index name="myfield" meta_type="FieldIndex">
<indexed_attr value="myfield"/>
</index>
<column value="myfield"/>
</object>
Folgende Typen von Indizees sind möglich:
DateIndex
DateRangeIndex
ExtendePathIndex
FieldIndex
KeywordIndex
PathIndex
TextIndex
TopicIndex
ZCTextIndex
Diese werden im portal_atct.xml
-Profil hinzugefügt:
<?xml version="1.0"?>
<atcttool>
<topic_indexes>
<index name="myfield"
description="myfield's description"
enabled="True"
friendlyName="mytype's myfield">
<criteria>ATSimpleStringCriterion</criteria>
</index>
</topic_indexes>
<topic_metadata>
<metadata name="myfield"
description="myfield's description"
enabled="True"
friendlyName="mytype's myfield"/>
</topic_metadata>
</atcttool>
Als Änderungsanforderung kam die Darstellung eines Fotos für Registration.
Das Interface wird in registration/interfaces.py
erweitert:
from zope.interface import Interface
from Products.ATContentTypes.interface.image import IImageContent
...
class IRegistration(Interface, IImageContent):
...
Das Schema wird in content/registration.py
definiert:
RegistrationSchema = folder.ATFolderSchema.copy() + atapi.Schema((
...
atapi.ImageField('image',
primary=True,
languageIndependent=True,
storage = AnnotationStorage(migrate=True),
swallowResizeExceptions = zconf.swallowImageResizeExceptions.enable,
pil_quality = zconf.pil_config.quality,
pil_resize_algo = zconf.pil_config.resize_algo,
max_size = zconf.ATImage.max_image_dimension,
sizes= {'large' : (768, 768),
'preview' : (400, 400),
'mini' : (200, 200),
'thumb' : (128, 128),
'tile' : (64, 64),
'icon' : (32, 32),
'listing' : (16, 16),
},
validators = (('checkImageMaxSize', V_REQUIRED)),
widget = ImageWidget(
description = 'Image for this registration',
label= _(u'label_image', default=u'Image'),
show_content_type = False,)),
), marshall=PrimaryFieldMarshaller()
)
...
RegistrationSchema['image'].storage = atapi.AnnotationStorage()
Anschließend wird das neue Feld implementiert:
class Registration(folder.ATFolder):
"""Contains multiple registrants
"""
implements(IRegistration)
...
Anmerkung: Wenn das Feld nicht image
heisst, sollten zusätzlich neue getter- und setter-Methoden zur Verfügung gestellt werden. Hierzu können Sie sich z.B. in ATContentTypes getImage
und setImage
anschauen.
IImageContent
schreiben¶Der Adapter wird implementiert in content/registration.py
:
from zope.interface import implements
...
from zope.publisher.interfaces import IPublishTraverse
from ZPublisher.BaseRequest import DefaultPublishTraverse
...
@adapter(IRegistration)
class ImageTraverser(DefaultPublishTraverse):
implements(IPublishTraverse)
def __init__(self, context, request):
self.context = context
self.request = request
def publishTraverse(self, request, name):
if name.startswith('image'):
field = self.context.getField('image')
image = None
if name == 'image':
image = field.getScale(self.context)
else:
scalename = name[len('image_'):]
if scalename in field.getAvailableSizes(self.context):
image = field.getScale(self.context, scale=scalename)
if image is not None and not isinstance(image, basestring):
# image might be None or '' for empty images
return image
else:
return super(ImageTraverser, self).publishTraverse(request, name)
Anschließend wird der Adapter konfiguriert in der Datei content/configure.zcml
:
<adapter
for="Products.ATContentTypes.interface.image.IImageContent zope.publisher.interfaces.http.IHTTPRequest"
factory=".registration.ImageTraverser"
provides="zope.publisher.interfaces.IPublishTraverse" />
Zunächst wird der View registriert in browser/configure.zcml
:
<browser:page
for="Products.ATContentTypes.interface.image.IImageContent"
class=".imagesupport.ImageView"
permission="zope2.View"
name="img_view"
allowed_interface="..interfaces.IRegistration"
/>
<browser:page
for="Products.ATContentTypes.interface.image.IImageContent"
name="fullscreen"
class=".views.FullscreenView"
permission="zope2.View"
/>
<browser:page
for="..interfaces.IRegistration"
name="view"
class=".registration.RegistrationView"
permission="zope2.View"
/>
Anschließend wird der View implementiert wobei zunächst die Datei browser/imagesupport.py
angelegt wird mit folgendem Inhalt:
from zope.interface import implements
from Products.CMFCore.utils import getToolByName
from Products.Five.browser import BrowserView
import urllib
class ImageView(BrowserView):
def __init__(self, context, request):
self.context = context
self.request = request
def tag(self, **kwargs):
""" tag """
return self.context.getField('image').tag(self.context , **kwargs)
def getImageSize(self, scale=None):
""" image size """
field = self.context.getField('image')
return field.getSize(self.context,scale=scale)
def hasImage(self):
""" image size
"""
field = self.context.getField('image')
return field.get_size(self.context)
Anschließend wird noch der FullscreenView
in browser/registration.py
angegeben:
...
class FullscreenView(BrowserView):
"""
"""
__call__ = ViewPageTemplateFile('fullscreen_view.pt')
Nun kopieren wir uns das PageTemplate parts/plone/CMFPlone/skins/plone_content/image_view_fullscreen.pt
in src/vs.registration/vs/registration/browser/
und ändern es folgendermaßen ab:
...
<div class="visualWrapper"
tal:define="img_view context/@@img_view">
<a href=""
tal:attributes="href request/HTTP_REFERER"
tal:condition="request/HTTP_REFERER">
<span i18n:translate="label_back_to_site">
Back to site
</span>
<br />
<tal:block replace="structure img_view/tag" />
</a>
<a href=""
tal:attributes="href here/portal_url"
tal:condition="not: request/HTTP_REFERER">
<span i18n:translate="label_home">
Home
</span>
<br />
<tal:block replace="structure img_view/tag" />
</a>
</div>
Schließlich ergänzen wir auch noch browser/registration.pt
:
...
<div metal:fill-slot="main">
<tal:main-macro
metal:define-macro="main"
tal:define="img_view context/@@img_view;
...
<span tal:condition="img_view/hasImage" tal:omit-tag="">
<a href=""
class="discreet"
tal:attributes="href string:$here_url/fullscreen">
<tal:block replace="structure python: img_view.tag(scale='preview')" />
<br />
<span class="visualNoPrint">
<img src="" alt="" tal:replace="structure here/search_icon.gif" />
<span i18n:translate="label_click_to_view_full_image" i18n:domain="plone">
Click to view full-size image…
</span>
</span>
</a>
</span>
Dexterity ist ein modernes Content-Type-Framework, das die schnelle Erstellung von Artikeltypen erlaubt.
Im folgenden werden wir zwei Artikeltypen erstellen:
Eine Veranstaltung, die Anmeldungen enthalten kann
Anmeldung zu einer Veranstaltung
Dabei wird in der Anleitung gezeigt:
die Erstellung von Artikeltypen durch ihre Schemadefinition
die Registrierung dieser Artikeltypen
die Erstellung von Ansichten für diese Artikeltypen
Das Ändern von Widgets für einzelne Felder
Die Eingabe von Standardwerten oder die Auswahl aus Vokabularien für bestimmte Felder
Validatoren zum Überprüfen der Feldinhalte
Das vollständige Produkt können Sie sich in unserm SVN-Repository anschauen und herunterladen: https://dev.veit-schiele.de/svn/pen/vs.registration/trunk/.
Unsere Artikeltypen werden in einem eigenständigen Paket erstellt: vs.registration
. Folgende Schritte sind hierzu notwendig:
Im /src
-Verzeichnis erstellen wir aus der ZopeSkel-Vorlage dexterity
das Grundgerüst für unser neues Produkt:
$ ../bin/zopeskel dexterity vs.registration
Dabei verwenden wir den èasy`-Modus und antworten mit True
bei der
Frage, ob wir eine GenericSetup_Profil erstellen wollen.
Dies erzeugt folgende Paketstruktur:
vs.registration
├── CHANGES.txt
├── CONTRIBUTORS.txt
├── DEXTERITY_README.rst
├── README.txt
├── bootstrap.py
├── buildout.cfg
├── docs
│ ├── LICENSE.GPL
│ └── LICENSE.txt
├── setup.cfg
├── setup.py
└── src
└── vs
├── __init__.py
└── registration
├── INTEGRATION.txt
├── __init__.py
├── configure.zcml
├── locales
│ └── README.txt
├── profiles
│ └── default
│ ├── metadata.xml
│ └── types.xml
├── static
│ └── README.txt
├── testing.py
└── tests
├── __init__.py
├── robot_test.txt
├── test_example.py
└── test_robot.py
In der generierten setup.py
-Datei sind unter install_requires
folgende Pakete eingetragen:
install_requires=[
'setuptools',
'plone.app.dexterity',
'plone.namedfile [blobs]',
Darüberhinaus werden meistens noch die folgenden Erweiterungen benötigt:
install_requires=[
...
'collective.autopermission',
'plone.app.referenceablebehavior',
'plone.app.relationfield',
],
erstellt Berechtigungen in Zope2 sobald eine <permission />
-Anweisung verwendet wird.
ermöglicht, dass unsere Dexterity-Artikeltypen von Archetypes-Referenzfeldern aus verfügbar werden.
erlaubt uns, Referenzfelder zu verwenden
erlaubt uns zusammen mit dem [blobs]
-Extra, ZODB-Blobs zum Speichern unserer Dateien und Bilder zu verwenden.
Weitere bekannte Erweiterungen sind
basierend auf plone.app.iterate bietet es Unterstützung von Staging für Dexterity-Artikeltypen. Erfordert Plone 4.1.
basierend auf Products.CMFEditions bietet Unterstützung beim Speichern von Versionen für Dexterity-Artikeltypen. Erfordert Plone 4.0
ein z3c.form
-Widget zum Editieren von Listen von Subobjects mittels eines tabellenförmigen UI.
Locking-Integration für Dexterity-Artikeltypen.
Als nächstes fügen wir in der configure.zcml
-Datei die folgenden Zeile ein:
<configure
...
xmlns:grok="http://namespaces.zope.org/grok">
<includeDependencies package="." />
<grok:grok package="." />
...
</configure>
<includeDependencies package="." />
schließt die Konfiguration für die in der
setup.py
-Datei aufgelisteten Abhängigkeiten ein.<grok:grok package="." />
ermöglicht Grok, automatisch Schema-Interfaces und
content
-Klassen zu initialisieren.
Als nächstes ändern wir die buildout.cfg
-Datei um Dexteritys Known Good Set of Versions hinzuzufügen:
[buildout]
extensions =
mr.developer
...
extends =
...
http://good-py.appspot.com/release/dexterity/1.0rc1?plone=4.1
versions.cfg
...
[test]
recipe = zc.recipe.testrunner
eggs =
vs.registration
defaults = ['--exit-with-status', '--auto-color', '--auto-progress']
[sources]
...
vs.registration = svn https://dev.plone.org/svn/pen/vs.registrtion/trunk
Nun können wir das vs.registration
-Paket auch als Abhängigkeit in unser vs.policy
-Produkt eintragen. Zunächst wird vs.registration
in die Liste ìnstall_requires` in src/vs.policy/vs/policy/setup.py
eingetragen:
install_requires=[
'setuptools',
'Plone',
'vs.event',
'vs.theme',
'vs.registration',
],
Schließlich editieren wir auch noch profiles/default/metadata.xml
im selben Paket:
<dependencies>
<dependency>profile-vs.event:default</dependency>
<dependency>profile-vs.theme:default</dependency>
<dependency>profile-vs.registration:default</dependency>
</dependencies>
Für unsere Registrierungssoftware erstellen wir zunächst den Artikeltyp Attendee in attendee.py
:
from five import grok
from zope import schema
from plone.directives import form, dexterity
from plone.app.textfield import RichText
from plone.namedfile.field import NamedImage
from vs.registration import _
class IAttendee(form.Schema):
"""An attendee for the event.
"""
title = schema.TextLine(
title=_(u"Name"),
)
description = schema.Text(
title=_(u"A short summary"),
)
from vs.registration import _
importiert die Message Factory aus der __init__.py
-Datei:
from zope.i18nmessageid import MessageFactory
_ = MessageFactory("vs.registration")
Die Ereignisse, für die die Anmeldungen erfolgen können, werden in registration.py
definiert:
from five import grok
from zope import schema
from plone.directives import form, dexterity
from plone.app.textfield import RichText
from vs.registration import _
class IRegistration(form.Schema):
"""A registration container for attendees.
"""
title = schema.TextLine(
title=_(u"Event name"),
)
description = schema.Text(
title=_(u"Event summary"),
)
start = schema.Datetime(
title=_(u"Start date"),
required=False,
)
end = schema.Datetime(
title=_(u"End date"),
required=False,
)
details = RichText(
title=_(u"Details"),
description=_(u"Details about the event"),
required=False,
)
Um die Factory Type Informations hinzuzufügen, erstellen wir in profiles/default
das Profil types.xml
:
<?xml version="1.0"?>
<object name="portal_types">
<object name="vs.registration.registration" meta_type="Dexterity FTI" />
<object name="vs.registration.attendee" meta_type="Dexterity FTI" />
</object>
Um Konflikte zu vermeiden, wird der Paketname vorangestellt.
Als meta_type
muss Dexterity FTI
verwendet werden.
Anschließend wird der Ordner profiles/default/types
erstellt, in dem wir die Profile unserer beiden Artikeltypen erstellen, zunächst vs.reigistration.attendee.xml:
<?xml version="1.0"?>
<object name="vs.registrtion.attendee" meta_type="Dexterity FTI"
i18n:domain="vs.registration" xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<!-- Basic metadata -->
<property name="title" i18n:translate="">Attendee</property>
<property name="description" i18n:translate="">An attendee</property>
<property name="content_icon">user.gif</property>
<property name="allow_discussion">True</property>
<property name="global_allow">False</property>
<property name="filter_content_types">False</property>
<property name="allowed_content_types" />
<!-- schema interface -->
<property name="schema">vs.registration.attendee.IAttendee</property>
<!-- class used for content items -->
<property name="klass">plone.dexterity.content.Item</property>
<!-- add permission -->
<property name="add_permission">cmf.AddPortalContent</property>
<!-- enabled behaviors -->
<property name="behaviors">
<element value="plone.app.content.interfaces.INameFromTitle" />
</property>
<!-- View information -->
<property name="default_view">view</property>
<property name="default_view_fallback">False</property>
<property name="view_methods">
<element value="view"/>
</property>
<!-- Method aliases -->
<alias from="(Default)" to="(dynamic view)"/>
<alias from="edit" to="@@edit"/>
<alias from="sharing" to="@@sharing"/>
<alias from="view" to="(selected layout)"/>
<!-- Actions -->
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}" visible="True">
<permission value="View"/>
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content"/>
</action>
</object>
Anschließend erstellen wir noch registration.xml
:
<?xml version="1.0"?>
<object name="vs.registration.registration" meta_type="Dexterity FTI"
i18n:domain="vs.registration" xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<!-- Basic metadata -->
<property name="title" i18n:translate="">Registration</property>
<property name="description" i18n:translate="">A folderish content type for attendees</property>
<property name="content_icon">folder_icon.gif</property>
<property name="allow_discussion">False</property>
<property name="global_allow">True</property>
<property name="filter_content_types">True</property>
<property name="allowed_content_types">
<element value="vs.registration.attendee" />
</property>
<!-- schema interface -->
<property name="schema">vs.registration.registration.IRegistration</property>
<!-- class used for content items -->
<property name="klass">plone.dexterity.content.Container</property>
<!-- add permission -->
<property name="add_permission">cmf.AddPortalContent</property>
<!-- enabled behaviors -->
<property name="behaviors">
<element value="plone.app.content.interfaces.INameFromTitle" />
</property>
<!-- View information -->
<property name="default_view">view</property>
<property name="default_view_fallback">False</property>
<property name="view_methods">
<element value="view"/>
</property>
<!-- Method aliases -->
<alias from="(Default)" to="(dynamic view)"/>
<alias from="edit" to="@@edit"/>
<alias from="sharing" to="@@sharing"/>
<alias from="view" to="(selected layout)"/>
<!-- Actions -->
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}" visible="True">
<permission value="View"/>
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content"/>
</action>
</object>
Schließlich können Sie Ihre Instanz starten mit:
$ ./bin/instance fg
Beim Erstellen einer neuen Plone-Site wählen Sie das Profil vs.registration
. Und nachdem die Site erstellt wurde, sollten Sie Registration im hinzufügen-Menü finden. Schließlich sollten Sie in dem Artikel vom Typ Registration einen Attendee hinzufügen können.
Üblicherweise werden die benötigten Views zum Editieren und Ansehen der Artikel automatisch generiert. Falls diese Ansichten geändert werden sollen kann dies mittels five.grok
einfach geschehen, indem z.B. in registration.py
ein View erstellt werden mit:
from Acquisition import aq_inner
from Products.CMFCore.utils import getToolByName
from vs.registration.attendee import IAttendee
class View(grok.View):
grok.context(IRegistration)
grok.require('zope2.View')
def attendees(self):
"""Return a catalog search result of attendees to show
"""
context = aq_inner(self.context)
catalog = getToolByName(context, 'portal_catalog')
return catalog(object_provides=IAttendee.__identifier__,
path='/'.join(context.getPhysicalPath()),
sort_on='sortable_title')
class View
Der Klassenname wird umgewandelt in Kleinbuchstaben und biltet dann den Namen des Views, in unserem Fall @@view
.
Ggf. kann mit grok.name('other-name')
ein anderer Klassenname vergeben werden.
grok.context()
Diese Anweisung spezifiziert, dass der View in Artikeln mit dem ÌRegistration
-Interface verwendet wird.
Soll zusätzlich ein Browser-Layer spezifiziert werden, so kann dies mit der grok.layer()
-Anweisung erfolgen.
grok.require()
Diese Anweisung spezifiziert die erforderliche Berechtigung zur Anzeige dieses Views.
Es wird der Zope3-Deklaration verwendet.
zope2.View
und zope.Public
sind die am häufigsten verwendeten Berechtigungen. Eine Liste weiterer Berechtigungen finden Sie z.B. in parts/omelette/Products/Five/permissions.zcml
.
Anschließend kann auf derselben Ebene wie registration.py
ein Ordner registration_templates
und darin die Datei view.pt
erstellt werden:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="vs.registration">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main"
tal:define="toLocalizedTime nocall:context/@@plone/toLocalizedTime">
<div tal:replace="structure provider:plone.abovecontenttitle" />
<h1 class="documentFirstHeading" tal:content="context/title" />
<div class="discreet">
<tal:block condition="context/start">
<span i18n:translate="label_from">From:</span>
<span tal:content="python:context.start.strftime('%x %X')" />
</tal:block>
<tal:block condition="context/end">
<span i18n:translate="label_to">To:</span>
<span tal:content="python:context.end.strftime('%x %X')" />
</tal:block>
</div>
<div tal:replace="structure provider:plone.belowcontenttitle" />
<p class="documentDescription" tal:content="context/description" />
<div tal:replace="structure provider:plone.abovecontentbody" />
<h2 i18n:translate="heading_attendees">Attendees</h2>
<dl>
<tal:block repeat="attendee view/attendees">
<dt>
<a tal:attributes="href attendee/getURL"
tal:content="attendee/Title" />
</dt>
<dd tal:content="attendee/Description" />
</tal:block>
</dl>
<div tal:replace="structure provider:plone.belowcontentbody" />
</tal:main-macro>
</metal:main>
</body>
</html>
Bemerkung
Damit im Feld mit dem visuellen Editor die HTML-Tags nicht ausgefiltert weren, muss hier die Angabe /output
angegeben werden.
Falls die übliche Verwendung von Widgets nicht ausreicht, können auch spezifische Widgets verwendet werden, die eine größere Kontrolle über die Ausgabe erlauben.
Als Beispiel verwenden wir die View
-Klasse aus Views, die wir jedoch diesmal von plone.directives.dexterity.DisplayForm
ableiten:
class View(dexterity.DisplayForm):
grok.context(ISession)
grok.require('zope2.View')
Hierdurch erhalten wir einige zusätzliche Eigenschaften, die wir in unserem Template verwenden können:
view.w
ist ein Dictionary aller Display-Widgets.
Als Schlüssel für diese Widgets wird der Feldname verwendet oder, sofern das Feld aus einem Behavior kommt, wird dem Feldnamen das Interface dieses Behavior vorangestellt
view.widgets
enthalten eine Liste von Widgets in der Reihenfolge des Standard-Fieldset
view.groups
enthält eine Liste von Fieldsets
view.fieldsets
enthält ein Distionary, das Fieldset-Namen Fieldsets zuweist.
Auf einen Fieldset (group) können alle dort verfügbaren Widgets aufgelistet werden.
In project_templates/view.pt
kann dann z.B.:
<div tal:content="structure context/details/output" />
ersetzt werden durch:
<div tal:content="structure view/w/details/render" />
Oft vereinfacht es die Bedienung deutlich, wenn in Feldern Standardwerte eingetragen werden. Diese Werte werden im Hinzufügen-Formular gesetzt.
In unserem Beispiel sollen die Standardwerte für den Beginn und das Ende einer Veranstaltung eine Woche in der Zukunft liegen. Hierzu fügen wir in registration.py
folgendes hinzu:
import datetime
...
@form.default_value(field=IRegistration['start'])
def startDefaultValue(data):
# To get hold of the folder, do: context = data.context
return datetime.datetime.today() + datetime.timedelta(7)
@form.default_value(field=IRegistration['end'])
def endDefaultValue(data):
# To get hold of the folder, do: context = data.context
return datetime.datetime.today() + datetime.timedelta(10)
Der Decorator kann ein oder mehrere Unterscheidungskriterien haben. Folgende Unterscheidungskriterien sind möglich:
context
Der Kontexttyp, z.B. ein Interface
request
Der Request-Tp, z.B. ein Layer Marker Interface
view
Der Formulartyp, z.B. eine Formularinstanz oder ein Interface.
field
Die Feld-Instanz oder das Interface eines Feldes
Neben dem default_value
-Decorator gibt es noch zwei weitere Decorators:
widget
Der Widget-Typ, z.B. ein Interface
widget_label
bietet ein dynamische Label für Widgets wobei es dieselben Unterscheidungskriterien zulässt wie default_value
.
button_label
bietet dynamische Label für Tasten mit den Unterscheidungskriterien content, ``request
, form
, manager
und button
.
In der Dokumentation zu plone.directives.form finden Sie weitere Informationen hierzu.
Vokabularien werden üblicherweise zusammen mit Auswahlfeldern verwendet. Um nur eine Auswahl zuzulassen, kann das Choise
-Feld direkt verwendet werden:
class IMySchema(form.Schema):
myChoice = schema.Choice(...)
Für Multiple-Choice-Felder können List
, Tuple
, Set
oder Frozenset
mit value_type=schema.Choice
verwendet werden, also z.B.:
class IMySchema(form.Schema):
myList = schema.List(
title=u"My list",
value_type=schema.Choice(values=['red', 'green', 'blue', 'yellow']))
Ein Choice
-Feld kann eines der folgenden Argumente erhalten:
Werte aus einer Liste statischer Werte
Werte aus einer Quelle, die mit IContextSourceBinder
oder einer ISource
-Intanz angegeben werden
Werte können aus einem Vokabular stammen, das als ÌVocabulary
-Instanz oder als Name eines IVocabularyFactory
-Utility angegeben wird
term
Eintrag in ein Vokabular
token
ASCII-Zeichenkette, die beim Abschicken eines Formulars übermittelt wird um den Term eindeutig zu identifizieren.
value
Der aktuelle Wert, der in einem Objekt gespeichert wird
title
Übersetzbare Unicode-Zeichenkette
In Plone werden Ihnen bereits eine ganze Reihe von Vokabularien in plone.app.vocabularies
zur Verfügung gestellt. Die gebräuchlichsten sind:
plone.app.vocabularies.AvailableContentLanguages
Eine Liste aller verfügbaren Sprachen
plone.app.vocabularies.SupportedContentLanguages
Eine Liste aller aktuell unterstützten Sprachen
plone.app.vocabularies.Roles
Die in der Site verfügbaren Rollen
plone.app.vocabularies.PortalTypes
Eine Liste der im Portal Types Tool registrierten Artikeltypen
plone.app.vocabularies.ReallyUserFriendlyTypes
Eine Liste derjenigen Artikeltypen, die für Nutzer von Bedeutung sind
plone.app.vocabularies.Workflows
Eine Liste aller Arbeitsabläufe
plone.app.vocabularies.WorkflowStates
Eine Liste aller Arbeitsablaufstadien
plone.app.vocabularies.WorkflowTransitions
Eine Liste aller Übergänge zwischen Arbeitsablaufstadien
Mit plone.principalsource
steht uns ein weiteres Paket mit verschiedenen Vokabularien bereit, das zur Auswahl von Nutzern und Gruppen hilfreich ist:
plone.principalsource.Users
Eine Liste aller Nutzer
plone.principalsource.Groups
Eine Liste aller Gruppen
plone.principalsource.Principals
Eine Liste aller Berechtigungen für Nutzer und Gruppen
Hier ein Beispiel für ein statisches Vokabular:
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
organisers = SimpleVocabulary(
[SimpleTerm(value=u'vsc', title=_(u'Veit Schiele Communications')),
SimpleTerm(value=u'zopyx', title=_(u'Zopyx Limited'))]
)
organiser = schema.Choice(
title=_(u"Organiser"),
vocabulary=organisers,
required=False,
)
Statische Vokabularien sind in zweierlei Hinsicht beschränkt: zum einen sind sie hartkodiert in Python, zum anderen werden die gespeicherten Werte nicht getrennt von den Labels gespeichert.
Ein dynamische Vokabular kann nun erzeugt werden indem ein sog. Context Source Binder verwendet wird. Ein solcher kann einfach aufgerufen werden durch eine Funktion oder ein Objekt mit einer __call__
-Methode, die das IContextSourceBinder
-Interface zusammen mit einem Kontext-Argument bereitstellt. Der Aufruf soll ein Vokabular ausgeben, das am einfachsten zu bekommen ist, wenn die SimpleVocabulary
-Klasse aus zope.schema
verwendet wird.
Im folgenden nun ein Beispiel für eine Funktion, die alle Nutzer einer bestimmten Gruppe zurückgibt:
from zope.schema.interfaces import IContextSourceBinder
from zope.schema.vocabulary import SimpleVocabulary
from Products.CMFCore.utils import getToolByName
@grok.provider(IContextSourceBinder)
def possibleOrganisers(context):
acl_users = getToolByName(context, 'acl_users')
group = acl_users.getGroupById('organisers')
terms = []
if group is not None:
for member_id in group.getMemberIds():
user = acl_users.getUserById(member_id)
if user is not None:
member_name = user.getProperty('fullname') or member_id
terms.append(SimpleVocabulary.createTerm(member_id, str(member_id), member_name))
return SimpleVocabulary(terms)
Das obige Beispiel kann erweitert werden indem der Gruppenname aus der Funktion herausgenommen wird und sich dann für jedes Feld unabhängig setzen lässt. Hierfür wird dann IContextSourceBinder
in eine eigene Klasse ausgelagert, die mit dem Gruppennamen initialisiert wird:
class GroupMembers(object):
"""Context source binder to provide a vocabulary of users in a given
group.
"""
grok.implements(IContextSourceBinder)
def __init__(self, group_name):
self.group_name = group_name
def __call__(self, context):
acl_users = getToolByName(context, 'acl_users')
group = acl_users.getGroupById(self.group_name)
terms = []
if group is not None:
for member_id in group.getMemberIds():
user = acl_users.getUserById(member_id)
if user is not None:
member_name = user.getProperty('fullname') or member_id
terms.append(SimpleVocabulary.createTerm(member_id, str(member_id), member_name))
return SimpleVocabulary(terms)
Sollen Vokabularien nicht nur im Kontext verfügbar sein sondern als Komponenten, werden sog. Named Vocabularies erstellt. Diese werden als named utilities registriert werden und sind anschließend in einem Schema mit ihrem Namen referenziert werden. Damit lassen sich Vokabularien in eigenständigen Paketen erstellen.
collective.vdexvocabulary erlaubt die Verwendung von IMS VDEX-Vokabularien und bietet darüberhinaus noch weitere Vorteile wie:
i18n
-Unterstützung, wie sie im IMS VDEX-Standard definiert ist.
Unterstützung für Sortierung auch von Unicode-Zeichen. sofern zope.ucol installiert ist
Einfache Registrierung mit zcml
Relationen wie sie im IMS VDEX-Standard spezifiziert sind
collective.elephantvocabulary
¶collective.elephantvocabulary ist ein Wrapper für zope.schema
-Vokabularien wodurch diese keinen ihre Einträge mehr vergessen.
plone.formwidget.autocomplete
erweitert z3c.formwidget.query
um ein nutzerfreundlicheres Interface für Felder bereitzustellen, bei dem nach der Eingabe von wenigen Zeichen bereits die möglichen Werte angezeigt werden.
Das Widget wird bereits mit plone.app.dexterity
mitgeliefert, sodass wir es einfach z.B. in registration.py
verwenden können, mit:
form.widget(organiser=AutocompleteFieldWidget)
organiser = schema.Choice(
title=_(u"Organiser"),
vocabulary=u"plone.principalsource.Users",
required=False,
)
In unserem speziellen Fall wollen wir aus den zwei Feldern Vorname und Nachname den Titel unseres Employee-Artikeltyps erstellen lassen.
Hierzu erhält die Klasse Employee
die Eigenschaft title
, die sich aus den Feldern first_name
und surname
zusammensetzt:
class Employee(Item):
"""Customised Employee content class"""
@property
def title(self):
if hasattr(self, 'first_names') and hasattr(self, 'surname'):
return self.first_name + ' ' + self.surname
else:
return ''
Etwas komplexer ist das Erstellen der ID aus einem berechneten Titel:
from plone.app.content.interfaces import INameFromTitle
class INameFromEmployeeTitle(INameFromTitle):
def title():
"""Return a processed title"""
class NameFromEmployeeTitle(object):
implements(INameFromEmployeeTitle)
def __init__(self, context):
self.context = context
@property
def title(self):
return self.context.first_name + ' ' + self.context.surname
def setTitle(self, value):
return
Nun registrieren wir noch einen Adapter für die Dexterity-Interface-Klasse:
<adapter
for="vs.registration.employee.ISampleEmployee"
provides="vs.registration.employee.INameFromEmployeeTitle"
factory="vs.registration.employee.NameFromEmployeeTitle"
/>
Die einfachste Form der Validierung liefert bereits die z3c.form
-Bibliothek indem sie überprüft, ob die Eingaben dem Feldtyp entsprechen. Darüberhinaus können für jedes Feld die folgenden Eigenschaften angegeben werden, mit denen der Inhalt des Feldes überprüft wird:
required
Als Werte sind hier True
und False
zulässig um den Wert eines Feldes
als erforderlich oder optional zu kennzeichnen.
min
und max
wird verwendet für Felder vom Typ
Int
Float
Datetime
Date
Timedelta
Hiermit lassen sich die zulässigen Mindest- und Höchstwerte definieren.
min_length
und max_length
kann für folgende Felder angegeben werden:
Kollektionen
Tuple
List
Set
Frozenset
Dict
Textfelder
Bytes
BytesLine
ASCII
ASCIILine
Text
TextLine
Eine Constraint-Funktion sollte als Argument den Wert des Feldes erhalten und als Ergebnis True
oder False
liefern. Dabei sind Constraints zwar einfach zu schreiben, sie geben jedoch selten eine nutzerfreundliche Fehlermeldung aus. Mit z3c.form
-Error-View-Snippets lassen sich diese Fehlermeldungen jedoch anpassen. Weitere Informationen erhalten Sie in Customizing Error Messages.
Während Constraints immer nur ein einzelnes Feld überprüfen können, lassen sich mit Invarianten mehrere Felder abgleichen.
Ein Beispiel hierfür ist der Vergleich von Start- und Enddatum:
from zope.interface import invariant, Invalid
class StartBeforeEnd(Invalid):
__doc__ = _(u"The start or end date is invalid")
class IRegistration(form.Schema):
…
start = schema.Datetime(
title=_(u"Start date"),
required=False,
)
end = schema.Datetime(
title=_(u"End date"),
required=False,
)
…
@invariant
def validateStartEnd(data):
if data.start is not None and data.end is not None:
if data.start > data.end:
raise StartBeforeEnd(_(u"The start date must be before the end date."))
class StartBeforeEnd(Invalid):
__doc__ = _(u"The start or end date is invalid")
Mächtigere Validatoren können mit den z3c.form
-Widget-Validatoren geschrieben werden. Weitere Informationen hierzu erhalten Sie in der z3c.form-Dokumentation.
In Plone kann für viele verschiedene Nutzungsfälle eine Zuordnung zu Rollen vorgenommen werden, die zur Ausführung berechtigt sind.
Sie können sich diese Zuordnungstabelle anschauen, indem Sie im ZMI auf den Security-Reiter klicken:
In der Tabelle können für ein bestimmtes Objekt die verschiedenen Berechtigungen den einzelnen Rollen zugewiesen werden. Beachten Sie bitte, dass für die meisten Berechtigungen Acquire permission settings? angeklickt ist und damit die Berechtigungen vom übergeordneten Objekt übernommen werden. Weitere Rollen können dann einfach durch Anklicken hinzugefügt werden.
Plone 4 kommt mit Zope 2.12 und der damit gegebenen Möglichkeit, die Berechtigungen eines bestimmten Nutzers im Kontext anzuzeigen:
Anmerkung 1: Sie sollten niemals die Berechtigungen außerhalb des Wurzelverzeichnisses der Site ändern, da ansonsten kaum noch zu kontrollieren ist, welche Berechtigungen wo gesetzt sind.
Anmerkung 2: Im Quellcode sollte immer die Berechtigung (permission) und nie die Rolle (role) überprüft werden.
Die am häufigsten verwendeten Berechtigungen sind:
CMFCore Permission |
Five Permission |
Beschreibung |
---|---|---|
|
|
Niedrigschwellige Zope-Berechtigung, die den Zugang zu Objekten kontrolliert |
|
|
Zugang zu der Hauptansicht eines Inhaltsobjekts |
|
|
Berechtigung, die Verzeichnisübersicht zu sehen |
|
|
Editiermöglichkeit für die Inhalte |
|
|
Operationen, die typischerweise der Manager-Rolle zugewiesen werden |
|
|
Möglichkeit, neue Inhalte hinzuzufügen. Darüberhinaus haben viele Artikeltypen ihre eigenen Berechtigungen zum Hinzufügen von Inhalten, so dass beide Berechtigungen benötigt werden |
Eine vollständige Übersicht über alle CMFCore
-Berechtigungen erhalten Sie in Products.CMFCore.permissions
und über alle Five
-Berechtigungen in Products.Five.permissions.zcml
.
Diese Berechtigungen können in ZCML-Anweisungen angegeben werden, z.B. in src/vs.registration/vs/registration/browser/configure.zcml
:
<browser:page
for="..interfaces.IRegistration"
name="view"
class=".registration.RegistrationView"
permission="zope2.View"
/>
Obwohl die Standard-Berechtigungen verwendet werden sollten, um grundlegende Aktionen (anzeigen, ändern, löschen, schreiben) zu steuern, ist es manchmal sinnvoll, neue Berechtigungen erstellen. Kombiniert mit benutzerdefinierten Workflows können benutzerdefinierte Berechtigungen verwendet werden, um angepasste Anwendungen zu erstellen.
Die Berechtigungen können dann einfach in der configure.zcml
-Datei erstellt
werden:
<permission
id="vs.registration.AddRegistrant"
title="vs.registration: Add registrant"
/>
<permission
id="vs.registration.ModifyRegistrant"
title="vs.registration: Modify registrant"
/>
In Plone < 4.0 oder genauer in Zope < 2.12 muss zusätzlich
collective.autopermission installiert werden.
Hierfür wird in der setup.py
-Datei folgendes eingetragen:
install_requires=[
...
'collective.autopermission',
],
Neue Berechtigungen werden üblicherweise nur der Manager-Rolle übertragen. Um
anderen Rollen diese Berechtigungen ebenfalls zu übertragen, kann das Profil
rolemap.xml
erstellt werden:
<?xml version="1.0"?>
<rolemap>
<permissions>
<permission name="vs.registration: Add registrant" acquire="True">
<role name="Owner"/>
<role name="Manager"/>
<role name="Member"/>
<role name="Contributor"/>
</permission>
<permission name="vs.registration: Modify registrant" acquire="True">
<role name="Manager"/>
<role name="Reviewer"/>
</permission>
</permissions>
</rolemap>
Plone 4 ermöglicht mit plone.app.users neue Felder zum Registrieren an der Website und den persönlichen Einstellungen hinzuzufügen.
Das Standardschema von plone.app.users
ist in einem Hilfsprogramm (Utility) definiert, das überschrieben werden muss um ein neues Schema anzulegen. Dies geschieht in der Datei profiles/default/componentregistry.xml
:
<?xml version="1.0"?>
<componentregistry>
<utilities>
<utility
interface="plone.app.users.userdataschema.IUserDataSchemaProvider"
factory="vs.policy.userdataschema.UserDataSchemaProvider"
/>
</utilities>
</componentregistry>
Nun wird die factory in userdataschema.py
erstellt:
from plone.app.users.userdataschema import IUserDataSchemaProvider
class UserDataSchemaProvider(object):
implements(IUserDataSchemaProvider)
def getSchema(self):
"""
"""
return IEnhancedUserDataSchema
Schließlich wird eine Unterklasse des Standardschemas IUserDataSchema
erstellt:
from plone.app.users.userdataschema import IUserDataSchema
class IEnhancedUserDataSchema(IUserDataSchema):
""" Use all the fields from the default user data schema, and add various
extra fields.
"""
Nun können neue Felder definiert werden, z.B.:
newsletter = schema.Bool(
title=_(u'label_newsletter', default=u'Subscribe to newsletter'),
description=_(u'help_newsletter',
default=u"If you tick this box, we'll subscribe you to "
"our newsletter."),
required=False,
)
Felder mit Bedingungen für eine erfolgreiche Anmeldung können als constraint
angegeben werden, z.B.:
def validateAccept(value):
if not value == True:
return False
return True
class IEnhancedUserDataSchema(IUserDataSchema):
# ...
accept = schema.Bool(
title=_(u'label_accept', default=u'Accept terms of use'),
description=_(u'help_accept',
default=u"Tick this box to indicate that you have found,"
" read and accepted the terms of use for this site. "),
required=True,
constraint=validateAccept,
)
Neue Eigenschaften lassen sich in den memberdata properties speichern indem eine Datei memberdata_properties.xml
in profiles/default/
erstellt wird. Dabei werden alle Felder hinzugefügt bis auf das accept
-Feld, das zwingend für die Registrierung erforderlich ist:
<?xml version="1.0"?>
<object name="portal_memberdata" meta_type="Plone Memberdata Tool">
<property name="subscribe_newsletter" type="boolean"></property>
</property>
</object>
Die Felder für die Registrierung werden in profiles/default/propertiestool.xml
angegeben:
<?xml version="1.0"?>
<object name="portal_properties" meta_type="Plone Properties Tool">
<object name="site_properties" meta_type="Plone Property Sheet">
<property name="user_registration_fields" type="lines">
<element value="newsletter" />
<element value="accept" />
</property>
</object>
</object>
Um die Felder auch im Formular mit den persönlichen Informationen @@personal-information
zu sehen, wird zunächst der Adapter des Nutzerobjekts in der overrides.zcml
überschrieben:
<configure
xmlns="http://namespaces.zope.org/zope"
i18n_domain="vs.policy.userdata">
<adapter
provides=".userdataschema.IEnhancedUserDataSchema"
for="Products.CMFCore.interfaces.ISiteRoot"
factory=".adapter.EnhancedUserDataPanelAdapter"
/>
</configure>
Anschließend müssen leider die Felder nochmals angegeben werden. Hierzu fügen wir adapter.py
hinzu mit:
from plone.app.users.browser.personalpreferences import UserDataPanelAdapter
class EnhancedUserDataPanelAdapter(UserDataPanelAdapter):
"""
"""
def get_newsletter(self):
return self.context.getProperty('newsletter', '')
def set_newsletter(self, value):
return self.context.setMemberProperties({'newsletter': value})
newsletter = property(get_newsletter, set_newsletter)
def get_accept(self):
return self.context.getProperty('accept', '')
def set_accept(self, value):
return self.context.setMemberProperties({'accept': value})
accept = property(get_accept, set_accept)
Getting logged-in member, any member and member information
Python-Egg mit Beispielen, wie das Schema der Nutzerdaten erweitert werden kann.
Um neue Gruppen programmatisch hinzuzufügen, erstellen wir in der
setuphandlers.py
-Datei die Methode setupGroups
, die sowohl den Pluggable
Authentication Service (PAS) als auch das portal_groups
-Tool verwendet:
import plone.api
def setupGroups(portal):
acl_users = plone.api.portal.get_tool('acl_users')
if not acl_users.searchGroups(name='Staff'):
gtool = getToolByName(portal, 'portal_groups')
gtool.addGroup('Staff', roles=['StaffMember'])
def importVarious(context):
"""Miscellanous steps import handle
"""
if context.readDataFile('vs.policy-various.txt') is None:
return
portal = context.getSite()
setupGroups(portal)
Für Plone ≤ 4.2 steht plone.api noch
nicht zur Verfügung. Stattdessen muss noch getToolByName
verwendet werden:
from Products.CMFCore.utils import getToolByName
def setupGroups(portal):
acl_users = getToolByName(portal, 'acl_users')
if not acl_users.searchGroups(name='Staff'):
gtool = getToolByName(portal, 'portal_groups')
gtool.addGroup('Staff', roles=['StaffMember'])
…
def test_staffmember_group_added(self):
portal = self.layer['portal']
acl_users = portal['acl_users']
self.assertEqual(1,
len(acl_users.searchGroups(name='Staff')))
Rollen können sowohl Nutzern als auch Gruppen zugewiesen werden. Dabei empfiehlt sich im Allgemeinen, Rollen zu erstellen, denen bestimmte Rechte zugewiesen werden, anstatt jedem Nutzer die jeweiligen Rechte zuzuweisen.
So haben z.B. die Gruppen Reviewers und Administrators die entsprechenden Manager- oder Reviewer-Rollen.
Die globale Zuordnung von Nutzern und Gruppen zu einer Rolle können Sie in Plone Konfiguration → Benutzer und Gruppen vornehmen.
Die lokale Zuweisung von Rollen erfolgt im Allgemeinen im Zugriff-Reiter eines Inhaltsobjekts. Dort suchen Sie zunächst nach einem Nutzer oder einer Gruppe, um in den Suchergebnissen anschließend Zuweisungen zu bestimmten Rechten vornehmen zu können. Die lokal verfügbaren Rollen sind beschränkt auf die explizit angegebenen (s.a. plone/app/workflow/localroles.py
und plone/app/workflow/configure.zcml
).
Plone selbst kommt mit sieben verschiedenen Rollen:
Member
ist die Standardrolle für einen angemeldeten Nutzer, der nur wenige Rechte zugewiesen sind.
Manager
ist die super-user-Rolle; sie ist der Administrators-Gruppe zugeordnet.
Reviewer
erlaubt Inhabern der Rolle, Artikel zu sehen und zu bestätigen, deren Inhalte zur Veröffentlichung vorgeschlagen wurden.
Reader
ist lediglich als lokale Rolle vorgesehen, wenn Member
die Inhalte nicht sehen können.
Editor
ist das Pendant zu Reader
, um Eigentümern die Möglichkeit zu geben, lokal Schreibrechte zu erteilen.
Contributor
ermöglicht einem Eigentümer, das Hinzufügen-Recht in einem Ordner an andere zu übertragen. Es erscheint im Zugriff-Reiter unter Kann hinzufügen.
Site Administrator
hat Management-Rechte bis auf
die Verwendung des Wartung-Kontrollfeldes
den Zugang zum Zope Management Interface (ZMI)
das Hinzufügen und Entfernen von Produkten
die Änderung des Aussehens
die Änderung der Cache-Konfiguration
Darüberhinaus definiert Zope noch drei automatisch zugewiesene Rollen:
Owner
wird normalerweise dem Nutzer zugeordnet, der das Inhaltsobjekt erstellte.
Authenticated
wird angemeldeten Nutzern zugewiesen. Diese Rolle ist niedrigschwelliger als Member
und kann nicht explizit zugewiesen werden.
Anonymous
ist die Rolle für nicht-angemeldete Nutzer.
Die meisten niedrigschwelligen Sicherheitsmethoden werden in der AccessControl.Role.RoleManager
-Klasse definiert. Diese ist in allen Inhaltsobjekten verfügbar einschließlich des Plone Site-Objekts selbst. Sie können sich diese Methoden im Doc-Reiter im Wurzelverzeichnis Ihrer Plone-Site anschauen:
Darüberhinaus empfiehlt sich auch ein Blick in den Doc-Reiter des PlonePAS Membership Tool.
Und hier noch einige der am häufigsten verwendeten programmatischen Änderungen von Rechten und Rollen:
from AccessControl import getSecurityManager
from Products.CMFCore.permissions import ModifyPortalContent
sm = getSecurityManager()
if sm.checkPermission(ModifyPortalContent, context):
# do something
context.manage_permission("Portlets: Manage portlets",
roles=['Manager', 'Owner'], acquire=1)
from Products.CMFCore.utils import getToolByName
mtool = getToolByName(context, 'portal_membership')
if mtool.isAnonymousUser():
# do something
member = mtool.getAuthenticatedMember()
user_id = member.getId()
admin_user = mtool.getMemberById('admin')
Seit Plone 4.0 lassen sich Rollen mit dem GenericSetup-Profil sharing.xml
für die Freigabe-Ansicht konfigurieren.
Im folgenden Beispiel wird die Rolle Event-Manager erstellt:
<?xml version="1.0"?>
<sharing xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="vs.policy">
<role
id="Event-Manager"
title="Manages the registrations for events"
permission="Add Registration"
i18n:attributes="title"
/>
</sharing>
Soll z.B. die Rolle Reviewer an die Berechtigung Modify portal content geknüpft werden, so kann dies mit folgendem Eintrag geschehen:
<role
id="Reviewer"
title="Review submitted articles"
permission="Modify portal content"
i18n:attributes="title"
/>
<role
remove="True"
id="Reviewer"
/>
Um eine Rolle der Tabelle im Freigabe-Reiter hinzuzufügen kann einfach
ein entsprechendes GenericSetup-Profil sharing.xml
erstellt werden,
z.B.:
<sharing xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
<role
id="Site Manager"
title="Is a site coordinator"
permission="Manage portal"
i18n:attributes="title"
/>
</sharing>
The title is the name to be shown on the sharing page. The required_permission is optional. If given, the user must have this permission to be allowed to manage the particular role.
Zunächst wird in der permissions.py
-Datei die Rolle für die Ansicht im Freigabe-Reiter registriert und die Rolle angegeben, auf der unsere neue Rolle basieren soll:
from Products.CMFCore.permissions import setDefaultRoles
from AccessControl import ModuleSecurityInfo
security = ModuleSecurityInfo('vs.policy')
security.declarePublic('MyRole')
MyRole = 'Sharing page: My Role'
setDefaultRoles(MyRole, ('Reviewer',))
Dann wird die Rolle für die Freigabe-Seiten registriert in localroles.py
:
from zope.interface import implements
from plone.app.workflow.interfaces import ISharingPageRole
import permissions
class ManagerRole(object):
implements(ISharingPageRole)
title = u'My Role'
required_permission = permissions.MyRole
Schließlich wird in der configure.zcml
-Datei die entsprechende Berechtigung angelegt:
<permission
id="plone.MyRole"
title="Sharing page: My Role"
/>
Arbeitsabläufe (Workflows) erlauben, lokal die Rechte für Inhaltsobjekte zu verändern, ohne die Rechte für jedes dieser Objekte einzeln ändern zu müssen. Damit bleibt die Rechteverwaltung übersichtlich.
Die Arbeitsabläufe selbst werden für eine Site zentral im Workflow Tool (portal_workflow
) verwaltet. Zunächst werden Sie den Workflows-Reiter sehen, in dem den verschiedenen Artikeltypen Arbeitsabläufe zugeordnet werden. Die Definitionen der Arbeitsabläufe lassen sich im Contents-Reiter anschauen. Jeder der Arbeitsabläufe besteht aus verschiedenen Stadien (states), wie z.B. private oder published, und Übergängen (transitions) zwischen ihnen.
Mit collective.workflowed gibt es einen Javascript-basierten graphischen Editor für Arbeitsabläufe, der in Plones Website-Konfiguration aufgerufen werden kann. Die dort editierten Workflows können anschließend im Generic Setup Tool exportiert und in das eigene Produkt integriert werden.
Solche Übergänge können durch bestimmte Rechte, Rollen und Gruppen geschützt werden.
Anmerkung 1: Wie schon an früherer Stelle bemerkt, sollten die Sicherheitseinstellungen durch die Zuweisung entsprechender Rechte (Permissions) erfolgen, nicht durch die Zuweisung von Rollen oder Gruppen.
Für jedes Stadium können eine Reihe von Rechten angegeben werden, die für ein entsprechendes Inhaltsobjekt gelten. Die in einem Arbeitsablauf zu vergebenden Rechte werden im Permissions-Reiter für jeden Workflow angegeben:
Beachten Sie bitte, dass Änderungen an den Rechten keinen unmittelbaren Einfluss auf die Rechte bestehender Objekte haben. Hierzu müssen in Plone-Konfiguration → Artikeltypen zunächst die Artikeltypen angegeben werden, für die der Workflow geändert werden soll. Anschließend lassen sich Zuordnungen von alten auf neue Stadien treffen.
Wir wollen nun für unsere beiden Artikeltypen spezifische Arbeitsabläufe erstellen. Hierzu fügen wir in src/vs.registration/vs/registration/profiles/default/
die Datei workflows.xml
mit folgendem Inhalt hinzu:
<?xml version="1.0"?>
<object name="portal_workflow"
meta_type="Plone Workflow Tool">
<object name="registrant_workflow" meta_type="Workflow"/>
<object name="registration_workflow" meta_type="Workflow"/>
<bindings>
<type type_id="Registrant">
<bound-workflow workflow_id="registrant_workflow"/>
</type>
<type type_id="Registration">
<bound-workflow workflow_id="registration_workflow"/>
</type>
</bindings>
</object>
Anschließend werden die verschiedenen Stadien und Übergänge der neuen Arbeitsabläufe definiert. Dazu werden die Ordner src/vs.registration/vs/registration/profiles/default/workflows/registrant_workflow/
und src/vs.registration/vs/registration/profiles/default/workflows/registration_workflow/
erstellt. Der Name der Ordner muss dabei exakt der in workflows.xml
angegebenen ID entsprechen. In jedem dieser Ordner wird dann die Datei definition.xml
angelegt. Für den Artikeltyp registrant
sieht sie z.B. so aus:
<?xml version="1.0"?>
<dc-workflow workflow_id="registrant_workflow"
title="registrant_workflow"
state_variable="review_state"
initial_state="unconfirmed">
Zunächst werden allgemeine Angaben zum Arbeitsablauf wie ID, Titel, Variablenname und initialer Status gemacht. Der Variablenname state_variable
sollte dabei immer review_state
sein.
Anschließend werden die Rechte (Permissions) angegeben, die durch den Arbeitsablauf geändert werden sollen:
<permission>Delete objects</permission>
<permission>Modify portal content</permission>
<permission>View</permission>
Nun werden die verschiedenen Stadien definiert. Dabei wird für jedes Stadium in exit-transition
angegeben, welche Übergänge möglich sind und eine Zuweisung der Rollen und Rechte vorgenommen:
<state state_id="confirmed"
title="Confirmed">
<exit-transition transition_id="reject-open"/>
<permission-map name="Delete objects"
acquired="False">
<permission-role>Manager</permission-role>
</permission-map>
<permission-map name="Modify portal content"
acquired="False">
<permission-role>Owner</permission-role>
<permission-role>Manager</permission-role>
</permission-map>
<permission-map name="View"
acquired="False">
<permission-role>Manager</permission-role>
</permission-map>
</state>
...
Den Übergängen werden ID, Titel und Auslöser (trigger
) zugewiesen. trigger
kann dabei die Werte USER
oder AUTOMATIC
annehmen. Der <action />
-Tag enthält den Namen, der in Plone’s Status-Menü angezeigt wird und die URL, auf die diese Aktion verlinkt. Üblicherweise wird hier das content_status_modify
-Skript verwendet. Schließlich wird der Übergang noch geschützt durch die Angabe im <guard />
-Tag:
<transition transition_id="confirm"
title="Confirm"
new_state="confirmed"
trigger="USER"
before_script=""
after_script="">
<action url="%(content_url)s/content_status_modify?workflow_action=confirm"
category="workflow">
Confirm
</action>
<guard>
</guard>
</transition>
...
</dc-workflow>
Der Arbeitsablauf kann mittels i18n:
-Attributen internationalisiert werden.
Dabei besteht prinzipiell Zugriff auf alle verwendeten Zeichenketten. Siehe auch
Übersetzungen in der Plone-Domäne.
Für Plone 4.3 kann das aktuelle Stadium mit plone.api ermittelt werden:
import plone.api
wftool = plone.api.portal.get_tool('portal_workflow')
review_state = wftool.getInfoFor(context, 'review_state')
Für Plone ≤ 4.2 erhalten wir das Workflow-Tool noch mit getToolByName
:
from Products.CMFCore.utils import getToolByName
wftool = getToolByName(context, 'portal_workflow'
review_state = wftool.getInfoFor(context, 'review_state')
Bemerkung
Wird das Stadium im Catalog Tool (portal_catalog
)
abgefragt, so wird das Stadium als Metaangabe des Objekts ausgegeben:
from Products.CMFCore.utils import getToolByName
catalog = getToolByName(context, 'portal_catalog')
for result in catalog(portal_type = ('Document', 'News Item'),
review_state = ('published', 'public', 'visible',)):
review_state = result.review_state
# do something with the review state
wftool.doActionFor(context, action='publish')
def test_role_added(self):
portal = self.layer['portal']
self.assertTrue("StaffMember" in portal.validRoles())
def test_workflow_installed(self):
portal = self.layer['portal']
workflow = getToolByName(portal, 'portal_workflow')
self.assertTrue('vs_sitecontent_workflow' in workflow)
def test_workflows_mapped(self):
portal = self.layer['portal']
workflow = getToolByName(portal, 'portal_workflow')
self.assertEqual(('vs_sitecontent_workflow',),
workflow.getDefaultChain())
def test_view_permisison_for_staffmember(self):
portal = self.layer['portal']
self.assertTrue('View' in [r['name']
for r in portal.permissionsOfRole('Reader')
if r['selected']])
self.assertTrue('View' in [r['name']
for r in portal.permissionsOfRole('StaffMember')
if r['selected']])
def test_staffmember_group_added(self):
portal = self.layer['portal']
acl_users = portal['acl_users']
self.assertEqual(1,
len(acl_users.searchGroups(name='Staff')))
Sicherheitsannahmen werden an den folgenden Stellen getroffen:
browser
-Komponenten wie views
und resource
, die in ZCML deklariert und mit einer Berechtigung versehen werden.
Page Templates und andere Ressourcen, die durch das Skins-Tool verwaltet werden, werden explizit bestimmten Rollen zugewiesen. So wird z.B. prefs_install_products_form.pt
in der assoziierten Datei prefs_install_products_form.pt.metadata
explizit auf die Manager
-Rolle eingeschränkt:
[security]
View = 0:Manager
Attribute und Methoden von persistenten Objekten wie Artikeltypen und Tools können auf zweierlei Art geschützt werden:
durch ZCML-Berechtigungen mit class
- oder require
-Anweisungen
durch Python-Code, der ein AccessControl.ClassSecurityInfo
–Objekt verwendet.
Zusätzlich kann die Variable __allow_access_to_unprotected_subjects__
einer Klasse hinzugefügt werden um zu bestimmen, wie sich Attribute, die ihrerseits nicht durch Security-Annahmen geschützt sind, verhalten sollen.
Zope erlaubt privilegierten Nutzern Page Templates, DTML-Methoden und Python-Skripte *through-the-web zu erstellen. Restricted Python gewährleistet nun, dass diese Nutzer nicht Skripte oder Templates erstellen können, die Zugang zu Ressourcen oder Methoden erlauben würden, die ihnen nicht zugestanden wurden. Die Berechtigungen werden automatisch überprüft und führen ggf. zu einer Unauthorized-Exception.
Es lassen sich sog. Proxy Roles für Templates oder Python-Skripte entweder im ZMI oder in einer *.metadata
-Datei angeben. So gibt es z.B. für das Controller Python Script send_feedback.cpy
eine korrespondierende Datei send_feedback.cpy.metadata
mit folgendem Inhalt:
[default]
proxy=Manager,Anonymous
[security]
View=0:Authenticated
Dies ist notwendig, da das Skript normalerweise von Nutzern aufgerufen wird, die nicht auf die E-Mail-Konfiguration der Site zugreifen dürften, das Skript jedoch bestimmte angaben aus dieser Konfiguration benötigt.
Restricted Python gewährleistet ebenfalls, dass through-the-web erstellte Skripte nicht auf das Dateisystem zugreifen oder unautorisierte Module importieren können, die die Sicherheit des Servers kompromittieren könnten. Lediglich die von``AccessControl`` in allow_module()
und allow_class()
angegebenen Module und Klassen können importiert werden. Zusätzlich werden alle Methoden und Variablen, deren Namen mit einem Unterstrich _
beginnen, als privat betrachtet und können nicht aufgerufen werden.
Durch diese Sicherheitsmechanismen sollten Administratoren bei sachgemäßer Handhabung daran gehindert werden, versehentlich Sicherheitslücken in eine Anwendung zu reißen. Bei unsachgemäßer Handhabung kann jedoch weiterhin erheblicher Schaden angerichtet werden.
Hierzu wird in der Datei vs/policy/setuphandlers.py
folgendes angegeben:
def installUsers(site):
site.portal_membership.addMember('me', 'secret', ['Member'], ())
site.portal_membership.addMember('myself', 'secret', ['Editor', 'Member'], ())
site.portal_membership.addMember('i', 'secret', ['Manager', 'Member'], ())
def setupVarious(context):
if context.readDataFile('vs.policy_various.txt') is None:
return
...
installUsers(site)
Für das Anlegen mehrerer Nutzer stehen zwei Erweiterungen zur Verfüugung:
Um die Rollen eines Nutzers in einem bestimmten Kontext angezeigt zu bekommen, erstellen wir folgenden View.
Zunächst wird der View konfiguriert in browser/configure.zcml
:
<browser:page
name="debug-user"
for="*"
permission="zope2.View"
class=".debug.DebugUser"
/>
Anschließend erstellen wir noch das Python-Skript browser/debug.py
:
from Products.Five.browser import BrowserView
from AccessControl import getSecurityManager
class DebugUser(BrowserView):
""" Current user debugging """
def __call__(self):
result = list()
user = getSecurityManager().getUser()
result.append('User: %s' % user.getUserName())
result.append('Roles: %s' % user.getRoles())
result.append('Roles in context: %s' % user.getRolesInContext(self.context))
result.append('')
result.append(self.request.text())
return '\n'.join(result)
Nach einem Neustart der Instanz können Sie nun in jedem Kontext den View im Browser aufrufen mit @@debug-user
und erhält neben der ID des Nutzers auch dessen globale und kontextabhängigen Rollen. Darüberhinaus erhalten Sie auch Informationen zur Session etc.
Bisher haben wir nur Formulare erstellt, deren Inhalte von Archetypes in der ZODB gespeichert wurden. In diesem Abschnitt werden wir nun eigenständige Formulare programmatisch erstellen.
Bemerkung
Mit PloneFormGen gibt es ein Produkt, das die einfache Erstellung von Formularen über ein Web-Interface erlaubt. PloneFormGen und die erforderlichen Zusatzprodukte lassen sich einfach installieren indem unter eggs
im [buildout]
-Abschnitt Products.PloneFormGen
eingetragen wird.
Im Abschnitt Portlet erstellen haben wir bereits gesehen, wie mit zope.formlib
aus einem Interface add- und edit-Formulare generiert wurden. Dies war jedoch nur ein Spezialfall, nun wollen wir die Formulargenerierung mit zope.formlib
allgemeiner betrachten. Hierzu definieren wir zunächst einmal einen View in browser/configure.zcml
:
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="enquiry"
class=".enquiry.EnquiryForm"
permission="zope2.View"
/>
Anschließend definieren wir das Formular vollständig in browser/enquiry.py
– es ist kein zusätzliches Page Template erforderlich:
import re
from zope.interface import Interface
from zope import schema
from zope.formlib import form
from Products.Five.formlib import formbase
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage
from Acquisition import aq_inner
from Products.CMFCore.utils import getToolByName
from vs.registration import RegistrationMessageFactory as _
# Define a valiation method for email addresses
class NotAnEmailAddress(schema.ValidationError):
__doc__ = _(u"Invalid email address")
check_email = re.compile(r"[a-zA-Z0-9._%-]+@([a-zA-Z0-9-]+.)*[a-zA-Z]{2,4}").match
def validate_email(value):
if not check_email(value):
raise NotAnEmailAddress(value)
return True
MESSAGE_TEMPLATE = """\
Enquiry from: %(name)s <%(email_address)s>
%(message)s
"""
class IEnquiryForm(Interface):
"""Define the fields of our form
"""
subject = schema.TextLine(title=_(u"Subject"),
required=True)
name = schema.TextLine(title=_(u"Your name"),
required=True)
email_address = schema.ASCIILine(title=_(u"Your email address"),
description=_(u"We will use this to contact you if you request it"),
required=True,
constraint=validate_email)
message = schema.Text(title=_(u"Message"),
description=_(u"Please keep to 1,000 characters"),
required=True,
max_length=1000)
Die constraint
-Eigenschaft des email_address
-Feldes sollte bei einem Aufruf True
zurückgeben, wenn der Wert gültig ist. Andernfalls wird die in __doc__
angegebene Fehlermeldung ausgegeben.
Eine Übersicht über die verfügbaren Felder und deren Optionen erhalten Sie in zope.schema.interfaces
.
Der View für das Formular wird dann abgeleitet aus der Basisklasse von zope.formlib
, die in einem Zope2-Produkt mittels Products.Five.formlib.formbase
zur Verfügung steht:
class EnquiryForm(formbase.PageForm):
form_fields = form.FormFields(IEnquiryForm)
label = _(u"Enquiry")
description = _(u"Got a question? Please make an enquiry using the form below!")
Die form_fields
-Variable enthält die darzustellenden Felder. Hier können auch eigene Widgets hinzugefügt oder Felder aus der Liste ausgenommen werden. Weitere Informationen hierzu erhalten Sie in den Interfaces IFormFields
und IFormField
in zope.formlib.interfaces
.
label
und description
werden als Titel und Beschreibung oben auf der Seite dargestellt.
Mit __call__
wird das Formular gerendert. Dabei ändern wir die übliche Darstellung indem der Rahmen und die Reiter beim Editieren nicht angezeigt werden:
def __call__(self):
self.request.set('disable_border', True)
return super(EnquiryForm, self).__call__()
Anschließend wird die einzige Taste dieses Formulars angegeben:
@form.action(_(u"Send"))
def action_send(self, action, data):
Beim Abschicken des Formulars wird die Decorator-Funktion aufgerufen und die Werte des Formulars in das Wörterbuch data
geschrieben.
Anschließend wird die E-Mail konstruiert:
context = aq_inner(self.context)
mailhost = getToolByName(context, 'MailHost')
urltool = getToolByName(context, 'portal_url')
portal = urltool.getPortalObject()
email_charset = portal.getProperty('email_charset')
to_address = portal.getProperty('email_from_address')
source = "%s <%s>" % (data['name'], data['email_address'])
subject = data['subject']
message = MESSAGE_TEMPLATE % data
mailhost.secureSend(message, to_address, str(source),
subject=subject, subtype='plain',
charset=email_charset, debug=False,
From=source)
Und schließlich wird auf die Startseite weitergeleitet und dort eine Statusmeldung angezeigt:
confirm = _(u"Thank you! Your enquiry has been received and we will respond as soon as possible")
IStatusMessage(self.request).addStatusMessage(confirm, type='info')
self.request.response.redirect(portal.absolute_url())
return ''
Wir wollen nun am Beispiel eines Boolschen Feld zum Abonnieren eines Newsletters zeigen, wie eigene Widgets definiert werden können. Hierzu erweitern wir zunächst unsere Schemadefinition:
class IEnquiryForm(Interface):
...
newsletter = schema.Bool(title=u'Subscribe to Newsletter?',
default=False,
required=True)
Nun können wir ein Widget definieren, wobei statt True und False die Werte Yes und No angezeigt werden sollen:
from zope.schema import vocabulary as schemavocabulary
from zope.app.form import browser as formbrowser
def YesNoWidget(field, request, true=_('yes'), false=_('no')):
vocabulary = schemavocabulary.SimpleVocabulary.fromItems(((true, True),
(false, False)))
return formbrowser.RadioWidget(field, vocabulary, request)
Dieses Widget wird schließlich in der EnquiryForm-Klasse dem newsletter
-Feld zugewiesen:
class EnquiryForm(formbase.PageForm):
form_fields = form.FormFields(IEnquiryForm)
form_fields['newsletter'].custom_widget = YesNoWidget
...
Das Formular sollte nun so aussehen:
In plone.app.form
stehen bereits diverse Widgets bereit:
CheckBoxWidget
Plone-spezifisches Widget, das eine Checkbox links neben label
anzeigt, mit dem die Felder title
, label
und required
ausgeblendet werden können.
DateComponents
Ein View, der einige Hilfsmehtoden für Datumswidgets bereitstellt.
LanguageDropdownChoiceWidget
Ein Dropdown-Widget, das eine lokalisierte Sprachauswahl darstellt.
UberSelectionWidget
Widget zum Manipulieren von Auswahlfeldern.
Ein Proof of Concept findet sich in https://svn.plone.org/svn/plone/CMFPlone/branches/plip124-ueberselection-widget/
WYSIWYGWidget
Widget für die Verwendung des WYSIWYG-Editors Kupu zum Editieren von Formularfeldern.
Um ein WYSIWYG-Feld in einem Formular anzuzeigen, kann z.B. in enquiry.py
folgendes angegeben werden:
from plone.app.form.widgets.wysiwygwidget import WYSIWYGWidget
...
class IEnquiryForm(Interface):
...
text = schema.Text(title=_(u"Text"),
description=_(u"A field which can contain rich text."),
required=True)
...
class EnquiryForm(base.EditForm):
form_fields = form.FormFields(IEnquiryForm)
...
form_fields['text'].custom_widget = WYSIWYGWidget
Weitere Widgets finden Sie auch in z3c.widget, u.a.:
Dropdown-Widget zur Auswahl eines Landes.
Das DateSelectWidget
-Widget bietet drei Auswahlboxen für Tag, Monat und Jahr.
Konfigurierbares Flash-Frontend
Dieses Widget kann als custom_widget
verwendet werden um Bilder hochzuladen.
Schließlich fügen wir in vs.policy
noch eine site action hinzu. Hierzu erstellen wir die Datei src/vs.policy/vs/policy/profiles/default/actions.xml
mit folgendem Inhalt:
<?xml version="1.0"?>
<object name="portal_actions" meta_type="Plone Actions Tool"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<object name="site_actions"
meta_type="CMF Action Category">
<object name="contact"
meta_type="CMF Action"
i18n:domain="vs.registration">
<property name="title" i18n:translate="">Enquiry</property>
<property name="description" i18n:translate=""></property>
<property name="url_expr">string:$portal_url/@@enquiry</property>
<property name="icon_expr"></property>
<property name="available_expr"></property>
<property name="permissions">
<element value="View"/>
</property>
<property name="visible">True</property>
</object>
</object>
</object>
Dabei wird mit name="contact"
die Aktion, die auf das Plone-Kontaktformular verweist, ersetzt.
Bemerkung
In Forms erhalten Sie weitere Informationen zur zope.formlib
.
Um ein eigenes Formular zu erstellen, erzeugen wir zunächst ein Page Template registrationreport
in src/vs.registration/vs/registration/browser/
mit folgendem Inhalt:
<form method="get"
tal:attributes="action string:${context/absolute_url}/${view/__name__}">
<div i18n:translate="registration_report_days_searched">
Show changes in the the last
<input type="text" size="2" name="days"
tal:attributes="value view/days"
i18n:name="num_days" />
days.
<input type="submit" class="context" name="form.button.UpdateDays"
value="Refresh"
i18n:name="submit_button"
i18n:attributes="value" />
</div>
</form>
Dabei wird das action
-Attribut dynamisch generiert um zu gewährleisten, dass immer dasselbe Formular im selben Kontext aufgerufen wird.
Anmerkung 1: Würde ein Skin-Template verwendet, müsste statt der Variablen ${view/__name__}
die Variable ${template/getId}
verwendet werden.
Das Formular wird verarbeitet, wenn der View in registrationreport.py
mit der __call__()
-Methode aufgerufen wird:
template = ViewPageTemplateFile('registrationreport.pt')
def __call__(self):
# Hide the editable-border
self.request.set('disable_border', True)
# The number for days with a silent fallback on the default
# if the input is invalid.
try:
self.days = int(self.request.get('days', 7))
except ValueError:
self.days = 7
return self.template()
Anmerkung 2: Falls eine Anfrage nicht nur auf den View selbst zugreifen muss, ist self.request
nicht mehr ausreichend; stattdessen ist dann die Anfrage zu akquirieren mit request = context.REQUEST
.
Anmerkung 3: Für HTTP POST
-Anfragen sollte statt self.request
self.request.form
angegeben werden, da dies versehentlich akzeptierte Variablen verhindert.
Die anderen Methoden des Views untersuchen self.days
zum Erstellen der Such-Parameter:
def recently_modified_registrants(self):
context = aq_inner(self.context)
catalog = getToolByName(context, 'portal_catalog')
results = []
for r in catalog(object_provides=IRegistrant.__identifier__,
modified=dict(query=self.modified_after(), range='min'),
sort_on='modified',
sort_order='reverse',):
results.append(dict(url=r.getURL(),
title=r.Title,
description=r.Description,
modified=self.localize(r.modified)))
return results
def localize(self, time):
return self._time_localizer()(time, None, aq_inner(self.context), domain='plonelocales')
def modified_after(self):
return DateTime() - self.days
@memoize
def _time_localizer(self):
context = aq_inner(self.context)
translation_service = getToolByName(context, 'translation_service')
submit
-Tasten¶Sind für ein Formular mehrere Tasten verfügbar, muss überprüft werden, welche der Tasten gedrückt wurde und welche Aktion hierfür auszuführen ist. Wie dies geschieht, können Sie sich z.B. in plone.app.workflow.browser.sharing
anschauen:
<input class="context" type="submit" name="form.button.Save" value="Save" i18n:attributes="value label_save" />
<input class="standalone" type="submit" name="form.button.Cancel" value="Cancel" i18n:attributes="value label_cancel"/>
In sharing.py
wird dann zunächst überprüft, welche Taste gedrückt wurde. Dabei ist zu beachten, dass Web-Browser immer nur den Wert für name
derjenigen Taste senden, auf die geklickt wurde. Anschließend wird die entsprechende Aktion zuzuweisen:
class SharingView(BrowserView):
# Actions
template = ViewPageTemplateFile('sharing.pt')
def __call__(self):
"""Perform the update and redirect if necessary, or render the page
"""
postback = True
form = self.request.form
submitted = form.get('form.submitted', False)
save_button = form.get('form.button.Save', None) is not None
cancel_button = form.get('form.button.Cancel', None) is not None
if submitted and not cancel_button:
if not self.request.get('REQUEST_METHOD','GET') == 'POST':
raise Forbidden
# Update the acquire-roles setting
inherit = bool(form.get('inherit', False))
self.update_inherit(inherit)
# Update settings for users and groups
entries = form.get('entries', [])
roles = [r['id'] for r in self.roles()]
settings = []
for entry in entries:
settings.append(
dict(id = entry['id'],
type = entry['type'],
roles = [r for r in roles if entry.get('role_%s' % r, False)]))
if settings:
self.update_role_settings(settings)
# Other buttons return to the sharing page
if cancel_button:
postback = False
if postback:
return self.template()
else:
context_state = self.context.restrictedTraverse("@@plone_context_state")
url = context_state.view_url()
self.request.response.redirect(url)
Aus dem sharing.pt
-Page Template lässt sich auch ablesen, wie Formularfelder berechnet werden können:
<input
type="hidden"
name="entries.id:records"
tal:attributes="value entry/id"
/>
<input
type="hidden"
name="entries.type:records"
tal:attributes="value entry/type"
/>
Dies wird dargestellt in einer tal:repeat
-Schleife. Wird das Formular abgeschickt, wird die Variable entries
mit einer Liste von Wörterbüchern aus den Schlüsselwörtern "id"
und "type"
übertragen. Ein Überblick über alle verfügbaren Konverter erhalten Sie hier:
Konverter |
Beispiel |
Anmerkung |
---|---|---|
|
<input type="hidden"
name="limit:int"
value="8 />"
|
Wandelt die Variable in den
zugehörigen Python-Typ um,
Solche Umwandlungen sind
normalerweise nur in
|
|
<textarea name="description:text" />
|
Konvertiert eine Zeichenkette mit normalisierten Zeilenumbrüchen entsprechend der Server-Plattform |
|
<input type="checkbox"
name="status:list"
value="1"
|
Erstellt eine Liste oder ein Tuple aus mehreren Feldern mit demselben Namen oder aus einer mehrwertigen Listenauswahl. Dieser Konverter kann mit
anderen kombiniert werden,
z.B. |
|
<input type="text"
name="keywords:tokens" />
|
Wandelt eine durch Leerzeichen (tokens) oder neue Zeilen (lines) getrennte Zeichenkette in eine Liste um |
|
<input type="text"
name="data.id:record" />
|
Erstellt ein Wörterbuch
( |
|
<input type="text"
name="title:required" />
|
Gibt eine Fehlermeldung aus wenn das Feld nicht ausgefüllt wurde. |
|
<input type="text"
name="id:ignore_empty" />
|
Die Variable wird bei einem Request nicht angegeben wenn sie leer ist. Dieser Konverter kann mit anderen kombiniert werden. |
|
<input type="hidden"
name="accept:boolean:default"
value="True" />
<input type="checkbox"
name="accept:boolean:default"
value="False" />
|
Standardwert, falls in keinem Feld mit demselben Namen ein Wert übermittelt wurde. Dies ist vor allem für Checkboxen sinnvoll, die nicht übertragen werden wenn für sie keine Angabe gemacht wurde. Dieser Konverter kann mit anderen kombiniert werden. |
Plone kommt mit dem CMF Form Controller-Produkt, mit dem die Abläufe zwischen Formularen und Skripten geregelt werden können. Gerade für komplexe Abläufe ist es sehr hilfreich, unterstützt jedoch keine Zope3-Views und kann daher nur in Skin-Layern definiert werden.
Schauen wir uns nun als Beispiel Plone’s Send this page to someone-Formular an, das in CMFPlone/skins/plone_forms/sendto_form.cpt
definiert ist. Dabei steht der cpt
-Suffix für Controller Page Template:
<div metal:fill-slot="main"
tal:define="errors options/state/getErrors;">
...
<form name="sendto_form"
class="enableAutoFocus"
action="sendto_form"
method="post"
enctype="multipart/form-data"
tal:attributes="action string:$here_url/$template_id">
...
<div class="field"
tal:define="error errors/send_to_address|nothing;"
tal:attributes="class python:test(error, ``field error``, ``field``)">
...
<div class="formControls">
<input class="context"
type="submit"
name="form.button.Send"
value="Send"
i18n:attributes="value label_send;"
/>
</div>
...
</form>
Zunächst fällt auf, dass eine Variable errors
definiert wird, die das Auffinden von Validierungsfehlern erlaubt.
Dann sehen wir, dass das Formular – wie bei CMFFormController üblich – wieder auf sich selbst verweist.
Schließlich erkennen wir die versteckte Variable form.submitted
, wobei das Controller Page Template überprüft, ob das Formular einfach aufgerufen oder bereits abgeschickt wurde.
CMFFormController benötigt zur Auswertung des Formulars eine korrespondierende Datei sendto.cpy.metadata
im selben Verzeichnis:
[default]
title=Send this page to somebody
[validators]
validators=validate_sendto
[actions]
action.success=traverse_to:string:sendto
action.failure=traverse_to:string:sendto_form
Schauen wir uns nun die Validatoren und Aktionen genauer an.
Allgemein lassen sich für Für Controller Page Templates folgendermaßen Validatoren angeben:
[validators]
validators = validate_script1, validate_script2
Diese Angabe startet zwei Prüfskripte: zunächst validate_script1 , dann validate_script2
. Ein Prüfskript untersucht die Formulardaten wobei Fehler dem Form Controller Status hinzugefügt werden.
Soll die Validierung vom Objekttyp Document verschieden sein von der des Objekttyps Image sieht die Metaangabe so aus:
validators.Document = validate_script1
validators.Image = validate_script2
Kommen im Formular mehrere Buttons (Tasten) vor, z.B.:
<input type="submit" name="form.button.button1" value="Value1" />
<input type="submit" name="form.button.button2" value="Value2" />
können für diese auch unterschiedliche Validierungen angegeben werden:
validators.button1 = validate_script1
validators.button2 = validate_script2
Für Controller Page Templates lassen sich auch Aktionen angeben, z.B.:
[actions]
action.success = traverse_to:string:script1
Haben die Prüfskripte den Status success
ausgegeben, wird die Aktion traverse_to
mit dem Argument string:script1
aufgerufen.
Schlägt ein Prüfskript fehl, wird gemäß den Standardeinstellungen das Formular erneut geladen.
Wie bei Validatoren kann auch bei Aktionen zwischen Dokumenttypen und Buttons unterschieden werden:
action.success.Document = traverse_to:string:document_script
action.success.Image = traverse_to:string:image_script
action.success.button1 = traverse_to:string:script1
action.success.button2 = traverse_to:string:script2
Folgende Aktionen sind möglich:
redirect_to
redirect_to_action
traverse_to
traverse_to_action
.
Dabei rufen die traverse_to
-Aktionen direkt ein Template oder Skript auf dem Server auf, wohingegen die redirect_to
-Aktionen eine Weiterleitung des Browsers bewirken. Normalerweise werden die Zwischenschritte mit traverse_to
-Aktionen und nur der letzte Schritt mit einer
redirect_to
-Aktion angegeben, sodass die Angabe der URL im Browser die aktuelle Seite wiedergibt. So ist z.B. in unserem Beispiel in sendto.cpy.metadata
folgendes angegeben:
[validators]
validators=validate_sendto
[actions]
action.success = redirect_to_action:string:view
action.failure = redirect_to_action:string:view
Schauen wir uns nun das Validator-Skript validate_sendto.vpy
genauer an, auf das in sendto.cpy.metadata
verwiesen wurde:
## Controller Script Python "validate_sendto"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind state=state
##bind subpath=traverse_subpath
##parameters=send_to_address='',send_from_address=''
##title=validates the email adresses
from Products.CMFPlone import PloneMessageFactory as _
plone_utils=context.plone_utils
if not send_to_address:
state.setError('send_to_address', _(u'Please submit an email address.'), 'email_required')
...
if state.getErrors():
context.plone_utils.addPortalMessage(_(u'Please correct the indicated errors.'), 'error')
return state.set(status='failure')
else:
return state
Ist die Validierung erfolgreich, fährt der CMFFormController, wie in sendto_form.cpt.metadata
angegeben mit dem Skript sendto.cpy
fort. Dieses Skript gibt schließlich den Wert für state
aus:
## Controller Python Script "sendto"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind state=state
##bind subpath=traverse_subpath
##parameters=
##title=Send an URL to a friend
##
REQUEST=context.REQUEST
...
if not mtool.checkPermission(AllowSendto, context):
context.plone_utils.addPortalMessage(_(u'You are not allowed to send this link.'), 'error')
return state.set(status='failure')
...
context.plone_utils.addPortalMessage(_(u'Mail sent.'))
return state
Das Plone User Interface ist bereits in viele Sprachen übersetzt worden. Die Übersetzungen werden jedoch nicht mit Plone selbst mitgeliefert, sondern in einem eigenen Modul:
Bis zu Version Plone 3.1 in Plone Translations,
ab Version Plone 3.3 in plone.app.locales.
Üblicherweise erhalten Sie je nach Spracheinstellung ihres Browsers die entsprechende Sprache für das Plone-Interface angezeigt.
Da vermutlich jedoch die wenigsten Betrachter ihrer Website bewusst die Spracheinstellung in ihrem Browser vorgenommen haben, gibt es mit dem Plone Language Tool ein Produkt, das vielfältige Spracheinstellungen für eine Plone-Sie ermöglicht.
Das Plone Language Tool kann, nachdem es im Products-Ordner ihrer Zope-Instanz installiert wurde, in der Plone-Konfiguration unter Produkte hinzufügen/entfernen für jede Plone-Site installiert werden.
Anschließend können sie folgende Spracheinstellungen vornehmen:
Die hier ausgewählte Sprache wird verwendet sofern die vom Nutzer bevorzugte Sprache nicht verfügbar ist.
Hier können sie diejenigen Sprachen angeben, die in ihrer Website ausgewählt werden können.
Verwenden Sie die ctrl-Taste zum Aus- und Abwählen der einzelnen Sprachen.
Erlauben spezifischer Sprachvarianten wie de_DE
, de-AT
und de-CH
.
Anzeigen von Flaggen für die Sprachauswahl.
Die Reihenfolge, in der die Sprache ausgehandelt wird.
Sollen Sprachcodes in der URL für Sprachangaben des Betrachters verwendet werden?
Sollen Cookies für die Sprachangaben des Betrachters verwendet werden?
Soll die Spracheinstellung des Browsers berücksichtigt werden?
Die voreingestellte Standardsprache (s.o.) wird verwendet.
Einstellungen, die auf mehrsprachigen Inhalt zutreffen.
Erzwinge unterschiedliche URL’s für jede Sprache (Umleitung).
Erlaube, auf eine andere Sprache auszuweichen, sollte dieses von der Implementierung unterstützt werden.
Erstelle Artikel anfänglich als sprachneutral.
Dies Einstellungen können auch im Profil profiles/default/portal_languages.xml
vorgenommen werden:
<?xml version="1.0"?>
<object name="portal_languages" meta_type="Plone Language Tool">
<default_language value="en"/>
<use_content_negotiation value="False"/>
<use_path_negotiation value="False"/>
<use_cookie_negotiation value="True"/>
<authenticated_users_only value="False"/>
<use_request_negotiation value="False"/>
<use_cctld_negotiation value="False"/>
<use_subdomain_negotiation value="False"/>
<use_combined_language_codes value="False"/>
<display_flags value="False"/>
<start_neutral value="False"/>
<supported_langs>
<element value="en"/>
<element value="de"/>
</supported_langs>
</object>
def testLanguageSettings(self):
default_language = self.portal.portal_languages.getDefaultLanguage()
self.assertEqual(default_language == 'de', True)
# return [(country code, countryname), ...]
supported_languages = [r[0] for r in self.portal.portal_languages.listSupportedLanguages()]
self.assertEqual('en' in supported_languages, True)
self.assertEqual('de' in supported_languages, True)
Plone nutzt den Placeless Translation Service um das User-Interface zu übersetzen. Ist das Produkt installiert, werden alle verfügbaren Übersetzungsdateien in /Control_Panel/TranslationService/manage_main
angezeigt. Dabei schaut der Placeless Translation Service nach Übersetzungsdateien in i18n
- und locales
-Ordnern innerhalb von INSTANCE_HOME
und INSTANCE_HOME/Products
.
Für jedes Page Template kann im HTML-Header die Standard-Übersetzungsdomäne angegeben werden, z.B.:
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="vs.registration">
Existiert jedoch für eine Zeichenkette bereits eine Übersetzung in einer anderen Domain, verweisen Sie auf diese, z.B.:
<a href=""
tal:attributes="href python:test(item_type in use_view_action, item_url+'/view', item_url);"
i18n:translate="read_more">
Read More…
</a>
Text wird mit einer bestimmten message id, hier read_more
, verknüpft um übersetzt werden zu können. Würde die message id leer sein, also i18n:translate=""
, dann wird stattdessen der zu übersetzende Text selbst, hier Read More
, verwendet. Dabei sind für die Präfixe der message ids in Plone bestimmte, unten genannte Regeln festgelegt worden.
Um Attribute zu übersetzen wird nicht i18n:translate
sondern i18n:attributes
verwendet. Auch können mehrere Attribute gleichzeitig adressiert werden, wie z.B. in:
<img src="plone.gif"
alt="Plone Icon"
title="Plone Icon Title"
i18n:attributes="alt vs_registration_plone_icon;
title vs_registration_plone_icon_title">
Mit i18n:name
lassen sich auch dynamische Inhalte übersetzen, z.B.:
<p i18n:translate="text_download">There have been
<span tal:content="here/download_count"
i18n:name="count">100.000</span>
downloads of Plone. </p>
Der Eintrag in die Übersetzungsdatei sieht dann folgendermaßen aus:
msgid "text_download"
msgstr "There have been ${count} downloads of Plone."
Für Datum und Zeit wird die localized_time
-Methode mit den zwei message ids date_format_long
und date_format_short
verwendet. Gibt es keine Übersetzungen für eine Sprache wird das Standardformat strftime
, wie in den portal_properties
angegeben, verwendet. Siehe auch Übersetzen des User-Interfaces: Datum und Urzeit.
Message ids lassen sich auch verschachteln, z.B.:
<p i18n:translate="contentrules_controlpanel_link">
Use the
<a i18n:name="controlpanel_link"
i18n:translate="contentrules_control_panel"
tal:attributes="href string:${portal/absolute_url}/@@rules-controlpanel">
content rules control panel
</a>
to create new rules or delete or modify existing ones.
</p>
In der Übersetzungsdatei findet die Übersetzung in verschiedenen Abschnitten statt:
msgid "contentrules_controlpanel_link"
msgstr "Benutzen Sie ${controlpanel_link} um neue Regeln zu erstellen, zu löschen oder zu modifizieren."
msgid "contentrules_control_panel"
msgstr "die Regeleinstellungen"
In Pythonskripten sollten eigentlich keine Angaben zum User Interface stehen. Meist sind jedoch zumindest label
und description
von Widgets in den Pythonskripten selbst enthalten. Um diese lokalisieren zu können, werden gegebenenfalls label_msgid
und description_msgid
eingefügt, z.B.:
StringField('event_type', vocabulary='EventTypes',
widget=SelectionWidget(
label='Event Type',
label_msgid='label_event_type',
description='The type of the event',
description_msgid='help_event_type',
i18n_domain='vs.registration'),),
Seit Plone 2.5 kann die Zope Message Factory in Pythonskripten verwendet werden. Hierzu wird zunächst in der __init__.py
-Datei des Pakets die Message Factory registriert:
from zope.i18nmessageid import MessageFactory
RegistrationMessageFactory = MessageFactory('vs.registration')
Anschließend lässt sich diese Message Factory in ein Pythonskript importiert mit:
from vs.registration import RegistrationMessageFactory as _
Und nun lassen sich folgende Angaben einfach übersetzen:
label=_(u"Body Text"),
description=_(u"Text for front page of registration")
Mit dem u
-Präfix wird Unicode als Kodierung für die Zeichenketten festgelegt. Dieses Beispiel wird in einem Page Template weiterverwendet werden (z.B. in tal:replace
oder tal:content
).
Falls das Pythonskript nicht in einem Page Template verarbeitet wird, muss translation_service
direkt aufgerufen werden:
from Products.CMFCore.utils import getToolByName
...
translation_service = getToolByName(self, 'translation_service')
value = u'John Doo'
return translation_service.utranslate('plone',
u'My name is ${fullname}',
mapping={u'fullname' : value})
Schließlich können auf diese Weise die Titel und Aktionen von Inhaltstypen in einer eigenen Produktdomäne verwaltet werden – es wird keine zusätzliche .pot
-Datei für die Plone-Domäne benötigt.
Siehe auch Translating text in code.
Verschiedene Angaben in den .xml
-Dateien des profiles
-Ordner können ebenfalls lokalisert werden, z.B. src/vs.registration/vs/registration/profiles/default/types/Registration.xml
:
<object name="Registration"
meta_type="Factory-based Type Information with dynamic views"
i18n:domain="vs.registration"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title"
i18n:translate="">Registration</property>
In Plone werden bestimmte Präfixe für message ids verwendet. Da die IDs später alphabetisch sortiert werden, lassen sich semantische Differenzen leichter erkennen.
heading_
für <h>
-Elemente.
description_
erläuternder Text direkt unterhalb von <h1>
.
legend_
verwendet für <legend>
-Elemente.
label_
Für label
von input
- und textarea
-Felder sowie <a>
-Elemente.
help_
für Hilfetexte von input
-Fledern.
box_
für die Inhalte von Portlets.
listingheader_
für header
-Angaben in Tabellen (üblicherweisse in der Klasse``listing``).
date_
für datumsspezifische Angaben, wie z.B. Gestern, letzte Woche.
text_
Nachrichten, die keiner anderen Kategorie zugeordnet werden konnten, üblicherweise innerhalb von <p>
.
batch_
für Stapeldarstellungen wie X bis Y von Z.
summary_
für Zusammenfassungen in Tabellen.
title_
für Titel aller Elemente.
message_
für Text in portal_status_message
Den Präfixen folgt nach dem Unterstrich eine kurze Beschreibung der Nachricht, wie z.B. label_address.
Siehe auch: Guide to Prefixes.
Es ist möglich, z.B. zwischen Singular und Plural zu unterscheiden:
<p i18n:translate="">
Cart has <tal:block replace="number">#</tal:block>
book<tal:block condition=
"python: number <> 1">s</tal:block>.</p>
Dies macht jedoch die Arbeit der Übersetzer sehr schwierig, da in manchen Sprachen der Plural nicht einfach durch ein bis zwei angehängte Buchstaben gebildet wird.
Schließen sie die gesamte Phrase einschließlich der Satzzeichen in die zu übersetzende message id ein.
Verwenden sie für den gesamten Satz den i18n:translate
-Tag (s.o.) und verwenden i18n:name
für den eingeschlossenen Teil.
jarn.jsi18n bietet eine i18n-Infrastruktur für Javascripts in Plone.
Im Einzelnen bietet es
das einfache Laden von gettext
-Katalogen aus
Plone
Message Factories, die denen in Python sehr ähnlich sind
die Verweundung von local storage moderner Browser um das wiederholte Laden der Message Cataologs zu vermeiden.
Ihr Paket sollte abhängen von jarn.jsi18n
. Hierzu wird in der setup.py
-Datei unter install_requires
folgendes eingetragen:
install_requires=[
'setuptools',
'jarn.jsi18n'
],
Für die Aktivierung auf der Plone-Site wird es noch in die profiles/default/metadata.xml
eingetragen:
<?xml version="1.0"?>
<metadata>
...
<dependencies>
...
<dependency>profile-jarn.jsi18n:default</dependency>
</dependencies>
</metadata>
MessageFactory
¶Zum Instanziieren und Verwenden Um einer MessageFactory
muss der ì18n
-Katalog geladen werden. Dies kann z.B. so geschehen:
$(document).ready(function () {
jarn.i18n.loadCatalog('plone', 'de');
_ = jarn.i18n.MessageFactory('plone')
});
Der zweite Parameter in loadCatalog
spezifiziert
die Sprache. Diese Angabe ist optional wenn das lang
-Attribut im HTML-Tag verwendet wird.
Nachdem wir nun eine Message Factory haben, können wir auch Zeichenketten übersetzen:
> _('contribute');
hinzufügen
oder mit Keyword-Parametern:
> _('Groups are: ${names}', {names: 'staff'})
"Gruppen sind: Mitarbeiter"
Ggf. können auch mehrere Kataloge oder mehrere Sprachen für denselben Katalog geladen und instanziiert werden, z.B.:
> jarn.i18n.loadCatalog('plone', 'en');
> _en = jarn.i18n.MessageFactory('plone', 'en');
> _en('Contributor');
"Contributor"
Sofern der Browser local storage unterstützt, werden die Kataloge lokal gespeichert. Somit lässt sich
vermeiden, dass nicht jedesmal ein Ajax-Request
ausgelöst wird um den Katalog erneut zu laden. Der
gespeicherte Katalog ist üblicherweise für 24 Stunden
gültig. Die Lebensdauer kann ggf. geändert werden mit
jarn.i18n.setTTL(86400000)
wobei der Wert in Millesekunden angegeben wird.
Zum Erstellen des GNU gettext-message catalogs verwenden wir i18ndude.
i18ndude
-Installation¶i18ndude
lässt sich am einfachsten als eigenständiges Buildout-Projekt installieren um Versionskonflikte in den Abhängigkeiten zu vermeiden. Hierzu erhält die buildout.cfg
-Datei ausschließlich den Abschnitt scripts
:
[buildout]
parts =
scripts
eggs =
i18ndude
[scripts]
recipe = zc.recipe.egg
eggs = i18ndude
.pot
-Datei¶Die .pot
-Datei ist ein Template, aus dem die sprachspezifischen Übersetzungsdateien abgeleitet werden. Im Template tragen üblicherweise die msgid
-Zeilen Strings aus Page Templates (*.pt
) und Python-Dateien (*.py
), die msgstr
sind leer. Strings aus .xml
-Dateien werden über die passende Angabe i18n:domain
im Header erfasst.
Um Fehlermeldungen zu vermeiden erstellen wir zuerst die benötigten Verzeichnisse.
$ mkdir -p src/vs.registration/vs/registration/locales/de/LC_MESSAGES
$ cd /home/veit/i18ndude_buildout
$ ./bin/i18ndude rebuild-pot --pot /home/veit/myproject/src/vs.registration/vs/registration/locales/vs.registration.pot --create vs.registration /home/veit/myproject/src/vs.registration/vs/registration
Mit der Option rebuild-pot --pot
gibt man die Datei an, in die das neue Template geschrieben wird. Mit dem zweiten Parameter --create
gibt man zuerst die Domäne und danach das Verzeichnis an, das rekursiv nach relevanten Dateien durchsucht wird.
Weitere Optionen liefert der Aufruf von ./bin/i18ndude --help
.
.po
-Datei¶Die .po
-Dateien enthalten die Übersetzungen in den msgstr
-Zeilen. Diese werden für die Anzeige der Übersetzung verwendet. Mit dem folgenden Befehl werden sie erstellt und in src/vs.registration/vs/registration/locales/de/LC_MESSAGES/
abgelegt. Um Fehlermeldungen zu vermeiden erstellen wir zuerst eine leere .po
-Datei.
$ touch src/vs.registration/vs/registration/locales/de/LC_MESSAGES/vs.registration.po
$ ./bin/i18ndude sync --pot src/vs.registration/vs/registration/locales/vs.registration.pot src/vs.registration/vs/registration/locales/de/LC_MESSAGES/vs.registration.po
Der Befehl liefert beim ersten Durchlauf eine Rückmeldung wie z.B. diese:
src/vs.registration/vs/registration/locales/de/LC_MESSAGES/vs.registration.po: 31 added, 0 removed
Sollte eine Fehlermeldung besagen, dass die Zieldatei nicht existiert, dann wurde womöglich der XML-Namespace i18n
nicht verwendet oder die Eigenschaft i18n:translate
nicht korrekt verwendet.
Anmerkung 1: Werden die Dateien erneut synchronisiert, werden auch die Kommentare verglichen. Dabei wird ein fuzzy
-Kommentar hinzugefügt sofern sich die msgid
geändert hat. Diese Angaben sollten dann überprüft werden.
Anmerkung 2: Kommen in einer msgid
URLs vor, die in Anführungszeichen ("
) gesetzt sind, so müssen diese escaped werden mit \
, also z.B. <a href=\"http://www.veit-schiele.de\">www.veit-schiele.de</a>
.
Um die Übersetzungen zu aktualisieren, etwa wenn sich die Zahl der Strings verändert hat, wird zunächst die .pot
-Datei wie oben beschrieben aktualisiert. Anschließend wird die deutsche Übersetzungsdatei mit diesem Befehl aktualisiert:
$ ./bin/i18ndude sync --pot src/vs.registration/vs/registration/locales/vs.registration.pot src/vs.registration/vs/registration/locales/de/LC_MESSAGES/vs.registration.po
Um nicht bei jeder Aktualisierung erneut die obigen Shell-Kommandos eingegeben werden müssen, kann auch ein Shell-Skript angelegt werden, z.B. rebuild.sh
mit folgendem Inhalt:
#!/usr/bin/env bash
./bin/i18ndude rebuild-pot --pot src/vs.policy/vs/policy/locales/vs.policy.pot --create "vs.policy" --merge src/vs.policy/vs/policy/locales/vs.policy-manual.pot src/vs.policy*
./bin/i18ndude rebuild-pot --pot src/vs.policy/vs/policy/locales/plone.pot --create "plone" --merge src/vs.policy/vs/policy/locales/plone-manual.pot src/vs.policy/vs/policy/profiles/
./bin/i18ndude sync --pot src/vs.policy/vs/policy/locales/vs.policy.pot src/vs.policy/vs/policy/locales/de/LC_MESSAGES/vs.policy.po
msgfmt --no-hash -o src/vs.policy/vs/policy/locales/de/LC_MESSAGES/vs.policy.mo src/vs.policy/vs/policy/locales/de/LC_MESSAGES/vs.policy.po
./bin/i18ndude sync --pot src/vs.policy/vs/policy/locales/plone.pot src/vs.policy/vs/policy/locales/de/LC_MESSAGES/plone.po
msgfmt --no-hash -o src/vs.policy/vs/policy/locales/de/LC_MESSAGES/plone.mo src/vs.policy/vs/policy/locales/de/LC_MESSAGES/plone.po
.po
-Dateien¶i18ndude
sortiert die Übersetzungen alphabetisch nach der msgid
. Bei umfangreichen Übersetzungen, die sich bezüglich der Anzahl der Strings nicht mehr ändern, kann es sinnvoll sein diese nach der Herkunftsdatei zu sortieren, wie von gettext empfohlen. Fertige .po
-Dateien lassen sich mit gettext
entsprechend bearbeiten, das Paket stellt hierfür den Befehl msgcat
und den Parameter --sort-by-file
zur Verfügung. Siehe die Dokumentation von GNU gettext.
Übersetzungen können nicht nur zur Übersetzung in völlig andere Sprachen verwendet werden, sondern auch z.B. zur Unterscheidung von österreichischen und deutschen Redewendungen. Desweiteren ermöglichen sie auch eine standardisierte Terminologie bei der Verwendung von Zusatzprodukten.
Zope und Plone nutzen GNU gettext und dessen message catalogs, eine Liste übersetzter Texte.
Wie eine sprachspezifische .po
-Datei erstellt wird ist bereits in Erstellen der *.po-Datei beschrieben.
Als Werkzeug zum Übersetzen können Sie poEdit oder einen einfachen Texteditor verwenden. Bei einem Texteditor sollte utf-8
als Zeichenkodierung verwendet werden.
Jeder Abschnitt gliedert sich üblicherweise in folgende Angaben:
#:
Eine Zeile, die mit diesen Zeichen beginnt, gibt das Template an, in dem die Zeichenkette verwendet wird. Kommt eine Zeichenkette in mehreren Templates vor, wird erhält jedes Template eine eigene Zeile.
#.
Zeilen, die mit diesen Zeichen beginnen, geben den Abschnitt an, dem die Zeichenkette entstammt.
msgid
Diese Zeile gibt die exakte Zeichenkette des zu übersetzenden Textes an. Variablen wie ${foo}
sind in die Übersetzung ohne Veränderung zu übernehmen.
msgstr
Diese Zeile enthält den übersetzten Text.
Bevor sie nun mit dem Übersetzen beginnen, sollten sie sich noch die grundlegenden Begriffe anschauen, die Plone in den Language Specific Terms festlegt. Produkte, die auf Plone aufsetzen, sollten diese Begriffe übernehmen um konsistente Bezeichnungen zu gewährleisten. Daher empfiehlt sich auch für Ihr Produkt eine solche Liste spezifischer Begriffe.
Datum und Zeit werden im strftime
-Format lokalisiert, z.B.:
msgid "date_format_long"
msgstr "${Y}-${m}-${d} ${H}:${M}"
msgid "date_format_short"
msgstr "${Y}-${m}-${d}"
Anweisung |
Beschreibung |
---|---|
|
Lokalisierter abgeküzter Name des Wochentags. |
|
Lokalisierter Wochentag. |
|
Lokalisierter abgekürzter Name des Monats. |
|
Lokalisierter Monatsname |
|
Lokalisierte entsprechende Datums- und Zeitdarstellung. |
|
Tag des Monats als Dezimalzahl |
|
Stunde als Dezimalzahl |
|
Stunde als Dezimalzahl |
|
Tag des Jahres als Dezimalzahl. |
|
Monat als Dezimalzahl |
|
Minute als Dezimalzahl |
|
Lokalisiertes Äquivalent von AM oder PM. 1 |
|
Sekunde als Dezimalzahl |
|
Wochenzahl eines Jahres als Dezimalzahl |
|
Wochentag als Dezimalzahl |
|
Wochenzahl eines Jahres als Dezimalzahl |
|
Lokalisierte angemessene Datumsdarstellung. |
|
Lokalisierte angemessene Zeitdarstellung. |
|
Jahr ohne Jahrhundert als Dezimalzahl |
|
Jahr mit Jahrhundert als Dezimalzahl. |
|
Name der Zeitzone (keine Zeichen, wenn keine Zeitzone existiert. |
|
Das Zeichen |
Fügen sie in ihr Paket folgende Unterverzeichnisse für jede gewünschte Sprache ein: locales/<locale>/LC_MESSAGES/
, z.B.:
locales/de/LC_MESSAGES/
locales/en/LC_MESSAGES/
Erstellen Sie die entsprechenden vs.theme.po
-Dateien in den LC_MESSAGES
-Ordnern und editieren diese z.B. mit poEdit.
Registrieren sie die Übersetzungen in der configure.zcml
-Datei mit folgenden Anweisungen:
<configure
...
xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="vs.theme">
...
<i18n:registerTranslations directory="locales" />
</configure>
Normalerweise werden die .po
-Dateien beim Starten des Zope-Servers vom PlacelessTranslationService kompiliert, d.h. .mo
-Dateien erzeugt.
Gegebenenfalls können auch mit folgendem Skript die .po
-Dateien kompiliert werden:
# Compile po files
for lang in $(find locales -mindepth 1 -maxdepth 1 -type d); do
if test -d $lang/LC_MESSAGES; then
msgfmt -o $lang/LC_MESSAGES/${PRODUCT_NAME}.mo $lang/LC_MESSAGES/${PRODUCT_NAME}.po
fi
done
In Plone 4 nutzt der PlacelessTranslationService
zope.i18n
. Die Ordner i18n
und locales
verwenden dieselbe Katalog-Engine.
In Plone 3 kompilierte der PlacelessTranslationService
die *.po
-Dateien erneut. In Plone 4 jedoch nutzt er das zope.i18n
-Modul, für das explizit angegeben werden muss, dass es *.po
-Dateien kompilieren soll. Hierzu wird in der Buildout-Konfiguration dann folgendes angegeben:
[instance]
...
environment-vars =
zope_i18n_compile_mo_files = true
Um den Startprozess von der Zope-Instanzen zu beschleunigen und weniger Speicher zu verbrauchen, kann eine Umgebungsvariable gesetzt werden, die das Kompilieren und Laden von *.po
-Dateien einschränkt. Um die Sprachen z.B. auf Englisch und Deutsch zu beschränken, kann in der buildout.cfg
-Datei folgendes angegeben werden:
[instance]
...
environment-vars =
...
PTS_LANGUAGES en de
zope_i18n_allowed_languages en de
PTS_LANGUAGES
In Plone 3 beeinflusst diese Angabe sowohl das Kompilieren der Übersetzungsdateien in i18n
- und locales
-Ordnern.
In Plone 4 wird hierdurch nur noch das Kompilieren der Übersetzungsdateien in i18n
-Ordnern.
zope_i18n_allowed_languages
In Plone 4 wird hierdurch das Kompilieren der Übersetzungsdateien in locales
-Ordnern gesteuert.
Siehe auch
In der strftime
-Funktion ändert die %p
-Anweisung nur die Ausgabe, wenn %I
verwendet wird.
Der Umfang ist tatsächlich 00
– 61
um Schaltsekunden («leap seconds» und «double leap seconds») berücksichtigen zu können.
In strftime
-Funktionen wird %U
und %W
nur berechnet, wenn Tag, Woche und Jahr angegeben sind.
Die folgenden Konfigurationsdateien des GenericSetup Tools müssen in der Plone-Domäne übersetzt werden:
portal_atct.xml
portlets.xml
workflows/MYWORKFLOW/definition.xml
In Plone 3 können Übersetzungen der Übersetzungsdomäne plone
nicht im locales
-Ordner angepasst werden. Sollten Sie dies dennoch gemacht haben, werden Sie feststellen, dass nur noch Ihre eigenen Übersetzungen angezeigt werden. Damit Ihre Übersetzungen zusätzlich verwendet werden, müssen die Übersetzungsdateien im i18n
-Ordner erstellt werden.
Zur Lokalisierung von Schemata wird die message_id
generiert. Dabei wird der
ID des Schemata label_schema_
vorangestellt. Im Folgenden ein Beispiel aus
plone/app/locales/locales/de/LC_MESSAGES/plone.po
:
#. Default: "Categorization"
#: ./ATContentTypes/content/schemata.py
msgid "label_schema_categorization"
msgstr "Kategorisierung"
Hierzu erstellen wir zunächst in unserem Buildout-Projekt einen Ordner namens i18n
und darin plone-vs-de.po
. Anschließend fügen wir in der buildout.cfg
-Datei einen neuen Abschnitt hinzu:
[buildout]
...
instance
i18n-overwrites
...
[i18n-overwrites]
recipe = plone.recipe.command
command =
ln -sf ${buildout:directory}/i18n ${instance:location}/
update-command =
${i18n-overwrites:command}
In Plone 4 hingegen wird das i18n
-Verzeichnis in der Instanz ignoriert. Um Übersetzungen der Domäne Plone zu überschreiben, müssen Sie ein eigenes locales
-Verzeichnis anlegen und die Plone-Übersetzungsdatei plone.app.locales-4.0.2-py2.6.egg/plone/app/locales/locales/de/LC_MESSAGES/plone.po
dahin kopieren, also z.B. nach vs.theme/vs/theme/locales/de/LC_MESSAGES/plone.po
. Anschließend können Sie aus dieser Datei die Übersetzungen löschen, die Sie nicht ändern wollen und Ihre Übersetzungen hinzufügen.
Nun müssen wir in unserer Buildout-Konfiguration nur noch beachten, dass die zcml
-Datei von vs.theme
vor allen anderen Paketen geladen wird. Dies kann gewährleistet werden indem vs.theme
als erstes in der Liste der zcml
-Optionen gelistet wird, z.B.:
[instance]
...
eggs =
Zope2
Plone
${buildout:eggs}
zcml =
vs.theme
...
LinguaPlone ist ein Produkt, das mehrsprachige Inhalte in einer Plone-Site ermöglicht. Um LinguaPlone verwenden zu können, müssen zunächst mehrere Sprachen im Plone Language Tool ausgewählt worden sein, für das ein Profil mit der Datei portal_languages.xml
angelegt werden kann. Diese sieht z.B. so aus:
<?xml version="1.0"?>
<object name="portal_languages">
<default_language value="de"/>
<use_cookie_negotiation value="True"/>
<use_content_negotiation value="True"/>
<use_request_negotiation value="True"/>
<display_flags value="False"/>
<start_neutral value="False"/>
<supported_langs>
<element value="en"/>
<element value="de"/>
</supported_langs>
</object>
Anschließend sollte der View /@@language-setup-folders
aufgerufen werden, damit auch die Startseite für beide Sprachen zur Verfügung gestellt werden kann. Dieser View liefert dann folgende Ausgabe:
Setup of language root folders on Plone site 'mysite'
Added 'en' folder: en
INavigationRoot setup on folder 'en'
Added 'de' folder: de
INavigationRoot setup on folder 'de'
Translations linked.
Portal default page removed.
Moved default page 'front-page' to folder 'de'.
Root language switcher set up.
Der View strukturiert die Site in folgenden Schritten um:
Zunchst wird für jede der angegebenen Sprachen ein Ordner erstellt.
Jeder dieser Ordner wird an das INavigationRoot
-Interface gebunden.
Nun werden die entsprechenden Verlinkungen zwischen diesen Ordnern für die Übersetzung erstellt.
Anschließend wird die Standardseite in den Ordner verschoben, dessen Sprache sie zugeordnet ist.
Schließlich wird der root language switcher aufgesetzt um vom ISiteRoot
-Objekt auf den Ordner der Standardsprache umzuleiten.
Programmatisch kann dies in der setuphandlers.py
-Datei geschehen:
import transaction
...
class Generator:
def configureLinguaPlone(self, portal):
pl = getToolByName(portal, 'portal_languages')
pl.supported_langs = ('de', 'en')
transaction.savepoint(1)
portal.restrictedTraverse('@@language-setup-folders')()
def setupVarious(context):
...
gen.configureLinguaPlone(site)
Werden sprachneutrale Inhalte in mehreren Sprachen benötigt, z.B. für Bilder, so kann es sich empfehlen, einen Ordner mit diesen Inhalten auf derselben Ebene wie die Sprachenordner anzulegen.
Damit nun die Bilder in diesem Ordner auch aus dem Popup-Fenster von TinyMCE eingebunden werden können, erstellen wir einen MonkeyPatch mit collective.monkeypatcher. Zunächst wird folgendes in die configure.zcml
-Datei eingetragen:
<configure
...
xmlns:monkey="http://namespaces.plone.org/monkey">
...
<monkey:patch
description="TinyMCE JSON Folder listing should ignore INavigationRoot"
class="Products.TinyMCE.adapters.JSONFolderListing.JSONFolderListing"
original="getListing"
replacement=".patches.getListing"
/>
Anschließend erstellen wir die patches.py
-Datei mit der Methode getListing
. Diese ist übernommen aus Products.TinyMCE.adapters.JSONFolderListing.py
mit folgender Änderung:
68c68,69
< if INavigationRoot.providedBy(object) or (rooted == "True" and document_base_url[:-1] == object.absolute_url()):
---
> #if INavigationRoot.providedBy(object) or (rooted == "True" and document_base_url[:-1] == object.absolute_url()):
> if (rooted == "True" and document_base_url[:-1] == object.absolute_url()):
Damit auch sprachneutrale Referenzen eingebunden werden können, muss auch für das ReferenceBrowserWidget ein MonkeyPatch bereitgestellt werden. Zunächst wird dieser registriert in der configure.zcml
:
<monkey:patch
description="Navigation support for the ReferenceBrowserWidget across INavigationRoot"
class="archetypes.referencebrowserwidget.browser.view.ReferenceBrowserPopup"
original="breadcrumbs"
replacement=".patches.breadcrumbs"
/>
Nun wird in patches.py` die Methode ``breadcrumbs
kopiert aus archetypes.referencebrowserwidget.browser.view.ReferenceBrowserPopup
und folgendermaßen abgeändert:
12c14,21
< portal_state.navigation_root_url())}]
---
> portal_state.portal_url())}]
>
> if portal_state.portal_url() != portal_state.navigation_root_url():
> nav_root_path = portal_state.navigation_root_path()
> nav_root = self.context.restrictedTraverse(nav_root_path)
> newcrumbs.append({'Title': nav_root.Title(),
> 'absolute_url': self.genRefBrowserUrl(
> portal_state.navigation_root_url())})
Wollen Sie LinguaPlone mit ihren eigenen Inhaltstypen verwenden, werden die Klassen und Methoden nicht direkt von Archetypes importiert, sondern es wird zunächst versucht, sie von LinguaPlone zu übernehmen:
try:
from Products.LinguaPlone.public import *
except ImportError:
# No multilingual support
from Products.Archetypes.public import *
Sprachunabhängige Felder, z.B. für Namen und Datum, werden vom Originalartikel (canonical item) übernommen. Die Werte werden jedoch in jedem übersetzten Artikel gespeichert, sodass jeder Artikel jedes Attribut enthält und damit aus dem Kontext verschoben oder direkt referenziert werden können.
Die Sprachunabhängigkeit wird für ein Feld in der AT-Schemadefinition angegeben mit languageIndependent=1
.
Beim ersten Aufruf einer LinguaPlone-Site wird der Header HTTP_ACCEPT_LANGUAGE
, der vom Browser gesendet wird, verwendet, um zu entscheiden, welche Sprache verwendet wird. Anschließend wird ein Cookie mit dieser Entscheidung zurückgesendet. Diese Sprache wird dann solange verwendet, bis vom Nutzer explizit eine andere Sprache in der Plone-Site ausgewählt wird – dann wird auch der Cookie aktualisiert. Steht an einer anderen Stelle der Site ein Artikel nicht in der gewünschten Sprache zur Verfügung, wird eine Seite mit den verfügbaren Sprachen angezeigt.
LinguaPlone filtert in der Suche für alle Artikeltypen, die verschiedene Sprachen unterscheiden, diejenigen heraus, die nicht der Sprachauswahl entsprechen.
Soll in allen verfügbaren Sprachen gesucht werden, kann in der Suche Language=all
angegeben werden.
Mit slc.linguatools und raptus.multilanguagefields stehen noch zwei weitere Werkzeuge für mehrsprachige Inhalte in Plone zur Verfügung.
In diesem Kapitel erfahren Sie, wie auch die Inhalte relationaler Datenbanken in Ihre Plone-Site integriert werden können.
Für Registration als auch Registrant werden die Felder registration_key
, respektive occurrence_key
angelegt, die auch im Katalog indiziert werden sollen. Mit ihnen werden die hierarchischen ZODB-Inhalte auf ein relationales Datenbankmodell abgebildet.
Anschließend erstellen wir entsprechende Interfaces in vs.registration.interfaces
:
class IRegistration(Interface):
"""A folder containing registrants
"""
contains('vs.registration.interfaces.IRegistrant',)
registration_key = schema.ASCIILine(title=_(u"Registrant key"),
description=_(u"This should match the registration key used by the booking system"),
required=True)
...
class IRegistrant(Interface):
"""A registrant
"""
occurrence_key = schema.ASCIILine(title=_(u"Occurrence key"),
description=_(u"This should match the occurrence key used by the booking system"),
required=True)
...
Obwohl sie nicht in der ZODB gespeichert werden, werden Occurrence- und Reservation-Datensätze in Python-Code verarbeitet. Daher werden für beide zunächst ebenfalls Interfaces definiert:
class IOccurrence(Interface):
"""A single occurrence
"""
occurrence_key = schema.Int(title=_(u"Occurrence identifier"),
description=_(u"A unique id for this occurrence"),
required=True,
readonly=True)
registration = schema.Object(title=_(u"Registration"),
schema=IRegistration
required=True,
readonly=True)
occurrence_time = schema.Date(title=_(u"Date/time"),
required=True,
readonly=True)
vacancies = schema.Int(title=_(u"Vacancies"),
description=_(u"Vacancies for this Occurrence"))
class IReservation(Interface):
"""A reservation for a particular occurrence
"""
customer_name = schema.TextLine(title=_(u"Customer name"),
description=_(u"The name of the customer making the reservation"),
required=True)
num_reservations = schema.Int(title=_(u"Number of reservations"),
description=_(u""),
required=True,
min=1)
occurrence = schema.Object(title=_(u"Occurrence"),
description=_(u"Occurrence to book for"),
schema=IOccurrence,
required=True)
Diese Interfaces sind implementiert als einfache Domainklassen, die wir später auf die Datenbanktabellen abbilden.
occurrence.py
:
class Occurrence(object):
"""A single occurrence.
"""
implements(IOccurrence)
occurrence_key = None
registration = None
occurrence_time = None
vacancies = 0
reservation.py
:
class Reservation(object):
"""A reservation for a particular occurrence
"""
implements(IReservation)
customer_name = u""
num_reservations = 0
occurrence = None
In diesem Fall wird nicht wie in Archetypes-Artikeltypen letztlich abgeleitet von persistence.Persistent
, und somit sind die Daten auch nicht in der ZODB verfügbar.
Im weiteren Verlauf sollen Listen der Occurrences in die Ansichten des Registration-Artikeltyps eingeblendet werden. Angemeldete Nutzer sollen so in der Lage sein, für eine bestimmte Occurrence Reservierungen vorzunehmen.
Um jedoch die Views nicht direkt mit der Datenbank kommunizieren zu lassen, werden Hilfsmethoden eingeführt, wobei die Datenbankoperationen abstrahiert werden. Hiermit werden dann auch Tests möglich, die nicht auf eine produktive Datenbank zurückgreifen müssen.
Zunächst werden die Interfaces für die Hilfsmethoden in vs.registration.interfaces
definiert:
class IRegistrations(Interface):
"""Searches appropriate occurrences
"""
def registrations_for_registrant(registrant):
"""Return a list of all registrations of a specific registrant as
list of dictionaries with keys 'registration_key', 'url' and
'title'.
"""
def occurrence_by_key(occurence_key):
"""Get an IOccurrence from an occurrence key
"""
class IReservations(Interface):
"""A utility capable of making reservations
"""
def __call__(reservation):
"""Make a reservation
"""
Im weiteren Verlauf werden diese Hilfsmethoden dann implementiert und Views erstellt, die diese nutzen.
In diesem Beispiel verwenden wir MySQL 5.0 mit InnoDB-Tabellen, die grundlegenden Konzepte lassen sich jedoch leicht auch auf andere relationale Datenbanken übertragen.
Hier das Skript zur Definition der Tabelle registration
und einiger Beispieldaten:
create database if not exists registration;
use registration;
-- Occurrences
create table if not exists occurrence (
occurrence_key integer unsigned not null auto_increment primary key,
registration_key char(4) not null,
registrant_key char(4) not null,
occurrence_time datetime not null,
vacancies integer unsigned not null,
index showing_registration_key(registration_key),
index showing_registrant_key(registrant_key),
index showing_occurrence_time(occurrence_time),
index showing_vacancies(vacancies)
) engine=InnoDB;
-- Reservations
create table if not exists reservation (
reservation_key integer unsigned not null auto_increment primary key,
occurrence_key integer unsigned not null,
num_reservations tinyint unsigned not null,
customer_name varchar(64) not null,
index reservation_num_reservations(num_reservations),
foreign key(occurrence_key)
references occurrence(occurrence_key)
on update restrict
on delete restrict
) engine=InnoDB;
Für unser Beispiel wird der Zugang zu MySQL durch root
ohne Passwort ermöglicht. Für Produktivsysteme sollte jedoch selbstverständlich ein Passwort vergeben werden.
SQLAlchemy ist eine Python-Bibliothek zur Integration relationaler Datenbanken. SQLAlchemy unterstützt eine Vielzahl relationaler Datenbanken, bietet eine niedrigschwellige Verwaltung der Verbindungen, eine Python-API zur Erstellung von SQL-Anfragen und Object/Relational Mapping (ORM)-Funktionalität.
In unserem Beispiel werden wir SQLAlchemy verwenden um unsere beiden Datenbanken zu implementieren, um Objekte auf Relationen abzubilden und um SQL-Anfragen zu erstellen. Darüberhinaus werden wir mit collective.lead
Datenbankverbindungen verwalten und SQL-Transaktionen in Zope-Transaktionen zu überführen.
Wesentliche Komponenten von SQLAlchemy sind:
Engine
verwaltet die Datenbankverbindungen.
In unserem Fall wird Engine
verwaltet von collective.lead.interfaces.IDatabase
.
Table
repräsentiert eine Datenbanktabelle.
Metadata
bindet Tabellen an eine spezifische Engine
.
Mapper
repräsentiert ein Eintrag in einer Datenbank als Python-Klasse.
Session
verwaltet Instanzen von Mapper
-Klassen. Eine Session kann neue Instanzen von einer Datenbank laden, Änderungen an Objekten sichern und neue Objekte als Records in der Datenbank speichern.
Connection
erlaubt die Ausführung von SQL-Anfragen, entweder als Python-Anweisung oder als String.
SQLAlchemy bietet standardisierte Interaktionsmuster zur Erstellung von Engines, Metadata, Tabellen und Mapper. Dabei ist zu beachten, dass
verschiedene Produkte ihre eigenen Datenbankverbindungen aufbauen;
jede geteilte Datenbankquelle thread-safe ist;
Datenbanktransaktionen mit Zope-Transaktionen synchronisiert werden;
Data Source names (DSN) zur Laufzeit nicht bekannt sind.
collective.lead bietet eine Basisklasse zur Erstellung von Hilfsmethoden, die Verbindungseinstellungen, Tabellen und Mapper kapseln. Damit wir dies in unseren Hilfsmethoden vs.registrations
und vs.reservations
verwenden können, wird collective.lead
als Abhängigkeit in vs.registration/setup.py
eingetragen:
install_requires=[
'setuptools',
# -*- Extra requirements: -*-
'MySQL-python',
'collective.lead>=1.0b3,<2.0dev',
],
Bei einem Aufruf von ./bin/buildout
sollte nun collective.lead
mitinstalliert werden, welches dann die letzte unterstützte Version von SQLAlchemy installiert. Daneben benötigen wir noch das MySQL-python-Paket, welches MySQL-Treiber für Python bereitstellt.
Das Datenbank-Hilfsprogramm selbst wird dann in vs.registration/vs/registration/db.py
erstellt. Die Datei enthält ebenfalls die Implementierung von IDatabaseSettings
, eine persistente, lokale Hilfsmethode zum Speichern der Verbindungseinstellungen:
from persistent import Persistent
from zope.interface import implements
from zope.component import getUtility
from collective.lead import Database
from vs.registration.interfaces import IDatabaseSettings
from sqlalchemy.engine.url import URL
from sqlalchemy import Table, mapper, relation
from vs.registration.occurrence import Occurrence
from vs.registration.reservation import Reservation
class ReservationsDatabaseSettings(Persistent):
implements(IDatabaseSettings)
drivername = 'mysql'
hostname = 'localhost'
port = None
username = ''
password = None
database = ''
class ReservationsDatabase(Database):
@property
def _url(self):
settings = getUtility(IDatabaseSettings)
return URL(drivername=settings.drivername, username=settings.username,
password=settings.password, host=settings.hostname,
port=settings.port, database=settings.database)
def _setup_tables(self, metadata, tables):
tables['occurrence'] = Table('occurrence', metadata, autoload=True)
tables['reservation'] = Table('reservation', metadata, autoload=True)
def _setup_mappers(self, tables, mappers):
mappers['occurrence'] = mapper(Occurrence, tables['occurrence'])
mappers['reservation'] = mapper(Reservation, tables['reservation'],
properties = {
'occurrence' : relation(Occurrence),
})
Die collective.lead.Database
-Klasse erlaubt uns, nur wenige Eigenschaften anzugeben, um eine Datenbankverbindung, Tabellen und Mapper zu erstellen.
Nun wird die ReservationsDatabase
-Methode noch in vs.registration/vs/registration/configure.zcml
registriert:
<utility
provides="collective.lead.interfaces.IDatabase"
factory=".db.ReservationsDatabase"
name="vs.reservations"
/>
Da die lokale ReservationsDatabaseSettings
-Hilfsmethode jedoch erst mit der Installation des Produkts registriert werden muss, kann das Generic Setup-Profil vs.registration/vs/registration/profiles/default/componentregistry.xml
hierfür verwendet werden:
<?xml version="1.0"?>
<componentregistry>
<utilities>
<utility
interface="vs.registration.interfaces.IDatabaseSettings"
factory="vs.registration.db.ReservationsDatabaseSettings"
/>
</utilities>
</componentregistry>
Nun sollte auf die Datenbank zugegriffen werden können mit:
>>> from zope.component import getUtility
>>> from collective.lead.interfaces import IDatabase
>>> db = getUtility(IDatabase, name='vs.reservations')
Dem db
-Objekt stehen anschließend die Eigenschaften von collective.lead.interfaces.IDatabase
zur Verfügung.
Um die Datenbankeinstellungen konfigurierbar zu machen, erstellen wir eine spezielle Website-Konfigurationsseite. Wir werden hierzu zope.formlib
und folgendes Interface verwenden:
class IDatabaseSettings(Interface):
"""Database connection settings.
"""
drivername = schema.ASCIILine(title=_(u"Driver name"),
description=_(u"The database driver name"),
default='mysql',
required=True)
hostname = schema.ASCIILine(title=_(u"Host name"),
description=_(u"The database host name"),
default='localhost',
required=True)
port = schema.Int(title=_(u"Port number"),
description=_(u"The database port number. Leave blank to use the default."),
required=False)
username = schema.ASCIILine(title=_(u"User name"),
description=_(u"The database user name"),
required=True)
password = schema.Password(title=_(u"Password"),
description=_(u"The database password"),
required=False)
database = schema.ASCIILine(title=_(u"Database name"),
description=_(u"The name of the database on this server"),
required=True)
Für das Formular selbst nutzen wir dann die Infrastruktur von plone.app.controlpanel
in vs.registration/vs/registration/browser/dbsettings.py
verwendet:
from zope.component import getUtility
from zope.formlib import form
from plone.app.controlpanel.form import ControlPanelForm
from collective.lead.interfaces import IDatabase
from vs.registration.interfaces import IDatabaseSettings
from vs.registration import RegistrationMessageFactory as _
def reservations_database_settings(context):
return getUtility(IDatabaseSettings)
class ReservationsDatabaseControlPanel(ControlPanelForm):
form_fields = form.FormFields(IDatabaseSettings)
form_name = _(u"Reservations Database settings")
label = _(u"Reservations Database settings")
description = _(u"Please enter the appropriate connection settings for the database")
def _on_save(self, data):
db = getUtility(IDatabase, name='vs.reservations')
db.invalidate()
reservations_database_settings
Der Adapter bindet die Werte aus dem Konfigurationsformular an context
, also an Plone Site Root.
ReservationsDatabaseControlPanel
Die Klasse definiert den View für das Formular, das zope.formlib
aus dem IDatabaseSettings
-Interface generiert.
_on_save
Funktion, die von der Basisklasse aufgerufen wird sobald auf Speichern geklickt wird. Dabei leert die invalidate
-Methode alle internen Caches, sodass die neuen Einstellungen wirksam werden.
Anschließend werden in vs.registration/vs/registration/browser/configure.zcml
Icon, View und Adapter registriert:
<browser:resource
name="dbsettings_icon.gif"
image="dbsettings_icon.gif"
/>
<browser:page
name="reservations-database-controlpanel"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
class=".dbsettings.ReservationsDatabaseControlPanel"
permission="cmf.ManagePortal"
/>
<adapter
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
provides="vs.registration.interfaces.IDatabaseSettings"
factory=".dbsettings.reservations_database_settings"
/>
Damit das Konfigurationsformular auf der Plone-Konfigurationsseite angezeigt wird, wird die Datei vs.registration/vs/registration/profiles/default/controlpanel.xml
mit folgendem Inhalt erstellt:
<?xml version="1.0"?>
<object name="portal_controlpanel" meta_type="Plone Control Panel Tool">
<configlet title="Reservations Database" action_id="ReservationsDatabase"
appId="ReservationsDatabase" category="Products" condition_expr=""
url_expr="string:${portal_url}/@@reservations-database-controlpanel"
visible="True">
<permission>Manage portal</permission>
</configlet>
</object>
Nun wird noch das Icon in vs.registration/vs/registration/profiles/default/actionicons.xml
registriert:
<?xml version="1.0"?>
<action-icons>
<action-icon category="controlpanel"
action_id="ReservationsDatabase"
title="Reservations Database"
priority="0" icon_expr="++resource++dbsettings_icon.gif"/>
</action-icons>
Beachten Sie bitte, dass das action_id
-Attribut in beiden Profilen gleich ist. Und so sieht das Plone-Konfigurationsformular aus:
Die Implementierung des IRegistrations
-Interfaces aus Hilfsmethoden erfolgt nun in occurrence.py
:
from zope.interface import implements
from zope.component import getUtility
from zope.app.component.hooks import getSite
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.utils import getToolByName
from vs.registration.interfaces import IRegistrant
from vs.registration.interfaces import IRegistration
from vs.registration.interfaces import IOccurrence
from vs.registration.interfaces import IRegistrations
import sqlalchemy as sql
from collective.lead.interfaces import IDatabase
...
class Registrations(object):
implements(IRegistrations)
def registrations_for_registrant(self, registrant):
db = getUtility(IDatabase, name='vs.reservations')
connection = db.connection
statement = sql.select([Occurrence.c.registration_key],
sql.and_(
Occurrence.c.registrant_key == registrant.registrant_key,
),
distinct=True)
results = connection.execute(statement).fetchall()
registration_keys = [row['registration_key'] for row in results]
site = getSite()
catalog = getToolByName(site, 'portal_catalog')
return [ dict(registration_key=registration.registration_key,
url=registration.getURL(),
title=registration.Title,)
for registration in
catalog(object_provides=IRegistration.__keyentifier__,
registration_key=registration_keys,
sort_on='sortable_title')
]
In der Registrations
-Klasse werden Occurrences von Registrants in Registrations gefunden. Dabei wird zunächst die Datenbankverbindung hergestellt. Anschließend werden verschiedene SQLAlchemy-Konstrukte verwendet um eine Datenbankabfrage an der occurrence
-Datenbank vorzunehmen. So bedeutet z.B. die Syntax Occurrence.c.registrant_key
, dass ein Mapping zwischen der registrant_key
-Spalte (c
olumn) der occurrence
-Tabelle und der Occurrence
-Klasse stattfindet. Die Syntax ist umfangreich beschrieben in der SQLAlchemy-Dokumentation. Schließlich wird eine Plone-Katalogabfrage nach registrant
-Objekten mit den in der occurrence
-Tabelle gefundenen registration_keys
erstellt. Diese werden dann in eine Liste von Wörterbuchern, wie sie in IRegistrations
definiert sind, gepackt.
Die occurrences
-Methode nutzt hingegen SQLAlchemys ORM-API um die Occurrences eines gegebenen Registrant als Liste von Occurrence
-Objekten auszugeben:
def occurrences(self, registrant, registration):
db = getUtility(IDatabase, name='vs.reservations')
session = db.session
occurrences = session.query(Occurrence).select(sql.and_(
Occurrence.c.registrant_key==registrant.registrant_key,
Occurrence.c.registration_key==registration.registration_key,
),
)
for occurrence in occurrences:
occurrence.registrant = registrant
occurrence.registration = registration
return occurrences
Da kein Mapping zwischen den Registrant
- und Registration
-Klassen und der Datenbank stattfindet, können sie von SQLAlchemy nicht geladen und zurückgegeben werden. Stattdessen werden die registrant
- und registration
-Attribute direkt beim Laden des Objekts gesetzt.
Im Gegensatz zur Registrations
- werden mit der Reservations
-Hilfsmethode nicht nur Abfragen an die Datenbank gestellt sondern auch Datensätze geschrieben. Die reservations.py
-Datei sieht so aus:
from zope.interface import implements
from zope.component import getUtility
from vs.registration.interfaces import IReservations
from vs.registration.interfaces import ReservationError
from vs.registration.occurrence import Occurrence
from vs.registration import RegistrationMessageFactory as _
import sqlalchemy as sql
from collective.lead.interfaces import IDatabase
class Reservations(object):
implements(IReservations)
def __call__(self, reservation):
db = getUtility(IDatabase, name='vs.reservations')
session = db.session
occurrence = reservation.occurrence
session.refresh(occurrence)
if occurrence.vacancies <= 0:
raise ReservationError(_(u"There are not enough vacancies anymore!"))
elif occurrence.vacancies < reservation.num_reservations:
raise ReservationError(_(u"Not enough reservations remaining!"))
occurrence.vacancies -= reservation.num_reservations
session.update(occurrence)
session.save(reservation)
session.flush()
Die Klasse Reservations
macht Reservierungen wobei zunächst überprüft wird, ob nach Plätze frei sind. Anschließend wird``occurrence`` aktualisiert (refresh
) um zu vermeiden, dass eine andere Transaktion sich die freien Plätze genommen hat. Dann wird die Zahl der verbleibenden freien Plätze aktualisiert (update
) und fügen die neue Reservierung hinzu (save
). Sofort danach wird die Session beendet (flush
) um zu gewährleisten, dass die Änderungen gespeichert werden.
Sind für die angefragte Occurrence keine Plätze mehr frei, wird die Fehlermeldung ReservationError
ausgegeben. Diese ist definiert in interfaces.py
:
class ReservationError(Exception):
def __init__(self, message):
Exception.__init__(self, message)
self.error_message = message
Folgende Änderungen können nun in browser/registrant.py
hinzugefügt werden:
from zope.component import getUtility
...
@memoize
def registrations(self):
context = aq_inner(self.context)
registrations = getUtility(IRegistrations)
return registrations.registrations_for_registrant(context)
und in browser/registrant.pt
:
<h2 i18n:translate="title_registrated_at">Registrated at</h2>
<ul>
<tal:block repeat="registration view/registrations">
<li>
<a tal:attributes="href registration/url"
tal:content="registration/title" />
</li>
</tal:block>
</ul>
Wir haben eine Entwicklungsumgebung aufgesetzt, die einige Entwicklungswerkzeuge enthält und für die an mehreren Stellen Debugging ermöglicht wurde. Mit Buildout ist es nun einfach möglich, dieses Projekt in eine Produktivumgebung zu überführen.
Zum Entwickeln haben wir eine einfache Zope-Instanz aufgesetzt. Für den Produktivserver wollen wir Zope Enterprise Objects (ZEO) verwenden, da so mehrere Zope-Instanzen auf eine ZODB, die vom ZEO-Server verwaltet wird, zugreifen können. Dies bietet mehrere Vorteile:
Der ZEO-Server und die ZEO-Clients sollten auf verschiedenen Maschinen sitzen und so für eine höhere Ausfallsicherheit sorgen.
Mehrere ZEO-Clients verteilen ggf. auftretende Last in der Anwendungslogik.
ZEO-Clients können unterschiedliche Aufgaben übernehmen und speziell dafür konfiguriert werden, z.B. für anonyme Betrachter, Redakteure und Administratoren.
Wir verwenden unser bisheriges Buildout-Projekt und ergänzen es um eine
Konfiguration für den Produktivbetrieb. Hierzu erstellen wir eine weitere
Konfigurationsdatei deploy.cfg
:
[buildout]
extends =
base.cfg
parts =
zeoserver
instance1
[zeoserver]
recipe = plone.recipe.zeoserver
zeo-address = 127.0.0.1:8000
blob-storage = ${buildout:directory}/var/blobstorage
[instance-base]
zeo-client = True
zeo-address = ${zeoserver:zeo-address}
blob-storage = ${zeoserver:blob-storage}
shared-blob = on
zserver-threads = 4
http-fast-listen = off
[instance1]
<= instance-base
http-address = 8010
debug-mode = off
verbose-security = off
[zeoserver]
¶zeo-address
gibt die Adresse des ZEO-Servers an.
Der Standardwert ist 8100.
[instance1], [instance-profile]
, [instance-debug]
¶zeo-client
wird der Wert auf true
gesetzt, wird aus der Instanz ein ZEO-Client, der
auf einen ZEO-Server mit einer bestimmten zeo-address
verweist.
http-address
Port des HTTP-Servers. Der Standardwert ist 8080
.
ip-address
ist die Standard-IP-Adresse, an der der ZEO-Client auf Anfragen horcht. Ist
kein Wert angegeben, lauscht Zope auf allen IP-Adressen der Maschine. Die
Anweisung kann überschrieben werden in den Server-Konfigurationen
<http-server>
etc. Üblicherweise ist keine IP-Adresse angegeben.
zeo-address
gibt die Adresse des ZEO-Servers an, z.B. 212.42.230.152:8100
.
Der Standardwert ist 8100
.
In temporary storages
werden z.B. Session-Daten gespeichert. Da diese nicht
für jede einzelne Instanz gespeichert werden sollten sondern zentral, können Sie
auf den ZEO-Server verlagert werden.
Zunächst sollte hierzu jede Instanz so konfiguriert werden, dass sie den
temporary storage
auf dem ZEO-Server anlegen soll:
eggs =
tempstorage
zodb-temporary-storage =
<zodb_db temporary>
# Temporary storage database (for sessions)
<zeoclient>
server ${zeoserver:zeo-address}
storage temp
name zeostorage
var ${buildout:directory}/var/filestorage
</zeoclient>
mount-point /temp_folder
container-class Products.TemporaryFolder.TemporaryContainer
</zodb_db>
Um einen bestehenden Abschnitt zu ergänzen, kann mit += z.B. der PDBDebugMode für den zweiten ZEO-Client hinzugefügt werden:
[instance-profile]
<= instance-base
...
environment-vars +=
PROFILE_PUBLISHER 1
Umgekehrt können auch einzelne Werte entfernt werden:
eggs -=
Products.PDBDebugMode
[zeoserver]
verwendet plone.recipe.zope2zeoserver
, um einen ZEO-Server in
parts/zeoserver
aufzusetzen.
zope2-location
verweist auf die im zope2-Abschnitt angegebene Installation.
zeo-address
gibt den Port des ZEO-Servers an, der Standardwert ist 8100.
Es können auch noch weitere Konfigurationsoptionen angegeben werden, z.B.
zeo-conf
Ein relativer oder absoluter Pfad zur zeo.conf
-Datei. Wird kein Pfad
angegeben, wird eine zeo.conf
-Datei mit den in [zeoserver]
angegebenen Werten generiert.
zeo-conf-additional
Zusätzliche Angaben zur zeo.conf
-Datei. Dabei müssen die
nachfolgenden Zeilen eingerückt sein.
Einen vollständigen Überblick über alle Optionen erhalten Sie in plone.recipe.zope2zeoserver.
[instance]
, [instance2]
verwenden plone.recipe.zope2instance
zeo-client
wird der Wert auf true
gesetzt, wird aus der Instanz ein ZEO-Client,
der auf einen ZEO-Server mit einer bestimmten zeo-address
verweist.
zeo-address
gibt die Adresse des ZEO-Servers an, der Standardwert ist 8100.
Meist empfiehlt es sich, den Wert aus dem zeoserver
-Abschnitt zu
übernehmen:
${zeoserver:zeo-address}
zodb-cache-size
Anzahl der Objekte, die der ZEO-Client im Cache halten kann.
debug-mode
, verbose-security
Damit die instance-Instanz die Daten ausliefert und die instance2
-
Instanz zum Debuggen verwendet werden kann, werden nur für die
instance2
-Instanz die Werte auf On gesetzt.
zope-conf-additional
erlaubt weitere Einstellungen der Zope-Konfiguration, in unserem Fall
werden für den zweiten ZEO-Client die zserver-threads
auf 1
heruntergesetzt. Debugging und Maintenance werden deutlich vereinfacht,
da immer nur eine Anfrage gleichzeitig abgearbeitet wird.
Eine Übersicht über die für zope2instance verfügbaren Optionen erhalten Sie in http://pypi.python.org/pypi/plone.recipe.zope2instance.
Die Buildout-Umgebung unseres Projekts kann nun auf den Produktivserver verschoben werden. Hierzu sind mindestens folgende Dateien erforderlich:
bootstrap.py
erstellt die Struktur des Buildout-Projekts einschließlich bin/buildout
.
base.cfg
, devel.cfg
, deploy.cfg
, versions.cfg
die Konfigurationsdateien.
src/
das Verzeichnis, das die gesamte Eigenentwicklung des Projekts enthält.
Anschließend kann das Projekt neu erstellt werden mit:
$ python2.7 bbootstrap.py -c deploy.cfg
$ ./bin/buildout -c deploy.cfg
Würde die Konfigurationsdatei nicht spezifiziert, würde Buildout die Standard-Konfigurationsdatei buildout.cfg
-Datei erwarten.
Anschließend können ZEO-Server und ZEO-Client gestartet werden:
$ ./bin/zeoserver start
$ ./bin/instance1 start
Nun sollte Zope über den Port 8010 erreichbar sein. Falls dies nicht der Fall
sein sollte, können Sie statt start
auch fg
verwenden, um die Prozesse
im Vordergrund laufen zu lassen und eventuelle Fehlermeldungen auf der Konsole
ausgegeben zu bekommen.
Anmerkung 1: Wird die Zope-Instanz unter Linux oder Mac OS X von root
gestartet, muss in der buildout.cfg
-Datei im [instance]
-Abschnitt eine
Direktive für effective-user
angegeben werden, an dessen User ID der Prozess
gebunden wird, nachdem die Ports zugewiesen wurden, z.B.:
[instance]
...
effective-user = plone
So können für die Zope-Instanz auch Ports mit Nummern kleiner 1024 verwendet werden.
Anmerkung 2: Unter Windows lässt sich eine Zope-Instanz als Service installieren, z.B. mit:
> bin\instance install
zeoserver
:In /etc/exports
kann folgendes eingetragen werden:
/plone/vs_buildout/var/blobstorage 192.168.110.0/24(rw)
Damit erlaubt der NFS-Server zeoserver
NFS-Exporte an Server des
internen Netzes 192.168.110.0/24
.
Anschließend wird der NFS-Server neu gestartet mit:
# service nfs restart
NFS-Daemon beenden: [ OK ]
NFS mountd beenden: [ OK ]
NFS-Dienste beenden: [ OK ]
NFS-Dienste starten: [ OK ]
NFS-mountd starten: [ OK ]
NFS-Daemon starten: [ OK ]
instance1
, instance-profile
, instance-debug
Hier kann das NFS gemountet werden, z.B. mit:
mount -t nfs4 192.168.110.3:/plone/vs_buildout/var/blobstorage /plone/vs_buildout/var/blobstorage
oder /etc/mtab
konfigurieren:
...
192.168.110.3:/plone/vs_buildout/var/blobstorage /plone/vs_buildout/var/blobstorage nfs4 rw,addr=192.168.110.3,clientaddr=192.168.110.4 0 0
Für Windows sind entsprechende Dienste für den ZEO-Cluster einzurichten.
Für den ZEO-Server wird der Dienst erstellt mit:
> bin\zeoserver_service install
bzw. in Plone-Versionen < 3.3:
> bin\zeoservice install
Üblicherweise werden die Services mit --startup auto
intstalliert, sodass sie beim Systemstart automatisch gestartet werden.
> bin\instance install
> bin\instance2 install
Zum Deinstallieren dieser Services können Sie einfach folgendes aufrufen:
> bin\instance remove
> bin\instance2 remove
Um einen WebDAV-Server zu konfigurieren kann z.B. im instance
-Abschnitt folgendes angegeben werden:
webdav-address = 8091
webdav-force-connection-close = off
enable-ms-author-via
Wird der Wert auf true
gesetzt, können auch ältere Microsoft Web Folders- und Microsoft Office-Versionen sich mit dem Zope-Server verbinden. Der Standardwert ist off
, da hierdurch einige standardkonforme Anfragen schwierig werden. Weitere Informationen erhalten Sie unter http://www.zope.org/Collectors/Zope/1441
enable-ms-public-header
Wird der Wert auf true
gesetzt, wird ein Public
-Header als Antwort auf eine WebDAV-OPTIONS-Anfrage gesendet:
zope-conf-additional =
...
enable-ms-public-header on
Versionen von Microsofts Web Folders ab Januar 2005 benötigen diese Header-Angabe (s.a.: http://www.redmountainsw.com/wordpress/archives/webfolders-zope).
Mit dem Webserver Apache 2 lässt sich der Zugriff auf WebDAV-Ressourcen komfortabel einrichten. Benötigt werden die Module dav
und dav_fs
, die im Lieferumfang enthalten sind.
Die Standardkonfiguration umfasst neben der Anweisung zum Laden der Module lediglich eine Zeile zum Pfad der Datenbank für DAVLockDB
.
Nach dem Neustart des Webservers ist die Plonesite unter der browser-üblichen Adresse mit http://
zu erreichen. Die Angabe eines zusätzlichen Ports in der Form http://www.veit-schiele.de:8091/
entfällt. Die Konfiguration von Clients ist im Plone-Nutzerhandbuch näher beschrieben.
Für Clients, die mit fehlenden ´´/´´ am Ende nicht korrekt umgehen, kann der Apache wie folgt konfiguriert werden:
<IfModule mod_setenvif.c>
BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
</IfModule>
Zu den Details der möglichen Konfiguration mit mod_setenvif
siehe auch Apache-Umgebungsvariablen in der offiziellen Apache-Dokumentation.
Um einen Clock-Server zu konfigurieren kann z.B. folgendes angegeben werden:
zope-conf-additional =
<clock-server>
method /mysite/do_stuff
period 60
user admin
password secret
host localhost
</clock-server>
Für jeden clock-server
-Abschnitt kann angegeben werden, welcher Nutzer die angegebene Methode aufrufen darf. Im einzelnen:
method
Pfadangabe von Zope root zu einer ausführbaren Zope-Methode (Python-Skript, externe Methode etc.) Die Methode muss keine Argumente erhalten.
period
Sekunden zwischen jedem Aufruf der Methode. Üblicherweise wird mindestens 30
angegeben.
user
ein Zope-Nutzername
password
Das Passwort dieses Zope-Nutzers
host
Der Name des Host, der im Header eines Requests als Host:
angegeben wird. Dies kann bei in Zope angegebenen virtual host rules nützlich sein.
Um zu überprüfen, ob der clock-server
läuft, starten Sie die Instanz oder den ZEO-Client im Vordergrund und schauen, ob eine ähnliche Meldung wie die folgende ausgegeben wird:
2009-03-03 19:57:38 INFO ZServer Clock server for "/mysite/do_stuff" started (user: admin, period: 60)
Anmerkung: Ein Clock-Server sollte immer nur für einen ZEO-Client angegeben werden.
Weitere Informationen erhalten Sie unter Clock and asyncronous tasks.
Jede Zope-Instanz kann mehrere Zope-Datenbanken (ZODB) verwalten. Das Hinzufügen einer neuen ZODB erfolgt in der deploy.cfg
-Datei, z.B.:
[zeoserver]
...
zeo-conf-additional =
<filestorage extra>
path ${buildout:directory}/var/filestorage/Extra.fs
</filestorage>
[instance]
...
zope-conf-additional =
<zodb_db extra>
# Extra database
cache-size 10000
allow-implicit-cross-references false
# ZEOStorage database
<zeoclient>
server ${zeoserver:zeo-address}
storage extra
name zeostorage
var ${buildout:directory}/var
cache-size 500MB
</zeoclient>
mount-point /extra
</zodb_db>
[instance2]
...
zope-conf-additional =
${instance:zope-conf-additional}
zserver-threads 1
filestorage
-Abschnitt¶path
Pfadangabe der Speicherdatei. Die Pfadangaben weiterer Dateien wie index
- und lock
-Dateien werden daraus berechnet.
read-only
Ist die Angabe true
, sind nur Lesezugriffe auf diesen Speicher erlaubt. Beachten Sie, dass die pack
-Operation nicht als schreibender Zugriff angesehen wird und weiterhin erlaubt bleibt.
quota
Maximal zulässige Größe des Speichers. Operationen, die die angegebene Größe überschreiten würden, führen zu einem ZODB.FileStorage.FileStorageQuotaError
.
zodb_db
-Abschnitt¶cache-size
Die Anzahl der Objekte, die für jede Verbindung zwischengespeichert werden können.
Der Standardwert beträgt 5000
.
pool-size
Die erwartete maximale Anzahl gleichzeitig offener Verbindungen. Dies ist keine harte Begrenzung, jedoch führt eine größere Anzahl von Verbindungen zu einem warn
-Eintrag in der Log-Datei.
mount-point
Pfadangabe in dem die Datenbank im Zope-Server eingehängt wird.
Unterscheidet sich die Angabe für den ZEO-Server von der des mount-point, kann folgende Notation verwendet werden:
path_on_client:path_on_zeo_server
zeoclient
-Abschnitt¶storage
Verwaltet der ZEO-Server mehr als einen Speicher, wird für den ZEO-Client der Name des Speichers angegeben, den der benutzen möchte. Der Standardwert für ist 1
, der für die main
-ZODB vergeben wird.
name
Der Name des Speichers. Wird keine Angabe gemacht, wird die Adresse des Servers verwendet.
var
Das Verzeichnis, in dem die persistenten Cache-Dateien gespeichert werden. Üblicherweise werden die Cache-Dateien im aktuellen Verzeichnis gespeichert.
cache-size
Die maximale Größe des Client-Cache in bytes, KB oder MB.
Der Standardwert beträgt 20 MB
.
read-only
Markierung, die anzeigt, ob dies ein Speicher mit ausschließlichem Lesezugriff ist.
Der Standardwert ist false
.
Eine Übersicht über verfügbare Angaben erhalten Sie in parts/zope2/lib/python/ZODB/component.xml
.
Damit die Konfiguration wirksam wird, muss nun das Buildout-Skript erneut aufgerufen werden:
$ ./bin/buildout -Noc deploy.cfg
Schließlich kann im Zope Management Interface (ZMI) ein neuer Mount Point hinzugefügt werden.
collective.recipe.filestorage ist ein Rezept zum Verwalten mehrerer ZODBs:
[buildout]
parts =
filestorage
...
[filestorage]
recipe = collective.recipe.filestorage
parts =
extra
super
parts
Eine Liste von filestorage-Abschnitten. Im Gegensatz zu [buildout]
-Abschnitten muss jeder Abschnitt in einer neuen Zeile beginnen.
zeo
Der Name der plone.recipe.zope2zeoserver
oder plone.recipe.zeoserver
-Abschnitte, für die zusätzliche Filestorages definiert werden sollen.
Der Standardwert ist der erste Abschnitt in der Buildout-Konfiguration.
zopes
Eine Liste der Abschnitte, die plone.recipe.zope2instance
verwenden und in denen die Filestorages hinzugefügt werden sollen.
Üblicherweise werden alle Abschnitte mit plone.recipe.zope2instance
hinzugefügt, die dem zugehörigen zeoserver
-Abschnitt zugeordnet sind.
zodb-cache-size
Die Zahl der Objekte, die maximal im ZODB-Cache gehalten werden sollen.
Der Standardwert ist 5000
.
zodb-name
Der Name der ZODB.
Der Standardwert ist``%(fs_part_name)s``.
zodb-mountpoint
Der Pfad zu den Mount-Points.
Der Standardwert ist /%(fs_part_name)s
.
Wollen Sie z.B. jeder Datei noch die Endung _mountpoint
hinzufügen, ist die Angabe /%(fs_part_name)s_mountpoint
.
zeo-address
Port des zugehörigen ZEO-Servers.
Der Standardwert ist 8100
.
Sinnvollerweise wird er jedoch aus dem zeoserver
-Abschnitt genommen:
${zeoserver:zeo-address}
zeo-client-cache-size
Die maximale Größe des ZEO-Client-Cache in bytes
, KB
oder MB
.
Der Standardwert ist 30MB
.
zeo-storage
Die ID des ZEO-Storages.
Standardwert ist %(fs_part_name)s
.
Soll dem Namen noch die Endung _storage
hinzugefügt werden, so muss %(fs_part_name)s_storage
angegeben werden.
zeo-client-name
Der Name des ZEO-Client.
Der Standardwert ist %(fs_part_name)s_zeostorage
.
Weitere Konfigurationsmöglichkeiten sind in Supported options beschrieben.
In Plone 4 werden Bilder und Dateien als Binary Large Objects (BLOBs) im Dateisystem abgelegt. Daher sind auch für die jeweiligen Mount-Points passende Speicherorte im Dateisystem anzugeben. Hierbei vereinfacht collective.recipe.filestorage
die Buildout-Konfiguration erheblich, da die Angaben für jeden einzelnen Mount-Point nicht mehr im zeoserver
-und jedem instance
-Abschnitt angegeben werden müssen:
[buildout]
parts =
filestorage
...
[filestorage]
recipe = collective.recipe.filestorage
blob-storage = ${buildout:directory}/var/blobstorage-%(fs_part_name)s
parts =
extra
[zeoserver]
...
blob-storage = ${buildout:directory}/var/blobstorage-%(fs_part_name)s
Die Zope Objects Database (ZODB) speichert die Daten, indem sie neue Transaktionen anhängt. Daher wächst die ZODB mit jeder Änderung weiter an, auch wenn Dateien gelöscht oder Transaktionen rückgängig gemacht werden. Durch das Packen der ZODB werden nur noch die Ergebnisse der Transaktionen bis zu einem bestimmten Zeitpunkt zusammengefasst. Dabei wird der Zeitpunkt in Tagen angegeben, für die die Transaktionen noch erhalten werden.
Mit ZEO lässt sich das Packen der Datenbank einfach Automatisieren mit dem
Skript ClientStorage.py
in eggs/ZODB3-3.9.5-py2.6-linux-x86_64.egg/ZEO/
.
Und plone.recipe.zeoserver
stellt mit bin/zeopack
auch einen Wrapper mit
den nötigen Pfadangaben in unserem Buildout-Projekt bereit:
$ ./bin/zeopack days=7
Dabei lässt sich mit days
die Anzahl der Tage angeben, für die alle älteren
Objekte gepackt werden sollen.
Nach dem Packen ist die bisherige ZODB im Dateisystem verschoben worden nach
var/filestorage/Data.fs.old
. Soll also durch das Packen Festplattenspeicher
gewonnen werden, muss diese Datei noch gelöscht werden.
Bemerkung
In Plone 3 befindet sich das Skript ClientStorage.py
in
parts/zope2/lib/python/ZEO/
.
Schließlich kann mit crontab -e
noch ein Cron Job definiert werden, der das
Skript regelmäßig aufruft. Fügen Sie in der Tabelle z.B. folgende Zeile hinzu um
die Datenbank jeden Montag um 0:05 Uhr zusammenzupacken und alle Änderungen
älter als 7 Tage zu löschen:
5 0 * * 1 /home/veit/myproject/bin/zeopack days=7
Weitere Einstellmöglichkeiten erhalten Sie mit man 5 crontab
.
Dieser Eintrag kann auch automatisiert mit dem Rezept z3c.recipe.usercrontab
erstellt werden. Hierzu wird in der deploy.cfg
folgendes eingetragen:
[buildout]
parts =
...
zeopack-crontab
...
[zeopack-crontab]
recipe = z3c.recipe.usercrontab
times = 5 0 * * 1
command = ${buildout:bin-directory}/zeopack days=7
Sind zusätzliche ZODB-Mount-Points definiert worden, so sollten diese ebenfalls
gepackt werden können. Hierfür ist dann jedoch ein eigenes Skript notwendig,
z.B. zeopackall
:
#!/home/veit/myproject/bin/zopepy
username = None
blob_dir = "/home/veit/myproject/var/blobstorage-%(fs_part_name)s"
realm = None
storages = '1','extra'
days = "7"
unix = None
address = "localhost:8100"
host = "localhost"
password = None
port = "8100"
import getopt; opts = getopt.getopt(sys.argv[1:], 'S:B:W1')[0];
opts = dict(opts)
storage = opts.has_key('-S') and opts['-S'] or '1'
blob_dir = opts.has_key('-B') and opts['-B'] or blob_dir
import plone.recipe.zeoserver.pack
for storage in storages:
print 'Packing storage %s' % storage
plone.recipe.zeoserver.pack.main(host, port, unix, days, username, password, realm, blob_dir, storage)
storages
Liste der Namen der zu packenden ZODBs.
1
ist der Standardwert für die main
-Datenbank.
Bemerkung
plone.recipe.zeoserver
steht zopepy
üblicherweise nicht zur
Verfügung. Daher sollte in der deploy.cfg
-Datei folgendes eingetragen
werden:
[zeoserver]
eggs = plone.recipe.zeoserver
...
[zopepy]
...
eggs =
${instance:eggs}
${zeoserver:eggs}
Und auch im Abschnitt zeopack-crontab
sollte auf das neue Skript
verwiesen werden:
[zeopack-crontab]
...
command = ${buildout:directory}/zeopackall
Bemerkung
Für Plone 3 sollte statt plone.recipe.zeoserver
das Rezept
plone.recipe.zope2zeoserver
verwendet und die Zeilen mit blob_dir
im
zeopackall
-Skript gelöscht werden.
Zope bringt mit repozo.py
ein Skript mit, das das Backup der ZODB im laufenden Betrieb ermöglicht. Es befindet sich wie schon zeopack.py
in parts/zope2/utilities/ZODBTools/
. Zudem lassen sich mit repozo.py
auch inkrementelle Backups erstellen. plone.recipe.zope2instance
erstellt einen Wrapper bin/repozo
.
Um nun ein inkrementelles Backup zu erzeugen, erstellen wir zunächst das Verzeichnis backups
, bevor wir repozo
mit den entsprechenden Parametern aufrufen:
$ mkdir backups
$ ./bin/repozo -BvzQ -r backups -f var/filestorage/Data.fs
Soll ein Backup wieder zurückgespielt werden, sollte die Zope-Instanz zunächst gestoppt werden, eine Kopie der voraussichtlich korrupten Data.fs
erstellt werden und erst dann repozo
aufgerufen werden:
$ ./bin/repozo -Rv -r backups -o Data.fs
Anmerkung 1: Da repozo
nach jedem Packen der ZODB wieder nur ein vollständiges Backup durchführen kann, empfiehlt sich das Packen deutlich weniger häufig als das Backup.
Dieser Eintrag kann auch automatisiert mit dem Rezept z3c.recipe.usercrontab
erstellt werden. Hierzu wird in der deploy.cfg
folgendes eingetragen:
[buildout]
parts =
...
backup-crontab
...
[backup-crontab]
recipe = z3c.recipe.usercrontab
times = 15 0 * * *
command = ${buildout:bin-directory}/repozo -BvzQ -r ${buildout:directory}/backups -f ${buildout:directory}/var/filestorage/Data.fs
Mit collective.recipe.backup wird ein Skript erstellt, das für mehrere ZODBs Backups erstellen kann, z.B. zusätzlich für den Katalog in eigener ZODB:
[buildout]
parts =
...
backup
...
[backup]
recipe = collective.recipe.backup
additional_filestorages =
Extra
Super
Falls zum Anlegen mehrer Mount-Points collective.recipe.filestorage
verwendet wurde, kann der [backup]
-Abschnitt auch vereinfacht werden:
[backup]
recipe = collective.recipe.backup
additional_filestorages = ${filestorage:parts}
Folgende zusätzliche Optionen bietet collective.recipe.backup
:
location
Ort, an dem die Backups gespeichert werden.
Der Standardwert ist var/backups
innerhalb des Buildout-Verzeichnisses.
Bei der expliziten Verwendung von location
ist zu beachten, dass der letzte Teil der Angabe als Präfix verwendet wird. Die Angabe:
location = ${buildout:directory}/backups
werden im Ordner des Buildout-Projekts die Unterordner backups_Catalog
und backups_Extra
erzeugt. Diese enthalten dann die Backups der jeweiligen Datenbank.
keep
Anzahl der vollständigen Backups, die aufbewahrt werden.
Der Standardwert ist 2
.
Alle älteren Backups einschließlich ihrer inkrementellen Backups werden automatisch gelöscht.
Wird der Wert auf 0
gesetzt, werden alle Backups aufbewahrt.
datafs
Falls sich die Data.fs nicht im Standardordner var/filestorage/Data.fs befindet kann der Pfad mit dieser Option überschrieben werden.
full
Üblicherweise werden inkrementelle Backups erstellt. Wird der Wert hier auf true
gesetzt, werden jedesmal vollständige Backups erstellt.
debug
In seltenen Fällen sollte in die Log-Datei im debug
-Level geschrieben werden. Dann sollte hier der Wert auf true
gesetzt werden.
snapshotlocation
Ort, an dem die Schnappschüsse gespeichert werden sollen.
Der Standardwert ist var/snapshotbackups
innerhalb des Buildout-Verzeichnisses. Bei expliziter Festlegung gelten bezüglich des Pfads dieselben Regeln für das Ordner-Präfix wie bei location
.
gzip
Der Standardwert ist true
.
Dabei ist die Endung gezippter ZODBs *.fsz
und nicht *.fs.gz
.
additional_filestorages
Hier können Sie zusätzliche Angaben machen, z.B. wenn Sie Ihren Katalog in eine eigene ZODB ausgelagert oder weitere ZODBs als Mount-Point eingebunden haben.
Bei Verwendung von collective.recipe.backup
nach diesem Muster ändert man den command
im Abschnitt [backup-crontab]
auf:
[backup-crontab]
...
command = ${buildout:bin-directory}/backup -q
Alte Backups sollten nach einer bestimmter Zeit wieder gelöscht werden. In unserem folgenden Beispiel werden inkrementelle Backups nach zwei Wochen und vollständige Backups nach fünf Wochen gelöscht:
[buildout]
parts =
...
remove-incremental-backups
remove-full-backups
...
[remove-incremental-backups]
recipe = z3c.recipe.usercrontab
times = 8 0 * * *
command = find ${buildout:directory}/backups -name \*deltafs -ctime +14 -delete
[remove-full-backups]
recipe = z3c.recipe.usercrontab
times = 8 0 * * *
command = find ${buildout:directory}/backups -name \*dat -ctime +35 -delete
Mit plone.recipe.backup
ab Version 2.0 lassen sich auch Sicherungskopien des Blob-Storage anlegen. Plone speichert seit ersion 4.0 üblicherweise alle Bilder und Dateien (Binary large objects) im Dateisystem. Daher müssen von diesen Blob-Storages ebenfalls Sicherheitskopien erstellt werden. Falls der Speicherort der Blob-Storages nicht aus plone.recipe.zope2instance
hervorgeht, kann mit blob_storage
auch explizit der Pfad angegeben werden:
[buildout]
parts =
instance
backup
[instance]
recipe = plone.recipe.zope2instance
user = admin:admin
blob-storage = ${buildout:directory}/var/blobstorage
[backup]
recipe = collective.recipe.backup
Falls erforderlich, kan Buildout verschiedene Skripts zum Erstellen der Sicherungskopien für die ZODBs und die Blob-Storages erstellen:
[buildout]
parts =
...
filebackup
blobbackup
[filebackup]
recipe = collective.recipe.backup
backup_blobs = false
[blobbackup]
recipe = collective.recipe.backup
blob_storage = ${buildout:directory}/var/blobstorage
only_blobs = true
Folgende Attribute kamen neu hinzu:
blob-storage
Verzeichnis, in dem die Blob-Storages gespeichert werden.
Diese Option wird ignoriert, falls backup_blobs = false
.
Falls nichts für blob-storage
angegeben wird, wird versucht, einen Wert zu ermitteln aus einem Abschnitt, in dem eines der folgenden Rezepte verwendet wird:
plone.recipe.zeoserver
plone.recipe.zope2instance
plone.recipe.zope2zeoserver
blob_storage
Alternative Schreibweise für blob_storage
da plone.recipe.zope2instance
ebenfalls diese Variable verwendet, in collective.recipe.backup
jedoch Unterstriche verwendet werden.
backup_blobs
Sofern ein Wert für blob-storage
angegeben ist oder ermittelt werden kann, werden üblicherweise Sichrungskopien der Blob-Storages erstellt. Mit backup_blobs = false
kann dies unterbunden werden.
blobbackuplocation
Verzeichnis, in dem die Sicherungskopien gespeichert werden.
Der Standardwert ist var/blobstoragebackups
innerhalb des Buildout-Verzeichnisses.
blobsnapshotlocation
Verzeichnis, in dem die Schnappschüsse erstellt werden.
Der Standardwert ist var/blobstoragesnapshots
im Buildout-Verzeichnis.
only_blobs
Es wird ausschließlich ein Backup der Blob-Storages erstellt, nicht der ZODBs.
Der Standardwert ist false
.
use_rsync
Das Programm rsync
mit Hard Links zum Erstellen der Blob-Backups wird verwendet.
Der Standardwert ist true
.
Sofern rsync
nicht installiert ist, oder Hard Links nicht funktionieren (Windows), sollte dieses Attribut auf false
gesetzt werden. Dann wird eine einfache Kopie mit Pythons shutil.copytree
erstellt.
Aktuell unterstützt collective.recipe.backup
nicht zusätzliche Blob-Storages. Für diese müsste ggf. ein eigener Buildout-Abschnitt erstellt werden, der ein zweites Set von Backup-Skripten erstellt, z.B.:
[extrablobbackup]
recipe = collective.recipe.backup
blob_storage = ${buildout:directory}/var/extrablobstorage
only_blobs = true
Üblicherweise verwendet collective.recipe.backup
rsync
zum Erstellen der Backups. Dabei werden sog. hard links erstellt um Plattenplatz zu sparen und inkrementelle Backups zu erzeugen. Dies erfordert jedoch Linux/Unix oder Mac OS X.
rsync
kann nun auch verwendet werden um Backups auf entfernte Hosts zu erstellen: rsync-backup.sh
Für Windows liegen uns zum aktuellen Zeitpunkt keine Erfahrungen vor, auf der Basis von Cygwin sollte es jedoch auch auf Windows-Systemen lauffähig sein. Falls nicht, kann use_rsync = false
gesetzt werden und das Blob-Storage-Verzeichnis wird nach dem Backup einfach kopiert.
Alternativ kann das Rezept collective.recipe.rsync verwendet werden. Hierzu kann z.B. die Datei rsync.cfg
mit folgendem Inhalt erstellt werden:
[rsync-file]
recipe = collective.recipe.rsync
source = veit-schiele.de:/srv/www.veit-schiele.de/var/filestorage/Data.fs
target = var/filestorage/Data.fs
script = true
[rsync-blob]
recipe = collective.recipe.rsync
source = veit-schiele.de:/srv/www.veit-schiele.de/var/blobstorage/
target = var/blobstorage/
script = true
script
Üblicherweise ruft collective.recipe.rsync
rsync
während der Installation des Rezepts auf. Sofern ein entsprechendes Skript erstellt wird, kann dieses später z.B. als Cronjob aufgerufen werden um rsync
auszuführen. Hierbei ist lediglich darauf zu achten, dass rsync-file
vor rsync-blob
ausgeführt wird.
port
Optional kann ein alternativer Port für rsync
angegeben werden.
Siehe auch
Weitere Informationen zu rsync
erhaltet Ihr in den Artikel von Mike Rubel: Easy Automated Snapshot-Style Backups with Linux and Rsync.
Eine Zope-Datenbank kann z.B. durch einen Systemabsturz oder einen Festplattendefekt korrumpiert werden. Dies macht sich meist durch einen POSKeyError oder einen CorruptedError und nicht mehr bearbeitbare Objekte bemerkbar.
Dieser Fehler kann verschiedene Ursachen haben, z.B. falsche Längen oder Zeiten von Transaktionen.
fsrecover.py
ist ein Skript, das die Integrität von Transaktionen überprüft
und diejenigen mit korrupten Daten entfernt. Daher ist es auch nicht für
POSKeyErrors geeignet sondern empfiehlt sich vielmehr für CorruptedErrors.
Darüberhinaus kann es auch zu weiteren POSKeyErrors führen wenn eine
fehlerhafte Transaktion entfernt wird und dadurch den Verweis auf ein nicht mehr
vorhandenes Objekt zurücklässt:
$ ./bin/zopepy -m ZODB.scripts.fsrecover -P 0 var/filestorage/Data.fs var/filestorage/Data.fs.recovered &> logrecover.txt
In logrecover.txt
können Sie anschließend nachschauen, wieviele Daten Ihnen
verloren gingen, z.B.:
Recovering var/filestorage/Data.fs into var/filestorage/Data.fs.recovered
. 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . 9 . 0
0 bytes removed during recovery
Packing ...
Um diesen Fehler zu verstehen ist es wichtig zu wissen, dass jedes Objekt in der
Datenbank eine eindeutige ID (OID
) zugewiesen bekommen hat. Diese OID
ist eine binäre Zahl, wie z.B. 0x40A90L
, die auf ein serialisiertes Objekt
verweist. Bei einem POSKeyError kann nun für eine OID
kein passendes
Objekt gefunden werden. So speichert z.B. ein Ordner, der von
OFS.ObjectManager
abgeleitet ist, die enthaltenen Objekte als Werte des
_objects
-Attributs. Die daraus resultierende Liste wird beim Speichern in
eine Liste von OIDs übersetzt. Kann nun beim Laden von objectValues()
eine
OID nicht mehr einem serialisierten Objekt zugewiesen werden, wird ein
POSKeyError ausgegeben.
Mit zc.zodbdgc kommt ein Skript mit, das die Überprüfung von mehreren
ZODBs erlaubt: multi-zodb-check-refs
. Dabei traversiert es ab der Wurzel
durch die gesamten Datenbanken. Dies soll sicherzustellen, dass alle Objekte
erreichbar sind und jedes nicht-erreichbare Objekt protokolliert werden kann.
Darüberhinaus wird bei Blob-Eintragen überprüft, ob ihre Dateien geladen
werden können.
Zum Installieren von zc.zodbdgc wird zunächst eine virtualenv
-
Umgebung aufgesetzt:
$ easy_install-2.6 virtualenv
$ virtualenv --no-site-packages zeo_check
Anschließend wird in dieser virtuelenv
-Umgebung zc.zodbdgc
installiert:
$ cd zeo_check
$ ./bin/easy_install zc.zodbdgc
Packen Sie anschließend Ihre ZODB und kopieren diese in Ihre virtuelenv
-
Umgebung.
Erzeugen Sie eine Konfigurationsdatei storages.cfg
mit folgendem Inhalt:
<zodb>
<filestorage my>
path var/filestorage/my.fs
blob-dir var/blobstorage-my
</filestorage>
</zodb>
Anschließend kann das multi-zodb-check-refs
-Skript aufgerufen werden
mit:
$ ./bin/multi-zodb-check-refs storages.cfg
Sind alle Referenzen Ihrer Datenbank gültig, so erhalten Sie keine Ausgabe. Bei POSKeyErrors sieht die Ausgabe beispielsweise so aus:
!!! main 26798 ?
POSKeyError: 0x68ae
Dieses Skript sollte nun regelmäßig als Cronjob ausgeführt werden:
# Check ZEO Storages
0 6 * * * cd /home/veit/zeo_check; ./bin/multi-zodb-check-refs |mailx -s "Check Storages" -c admin@veit-schiele.de
Möglicherweise können die fehlenden Objekte aus dem Backup zurückgespielt werden.
Mit der -r
-Option erhalten Sie eine Datenbank mit entgegengesetzten
Referenzen, womit sich gegebenenfalls herausfinden lässt, welche Objekte
fehlen:
$ ./bin/multi-zodb-check-refs -r var/filestorage/refdb.fs storages.cfg
!!! main 26798 main 16717
POSKeyError: 0x68ae
Nun schreiben Sie eine refdb.cfg
mit folgendem Inhalt:
<zodb main>
<filestorage 1>
path /home/veit/zeo_check/var/filestorage/refdb.fs
</filestorage>
</zodb>
Anschließend können Sie die Datenbank öffnen:
$ ../myproject/bin/zopepy
>>> import ZODB.config
>>> db = ZODB.config.databaseFromFile(open('./refdb.cfg'))
>>> conn = db.open()
>>> refs = conn.root()['references']
Sie dürften nun eine Fehlermeldung wie diese bekommen:
!!! main 13184375 ?
POSKeyError: 0xc92d77
Nun können Sie die OID desjenigen Objekts herausfinden, von dem aus referenziert wird:
>>> parent = list(refs['main'][13184375])
>>> parent
[13178389]
Wird nun dieses Objekt geladen, sollten Sie einen POSKeyError erhalten:
>>> app._p_jar.get('13178389')
2010-07-16 15:30:18 ERROR ZODB.Connection Couldn't load state for 0xc91615
Traceback (most recent call last):
…
ZODB.POSException.POSKeyError: 0xc92d77
Wir können jedoch die aktuellen Daten des Elternobjekts laden um eine Vorstellung von diesem Objekt zu erhalten:
>>> app._p_jar.db()._storage.load('\x00\x00\x00\x00\x00\xc9\x16\x15', '')
('cBTrees.IOBTree
IOBucket
q\x01.((J$KT\x02ccopy_reg
_reconstructor
q\x02(cfive.intid.keyreference
KeyReferenceToPersistent
…
Nun erzeugen wir ein Fake-Objekt, das dieselbe OID (13184375
) wie das fehlenden Objekt hat mit:
$ ./bin/instance-debug debug
Starting debugger (the name "app" is bound to the top-level Zope object)
…
>>> import transaction
>>> transaction.begin()
>>> from ZODB.utils import p64
>>> p64(26798)
'\x00\x00\x00\x00\x00\x00h\xae'
>>> from persistent import Persistent
>>> a = Persistent()
>>> a._p_oid = '\x00\x00\x00\x00\x00\x00h\xae'
>>> a._p_jar = app._p_jar
>>> app._p_jar._register(a)
>>> app._p_jar._added[a._p_oid] = a
>>> transaction.commit()
Sie sollten nun wieder das Objekt selbst wie auch das Elternobjekt aufrufen können:
>>> app._p_jar.get('\x00\x00\x00\x00\x00\x00h\xae')
<persistent.Persistent object at 0xab7f9cc>
>>> app._p_jar.get('\x00\x00\x00\x00\x00\xc9\x16\x15')
BTrees.IOBTree.IOBucket([(39078692, <five.intid.keyreference…
Schließlich sollten Sie noch die Verbindung zur Datenbank schließen:
>>> conn.close()
>>> db.close()
Falls Sie die Fehlermeldung erhalten POSKeyError: 'No blob file'
, hat Mikko
Ohtamaa das Skript fixblobs.py geschrieben, mit dem sich Inhalte aus der
ZODB löschen lassen, für die kein BLOB mehr vorhanden ist. Siehe auch Fixing
POSKeyError: ‘No blob file’ content in Plone.
analyze.py
zeigt Informationen wie OID, Größe etc. der Objekte in der Datenbank, z.B.:
$ ./Processed 123816 records in 2601 transactions
Average record size is 1276.43 bytes
Average transaction size is 60762.18 bytes
Types used:
Class Name Count TBytes Pct AvgSize
---------------------------------------------- ------- --------- ----- -------
AccessControl.User.UserFolder 1 185 0.0% 185.00
App.ApplicationManager.ApplicationManager 1 189 0.0% 189.00
App.Product.ProductFolder 1 34 0.0% 34.00
BTrees.IIBTree.IIBTree 6705 1783379 1.1% 265.98
BTrees.IIBTree.IIBucket 6957 4584392 2.9% 658.96
…
webdav.LockItem.LockItem 1203 323529 0.2% 268.94
...PersistentAdapterRegistry 2 7074 0.0% 3537.00
zope.ramcache.ram.RAMCache 1 288 0.0% 288.00
============================================== ======= ========= ===== =======
Total Transactions 2601 59.34k
Total Records 123816 154338k 100.0% 1276.43
Current Objects 74107 78439k 50.8% 1083.87
Old Objects 47124 75898k 49.2% 1649.27
fstest.py
überprüft die Datenbank auf korrupte Transaktionen.
fsrecover.py
repariert Transaktionsfehler in der Datenbank.
Üblicherweise lassen sich versehentlich gelöschte Objekte in der Zope Object Database (ZODB) mit dm.historical wiederherstellen sofern die ZODB in der Zwischenzeit nicht gepackt wurde.
Im folgenden Beispiel gehen wir davon aus, dass der Ordner news
in der
Plone-Site Plone
versehentlich gelöscht und nun wiederhergestellt werden
soll.
Hierzu fügen Sie zunächst dm.historical im Buildout-Abschnitt der debug-
instance
hinzu:
[instance-debug]
<= instance
...
eggs =
dm.historical
...
Nachdem das Buildout-Skript durchgelaufen ist, kann die Instanz im Debug-Modus gestartet werden:
$ ./bin/instance-debug debug
Starting debugger (the name "app" is bound to the top-level Zope object)
>>> from DateTime import DateTime
>>> from dm.historical import getObjectAt
>>> site = getObjectAt(app.Plone, DateTime('2014-03-28 14:00:00'))
>>> folder = site['news']
>>> folder.manage_exportObject()
Damit wird der Zustand der Plone-Site Plone
zum Zeitpunkt
2014-03-28 14:00:00
aufgerufen.
Anschließend wird das Objekt news
in das Dateisystem exportiert als
news.zexp
.
Dann kann die Datei news.zexp
verschoben werden in den import
-
Ordner von instance
, also in ${buildout:directory}/var/import/
.
Nun können Sie den Ordner news
im Zope-Management-interface
importieren.
Schließlich sollten Sie im portal_catalog
die Site neu indizieren.
Zope erstellt drei verschiedene Arten von Log-Dateien:
eventlog
Ereignisse, die Debug-Informationen über in der Zope-Instanz verwendete Produkte enthalten.
logger access
Zugriffe, mit denen sich Site-Statistiken produzieren lassen.
logger trace
Detaillierte Informationen über Server-Anfragen (requests).
Die Standardkonfiguration für eine Log-Datei sieht so aus:
<eventlog>
level info
<logfile>
path /home/veit/myproject/var/log/instance.log
level info
</logfile>
</eventlog>
Mögliche Angaben für level
sind critical
, error
, warn
, info
,
debug
und all
.
Jeder der drei logger
-Abschnitte der zope.conf
kann mehrere handler
-
Abschnitte enthalten.
Fünf verschiedene Handler lassen sich angeben:
logfile
syslog
win32-eventlog
http-handler
email-notifier
Um z.B. eine E-Mail-Benachrichtigung bei Fehlern zu erhalten kann der Eintrag folgendermaßen geändert werden:
<eventlog>
level info
<logfile>
path ${buildout:directory}/var/log/instance.log
level info
</logfile>
<email-notifier>
from zope@veit-schiele.de
to admin@veit-schiele.de
subject "Zope Error"
level error
</email-notifier>
</eventlog>
Mögliche Angaben für jeden Handler sind in
parts/zope2/lib/python/ZConfig/components/logger/handlers.xml
beschrieben.
Ab Plone 4.2.2 wird Plone mit einer Version von plone.recipe.zope2instance
ausgeliefert, die die Konfiguration der Log-Rotation in der deploy.cfg
-Datei
angegeben werden, z.B.:
[instance-base]
...
event-log-max-size = 5 MB
event-log-old-files = 7
access-log-max-size = 20 MB
access-log-old-files = 7
Hiermit werden 7 Generationen der Log-Dateien mit maximal 5 MB für Event-Logs und 10 MB für Access-Logs aufbewahrt.
Ab Zope 2.11 kann die Rotation der Zope-Log-Dateien von Zope selbst vorgenommen
werden. Hierzu kann in der deploy.cfg
-Datei z.B. Folgendes angegeben werden:
[instance]
recipe = plone.recipe.zope2instance
...
event-log-custom =
<logfile>
path ${buildout:directory}/var/log/${:_buildout_section_name_}.log
when D
old-files 7
</logfile>
access-log-custom =
<logfile>
path ${buildout:directory}/var/log/${:_buildout_section_name_}-Z2.log
when D
old-files 7
</logfile>
Folgende Angaben sind zum Rotieren der Log-Dateien möglich:
path
Erforderliche Pfadangabe, z.B. ${buildout:directory}/var/log/instance.log
old-files
Wieviele alte Log-Dateien sollen aufbewahrt werden?
Der Standardwert ist 0
.
max-size
Maximale Größe der Log-Datei
when
Wann sollen die Log-Dateien rotiert werden, z.B. D
für täglich
interval
Intervall zwischen den zu rotierenden Log-Dateien
format
Format der Log-Dateien.
Der Standardwert ist %(name)s %(message)s
und der Suffix .log_format
.
Alternativ können auf *ix-Betriebssystemen die Log-Dateien auch mit
logrotate
rotiert werden:
Ändern Sie die deploy.cfg
folgendermaßen:
[buildout]
parts =
...
logrotate
...
[logrotate]
recipe = collective.recipe.template
input = templates/logrotate.conf.in
output = ${buildout:directory}/etc/logrotate.conf
Mit collective.recipe.template lassen sich
Textdateien aus einer Vorlage generieren, wobei die buildout
-Variablen
verwendet werden können.
Dabei können Sie ein Verzeichnis templates
und darin eine
logrotate.conf
-Datei erstellen, z.B. mit folgendem Inhalt:
daily
missingok
rotate 14
mail zope-logs@veit-schiele.de
compress
delaycompress
notifempty
size 1k
${buildout:directory}/var/log/zeoserver.log {
postrotate
${buildout:bin-directory}/zeoserver logreopen
endscript
}
${buildout:directory}/var/log/instance.log ${buildout:directory}/var/log/instance-Z2.log {
sharedscripts
postrotate
${buildout:bin-directory}/instance logreopen
endscript
}
${buildout:directory}/var/log/instance2.log ${buildout:directory}/var/log/instance2-Z2.log {
sharedscripts
postrotate
${buildout:bin-directory}/instance2 logreopen
endscript
}
Damit werden alle Log-Dateien in /home/veit/myproject/var/log/
täglich
rotiert;
Log-Dateien älter als 14 Tage werden gelöscht.
Die Log-Dateien werden an die E-Mail-Adresse zope-logs@veit-schiele.de
gesendet.
Weitere Informationen zu logrotate
erhalten Sie mit man logrotate
.
Ein Eintrag in die crontab mit crontab -e
könnte z.B. so aussehen:
7 0 * * * /usr/sbin/logrotate -s /home/veit/myproject/var/log/logrotate-status /home/veit/myproject/etc/logrotate.conf
Damit wird täglich um 0:07 Uhr logrotate mit den Einstellungen von /home/veit/myproject/etc/logrotate-zope
aufgerufen und ein Statusbericht in /home/veit/myproject/var/log/logrotate-status
geschrieben.
Dieser Eintrag kann auch automatisiert mit dem Rezept z3c.recipe.usercrontab
erstellt werden. Hierzu wird in der deploy.cfg
folgendes eingetragen:
[buildout]
parts =
...
logrotate-crontab
...
[logrotate-crontab]
recipe = z3c.recipe.usercrontab
times = 7 0 * * *
command = /usr/sbin/logrotate -s ${buildout:directory}/var/log/logrotate-status ${buildout:directory}/etc/logrotate.conf
Anmerkung: Falls Sie unter Windows eine Version von Zope verwenden, die kleiner als Zope 2.11 ist, können Sie sich ein eigenes Batch-Skript schreiben, das Ihnen die Log-Dateien rotiert. Als Vorbild kann z.B. folgendes Skript genommen werden: Apache for Win32 Log file Rotation.
Damit der ZEO-Cluster beim Starten des Hosts automatisch mitgestartet wird, legen wir ein Shell-Skript an, das anschließend in das /etc/init.d-Verzeichnis eingebunden wird.
Das Skript /srv/myproject/zeo
kann z.B. so aussehen:
#!/bin/sh
# /etc/rc.d/init.d/zeo
# Startup script for a ZEOCluster
#
# chkconfig: 345 80 20
# description: Zope, a scalable web application server
#
# config: /srv/myproject/deploy.cfg
#
# LSB Source function library
. /lib/lsb/init-functions
RETVAL=0
# list zeo clients in the list below
zeoclients="instance instance2"
# this is for the default install path
clusterpath="/srv/myproject"
prog="ZEOCluster"
start() {
echo -n $"Starting $prog: "
output=`${clusterpath}/bin/zeoserver start`
# the return status of the zeoserver is not reliable, we need to parse
# its output via substring match
if echo $output | grep -q "start"; then
# success
touch /var/lock/zope/$prog
log_success_msg "zeoserver started successfully"
echo
RETVAL=0
else
# failed
log_failure_msg "zeo failed to start or was already started"
echo
RETVAL=1
fi
for client in $zeoclients
do
echo -n $"Starting $client: "
output=`${clusterpath}/bin/${client} start`
# the return status of the instance is not reliable, we need to parse
# its output via substring match
if echo $output | grep -q "start"; then
# success
touch /var/lock/zope/${client}
log_success_msg "$client started successfully"
echo
RETVAL=0
else
# failed
log_failure_msg "$client failed to start or was already started"
echo
RETVAL=1
fi
done
return $RETVAL
}
stop() {
for client in $zeoclients
do
echo -n $"Stopping $client: "
output=`${clusterpath}/bin/${client} stop`
# the return status of the instance is not reliable, we need to parse
# its output via substring match
if echo $output | grep -q "stop"; then
# success
rm /var/lock/zope/${client}
log_success_msg "$client stopped successfully"
echo
RETVAL=0
else
# failed
log_failure_msg "$client failed to stop or was already stopped"
echo
RETVAL=1
fi
done
echo -n $"Stopping $prog: "
output=`${clusterpath}/bin/zeoserver stop`
# the return status of the instance is not reliable, we need to parse
# its output via substring match
if echo $output | grep -q "stop"; then
# success
rm /var/lock/zope/$prog
log_success_msg "zeoserver stopped successfully"
echo
RETVAL=0
else
# failed
log_failure_msg "zeoserver failed to stop or was already stopped"
echo
RETVAL=1
fi
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
echo "ZEO Server:"
output=`${clusterpath}/bin/zeoserver status`
echo $output
for client in $zeoclients
do
echo "Zope Client" $client
output=`${clusterpath}/bin/${client} status`
echo $output
done
;;
restart)
restart
;;
condrestart)
[ -e /var/lock/zope/$prog ] && restart
;;
*)
echo $"Usage: $0 {start|stop|status|restart|condrestart}"
RETVAL=2
esac
exit $RETVAL
Dabei enthält das Skript folgende Optionen:
start
stop
status
restart
condrestart
Anmerkung 1: Da der effective-user
auf zope
gesetzt wurde (s.a. Buildout für Produktivserver) sollte der Nutzer zope
nun selbstverständlich in /var/lock/zope/
schreiben dürfen.
Anmerkung 2: Gegebenenfalls sollte auch die Environment-Variable für den PYTHON_EGG_CACHE
in der deploy.cfg
-Datei festgelegt werden:
[instance]
...
environment-vars =
PYTHON_EGG_CACHE = /home/zope/.python-eggs
Sofern symbolische Links in /etc/rc?.d
angelegt sind, wird beim Neustart des Hosts der ZEO-Cluster ebenfalls gestartet werden. Dabei ist ?
eine Zahl zwischen 0
und 6
, die für die unterschiedlichen Runlevel des Systems stehen. Üblicherweise wird Zope in den Runlevel 3
, 4
und 5
gestartet. Hierfür wird nun zunächst in /etc/init.d
ein symbolischer Link auf unser Skript erzeugt und dann beim Starten dieses Skripts die weiteren symbolischen Links für die genannten Runlevel erzeugt:
$ cd /etc/init.d
$ sudo ln -s /srv/myproject/zeo .
$ sudo /etc/init.d/zeo start
Und falls die symbolischen Links für die Runlevel wieder entfernt werden sollen, kann dies durch folgenden Aufruf geschehen:
$ sudo chkconfig --level 345 zeo off
Falls sie nicht die notwendigen Rechte besitzen sollten, um die entsprechenden init
-Skripte zu schreiben zu können, kann der Cluster beim Neustart auch über einen Eintrag in der User-crontab gestartet werden. Der Eintrag hierfür kann in der deploy.cfg
angegeben werden:
[buildout]
...
parts =
...
reboot
[reboot]
recipe = z3c.recipe.usercrontab
times = @reboot
command = ${buildout:directory}/zeo start
Soll das zeo
-Skript unter Versionsverwaltung von Subversion gestellt werden, muss Subversion noch mitgeteilt werden, dass es sich um eine ausführbare Datei handeln soll:
$ svn propset svn:executable ON zeo
Mt den ZODB Replication Services (ZRS) können Systemadministratoren ihre ZODB auf zwei oder mehrere Storage-Server replizieren.
Dies reduziert die Ausfallzeiten sowohl bei geplanten als auch bei ungeplanten Ausfällen, indem Administratoren:
unternehmenskritische Daten auf zwei oder mehrere Datenbank-Server replizieren
des Storage-Cluster in einem Wide Area Network (WAN) verteilen
die primären und sekundären Server verwalten und überwachen, sodass ggf. ein schneller Failover vom primärem zum sekundärem Storage-Server realisiert werden kann
Wenn der primäre Storage-Server ausfällt, nimmt der sekundäre Server seinen
Platz ein und die ZEO-Clients verbinden sich transparent mit dem neuen ZRS-
Server. Die sekundären Server können jedoch auch vom Netz genommen werden, z.B.
für die Überprüfung und ggf. Reparatur von POSKeyError
und CorruptedError
oder für Upgrades. Wenn dieser sekundäre Server wieder
als Service eingebunden wird, synchronisiert er sich automatisch mit dem
primären Server. Damit vereinfachen die ZODB Replication Services (ZRS) die
routinemäßige Wartung ungemein.
Die ZODB Replication Services (ZRS) verbessern auch die Skalierbarkeit, da sekundäre Server zusätzliche schreibgeschützte ZEO-Verbindungen unter Beibehaltung ihrer Replikationsfunktionen aufbauen können.
Die ZODB Replication Services (ZRS) erhalten einen primären Server für Schreib- und Lesezugriffe und eine beliebige Anzahl von sekundären Servern mit Nur-Lese- Zugriffen. Bei einer Transaktion auf dem primären Server wird diese auf alle verfügbaren sekundären Server repliziert. Wird ein sekundärer kurzzeitig vom Server vom Netz genommen oder ein weiterer sekundärer Server hinzugefügt, so werden diese automatisch auf den aktuellen Stand gebracht.
In der Buildout-Konfiguration muss zc.zrs
als zusätzliches Egg angegeben
werden.
Anschließend kann der primäre Server konfiguriert werden mit:
[zeoserver1]
...
zeo-conf-additional =
<zrs>
replicate-to 5000
<filestorage>
path ${buildout:directory}/var/filestorage/Data.fs
</filestorage>
</zrs>
replicate-to
Adresse des Replikationsservices
Dies kann nur eine Port-Nummer oder ein Hostname mit Portnummer durch einen Doppelpunkt getrennt sein.
Konfigurieren eines sekündären Servers:
[zeoserver2]
...
zeo-conf-additional =
<zrs>
replicate-from primary-host: 5000
replicate-to 5000
keep-alive-delay 60
<filestorage>
path ${buildout:directory}/var/filestorage/Data.fs
</filestorage>
</zrs>
replicate-from primary-host
Adresse des primären Servers
replicate-to
Optionale Angabe.
Wird diese Option genutzt, können andere sekundäre Server von diesem Service replizieren.
keep-alive-delay
Optionale Angabe.
In einigen Netzwerkkonfigurationen werden TCP-Verbindungen nach längerer
Inaktivität unterbrochen. Um dies zu verhindern, sendet der sekundäre
Server regelmäßige no-operation
-Nachrichten um die Verbindung
aufrechtzuerhalten.
Ab Version 1.2.6 von plone.recipe.zeoserver oder Plone 4.3.2 lässt sich der ZRS einfacher installieren:
[zeoserver]
recipe = plone.recipe.zeoserver[zrs]
...
replicate-to
Angabe von Host und Port für den primären Server.
replicate-from
Angabe von Host und Port für den sekundären Server, der die Daten repliziert.
keep-alive-delay
In manchen Netzwerkkonfigurationen wird die TCP-Verbindung unterbrochen bei einer längeren Zeit der Inaktivität. Um dies zu verhindern kann der sekundäre Server periodische Nachrichten an den primären Server schicken.
Siehe auch
Die Steigerung der Performance kann sehr schwierig sein. Es gibt jedoch eine Reihe von Werkzeugen, mit denen sich die Performance messen und die Flaschenhälse auffinden lassen.
erstellt Zope-Publisher-Profile, die im ZMI unter http://127.0.0.1:8080/Control_Panel/DebugInfo/manage_profile
angezeigt werden:
Die Installation erfolgt mit:
[instance]
...
zope-conf-additional =
publisher-profile-file ${buildout:directory}/var/instance/profile.dat
environment-vars =
PROFILE_PUBLISHER 1
erstellt Profile für Page Templates in Zope2. Für jeden TAL-Ausdruck wird die Zeit gemessen und eine Tabelle sortiert nach den benötigten Zeiten erstellt.
Um PTProfiler
zu installieren können Sie einfach folgendes in Ihre buildout.cfg
-Datei schreiben:
eggs =
...
Products.PTProfiler
Nachdem Sie das Buildout-Skript aufgerufen und die Instanz neu gestartet haben, können Sie an jeder Stelle Ihrer Instanz ein PTProfiler Viewer-Objekt hinzufügen und anschließend auf Enable klicken. Nun erhalten Sie eine Liste der Pfade aller aufgerufenen Page Templates und beim Klicken auf einen der Pfade auch die Zeiten für alle Aufrufe.
zeigt den Ablauf von DTML, ZSQL, ZPT, Python-Methoden und -Skripten an, wobei sowohl die absoluten als auch die relativen Zeiten ermittelt werden. Zum Installieren lässt sich in der buildout.cfg
-Datei im Abschnitt [productdistros]
als URL folgendes angegeben:
http://plone.org/products/callprofiler/releases/1.4-fixed/CallProfiler-1.4-fixed.tar.gz
Anschließend kann in /Control_Panel/CallProfiler
angegeben werden, für welche Aufrufe ein Profil erstellt werden soll.
liefert sowohl Zope object call level- als auch Python function call level-Statistiken, z.B.:
167 function calls (160 primitive calls) in 5.229 CPU seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 2.927 2.927 2.960 2.960 document_view:0(__call__)
1 0.754 0.754 3.715 3.715 mysite:0(Request)
1 0.186 0.186 0.187 0.187 search_icon.gif:0(Request)
1 0.132 0.132 0.132 0.132 folder_icon.gif:0(Request)
1 0.080 0.080 0.080 0.080 file_icon.gif:0(Request)
1 0.076 0.076 0.076 0.076 member-cachekey9666.css:0(Request)
1 0.071 0.071 0.073 0.073 document_icon.gif:0(Request)
...
Installieren lässt sich der ZopeProfiler durch die Angabe der URL im Abschnitt [productdistros]
der buildout.cfg
-Datei:
http://www.dieter.handshake.de/pyprojects/zope/ZopeProfiler.tgz
Eine Erläuterung der Tabelle finden Sie in The Python Profilers.
Python-Profiler, der für jede einzelne Funktion ein Profil ausgibt. Er kann einfach aufgerufen werden, z.B. mit:
./bin/instance test -pvvv --profile -m vs.registration
Dabei wird eine Liste der 50 zeitaufwendigsten Funktionen ausgegeben und eine Datei geschrieben wie z.B. myproject/tests_profile.bMAEin.prof
. Diese Datei lässt sich editieren, z.B. mit:
$ python
>>> from hotshot.stats import load
>>> s = load('tests_profile.bMAEin.prof')
s.print_stats('.*some_function.*')
Decorator für das Profiling einzelner Funktionen:
from profilehooks import profile
@profile
def my_function(args, etc):
pass
Die Installation erfolgt einfach mit easy_install profilehooks
.
Mit pydoc profilehooks
erhalten Sie eine Liste aller verfügbaren Decorator-Optionen.
Weitere Informationen erhalten Sie in der profilehooks-Dokumentation
gibt low level ZODB-Statistiken aus.
Installation:
[instance]
...
eggs =
...
collective.stats
Beim Starten der Instanz im Vordergrung mit ./bin/instance fg
erhalten Sie
dann z.B. folgende Ausgabe auf der Konsole:
2014-02-17 12:25:30 INFO Zope Ready to handle requests
2014-02-17 12:25:50 INFO collective.stats | 0.0021 0.0014 0.0018 0.0004 0.0000 0000 0000 0000 | GET:/favicon.ico | t: 0.0000, t_c: 0.0000, t_nc: 0.0000 | RSS: 116708 - 116744
2014-02-17 12:25:55 INFO collective.stats | 0.1783 0.0021 0.1779 0.0004 0.0000 0000 0000 0000 | GET:/manage_main | t: 0.0000, t_c: 0.0000, t_nc: 0.0000 | RSS: 116756 - 116948
Die Werte bedeuten dann im Einzelnen:
Header
Detail
time
Dauer innerhalb des Zope Publisher
t traverse
Zeit, zu dem der Zope Publisher
t commit
Dauer für transaction.commit()
t transchain
Dauer für plone.transformchain.applyTransform
setstate
Dauer in Connection.setstate
total
Anzahl der zodb object loads
total cached
Anzahl der Cache loads
modified
Anzahl modifizierter Objekte
rss before
RAM-Verbrauch vor dem Request
rss after
RAM-Verbrauch nach dem Request
Sog. stack traces lang laufender Requests an eine Zope2-Instanz werden periodisch in eine Log-Datei geschrieben. Die Konfiguration des Products.LongRequestLogger
erfolgt über Umgebungsvariablen für diese Instanz:
[instance]
...
eggs =
...
Products.LongRequestLogger
environment-vars =
longrequestlogger_file = ${buildout:directory}/var/log/${:_buildout_section_name_}-longrequest.log
longrequestlogger_timeout = 4
longrequestlogger_interval = 2
longrequestlogger_file
Erforderliche Pfadangabe zu der Datei, in die das Log geschrieben werden soll.
longrequestlogger_timeout
Die Anzahl in Sekunden als Fließkommazahl, nachdem das Logging beginnen soll.
Der Standardwert ist``2``.
longrequestlogger_interval
Die Frequenz, mit der der stack trace geschrieben werden soll.
Der Standardwert ist 1
.
Apache HTTP-Server-Benchmarking-Werkzeug, das einfache Performance-Tests erlaubt.
Unter Debian und Ubuntu lässt sich ab
installieren mit:
$ apt-get install apache2-utils
Anschließend kann es z.B. mit folgenden Optionen aufgerufen werden:
$ ab -n 100 -c 3 http://www.veit-schiele.de/
Damit wird 100-mal die Seite angefragt, wobei immer je 3 Anfragen gleichzeitig gestellt werden.
Um die jeweiligen Einstellungen zu testen, empfiehlt es sich, zunächst die einfache Plone-Site, dann mit CacheFu und schließlich mit Varnish zu testen.
Mehr über Apache Benchmark erfahren Sie mit:
$ ab -h
Beachten Sie, dass Apache Benchmark nur die angegebene URL prüft, nicht die gesamte Seite mit Bildern und CSS-Dateien.
Mit TinyLogAnalyzer lassen sich die Antowrtzeiten des HTTP-Access-Log auswerten. Hierzu muss zunächst die Log-Datei so konfiguriert werden, dass sie auch die Antwortzeiten protokolliert:
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %T/%D" combined
ergibt z.B. folgende Ausgabe:
[31/Oct/2011:13:36:07 +0000] "GET / HTTP/1.1" 200 7918 "" "..." 0/95491
Dabei ist 0/95491
die Zeit in Sekunden und Mikrosekunden, die de Beantwortung des Requests dauerte.
Eine solche Log-Datei kann nun von TinyLogAnalyzer ausgewertet werden.
Apache JMeter wurde entwickelt um Last und Performance von funktionalen Tests zu messen.
Mit FunkLoad lassen sich ebenfalls Lasttests auf Basis von funktionalen Tests erstellen
Mit Firebug lässt sich der Traffic zwischen Ihrem Browser und der Website beobachten:
Dabei werden alle Requests zum Darstellen der gesamten Seite analysiert. Zudem kann man sich die Response Headers für jedes Objekt anzeigen lassen und so herausfinden, ob, von wem und in welchem Umfang tatsächlich gecached wird.
analysiert Webseiten und teilt Ihnen mit, warum Ihre Seiten langsam dargestellt werden. YSlow ist ein Firefox Add-on, das in Firebug integriert ist.
Website, die die Ergebnisse der Messungen mit PageSpeed und YSlow anzeigt, ohne dass diese Plugins installiert sein müssten. Darüberhinaus lassen sich auch Timeline und History anzeigen.
Varnishstat erstellt kontinuierlich aktualisierte Statistiken einer laufenden varnishd
-Instanz, wobei zwischen Hit
und MISS
unterschieden wird.
Plone kann üblicherweise 6 bis 10 Anfragen je Sekunde verarbeiten. Einfache Web-Server, die nicht jedesmal die Seite dynamisch neu erzeugen, können häufig hunderte von Anfragen in der Sekunde verarbeiten.
plone.app.caching hat folgende Voraussetzungen:
plone.app.caching lässt sich einfach mit Buildout installieren indem in der Buildout-Konfigurationsdatei folgendes angegeben wird:
[buildout]
...
extends =
...
http://good-py.appspot.com/release/plone.app.caching/1.0b2
...
eggs =
...
plone.app.caching
Anschließend rufen Sie das Buildout-Skript auf und starten Ihre Instanz neu.
Gehen Sie nun in das Kontrollfeld Erweiterungen der Plone-Konfiguration und aktivieren dort HTTP caching support.
Dies aktiviert automatisch auch die Configuration registry und Plone z3c.form support.
Nach dem Aktivieren finden Sie ein Caching-Kontrollfeld in der Plone-Konfiguration. Dieses Kontrollfeld enthält die folgenden Reiter:
Falls diese Option nicht aktiviert ist, wird nicht gecacht.
Ist diese Option aktiv, werden die Inhalte momprimiert bevor sie zum Browser gesendet werden.
Ist diese Option aktiviert, schickt Plone ggf. HTTP-PURGE-Anfragen an den Caching-Proxy. Dieser muss PURGE erlauben, damit der Cache ggf. geleert werden kann.
Geben Sie hier den Domain-Namen jedes Caching Proxy an, einen je Zeile.
Ist diese Option aktiviert, kann Plone die Views von Artikeln löschen wenn sie verändert oder gelöscht wurden.
Aktivieren Sie diese Option, wenn Sie virtuelle Hosts mit Rewriting vor dem Caching Proxy verwenden.
Falls Sie Rewriting mit virtuellen Hosts konfiguriert haben und Ihre Site über mehrere Domains erreichbar ist (z.B. http://veit-schiele.de
und http://www.veit-schiele.de
), sollten alle verfügbaren Domains eingetragen werden, eine je Zeile. Dies gewährleistet, dass die PURGE-Anfragen für alle Domains gesendet werden.
Wie viele Artikel sollen im Cache gespeichert werden?
Die maximale Zeit in Sekunden, die ein Artikel im Cache gespeichert wird bevor es gelöscht wird.
Zeit in Sekunden, bevor der Cache gereinigt werden soll. Geringere WErte verringern den Speicherverbrauch, erhöhen jedoch die Last.
Regelsätze zur Verknüpfung von Views, PageTemplates und Ressourcen mit Caching-Operatoren. plone.app.caching
enthält sechs verschiedene Regelsätze, die zusammen mit den Beschreibungen im Kontrollfeld ausgewählt werden können:
plone.content.feed
)Regelsatz für Feeds, z.B. RSS oder ATOM.
plone.content.file
)Regelsatz für Dateien und Bilder im Inhaltsbereich.
plone.content.folderView
)Öffentliche Ansicht eines Artikels, der andere Artikel enthalten kann.
plone.content.itemView
)Öffentliche Ansicht eines Artikels, der keine anderen Artikel enthalten kann.
plone.resource
)Bilder und Dateien, die enweder über das Portal Skin Tool ausgeliefert werden oder in registrierten Verzeichnissen im Dateisystem bereitgestellt werden.
plone.stableResource
)Über die Resource Registries verwaltete Ressourcen wie CSS-, Javascript- und KSS-Dateien. Dies sind Dateien, die dauerhaft gespeichert werden können, da sich die URL ändert sobald sich das Objekt selbst ändert.
Für jeden dieser Regelsätze können Sie einen der Operatoren auswählen, der mit plone.app.caching
geliefert wird:
plone.app.caching.strongCaching
)cacht im Browser und Proxy für 24 Stunden und setzt folgende Header:
Last-Modified: <last-modified-date>
Cache-Control: max-age=<seconds>, proxy-revalidate, public
Achtung: Dies sollte nur für Stable Ressources* verwendet werden, die sich nie ändern ohne dass sich auch ihrre URL ändert.
plone.app.caching.moderateCaching
)cacht im Browser, aber läuft sofort ab und cacht im Proxy für 24 Stunden.
Der Operator wird verwendet für die Regelsätze plone.content.feed
und plone.content.file
. Für Feeds wird dann z.B. folgender Header ausgeliefert:
ETag: <etag-value>
Cache-Control: max-age=0, s-maxage=<seconds>, must-revalidate
Und für Dateien wird folgender Header gesetzt:
Last-Modified: <last-modified-date>
Cache-Control: max-age=0, s-maxage=<seconds>, must-revalidate
Achtung: Wird ein Proxy verwendet, dessen Cache nicht zuverlässig geleert werden kann, können veraltete Inhalte ausgeliefert werden.
plone.app.caching.weakCaching
)cacht im Browser aber läuft sofort ab und gibt anschließend den HTTP-Status-Code 304 Not Modified
aus.
Im Caching-Profil with-caching-proxy
wird der operator verwendet für die Regelsätze plone.content.itemView
und plone.content.folderView
.
plone.app.caching.noCaching
)die Antwort verfällt sofort im Browser und verhindert die Validierung.
plone.caching.operations.chain
)erlaubt die Verwendung mehrerer Operatoren zusammen.
Standard-Mapping von plone.app.caching
:
Regelsatz |
Profile |
||
---|---|---|---|
without-caching-proxy |
with-caching-proxy |
with-caching-proxy-splitviews |
|
Content Feed |
weakCaching |
moderateCaching |
moderateCaching |
Content files and images |
weakCaching |
moderateCaching |
moderateCaching |
Content folder view |
weakCaching |
weakCaching |
moderateCaching |
Content item view |
weakCaching |
weakCaching |
moderateCaching |
File and image resources |
strongCaching |
strongCaching |
strongCaching |
Stable file and image resources |
strongCaching |
strongCaching |
strongCaching |
Hier können Sie Parameter für individuelle Caching-Operatoren angeben:
maxage
)Zeit in Sekunden, die die Antwort im Browser oder Caching-Proxy gespeichert werden soll.
Fügt der Antwort einen Cache-Control: max-age=<value>
-Header und einen passenden Expires
-Header hinzu.
smaxage
)Zeit in Sekunden, die die Antwort im Caching-Proxy gespeichert wird.
Fügt der Antwort einen Cache-Control: s-maxage=<value>
-Header hinzu.
etags
)Eine Liste der ETag-Komponenten, die mit dem ETag-Header ausgegeben werden sollen.
Darüberhinaus wird eine 304 Not Modified
-Antwort generiert für Antworten auf If-None-Match
-Anfragen.
lastModified
)Fügt der Antwort einen Last-Modified
-Header hinzu und 304 Not Modified
-Antworten auf If-Modified-Since
-Anfragen.
ramCache
)cacht im Zope-RAM-Cache. Ist die URL nicht eindeutig, können entweder ETags oder Last-Modified der Liste der Parameter hinzugefügt werden um einen Unique Cache Key zu erzeugen.
vary
)Namen der HTTP-Headers in der Anfrage einer URL, die der Caching Proxy für das Ausliefern einer gecachten Antwort benötigt.
anonOnly
)Wird der Wert auf True
gesetzt, so erhalten angemeldete Nutzer immer die aktuellen Inhalte.
Dies funktioniert am besten zusammen mit dem moderate caching-Operator.
Beachten Sie bitte, dass im Caching Proxy der Vary-Header für X-Anonymous
gesetzt ist.
cacheStopRequestVariables
)Eine Liste von Variablen in der Anfrage (einschließlich Cookies), die das Caching verhindern sollen.
Hier können vordefinierte Profile mit Caching-Einstellungen importiert werden.
plone.app.caching
kommt mit drei Standard-Caching-Profilen:
Ohne Caching Proxy
Diese Einstellungen sind hilfreich, wenn kein Caching Proxy verwendet wird.
Mit Caching Proxy
Diese Einstellungen sind hilfreich wenn ein Caching Proxy wie Squid oder Varnish verwendet wird. Dieses Profil unterscheidet sich im wesentlichen dadurch vom Profil Ohne Caching Proxy, dass Dateien und Bilder im Proxy Cache gespeichert werden können.
Mit Caching Proxy (und Split-View-Caching)
Ein Beispiel für ein Profil, das unterschiedliche Ansichten bereitstellt.
Hier können manuell Inhalte des Caching-Proxy gelöscht werden.
Dieser Reiter wird nur angezeigt, wenn Sie in Change Settings Purging erlaubt haben.
Hier können Sie Statistiken zu Purging und RAM-Cache betrachten.
Zunächst wird ein GenericSetup-Profil registriert für das ICacheProfiles
-Marker-Interface registriert:
<genericsetup:registerProfile
name="with-caching-proxy"
title="With caching proxy"
description="Settings useful for setups with a caching proxy such as Squid or Varnish"
directory="profiles/with-caching-proxy"
provides="Products.GenericSetup.interfaces.EXTENSION"
for="plone.app.caching.interfaces.ICacheProfiles"
/>
Dies bewirkt zugleich, dass dieses Profil nicht als Profil im Erweiterungen-Kontrollfeld von Plone ausgewählt werden kann.
Das Verzeichnis profiles/with-caching-proxy
enthält eine registry.xml
-Datei mit Einstellungen für die Regelsätze zum Verknüpfen von Objekten mit Caching-Operationen, z.B.:
<record name="plone.caching.interfaces.ICacheSettings.operationMapping">
<value purge="False">
<element key="plone.resource">plone.app.caching.strongCaching</element>
<element key="plone.stableResource">plone.app.caching.strongCaching</element>
<element key="plone.content.itemView">plone.app.caching.weakCaching</element>
<element key="plone.content.feed">plone.app.caching.moderateCaching</element>
<element key="plone.content.folderView">plone.app.caching.weakCaching</element>
<element key="plone.content.file">plone.app.caching.moderateCaching</element>
</value>
</record>
Um z.B. RAM-Caching für die weak caching-Operation von Ressourcen zu erlauben, wird der Regelsatz plone.content.itemView
verwendet:
<record name="plone.app.caching.weakCaching.plone.content.itemView.ramCache">
<field ref="plone.app.caching.weakCaching.ramCache" />
<value>True</value>
</record>
CacheFu ist eine Sammlung von Produkten, die die Darstellung von Seiten einer Plone-Site deutlich beschleunigen kann, wobei auch Caching-Proxies wie Squid und Varnish unterstützt werden.
CacheFu lässt sich einfach mit Buildout installieren indem unter eggs
im [buildout]
-Abschnitt Products.CacheSetup
angegeben wird.
Nachdem ./bin/buildout
aufgerufen und die Instanz neu gestartet wurde, lässt sich CacheSetup in der Website-Konfiguration unter Zusatzprodukte für die Plone-Site installieren. Anschließend finden Sie in der Konfiguration von Zusatzprodukten Cache Configuration.
Im Main-Reiter kann CacheFu eingeschaltet und die verwendete Cache Policy angegeben werden. Außerdem kann zwischen verschiedenen Server-Konfigurationen gewählt werden.
Im Policy-Reiter kann zwischen verschiedenen Policies gewählt oder neue erstellt werden.
Im Rules-Reiter können Regeln angegeben werden, wobei zwischen drei verschiedenen Arten unterschieden wird:
Eine Regel für Artikeltypen.
Regel für Objekte, die mit einem PolicyHTTPCachingManager assoziiert sind.
Regel für Page Templates.
Dabei wird immer die erste passende Regel verwendet.
Im Headers-Reiter können HTTP-Header-Angaben für eine spezifische Policy angegeben werden.
Mit max_age
wird der Expires Header gesetzt, der dem Browser mitteilt, ob die Seite ohne Revalidierung erneut geladen werden darf. Wenn max_age
in einem der Header Sets von CacheSetup auf 0
gesetzt wird, werden in der Ausgabe der Response Headers 10 Jahre abgezogen, um nicht-synchronisierte Clients abzufangen.
s-maxage
gibt die Zeit an, die eine Seite im Proxy-Cache gehalten werden darf.
vs.registration
-Artikeltypen¶Um die beiden in vs.registration
definierten Artikeltypen für CacheFu zu konfigurieren, gehen Sie im Rules-Reiter zunächst zur Content-Cache-Regel und fügen dort Registrant zu den Content Types hinzu. Damit werden die Inhalte für nicht-angemeldete Nutzer bis zu einer Stunde im Proxy gespeichert.
Für angemeldete Nutzer werden die Inhalte mit einem ETag versehen, das verschiedenen Angaben wie Member ID und Modifikationsdatum enthält. ETags werden an den Browser gesendet, damit dieser entscheiden kann, ob eine Seite aus dem Cache geladen werden darf oder vom Server neu angefordert wird. Dabei nutzen verschiedene Nutzer jedoch nicht denselben Cache, damit auch das Caching personalisierter Seiten möglich ist. Der ETag hat normalerweise eine Laufzeit von einer Stunde (3600 Sekunden), danach wird das Objekt erneut abgerufen.
Schließlich ist noch Registration in die Container-Cache-Regel einzutragen.
Die Cache-Settings lassen sich exportieren und in andere Plone-Sites importieren indem im Import-Reiter des Generic Setup Tool das CacheSetup-Profil ausgewählt wird und anschließend im Export-Reiter die Einstellungen des CacheFu Settings Tool exportiert werden. Die cachesettings.xml
-Datei kann dann in das Standardprofil von vs.policy
übernommen werden.
Varnish ist ein sog. Caching Reverse Proxy, d.h. er sitzt unmittelbar nach dem Web-Server und bildet einen Zwischenspeicher für ausgehende Inhalte.
Zur Installation von Varnish empfehlen wir denPaket-Manager der jeweiligen Distribution.
Für Debian/Ubuntu:
$ sudo apt-get install varnish
Für CentOS/Fedora:
$ yum install varnish
Für Mac OS X:
$ sudo port install varnish
oder:
$ brew install varnish
zum Überprüfen einer Varnish-Instanz steht Ihnen varnishadm
zur Verfügung:
# varnishadm -T localhost:6082 -S /etc/varnish/secret
200
-----------------------------
Varnish Cache CLI 1.0
-----------------------------
Linux,4.4.0-92-generic,x86_64,-junix,-smalloc,-smalloc,-hcritbit
varnish-4.1.1 revision 66bb824
Type 'help' for command list.
Type 'quit' to close CLI session.
varnish>
quit
Cache vollständig löschen:
# varnishadm "ban req.url ~".
Alle .jpg
-Dateien aus dem Cache löschen:
# varnishadm "ban req.url ~ .jpg"
Eine neue *.vcl
-Datei kann geladen werden mit:
# vcl.load <name> <file>
also z.B. mit:
# vcl.load ploneconf /etc/varnish/plone.vcl
Anschließend kann sie aktiviert werden mit:
# vcl.use ploneconf
Anschließend kann Varnish mit Buildout konfiguriert werden. Hierzu tragen Sie
folgendes in Ihre deploy.cfg
-Datei ein:
[buildout]
parts =
...
varnish-config
...
[varnish-config]
recipe = collective.recipe.template
input = templates/plone.vcl.in
output = ${buildout:directory}/etc/plone.vcl
backend-host = 127.0.0.1
backend-port = 8010
Eine examplarische Varnish-Konfigurationsdatei finden Sie hier: plone.vcl.in.
In ihr wird das Backend spezifiziert, das auf localhost
an Port 8080
läuft und erlaubt Anfragen via HTTP-Basic Authentication oder Cookie-basierte
Authentifizierung.
Weitere Informationen zur Varnish-Konfiguration erhalten Sie in Varnish Configuration Language - VCL.
Um einen Einträge in den Log-Dateien in Echtzeit zu sehen, können Sie folgendes eingeben:
# varnishlog
Um sich in Echtzeit die Varnish-Statistik anzuzeigen ähnlich top
, rufen Sie
varnishstat
auf:
# varnishstat
Uptime mgt: 1+23:43:52
Hitrate n: 10 46 46
Uptime child: 1+23:43:53 avg(n): 0.0000 0.0000 0.0000
NAME CURRENT CHANGE AVERAGE AVG_10 AVG_100 AVG_1000
MAIN.uptime 1+23:43:53
MAIN.sess_conn 53834 0.00 . 0.07 0.07 0.07
MAIN.client_req 53834 0.00 . 0.07 0.07 0.07
MAIN.cache_hit 14119 0.00 . 0.00 0.00 0.00
MAIN.cache_miss 39568 0.00 . 0.07 0.07 0.07
MAIN.backend_reuse 38602 0.00 . 0.07 0.07 0.07
...
Üblicherweise schreibt Varnish keine Log-Datei sondern hält die Informationen
nur im Arbeitsspeicher. Wenn Apache-ähnliche Protokolle aus Varnish geschrieben werden sollen, kann dies mit varnishncsa
geschehen.
Mit der Verteilung der Last auf verschiedene Applikationsserver können die
angefragten Objekte schneller ausgeliefert werden.Das vmod_directors
-Modul
ermöglicht diese Lastverteilung auf verschiedene Weisen:
round-robin
(Rundlauf-Verfahren)greift nacheinander auf die einzelnen Instanzen zu.
fallback
versucht jede Instanz aus und wählt die erste, die antwortet.
hash
wählt die Instanz durch Berechnen des Hash eines Strings.
Dies wird häufig verwendet mit ``client.ip``oder einem Session-Cookie, um sog. sticky sessions zu bekommen.
random
verteilt die Last über die Instanzen mit einer gewichteten zufälligen Wahrscheinlichkeitsverteilung.
Das folgende Beispiel zeigt, wie Sie die Round-Robin-Lastverteilung von zwei Plone-Instanzen konfigurieren können:
backend instance1 {
.host = "127.0.0.1";
.port = "8081";
.connect_timeout = 0.4s;
.first_byte_timeout = 300s;
.between_bytes_timeout = 60s;
.probe = {
.url = "/";
.timeout = 5s;
.interval = 15s;
.window = 10;
.threshold = 8;
}
}
backend instance2 {
.host = "127.0.0.1";
.port = "8082";
.connect_timeout = 0.4s;
.first_byte_timeout = 300s;
.between_bytes_timeout = 60s;
.probe = {
.url = "/";
.timeout = 5s;
.interval = 15s;
.window = 10;
.threshold = 8;
}
}
...
import directors;
sub vcl_init {
new vdir = directors.fallback();
vdir.add_backend(instance1);
vdir.add_backend(instance1);
}
...
import std;
sub vcl_recv {
set req.backend_hint = vdir.backend();
...
if (! std.healthy(req.backend_hint)) {
set req.backend_hint = sorryserver();
return(pass);
}
...
}
probe
gibt in unserem Fall an, dass Varnish das /
-Objekt alle 5 Sekunden
aufruft. Falls die Antwort länger als eine Sekunde ausbleibt, nimmt Varnish
an, dass das Backend nicht erreichbar ist. Umgekehrt nimmt Varnish an, dass
das Backend erreichbar ist wenn drei der letzten fünf Verbindungsversuche
erfolgreich waren. Weitere Informationen hierzu erhalten sie in backend
health polling.
Bemerkung
Allgemeine Informationen zur Lastverteilung mit Varnish erhalten Sie unter Backend servers und vmod_directors.
Decorators zum Caching der Werte von Funktionen und Methoden.
Der generische memoize
-Decorator verwendet das GenericCache
-Modul als
Storage. Üblicherweise enthält es maximal tausend Objekte, die maximal eine
Stunde gecached werden.
view
und instance
¶view
und instance
sind zwei Cache-Decorators, die festgelegte cache keys
und storages haben. Hier ein Beispiel für den instance
-Decorator:
from plone.memoize import instance
class MyClass(object):
@instance.memoize
def some_function(self, arg1, arg2):
...
Wird die Funktion some_function()
das erste Mal aufgerufen, wird der
zurückgegebene Wert gespeichert. Anschließend wird bei Aufrufen mit den gleichen
Argumenten die gecachte Version ohne erneute Berechnung ausgeliefert.
Mit dem view
-Decorator wird für denselben Zope3-View im selben Kontext
gecached, z.B. mit:
from plone.memoize import view
class MyView(BrowserView):
@view.memoize
def some_function(self, arg1, arg2):
...
Schließlich kann mit @view.memoize_contextless
das Caching des Views
unabhängig vom Kontext veranlasst werden. Dabei muss die Anfrage jedoch zwingend
zope.annotation
verwenden.
Das Marshallers-Modul bietet mehrere Marshaller. Für Beispiele schauen Sie sich am besten die Docstrings in marshallers.py an.
volatile
-Modul¶Das volatile
-Modul definiert einen Decorator, der uns Angaben darüber
erlaubt, wie der cache key berechnet wird und wo er gespeichert wird.
Hier ein einfaches Beispiel für das Caching eines Five Views mit volatile
-
Caching im ram
-Modul:
from Products.Five import BrowserView
from plone.memoize import ram
def _render_cachekey(method, self, brain):
return (brain.getPath(), brain.modified)
class View(BrowserView):
@ram.cache(_render_cachekey)
def render(self, brain):
obj = brain.getObject()
view = obj.restrictedTraverse('@@obj-view')
return view.render()
Die Ergebnisse der render
-Methode werden über Anfragen hinweg und unabhängig
vom Nutzer gecached. Der Cache wird den Angaben in _render_cachekey
entsprechend aktualisiert sobald sich das Änderungsdatum oder der Pfad des
Brains ändern.
Sollen die Werte der Funktion nur für maximal eine Stunde gespeichert werden, kann derselbe Decorator verwendet werden:
from time import time
...
class View(BrowserView):
@ram.cache(lambda *args: time() // (60 * 60))
...
Weitere Beispiele und Erläuterungen des volatile
-Decorator finden Sie in
volatile.py.
memcached
¶memcached ist ein distributed memory caching
system, das ebenfalls von plone.memoize
unterstützt wird. Hier ein
einfaches Beispiel, wie ein solches Utility in der caching.py
definiert
werden kann:
from zope.interface import directlyProvides
import zope.thread
from plone.memoize.interfaces import ICacheChooser
from plone.memoize.ram import MemcacheAdapter
import os
import memcache
thread_local = zope.thread.local()
def choose_cache(fun_name):
global servers
client=getattr(thread_local, "client", None)
if client is None:
servers=os.environ.get(
"MEMCACHE_SERVER", "127.0.0.1:11211").split(",")
client=thread_local.client=memcache.Client(servers, debug=0)
return MemcacheAdapter(client)
directlyProvides(choose_cache, ICacheChooser)
Anschließend wird die bestehende Konfiguration überschrieben, sodass unser neues
Utility für das ICacheChooser
-Interface verwendet wird. Dies wird in der
overrides.zcml
-Datei angegeben:
<configure
xmlns="http://namespaces.zope.org/zope">
<utility
component=".caching.choose_cache"
provides="plone.memoize.interfaces.ICacheChooser"
/>
</configure>
Weitere Informationen zu memcached
erhaltet Ihr in Python + Memcached: Efficient Caching in Distributed Applications.
archetypes.schematuning bietet Caching für die Archetypes-Schemata. archetypes.schematuning verwendet plone.memoize um die Archetypes-Schema-Methoden des BaseObject
zu cachen. Üblicherweise werden diese faktorisiert und für jede Verbindung entsprechend modifiziert. So werden in einer normalen Plone-Site beim Zugriff auf ein ATDocument-Schema durchschnittlich 80 Requests benötigt. Mit archetypes.schematuning
können solche Zugriffe durchschnittlich um das 18-fache beschleunigt werden.
Für Anwendungen, die auf Archetypes aufsetzen und das Schema dynamisch ändern, steht mit invalidateSchema
eine Methode zur Verfügung, die das alte Schema aus dem Cache löscht. Hierzu müssen diese Anwendungen jedoch entsprechend angepasst werden.
Damit der Apache-Webserver Anfragen an Zope weiterleiten kann, muss das mod_rewrite
Modul mit seinen Abhängigkeiten in Apache’s httpd.conf
angegeben werden:
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
LoadModule proxy_module /usr/lib/apache2/modules/mod_proxy.so
LoadModule proxy_http /usr/lib/apache2/modules/mod_proxy_http.so
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
Bei Debian- und Ubuntu-Distributionen kann die Konfiguration der Module vereinfacht werden mit a2enmod
. Die oben angegebenen Module lassen sich dann einfach aktivieren mit:
$ a2enmod alias proxy proxy_http rewrite
In anderen Distributionen werden die Module meist schon verwendet oder die einzelnen Zeilen müssen nur noch auskommentiert werden.
Anschließend können Sie in httpd.conf
oder einer eingebundenen Datei einen Virtual Host für die Domain www.veit-schiele.de
angeben:
NameVirtualHost 83.223.91.163:80
<VirtualHost 83.223.91.163:80>
ServerName www.veit-schiele.de
RewriteEngine on
RewriteRule ^/(.*) http://83.223.91.163:8082/VirtualHostBase/http/%{SERVER_NAME}:80/vs/VirtualHostRoot/$1 [P]
</VirtualHost>
Dies definiert den Virtual Host für die Domain www.veit-schiele.de
wenn Anfragen am Port 80
hereinkommen. Die IP-Adresse 83.223.91.163
sollte derjenigen in der Listen
-Anweisung entsprechen:
Listen 83.223.91.163:80
Bei Debian- und Ubuntu-Distributionen kann die Erstellung von Virtual Hosts vereinfacht werden mit a2ensite
. Der oben angegebene Virtual Host lässt sich dann einfach aktivieren mit:
# a2ensite www.veit-schiele.de
Site www.veit-schiele.de installed; run /etc/init.d/apache2 reload to enable.
Schließlich wird unter Verwendung der RewriteEngine eine RewriteRule definiert, die für alle dem regulären Ausdruck ^(.*)
entsprechenden URLs weiterleitet:
http://83.223.91.163:8082
verweist auf den Zope-Server, auf den weitergeleitet wird;
VirtualHostBase
informiert das VirtualHostMonster über Protokoll und Host auf den umgeschrieben werden soll. In diesem Fall auf http
und veit-schiele.de:80
.
Als nächstes wird der Pfad auf das Objekt angegeben, das die Site Root sein soll, also unsere Plone-Site. VirtualHostRoot
beendet die Pfadangabe
Mit $1
wird Apache nun mitgeteilt, dass alle, dem regulären Ausdruck entsprechenden Teile der URL hier angehängt werden sollen.
L
weist Apache an, wenn diese Regel zutreffend war, nicht nach weiteren Regeln zu suchen und P
aktiviert das mod_proxy
-Modul, das das URL-Mapping übernimmt.
Anschließend kann Apache mit apachectl graceful
neu gestartet werden und die Plone-Site sollte dann unter http://www.veit-schiele.de
erreichbar sein.
Anmerkung: Das root-Verzeichnis des ZMI ist nicht über den virtuellen Host erreichbar. hierzu muss weiterhin 83.223.91.163:8082
aufgerufen werden.
Üblicherweise liefern wir die gesamte Site https
-verschlüsselt aus. Dabei
wird beim Zugriff auf http://www.veit-schiele.de
weitergeleitet mit:
Redirect permanent / https://www.veit-schiele.de/
Für https://www.veit-schiele.de
benötigt der Apache dann das SSL-Modul:
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
Anschließend können Sie einen weiteren Virtual Host für die Domain www.veit-schiele.de
am SSL-Port 443
angeben:
NameVirtualHost 83.223.91.163:443
<VirtualHost 83.223.91.163:443>
ServerName www.veit-schiele.de
SSLEngine on
SSLCertificateFile /etc/apache2/ssl.crt/server.crt
SSLCertificateKeyFile /etc/apache2/ssl.key/server.key
</VirtualHost>
Schließlich muss noch die Listen
-Anweisung für Anfragen am SSL-Port 443
eingetragen werden.
Falls z.B. aus Performance-Gründen die Kommunikation der anonymen Nutzer nicht verschlüsselt werden soll, kann bei der Anmeldung auf einen anderen
VirtualHost
mit SSL-Verschlüsselung weitergeleitet werden. Umgekehrt sollen
die Nutzer bei der Abmeldung wieder unverschlüsselt auf die Site zugreifen
können. Entsprechend kommt für den VirtualHost an Port 80
folgende Rewrite-Regel hinzu:
# Rewrites the came_from in the URL for https
RewriteCond %{QUERY_STRING} came_from=http(.*)
RewriteRule ^/(.*)login_form$ https://edit.veit-schiele.de/$1login_form?came_from=https%1 [NE,L]
Beachten Sie, dass ?came_from=
nicht direkt in einer RewriteRule
angegeben werden kann und daher der QUERY_STRING
zunächst in der RewriteCond
ausgelesen wird.
Wird login_form
direkt oder auf einem ungültigen Template (z.B. logged_out
) aufgerufen, wird folgende zusätzliche Regel benötigt:
# Switches to https when hit login_form or login_success
RewriteRule ^/login_(.*) https://edit.veit-schiele.de/login_$1 [NE,L]
Schließlich können in der Konfigurationsdatei des VirtualHost an Port 443
noch folgende RewriteRules angegeben werden:
# Switches to http upon logout
RewriteRule ^/(.*)logged_out http://www.veit-schiele.de/$1logged_out [L,P]
# Keeps on https until log out
RewriteRule ^/(.*) http://83.223.91.163:8080/VirtualHostBase/https/%edit.veit-schiele.de:443/vs/VirtualHostRoot/$1 [L,P]
Amerkung 1: Die Anleitung verweist bewusst auf einen anderen ServerName
,
da die Browser in ihrer Cookie-Verwaltung nicht zwischen http
und https
unterscheiden und daher versehentlich doch die Zugangsdaten unverschlüsselt
übertragen werden könnten.
Amerkung 2: Die Anleitung zur Anmeldung via SSL funktioniert nicht für das Login-Portlet.
Hierzu wird die Konfiguration des VirtualHost folgendermaßen erweitert:
# Forbidden HTTP status for all path components beginning with manage
RedirectMatch 403 /manage
zc.recipe.macro liefert eine Reihe von Macros, womit einzelne Buildout-Abschnitte dynamisch aus einem Macro- und einem Parameter-Abschnitt generiert werden können. Dies ermöglicht Buildout, Konfigurationsdaten unabhängig vom Ausgabeformat zu halten und ermöglicht so die Konfiguration verschiedener Dienste für mehrere Sites.
Üblicherweise wird in einem zc.recipe.macro-Abschnitt ein Macro mit den Parametern dieses Abschnitts aufgerufen. Dies läßt sich anschaulich an einer Apache-Konfiguration zeigen.
[buildout]
parts =
...
apache
[apache]
recipe = zc.recipe.macro
macro = apache-macro
result-recipe=collective.recipe.template
targets =
my-apache:mysite-parameters
Im folgenden werden dann die Abschnitte [apache-macro]
und [mysite-parameters]
definiert:
[apache-macro]
domain=$${:domain}
host=$${:host}
port=$${:port}
input = ${buildout:directory}/template/apache-vhost.in
output = ${buildout:parts-directory}/$${:__name__}
[mysite-parameters]
domain=mysite.org
host=83.223.91.163
port=8080
Die Datei templates/apache-vhost.in
sieht z.B. so aus:
${host}
${port}
${domain}
Das Ergebnis ist dann:
[apache]
recipe = zc.recipe.macro:empty
result-sections = my-apache
[my-apache]
recipe = collective.recipe.template
domain = mysite.org
host = 83.223.91.163
input = /home/veit/myproject/template/apache-vhost.in
output = /home/veit/myproject/parts/my-apache
port = 8080
Das Rezept zc.recipe.macro
ändert sich zu zc.recipe.macro:empty
, hat jedoch keine Wirkung mehr. Dennoch muss der Abschnitt vorhanden sein, da er in [buildout]
als Abschnitt angegeben wurde.
Und parts/my-apache
sieht schließlich so aus:
83.223.91.163
8080
mysite.org
Jede weitere Site kann dann einfach folgendermaßen eingetragen werden:
[apache]
...
targets =
...
vs-apache:vs-parameters
...
[vs-parameters]
domain=veit-schiele.de
host=83.223.91.163
port=8090
Analog kann mit tc.recipe.macro
auch awstats konfiguriert werden:
[buildout]
parts =
...
mysite-awstats
[awstats]
recipe = zc.recipe.macro
macro = awstats-macro
result-recipe=collective.recipe.template
targets =
mysite-awstats:mysite-parameters
vs-awstats:vs-parameters
[awstats-macro]
domain=$${:domain}
host=$${:host}
port=$${:port}
input = ${buildout:directory}/template/awstats-conf.in
output = ${buildout:parts-directory}/$${:__name__}
Die Datei templates/awstats-conf.in
sieht z.B. so aus:
${host}
${port}
${domain}
Das Ergebnis ist dann:
[buildout]
parts =
...
awstats
mysite-awstats
vs-awstats
...
[awstats]
recipe = zc.recipe.macro:empty
result-sections =
mysite-awstats
vs-awstats
[mysite-awstats]
domain = mysite.org
host = 83.223.91.163
input = /home/veit/myproject/template/awstats-conf.in
output = /home/veit/myproject/parts/mysite-awstats
port = 8080
recipe = collective.recipe.template
[vs-awstats]
domain = veit-schiele.de
host = 83.223.91.163
input = /home/veit/myproject/template/awstats-conf.in
output = /home/veit/myproject/parts/vs-awstats
port = 8090
recipe = collective.recipe.template
Und parts/my-awstats
sieht schließlich so aus:
83.223.91.163
8080
mysite.org
redturtle.munin stellt aktuell vier Plugins für Munin bereit:
zopethreads
protokolliert die freien Zope-Threads
zopecache
protokolliert die Datenbank-Cache-Parameter
zodbactivity
protokolliert die Aktivität der ZODB
zopememory
protokolliert die Speicherverwendung von Zope unter Linux
Dabei wird gocept.munin für die Registrierung dr Plugins verwendet.
Zur Installation kann folgendes in der deploy.cfg
-Datei angegeben werden:
[instance]
...
eggs =
...
redturtle.munin
zcml =
...
redturtle.munin
Um das Plugin-Hilfsskript zu erstellen, wird zusätzlich noch ein [munin]
-Abschnitt in die deploy.cfg
eingetragen:
[buildout]
parts =
...
munin1
[munin1]
recipe = zc.recipe.egg
eggs = redturtle.munin
arguments = http_address='${instance:http-address}', user='${instance:user}'
Die Werte in der arguments
-Option werden zur Generierung des Hilfsskripts verwendet, das dann als Munin-Plugin verwendet wird. Die Angaben für ip_address
, http_address
, port_base
und user
können hier kommasepariert angegeben werden.
Für jeden weiteren ZEO-Client wird dann ein weiterer Abschnitt eingefügt.
Nun sollte das Plugin folgendermaßen aufgerufen werden können:
http://localhost:8081/@@redturtle.munin.plugins/zopethreads
Der Name des View zopethreads
entspricht dabei dem Namen des Plugins. Der View selbst kann nur mit den Rechten View management screens
aufgerufen werden.
Nun werden noch Symlinks für die Hilfsskripte in myproject/bin
zum Munin-Plugin-Verzeichnis gelegt wobei das Hilfsskript selbst diese Symlinks setzen kann:
$ bin/munin install /opt/munin/etc/plugins [<prefix>] [<suffix>]
Alternativ können die Symlinks auch selbst erstellt werden:
$ cd /opt/munin/etc/plugins
$ ln -s ~/myproject/bin/munin vs_zodbactivity_mysite1
/opt/munin/etc/plugins
ist dabei Ihr munin
-Verzeichnis
~/myproject/
ist das Verzeichnis des Buildout-Projekts
zodb_activity
ist der Name des Plugins
vs
ist der Prefix-Wert
mysite1
ist der Suffix-Wert, der in Munin angezeigt wird.
Schließlich können Sie noch überprüfen, ob das Plugin in Munin ordentlich konfiguriert ist:
$ cd /opt/munin/etc/plugin-conf.d/
$ vim redturtle.conf
... [vs_*_mysite1]
... env.AUTH admin:secret
... env.URL http://localhost:8081/@@redturtle.munin.plugins/%s
Detaillierte Request-Logging-Funktionalität für die ab Zope 2.10 verfügbaren Publication Events.
Zope 2.12
Für Zope 2.10 ist ein Backport der Publication Events verfügbar in
ZPublisherEventsBackport. Dieser wird benötigt und lässt sich mit
Buildout einfach durch plone.postpublicationhook unter Angabe von
[Zope2.10]
installieren:
eggs =
...
plone.postpublicationhook [Zope2.10]
Um haufe.requestmonitoring
zu installieren, muss es einfach in der
buildout.cfg
-Datei den zcml
-Optionen im Instanz
-Abschnitt
hinzugefügt werden:
[instance-base]
...
eggs +=
...
haufe.requestmonitoring
threadframe
zope.app.appsetup
zcml +=
...
haufe.requestmonitoring
Zur Aktivierung von haufe.requestmonitoring
erhält die Instanz einen Abschnitt product-config
mit dem Namen successlogging
und dem Schlüssel filebase
. Dieser gibt den Basisnamen der Log-Dateien an, in unserem Fall request
:
[instance-base]
...
zope-conf-additional =
<product-config successlogging>
filebase /home/veit/vs_buildout/var/log/request
</product-config>
%import haufe.requestmonitoring
<requestmonitor requestmonitor>
period 5s
<monitorhandler dumper>
factory haufe.requestmonitoring.DumpTraceback.factory
# 0 means no repetition.
# A negative value means indefinitely.
repeat -1
time 10s
</monitorhandler>
</requestmonitor>
Beim Logging werden dann zwei Dateien unterschieden: <base>_good.<date>
und <base>_bad.<date>
. Üblicherweise werden Antwortzeiten über 500 ms
als unsuccessful bezeichnet. Falls dieser Standardwert geändert werden
soll, kann ein spezieller ISuccessFull
-Adapter registriert werden.
Nachdem das Buildout-Skript durchlaufen und der ZEO-Cluster neu gestartet
wurde, sind die Subscriber IPubStart
und IPubSuccess
bzw.
IPubFailure
registriert. Für jedes dieser Events wird nun ein Eintrag
in die Log-Datei geschrieben in der Form:
timestamp status request_time type request_id request_info
Mit DumpTraceback
werden diejenigen Anfragen protokolliert, die nach
der in period
angegebenen Zeit nach Anfragen sucht, die länger laufen
als die in time
angegebene Zeit:
%import haufe.requestmonitoring
<requestmonitor requestmonitor>
period 5s
<monitorhandler dumper>
factory Haufe.RequestMonitoring.DumpTraceback.factory
# 0 --> no repetition
repeat -1
time 10s
</monitorhandler>
</requestmonitor>
Eine typische Ausgabe sieht dann so aus:
2009-08-11 14:29:09 INFO Zope Ready to handle requests
2009-08-11 14:29:09 INFO RequestMonitor started
2009-08-11 14:29:14 INFO RequestMonitor monitoring 1 requests
2009-08-11 14:29:19 INFO RequestMonitor monitoring 1 requests
2009-08-11 14:29:24 INFO RequestMonitor monitoring 1 requests
2009-08-11 14:29:24 WARNING RequestMonitor.DumpTrace Long running request
Request 1 "/foo" running in thread -497947728 since 14.9961140156s
Python call stack (innermost first)
Module /home/junga/sandboxes/review/parts/instance/Extensions/foo.py, line 4, in foo
Module Products.ExternalMethod.ExternalMethod, line 231, in __call__
- __traceback_info__: ((), {}, None)
Module ZPublisher.Publish, line 46, in call_object
Module ZPublisher.mapply, line 88, in mapply
Module ZPublisher.Publish, line 126, in publish
Module ZPublisher.Publish, line 225, in publish_module_standard
Module ZPublisher.Publish, line 424, in publish_module
Module Products.ZopeProfiler.ZopeProfiler, line 353, in _profilePublishModule
Module Products.ZopeProfiler.MonkeyPatcher, line 35, in __call__
Module ZServer.PubCore.ZServerPublisher, line 28, in __init__
haufe.ztop erlaubt die Analyse von Zope-Requests zur Laufzeit.
Um haufe.ztop
zu installieren, muss es einfach in der buildout.cfg
-Datei hinzugefügt werden:
[buildout]
parts =
...
ztop
...
[ztop]
recipe = zc.recipe.egg
eggs = haufe.ztop
Nachdem Buildout durchlaufen wurde, stehen Ihnen die beiden Skripts ztop
und zanalyse
zur Verfügung.
ztop
stellt die Request-Informationen dar indem es die Zope-Request-Logfiles auswertet. Diese werden identifiziert durch requestsBasename
und startDate
.
zanalyse
gibt regelmäßig Angaben der Request-Informationen auf der Konsole aus indem es die Zope-Request-Logfiles auswertet. Diese werden identifiziert durch requestsBasename
und startDate
.
Monitoring von Sets von ZEO-Clients mit Aggregation von Error Logs, Instanzen, Threads Loads und VM-Datengröße.
[instance-base]
…
eggs +=
haufe.monitoring
zcml +=
haufe.monitoring
Nachdem das Buildout-Skript durchlaufen und der ZEO-Cluster neu gestartet
wurde, können Sie in den Monitoring-View @@monitor
aufrufen, z.B.
http://mysite/@@monitor
.
Bemerkung
Da momentan noch einige Sicherheitsangaben offen sind, sollte
haufe.monitoring
aktuell nur in internen Netzen verwendet werden.
Mit ZopeHealthWatcher können die Threads der Zope-Anwendung beobachtet werden.
ZopeHealthWatcher kann die Threads sowohl von ZEO-Clients als auch von einfachen Zope-Servern anzeigen.
Für jeden Thread wird angegeben, ob dieser beschäftigt ist, und wenn ja, wird der execution stack angezeigt.
ZopeHealthWatcher kann auch zum Debuggen von locked threads verwendet werden.
ZopeHealthWatcher basiert auf dem Code von DeadlockDebugger.
Die Darstellung erfolgt entweder auf der Konsole oder im Web-Browser.
Erweitern Sie Ihre buildout.cfg
-Datei folgendermaßen:
[buildout]
parts =
...
zhw
eggs =
...
ZopeHealthWatcher
[zhw]
recipe = zc.recipe.egg
eggs = ZopeHealthWatcher
scripts = zope_health_watcher
Nachdem das Egg installiert ist, müssen in eggs/ZopeHealthWatcher-0.3-py2.6.egg/Products/ZopeHealthWatcher/custom.py
die Werte für ACTIVATED
und SECRET
geändert werden, z.B.:
ACTIVATED = True
SECRET = 'secret'
ZopeHealthWatcher kann sowohl in der Konsole als auch im Web-Browser verwendet werden.
zope_health_watcher
-Skript¶Sie können nun zope_health_watcher
aufrufen mit der URL Ihres Zope-Servers oder ZEO-Clients:
$ zope_health_watcher http://localhost:8080
Idle: 3 Busy: 1
OK - Everything looks fine
Ist der Server mit hohem load
ausgelastet, z.B. mit 4 Threads, werden die relevanten Informationen wie Zeit, sysload
, Speicherinformationen und für jeden ausgelasteten Thread Stack, Query, URL als auch den User Agent anzeigt:
$ zope_health_watcher http://localhost:8080
Information:
Time: 2011-06-02T10:52:31.522557
Sysload: 0.25 0.18 0.20 4/1003 32523
Meminfo: MemTotal: 11759712 kB
MemFree: 4799368 kB
Buffers: 204 kB
Cached: 2933200 kB
SwapCached: 0 kB
Active: 4051368 kB
Inactive: 1678532 kB
Active(anon): 2830948 kB
Inactive(anon): 46644 kB
Active(file): 1220420 kB
Inactive(file): 1631888 kB
Unevictable: 2764 kB
Mlocked: 2764 kB
SwapTotal: 4000176 kB
SwapFree: 4000176 kB
...
Dump:
Thread -162882704
QUERY: GET /VirtualHostBase/http/www.plone-entwicklerhandbuch.de:80/pen/VirtualHostRoot/@@downloadPDF?
URL: http://www.plone-entwicklerhandbuch.de/@@downloadPDF
HTTP_USER_AGENT: Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.17) Gecko/20110422 Ubuntu/10.04 (lucid) Firefox/3.6.17
File "/home/veit/plone40_buildout/eggs/Zope2-2.12.17-py2.6-linux-x86_64.egg/ZServer/PubCore/ZServerPublisher.py", line 31, in __init__
response=b)
...
Thread -184550544
...
Ist der Server nicht erreichbar, gibt das Skript folgendes aus:
$ ./bin/zope_health_watcher http://localhost:8080
Idle: 0 Busy: 0
FAILURE - [Errno socket error] (61, 'Connection refused')
ZopeHealthWatcher gibt dabei Fehlercodes so aus, dass sie auch von Nagios o.ä. weiterverarbeitet werden können:
OK = 0
WARNING = 1
FAILURE = 2
CRITICAL =3
Die Ausgabe kann auch als HTML auf einen Web-Browser erfolgen, wenn z.B. folgende Adresse eingegeben wird:
http://www.plone-entwicklerhandbuch.de/manage_zhw?secret
Open-Source-Monitoring von Hosts, Services etc.
PNP ist ein Addonfür Nagios für einfach zu konfigurierende, auf RRDTools basierende Performance-Charts.
Nagios-Plugin zum Überprüfen von Web-Seiten
Weitere Informationen finden Sie in der nagios-check-webpage Documentation
Mit Sentry lassen sich in Echtzeit Fehler agregieren und protokollieren. Dabei kann Sentry plattform-unabhängig eingesetzt werden.
Das Sentry-Paket ist in seinem Kern nur ein einfacher Web-Server mit speziellem UI. Es behandelt die Authentifizierung von Clients (wie Raven) und die gesamte Logik zur Speicherung und Aggregation. Dabei liefert Sentry eine vollständige API zum Senden von Ereignissen aus jeder Sprache in jede Anwendung.
[buildout]
...
eggs =
...
raven
[instance1]
...
event-log-custom =
%import raven.contrib.zope
<logfile>
path ${buildout:directory}/var/{:_buildout_section_name_}.log
level INFO
</logfile>
<sentry>
dsn YOUR_DSN
level ERROR
</sentry>
Siehe auch
Supervisor ist ein Client/Server-System, das die Prozessüberwachung und -kontrolle auf Unix-Betriebssystemen erlaubt.
Supervisor ist ein Client/Server-System, das die Prozessüberwachung und -kontrolle auf Unix-Betriebssystemen erlaubt.
Dieses Python-Programm erlaubt start
, stop
und restart
anderer
Programme auf UNIX-Systemen wobei es auch abgestürzte Prozesse erneut
starten kann.
supervisord
daemon-Prozess, der andere Programme als Kind-Prozesse laufen lässt.
supervisorctl
Client-Programm, das den Status der supervisord
-Kind-Prozesse
kontrolliert und mitloggt
für start, stop, restart und Ansicht der Log-Dateien.
Die Supervisor-Konfigurationsdatei etc/supervisord.conf
kann z.B.
folgendermaßen aussehen:
[unix_http_server]
file=%(here)s/../var/supervisor.sock
chmod=0600
[supervisorctl]
serverurl=unix://%(here)s/../var/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
[supervisord]
logfile=%(here)s/../var/log/supervisord.log
logfile_maxbytes=5MB
logfile_backups=10
loglevel=info
pidfile=%(here)s/../var/supervisord.pid ;
childlogdir=%(here)s/../var/log
nodaemon=false ; (start in foreground if true; default false)
minfds=1024 ; (min. avail startup file descriptors; default 1024)
minprocs=200 ; (min. avail process descriptors; default 200)
directory=%(here)s
[program:zeoserver]
command = %(here)s/../bin/zeoserver fg
autostart= true
autorestart = true
startsecs = 10
priority = 100
[program: instance]
command = %(here)s/../bin/instance console
startsecs = 60
priority = 2
redirect_stderr = true
autostart= true
autorestart = true
priority = 500
[groups]
programs = instance
[program:varnish]
command = %(here)s/../bin/varnish -F
autostart= true
autorestart = true
priority = 1000
Supervisor kann einfach mit Buildout installiert werden:
[buildout]
parts =
...
supervisor
[supervisor]
recipe = zc.recipe.egg
eggs = supervisor
Nun kann Buildout aufgerufen und anschließend Supervisor gestartet werden:
$ ./bin/buildout
$ ./bin/supervisord
Schließlich können Sie Zeo-Server und -Client mit einem Aufruf starten und stoppen:
$ ./bin/supervisorctl start all
$ ./bin/supervisorctl stop all
In etc/supervisord.conf
lässt sich auch ein Monitoring-Server
konfigurieren:
[inet_http_server]
port=127.0.0.1:9001
username=admin
password=secret
Und im Abschnitt [supervisorctl]
muss noch die serverurl
geändert
werden:
[supervisorctl]
serverurl = http://localhost:9001
Dann starten Sie Supervisor und geben Nutzername und Passwort ein:
$ ./bin/supervisord
$ ./bin/supervisorctl
Server requires authentication
Username:admin
Password:
instance STARTING
varnish RUNNING pid 21683, uptime 0:00:10
zeoserver STARTING
In Ihrem Web-Browser können Sie nun unter http://localhost:9001
die
Prozesse steuern und die Log-Dateien ansehen:
Der supervisord
-Prozess lässt sich beenden mit:
supervisor> shutdown
Really shut the remote supervisord process down y/N? y
Shut down
Und anschließend kann auch der supervisorctl
-Prozess beendet werden mit Strg-D
.
superlance ist ein Plugin für supervisord zum Monitoring und Controlling der unter supervisor laufenden Prozesse.
superlance kann einfach mit Buildout installiert werden:
[supervisor]
recipe = collective.recipe.supervisor
plugins =
superlance
eventlisteners =
memmon TICK_60 ${buildout:bin-directory}/memmon [-g app=1GB]
httpok1 (startsecs=600) TICK_3600 ${buildout:bin-directory}/httpok [-p app:instance1 -t 30 http://127.0.0.1:8010/]
httpok2 (startsecs=600) TICK_3600 ${buildout:bin-directory}/httpok [-p app:instance2 -t 30 http://127.0.0.1:8020/]
Nach dem Durchlaufen von Buildout sollten u.a. zusätzlich folgende Plugins im bin
-Verzeichnis installiert sein:
crashmail
schickt eine E-Mail-Benachrichtigung an eine vorkonfigurierte Adresse wenn ein Prozess hängt.
memmon
startet einen Prozess neu, wenn dieser zu viel Arbeitsspeicher
verbraucht. Damit kann aktiv sog. MemoryErrors
vorgebeugt werden.
httpok
falls Threads hängen bleiben, wird der Prozess automatisch neu gestartet
Siehe auch
Fabric ist eine Python-Bibliothek, die die Verwaltung von Deployments und Aufgaben der Systemadministration deutlich vereinfacht.
Sie lässt sich einfach mit Buildout installieren. Hierzu erstellen wir z.B. eine fabric.cfg
-Datei mit folgendem Inhalt:
[buildout]
parts += fabric
[versions]
fabric = 1.0.0
paramiko = 1.7.6
pycrypto = 2.3
[fabric]
recipe = zc.recipe.egg
eggs =
setuptools
fabric
Anschließend kann das Skript fabfile.py
erstellt werden:
from __future__ import with_statement
from fabric.api import cd, env, local, run, sudo
def www():
env.hosts = ['www.veit-schiele.de']
env.shell = '/bin/sh -c'
env.sudo_prefix = "sudo -S -p '%s' -H "
env.code_root = '/srv/www.veit-schiele.de'
env.code_user = 'zope'
def update():
with cd(env.code_root):
sudo('nice svn up', user=env.code_user)
def rebuild():
with cd(env.code_root):
sudo('nice bin/buildout -c deploy.cfg', user=env.code_user)
def restart():
with cd(env.code_root):
sudo('nice bin/supervisorctl reload', user=env.code_user)
def start():
with cd(env.code_root):
sudo('nice bin/supervisord', user=env.code_user)
def stop():
with cd(env.code_root):
sudo('nice bin/supervisorctl shutdown', user=env.code_user)
def deploy():
update()
rebuild()
restart()
Ein weiteres Beispiel zur lokalen Installation eines auf dem Server erstellten Snapshots finden Sie in diesem fabric.py.
Siehe auch
Mit zope-testbrowser
lassen sich auch Performance-Tests durchführen. Dabei lässt sich für jedes Browser-Objekt angeben, wieviel Zeit jeder Request benötigte. Dies kann verwendet werden, um für einen Request eine tolerierbare Antwortzeit festzulegen. Dabei sollte jedoch nicht lastRequestSeconds
verwendet werden, da dies unterschiedliche Zeiteinstellungen der Maschinen mitberücksichtigt, sondern lastRequestPystones
:
>>> browser.open('http://localhost:8080/mysite/')
>>> browser.lastRequestPystones < 5
True
erlaubt es komplexe Tests mit komplexen Zyklen zu schreiben und gibt ansehnliche Reporte aus.
Sie können Performance-Tests für Ihr Diazo-Theme erstellen mit JMeter
Installation:
$ sudo apt-get install jmeter
Erstellen eines Testplans
Nachdem Sie JMeter gestartet haben, z.B. durch die Eingabe von ``jmeter `` in der Konsole, sehen Sie einen leeren Testplan.
Ein Testplan besteht mindestens aus den folgenden Elementen:
Thread Group Dies ist das Wurzelelement eines Testplans. Es simuliert die Nutzer, als die Anfragen ausgeführt werden. Dabei simuliert jeder Thread einen Nutzer.
Die Standardwerte für alle HTTP-Requests innerhalb einer Thread Group.
Eine Stichprobe (Sampler), die verwendet werden kann um die Zeit für eine Antwort an eine bestimmte URL zu messen.
Statistiken zu den HTTP Request können als aggregierte Graphen dargestellt werden.
Siehe auch
pnp4nagios ist ein Addon für nagios für einfach zu konfigurierende, auf RRDTools basierende Performance-Charts.
Der ZCatalog erlaubt die Indizierung und Suche in der ZODB. Darüberhinaus bietet er vielfältige Erweiterungsmöglichkeiten, mit denen sich deutlich effizientere Methoden implementieren lassen.
collective.indexing ersetzt die Standard-Indizierung des CMF durch einen asynchronen Mechanismus, der redundante Indizierungen vermeidet. Damit wird die Performance zur Aktualisierung der Indizes deutlich gesteigert.
Um collective.indexing zu installieren, muss es einfach als Egg im [buildout]
-Abschnitt eingetragen werden:
[buildout]
...
eggs =
...
collective.indexing
Anschließend kann es in der configure.zcml
-Datei eines Produkts eingetragen werden:
<include package="collective.indexing" />
Alternativ kann es auch als Wert für zcml
im Abschnitt der Zope-Instanz eingetragen werden:
[instance]
...
zcml =
collective.indexing
Anschließend kann collective.indexing
für die Plone-Site installiert werden.
Siehe auch
Wird der Katalog in einer eigenen ZODB gespeichert, können unterschiedliche Cache-Einstellungen für die Inhalte der Plone-Site und den Katalog angegeben werden. Damit werden bei einer umfangreichen Anfrage am Katalog keine Inhaltsobjekte mehr aus dem Cache verdrängt. Wird darüberhinaus die ZODB mit dem Katalog noch auf einer eigenen Platte gespeichert, lässt sich die Performance noch weiter steigern.
In ZODBs konfigurieren wird allgemein beschrieben, wie zusätzlich ZODBs angegeben werden können.
Fügen Sie in der deploy.cfg
-Datei eine neue ZODB hinzu:
[zeoserver]
...
zeo-conf-additional =
<filestorage 2>
path ${buildout:directory}/var/filestorage/CatalogData.fs
</filestorage>
[instance1]
...
zope-conf-additional =
<zodb_db catalog>
mount-point /mysite/portal_catalog
container-class Products.CMFPlone.CatalogTool.CatalogTool
cache-size 300000
<zeoclient>
server ${zeo:zeo-address}
storage 2
name catalogstorage
var ${buildout:parts-directory}/instance1/var
cache-size 400MB
</zeoclient>
</zodb_db>
Rufen Sie ./bin/buildout
aufund starten anschließend den ZEO-Cluster.
Nun erstellen wir eine neue Plone-Site mit der ID mysite
.
Löschen Sie portal_catalog
in dieser Plone-Site.
Beachten Sie, dass die Plone-Site anschließend nicht mehr funktioniert.
Gehen Sie in das ZMI dieser Site und wählen dort ZODB Mount Point aus.
Im folgenden Formular sollte der portal_catalog
-Mount-Point
verfügbar sein.
Aktivieren Sie create missing folders…
Wechseln Sie anschließend in das portal_catalog
-Objekt.
Wechseln Sie in den advanced-Reiter und aktivieren dann clear and rebuild. Beachten Sie, dass dies einige Zeit dauern kann.
Schließlich ist der Katalog Ihrer Plone-Site in einem eigenen Mount-Point verfügbar.
Üblicherweise iteriert die Suchmethode des ZCatalog über alle beteiligten Indizes und bildet anschließend jedesmal die Schnittmenge. Dabei bleibt die Reihenfolge der Indizes, an die die Anfrage gestellt wird, unbestimmt. Dieses Vorgehen lässt sich nun optimieren wenn die Anfrage zunächst an Indizes gestellt wird, dessen Trefferwahrscheinlichkeiten am geringsten sind.
experimental.catalogqueryplan berechnet die durchschnittliche Trefferwahrscheinlichkeit jedes Index. Darauf aufbauend werden die Indizes so sortiert, dass zunächst die Anfragen mit den wenigsten Ergebnissen abgearbeitet werden.
Um experimental.catalogqueryplan
zu installieren, wird es im [buildout]
-Abschnitt eingetragen:
[buildout]
...
eggs =
...
experimental.catalogqueryplan
Anschließend kann es in der configure.zcml
-Datei eines Produkts eingetragen werden:
<include package="experimental.catalogqueryplan" />
Alternativ kann es auch als Wert für zcml
im Abschnitt der Zope-Instanz eingetragen werden:
[instance]
...
zcml =
experimental.catalogqueryplan
Um unterschiedliche Query-Plans für ähnlich Anfragen zu erhalten, ermöglicht experimental.catalogqueryplan
auch zusätzliche Bogus Index Names. Diese werden zwar vom Katalog ignoriert, sind jedoch Teil des Schlüssels zur Erstellung des Query Plans. So wird z.B. die Suche nach Seiten im Entwurfsstadium in einer anderen Reihenfolge die Indizes abarbeiten als die suche nach veröffentlichten Seiten, da sich wohl nur sehr wenige Artikel im Entwurfsstadium auf einer Site finden dürften.
In Version 1.4 wird experimental.catalogqueryplan
auch langsame Anfragen in das Event-Log schreiben. Hierzu werden die Umgebungsvariablen im Abschnitt für den oder die Zope-Server in die Buildout-Konfigurationsdatei eingetragen:
[instance]
...
zope-conf-additional =
<environment>
LOG_SLOW_QUERIES True
LONG_QUERY_TIME 0.05
</environment>
LOG_SLOW_QUERIES
Wird der Wert auf True
gesetzt, werden langsame Anfragen in das Event-Log geschrieben.
LONG_QUERY_TIME
Nur diejenigen Anfragen, die länger als die hier angegebene Zeit in Sekunden dauern, werden in das Event-Log geschrieben.
Der Standardwert ist 0.01
.
Die Ausgabe im Event-Log kann dann z.B. so aussehen:
2009-04-17T16:56:47 INFO experimental.catalogqueryplan portal_catalog, query: 0.11ms (hits: 0), mean 128.41ms (key hits: 11), priority: ('path', 'review_state', 'is_default_page', 'allowedRolesAndUsers', 'portal_type')
Üblicherweise iteriert die Suchmethode des ZCatalog über alle beteiligten Indizes und bildet anschließend jedesmal die Schnittmenge. Dabei bleibt die Reihenfolge der Indizes, an die die Anfrage gestellt wird, unbestimmt. Dieses Vorgehen lässt sich nun optimieren wenn die Anfrage zunächst an Indizes gestellt wird, dessen Trefferwahrscheinlichkeiten am geringsten sind.
Bemerkung
Ab Zope 2.13, das mit Plone 4.1 kommt, unterstützt der ZCatalog Query Plans, s.a. ZCatalog.
Der Monkey Patch des unimr.catalogqueryplan berechnet die durchschnittliche Trefferwahrscheinlichkeit und Verarbeitungsdauer jedes Index. Darauf aufbauend werden die Indizes so sortiert, dass zunächst die effektivsten Anfragen abgearbeitet werden. Hierdurch kann die durchschnittliche Dauer von Anfragen auf die Hälfte reduziert werden.
Um unimr.catalogqueryplan
zu installieren, wird ein Abschnitt [eggcheckouts]
im [buildout
-Abschnitt eingetragen, anschließend dieser Abschnitt definiert und schließlich die Eggs dem ZEO-Client zur Verfügung gestellt:
[buildout]
parts =
...
eggcheckouts
...
[eggcheckouts]
recipe = infrae.subversion
urls =
https://svn.plone.org/svn/collective/unimr.catalogqueryplan/trunk unimr.catalogqueryplan
location = src
as_eggs = true
[instance]
...
eggs =
...
${eggcheckouts:eggs}
Darüberhinaus kann unimr.catalogqueryplan
langsame Anfragen in das Event-Log schreiben. Hierzu werden die Umgebungsvariablen im Abschnitt für den oder die Zope-Server in die Buildout-Konfigurationsdatei eingetragen:
[instance]
...
zope-conf-additional =
<environment>
LOG_SLOW_QUERIES True
LONG_QUERY_TIME 0.05
</environment>
LOG_SLOW_QUERIES
Wird der Wert auf True
gesetzt, werden langsame Anfragen in das Event-Log geschrieben.
LONG_QUERY_TIME
Nur diejenigen Anfragen, die länger als die hier angegebene Zeit in Sekunden dauern, werden in das Event-Log geschrieben.
Der Standardwert ist 0.0
.
Die Ausgabe im Event-Log kann dann z.B. so aussehen:
2009-04-17T16:56:47 INFO unimr.catalogqueryplan portal_catalog, query: 0.11ms (hits: 0), mean 128.41ms (key hits: 11), priority: ('path', 'review_state', 'is_default_page', 'allowedRolesAndUsers', 'portal_type')
Wie oft der Query-Plan aktualisiert wird, lässt sich ebenfalls über Umgebungsvariablen in der Buildout-Konfigurationsdatei angeben:
[instance]
...
zope-conf-additional =
...
<environment>
REFRESH_RATE 500
</environment>
REFRESH_RATE
Die Zeitspanne, nach der ein neuer Query Plan berechnet wird.
Der Standardwert ist 100
.
Um unterschiedliche Query-Plans für ähnliche Anfragen zu erhalten, können zusätzliche Bogus-Iindizes bereitgestellt werden. Diese werden zwar im Katalog ignoriert, können aber dennoch zur Erstellung eines Query-Plan verwendet werden.
CompositeIndex ist ein Index für den ZCatalog, der mehr als ein Attribut je Objekt enthalten kann.Solche Indizes sollten erstellt werden, wenn Anfragen mit mehreren Attributen in einer Suche zu erwarten ist und die kombinierte Suche deutlich weniger Ergebnisse erwarten lässt als die einzelnen Suchen mit nur einem Attribut. Hiervon profitieren vor allem große Sites mit mehr als 100.000 Objekten, bei denen die Anfragen zwei- bis dreimal schneller abgearbeitet werden können.
Viele Kataloganfragen basieren auf er Kombination indizierter Attribute. Üblicherweise arbeitet der ZCatalog jede dieser Anfragen sequentiell ab und berechnet die Schnittmenge zwischen jedem dieser Ergebnisse. Für große Sites mit vielen Objekten reduziert diese Strategie die Performance von Kataloganfragen deutlich. Der CompositeIndex
von unimr.compositeindex hingegen kann jedoch bereits die Schnittmenge von Indizees der Typen FieldIndex
und KeywordIndex
bilden.
Um unimr.compositeindex
zu installieren, muss es einfach als Egg im [buildout]
-Abschnitt eingetragen werden:
[buildout]
...
eggs =
...
unimr.compositeindex
Anschließend kann es in der configure.zcml
-Datei eines Produkts eingetragen werden:
<include package="unimr.compositeindex" />
Alternativ kann es auch als Wert für zcml
im Abschnitt der Zope-Instanz eingetragen werden:
[instance]
...
zcml =
unimr.compositeindex
Für das Generic Setup-Profil wird die catalog.xml
-Datei erstellt mit folgendem Inhalt:
<?xml version="1.0"?>
<object name="portal_catalog" meta_type="Plone Catalog Tool">
<index name="comp01" meta_type="CompositeIndex">
<indexed_attr value="is_default_page"/>
<indexed_attr value="review_state"/>
<indexed_attr value="portal_type"/>
<indexed_attr value="allowedRolesAndUsers"/>
</index>
<column value="comp01"/>
</object>
index name
Eine gültige ID Ihrer Wahl.
indexed_attr value
Name des Attributs eines Objekts, das in einer Anfrage verkettet werden soll.
Anschließend sollte im ZMI im Indexes-Reiter des Catalog Tools für diesen CompositeIndex noch Reindex angegeben werden. Jede Anfrage mit zwei oder mehr Komponenten des Composite key
wird nun automatisch umgewandelt in eine Anfrage an den CompositeIndex.
Chameleon ist eine Reimplementierung der ZopePageTemplates, die durch Precompiling eine deutliche Performance-Optimierng erlaubt. Durchschnittlich dürfte isch so eine Performance-Steigerung für ungecachte Inhalte um ca. 20% ergeben.
Chameleon kann einfach für Plone 4 installiert werden indem als einzige Abhängigkeit five.pt in einer Version ≥2.1 installiert wird:
[instance]
...
eggs =
...
five.pt
Die automatische Paketkonfiguration von Plone installiert dann automatisch Chameleon nach.
Eine ausführliche Dokumentation zu Chameleon erhalten Sie in Chameleon documentation.
Mit Varnish lässt sich zwar eine einfache Lastverteilung im Round-Robin-Verfahren realisieren, für zuverlässige Hochverfügbarkeit sind jedoch aufwendigere Verfahren notwendig.
Dies kann mit HAProxy realisiert werden. Dabei lässt sich HAProxy einfach mit Buildout installieren:
[haproxy-build]
recipe = plone.recipe.haproxy
url = http://www.haproxy.org/download/1.4/src/haproxy-1.4.26.tar.gz
target = linux26
cpu = i686
pcre = 1
[haproxy-config]
recipe = collective.recipe.template
input = ${buildout:directory}/templates/haproxy.conf.in
output = ${buildout:directory}/etc/haproxy.conf
user = haproxy
group = haproxy
frontend-host = 127.0.0.1
frontend-port = 8001
Die haproxy.conf.in
-Datei in templates
sieht dann z.B. so aus:
global
maxconn ${haproxy-conf:maxconn}
user ${haproxy-config:user}
group ${haproxy-config:group}
daemon
nbproc 1
spread-checks 3
ulimit-n 65536
defaults
mode http
# The zope instances have maxconn 1, and it is important that
# unused/idle connections are closed as soon as possible.
option httpclose
# Remove requests from the queue if people press stop button
option abortonclose
retries 3
option redispatch
monitor-uri /haproxy-ping
timeout connect 5s
timeout queue 30s
timeout client 50s
timeout server 120s
timeout check 50s
stats enable
stats uri /haproxy-status
stats refresh 60s
stats realm Haproxy\ statistics
frontend zeocluster
bind ${haproxy-config:frontend-host}:${haproxy-config:frontend-port}
default_backend zope
option httplog
log 127.0.0.1:1514 local6
# Load balancing over the zope instances
backend zope
# Use Zope's __ac cookie as a basis for session stickiness if present.
appsession __ac len 32 timeout 1d
# Otherwise add a cookie called "serverid" for maintaining session stickiness.
# This cookie lasts until the client's browser closes, and is invisible to Zope.
cookie serverid insert nocache indirect
# If no session found, use the roundrobin load-balancing algorithm to pick a backend.
balance roundrobin
# Use / (the default) for periodic backend health checks
option httpchk GET /
# Server options:
# "maxconn" is how many connections can be sent to the server at once
# "check" enables health checks
# "rise 1" means consider Zope up after 1 successful health check
server instance1 127.0.0.1:${instance1:http-address} weight 1 check inter 15s rise 2 fall 1 maxconn 2
server instance2 127.0.0.1:${instance1:http-address} weight 1 check inter 15s rise 2 fall 1 maxconn 2
HAProxy kann dann gestartet werden mit:
${buildout:directory}/bin/haproxy -f ${buildout:directory}/etc/haproxy.conf -db
Unter http://localhost:8001/haproxy-status können Sie sich dann den aktuellen Status des HAProxy anschauen:
Siehe auch
Eine Mailingliste mit extrem geringem Datenaufkommen, für neue Releases und Sicherheitshinweise. Die Liste ist moderiert und nur das Plone-Team kann auf dieser Liste veröffentlichen.
RSS 1.0-Feed
Das Abonnieren der Mailingliste oder des RSS-Feed wird unbedingt empfohlen.
Einen Überblick, welche Plone-Versionen welche Hotfixes benötigen, erhalten Sie unter Plone Hotfixes.
plone.protect bietet Methoden um die Sicherheit von Web-Formularen in Plone zu erhöhen.
from plone.protect import PostOnly
from plone.protect import protect
@protect(PostOnly)
def something(self, param, REQUEST=None):
pass
Cross-Site-Request-Forgery CSRF ist ein Angriffsverfahren, bei dem ein ein HTTP-Formular an einen anderen Ort übermittelt wird und anschließend die Parameter ausgewertet werden. Form authentication soll verhindern, dass diese Parameter ausgewertet werden können da zunächst die Authentizität überprüft wird.
Der erforderliche Token kann einfach generiert werden mit:
<span tal:replace="structure context/@@authenticator/authenticator"/>
Zur Überprüfung des Tokens können verschiedene Methoden verwendet werden:
ZCA:
authenticator=getMultiAdapter((context, request), name=u"authenticator")
if not authenticator.verify():
raise Unauthorized
mit einem Decorator:
from plone.protect import CheckAuthenticator
from plone.protect import protect
@protect(CheckAuthenticator)
def something(self, param, REQUEST=None):
pass
Anfrage an einen Funktionsvalidator weiterreichen:
from plone.protect import CheckAuthenticator
...
CheckAuthenticator(self.context.REQUEST)
...
Seit Version 3 bietet plone.protect
einen automatischen CSRF-Schutz indem automatisch ein Auth-Token in allen internen Formularen verwendet wird, wenn der Benutzer angemeldet ist oder in die ZODB geschrieben werden soll.
Zum Schutz vor Clickjacking
setzt Plone zudem den X-Frame-Options-Header auf
SAMEORIGIN
.
Um diesen Wert zu ändern gibt es drei Möglichkeiten:
In einem View überschreiben, z.B. mit:
self.request.response.setHeader ('X-Frame-Options "," AllowAll'))
Im Proxy-Server überschreiben
Die Umgebungsvariable PLONE_X_FRAME_OPTIONS
ändern
s.a. Debugging CSRF Protection False Positives in Plone
Siehe auch
Zope security facilities, Sandboxing and SELinux
Anhand des deutschen Bundesdatenschutzgesetz (BDSG) wird überprüft, wie Zope-Anwendungen diesen Anforderungen gerecht werden können.
Declare IP-Addresses and iptables config
Presentation from Plone Conference 2008 in Washington, D.C.
The ten most common security issues in web applications, and how Plone addresses them.
Show all assigned local roles within a subtree for any Plone 4 site.
Auf der virtuellen Maschine mit dem ZEO-Server ist der Port 9000 zu öffnen:
# system-config-firewall-tui
Firewall-Konfiguration: Anpassen
Trusted Dienste: WWW (HTTP)
Andere Ports: Hinzufügen
Port/Port-Bereich: 9000
Protokoll: tcp
Dies generiert die Datei /etc/sysconfig/iptables
:
# Firewall configuration written by system-config-firewall
# Manual customization of this file is not recommended.
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 9000 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT
Eingehende Anfragen lassen sich überprüfen mit:
# watch iptables --list -v
Every 2,0s: iptables --list -v Wed Mar 20 18:30:34 2013
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
1476 202K ACCEPT all -- any any anywhere anywhere state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- any any anywhere anywhere
1 60 ACCEPT all -- lo any anywhere anywhere
1 60 ACCEPT tcp -- any any anywhere anywhere state NEW tcp dpt:ssh
0 0 ACCEPT tcp -- any any anywhere anywhere state NEW tcp dpt:nfs
6 360 ACCEPT tcp -- any any anywhere anywhere state NEW tcp dpt:cslistener
18 576 REJECT all -- any any anywhere anywhere reject-with icmp-host-prohibited
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 REJECT all -- any any anywhere anywhere reject-with icmp-host-prohibited
Chain OUTPUT (policy ACCEPT 1405 packets, 117K bytes)
pkts bytes target prot opt in out source destination
Nun können wir uns die aktiven Internetverbindungen anschauen mit:
# netstat -tulpen
Aktive Internetverbindungen (Nur Server)
Proto Recv-Q Send-Q Local Address Foreign Address State Benutzer Inode PID/Program name
...
... 0 0 0.0.0.0:9000 0.0.0.0:* LISTEN 502 1302676 23572/python
...
Analog sollten nun auch iptables
für die Instanzen konfiguriert werden.
Sollten bei einer bereits existierenden Zope-Datenbank die Zugangsdaten verlorengegangen oder die Sicherheitseinstellungen so verändert haben, dass Sie selbst nicht mehr die notwendigen Änderungen vornehmen können, so gibt es ein Skript für die Zope-Instanz, mit dem Sie erneut Zugang zum ZMI erhalten können. Dieses Skript kann im Buildout-Verzeichnis folgendermaßen aufgerufen werden:
$ cd parts/instance/
$ python ../../parts/zope2/utilities/zpasswd.py access
Username: veit
Password:
Verify password:
Please choose a format from:
SHA - SHA-1 hashed password (default)
CRYPT - UNIX-style crypt password
CLEARTEXT - no protection
Encoding: SHA
Domain restrictions:
encoding
gibt die Art der Verschlüsselung an.
domains
Domainnamen, von denen aus dieser Nutzer sich anmelden kann.
Anschließend sollte sich die Instanz starten lassen und der soeben erzeugte Nutzer sollte sich nun im ZMI anmelden können. Innerhalb des ZMI hat dieser sog. emergency user dann allerdings nicht die vollen Administrationsrechte sondern nur diejenigen zum Anlegen neuer Nutzer und Ändern der Sicherheitseinstellungen. Sie können also nun z.B. einen neuen Nutzer mit Management-Rechten anlegen, anschließend die Instanz stoppen. Nach dem erneuten Starten der Instanz sollten Sie als der im ZMI angelegt Nutzer wieder anmelden können. Vergessen Sie bitte nicht, die access
-Datei wieder zu löschen.
Die LDAP-Verbindung wird anhand eines OpenLDAP-Servers demonstriert.
OpenLDAP kann heruntergeladen werden von http://www.openldap.org/. Es ist jedoch auch in vorkonfigurierten Pakete für viele Betriebssysteme verfügbar. Für Debian und Ubuntu können Sie diese installieren mit:
# apt-get install slapd ldapscripts
Falls der LDAP-Server noch nicht gestartet wurde, kann dies mit folgender Angabe erfolgen:
# /etc/init.d/slapd restart
Anschließend lassen sich einige der von Ubuntu im LDIF
-Format mitgelieferten Schemata hinzufügen:
# ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/cosine.ldif
# ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/inetorgperson.ldif
# ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/nis.ldif
Nachdem diese allgemeinen Schemata hinzugefügt worden sind, wird nun noch eine initiale cn=config
-Datenbank aufgesetzt, die die gesamte Konfiguration des OpenLDAP-Servers enthält. Hierzu erstellen wir zunächst die Datei db.ldif
mit folgendem Inhalt:
###########################################################
# DATABASE SETUP
###########################################################
# Load modules for database type
dn: cn=module{0},cn=config
objectClass: olcModuleList
cn: module{0}
olcModulePath: /usr/lib/ldap
olcModuleLoad: {0}back_hdb
# Create directory database
dn: olcDatabase={1}hdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcHdbConfig
olcDatabase: {1}hdb
olcDbDirectory: /var/lib/ldap
olcSuffix: dc=veit-schiele,dc=de
olcRootDN: cn=admin,dc=veit-schiele,dc=de
olcRootPW: 1234
olcAccess: {0}to attrs=userPassword,shadowLastChange by dn="cn=admin,dc=veit-schiele,dc=de" write by anonymous auth by self write by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by dn="cn=admin,dc=veit-schiele,dc=de" write by * read
olcLastMod: TRUE
olcDbCheckpoint: 512 30
olcDbConfig: {0}set_cachesize 0 2097152 0
olcDbConfig: {1}set_lk_max_objects 1500
olcDbConfig: {2}set_lk_max_locks 1500
olcDbConfig: {3}set_lk_max_lockers 1500
olcDbIndex: uid pres,eq
olcDbIndex: cn,sn,mail pres,eq,approx,sub
olcDbIndex: objectClass eq
###########################################################
# DEFAULTS MODIFICATION
###########################################################
# Some of the defaults need to be modified in order to allow
# remote access to the LDAP config. Otherwise only root
# will have administrative access.
dn: cn=config
changetype: modify
delete: olcAuthzRegexp
dn: olcDatabase={-1}frontend,cn=config
changetype: modify
delete: olcAccess
dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {CRYPT}7hzU8RaZxaGi2
dn: olcDatabase={0}config,cn=config
changetype: modify
delete: olcAccess
Diese Konfiguration wird nun mit folgendem Befehl eingelesen:
# ldapadd -Y EXTERNAL -H ldapi:/// -f db.ldif
Nun sollten noch minimale Einträge für den LDAP DIT (Directory Information Tree) angelegt werden. Hierzu erstellen wir eine weitere Datei base.ldif
mit folgendem Inhalt:
# Top level - the organization
dn: dc=veit-schiele,dc=de
objectClass: dcObject
objectclass: organization
o: veit-schiele.de
dc: veit-schiele
description: Top level
# LDAP admin
dn: cn=admin,dc=veit-schiele,dc=de
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
userPassword: 1234
description: LDAP admin
Dadurch wird der Benutzer "cn=admin,dc=veit-schiele,dc=de"
mit dem Passwort 1234
erstellt, der anschließend alle Rechte am LDAP-Server hat. Das Passwort sollte natürlich angepasst werden.
Diese Datei wird eingelesen mit:
# ldapadd -x -D cn=admin,dc=veit-schiele,dc=de -W -f base.ldif
Zum Testen kann nun der Directory Information Tree ausgelesen werden mit:
ldapsearch -xLLL -b dc=veit-schiele,dc=de
Nun sollten wir noch einige Einträge in das LDAP-Repository erstellen. Hierzu erzeugen wir die Datei veit-schiele.ldif
:
# Second level - organizational units
dn: ou=people, dc=veit-schiele,dc=de
ou: people
description: All people in organisation
objectclass: organizationalunit
dn: ou=groups, dc=veit-schiele,dc=de
ou: groups
description: All groups in the organization
objectclass: organizationalunit
# Third level - people
dn: uid=vschiele,ou=people,dc=veit-schiele,dc=de
objectClass: pilotPerson
objectClass: uidObject
uid: vschiele
cn: Veit Schiele
sn: Schiele
userPassword:: e1NIQX01ZW42RzZNZXpScm9UM1hLcWtkUE9tWS9CZlE9
mail: kontakt@veit-schiele.de
# Third level - groups
dn: cn=Staff,ou=groups,dc=veit-schiele,dc=de
objectClass: top
objectClass: groupOfUniqueNames
cn: Staff
uniqueMember: uid=vschiele,ou=people,dc=veit-schiele,dc=de
Die Organisation dc=veit-schiele,dc=de
erhält zunächst zwei Organisationseinheiten (organizational units): people und groups.
Nun wird ein Nutzer vschiele
der Organisationseinheit people und eine Gruppe staff
der Organisationseinheit groups hinzugefügt.
Die Eigenschaft userPassword
ist durch die beiden Doppelpunkte als SHA1 Hash-Wert gekennzeichnet.
Schließlich kann die LDIF-Datei importiert werden mit:
# ldapadd -xWD 'cn=admin, dc=veit-schiele,dc=de' -f veit-schiele.ldif
python-ldap
-Moduls¶Um LDAP mit Python nutzen zu können, muss das python-ldap
-Modul installiert sein. Sie können testen, ob dieses Modul der Zope-Instanz zur Verfügung steht mit:
$ ./bin/zopepy
$ import _ldap
Sollten Sie keine Fehlermeldung erhalten. können Sie den Python-Interpreter wieder mit Strg-D
(unter Windows Strg-Z
) verlassen.
Falls Sie eine Fehlermeldung erhalten, können Sie das python-ldap
-Modul einfach mit Buildout installieren. Überprüfen Sie zunächst, ob die erforderlichen Pakete bereits installiert sind:
≥ Version 2.3
MIT oder heimdal
Für Debian und Ubuntu können Sie diese installieren mit:
$ sudo apt-get install libldap2-dev libsasl2-dev libssl-dev
Anschließend können Sie Ihre base.cfg
-Datei folgendermaßen ändern:
[buildout]
parts =
python-ldap
...
[python-ldap]
recipe = zc.recipe.egg:custom
eggs = python-ldap
find-links =
include-dirs = /usr/include /usr/lib/sasl2
library-dirs = /usr/lib
Die hier für include-dirs
und library-dirs
eingetragenen Pfade sind die für Ubuntu erforderlichen Angaben.
Weitere Informationen zur Installation des python-ldap
-Moduls erhalten Sie unter http://www.python-ldap.org/doc/html/installing.html.
plone.app.ldap
¶Hierzu tragen wir in der setup.py
-Datei unseres Policy-Produkts folgendes
ein:
setup(name='vs.policy',
...
install_requires=[
...
'plone.app.ldap',
],
Das plone.app.ldap
-Egg installiert automatisch die erforderlichen Produkte
LDAPMultiPlugins,
LDAPUserFolder,
PloneLDAP,
dataflake.fakeldap und
python-ldap mit.
Anschließend wird noch in der metadata.xml
-Datei des Policy-Produkts plone.app.ldap
als Abhängigkeit eingetragen:
<?xml version="1.0"?>
<metadata>
...
<dependencies>
...
<dependency>profile-plone.app.ldap:ldap</dependency>
</dependencies>
</metadata>
Nachdem das Buildout-Projekt aktualisiert, die Zope-Instanz neu gestartet und
eine neue Plone-Site mit vs.policy
hinzugefügt wurde, sollte sich LDAP Connection in Website-Konfiguration konfigurieren lassen.
Anschließend erscheint in der Website-Konfiguration die Konfiguration der LDAP Connection:
unterscheidet zwischen LDAP und Active Directory.
relative distinguished name, wird als erster Abschnitt des DN
für in Plone angelegte Nutzer verwendet.
Für LDAP wird hier meist uid
angegeben, für Active Directory hingegen cn
.
Attribut, das für die ID von Nutzern angegeben werden soll.
Auch hier wird dasselbe wie für den relative distinguished name angegeben.
wird während der Authentifizierung verwendet und ist üblicherweise dieselbe Angabe wie bei user id attribute.
Bei einem Active Directory-Server kann entweder userPrincipalName
oder sAMAccountName
als user id und login name verwendet werden. Dabei wird für sAMAccountName
nur der einfache Nutzername angegeben während userPrincipalName
auch den Domainnamen enthält. sAMAccountName
ist daher gut geeignet, wenn das Active Directory nur für eine Domain konfiguriert werden muss.
Strukturelle Objektklasse und weitere Objektklassen, durch Komma getrennt. Diese Objektklassen werden in Plone neu hinzugefügten Nutzern zugewiesen. Das hier verwendete PilotPerson,uidObject
ergibt eine einfaches Objekt mit einer uid
.
ist der distinguished name, der von Plone verwendet wird um Zugang zum LDAP-Server zu erhalten. Wir verwenden den rootdn
:
"cn=admin,dc=veit-schiele,dc=de"
ist das Passwort für den in Bind DN angegebenen Nutzer, in unserem Fall also das in der slpad.conf
-Datei angegebene Passwort 1234
.
Ort im LDAP-Repository, an dem die Nutzer verwaltet werden. In unserem Fall in der organizational unit people
:
ou=people,dc=veit-schiele,dc=de
Angabe, ob Nutzer direkt in der organizational unit gefunden werden oder auch in Untereinheiten. In unserem Fall könnten wir zwar one level
angeben, subtree
ist jedoch flexibler und wird daher üblicherweise verwendet.
Ort im LDAP-Repository, an dem die Gruppen verwaltet werden. In unserem Fall in der organizational unit groups
:
ou=groups,dc=veit-schiele,dc=de
Angabe, ob Gruppen direkt in der organizational unit gefunden werden oder auch in Untereinheiten. Auch hier geben wir wie in Search scope for users subtree
an.
Im LDAP Schema-Reiter können LDAP-Attribute mit Eigenschaften der Plone-Nutzer verknüpft werden.
Üblicherweise wird das uid
-Attribut nicht mit einer Plone-Eigenschaft verknüpft, da es im Global Settings-Reiter frei als rDN, user id und login name angegeben werden soll.
Auch sn wird hier angegeben, jedoch nicht mit einer Plone-Eigenschaft verknüpft. Dies führt dazu, dass für jeden neu angelegten Plone-Nutzer ein leeres Feld im LDAP-Repository angelegt wird. Dennoch darf sn nicht aus der Plone-LDAP-Konfiguration gelöscht werden, da die sn-Eigenschaft zwingend für die pilotPerson-Objektklasse erforderlich ist.
Plone-Eigenschaften können mit LDAP-Attributen gemischt werden wobei der Pluggable Authentication Service (PAS) die LDAP-Eigenschaften bevorzugt.
Im LDAP Server-Reiter können ein oder mehrere LDAP-Server angegeben werden. Sind mehrere Server konfiguriert werden sie von oben nach unten durchsucht.
Um nun zu testen, ob die Plone-Site mit dem LDAP-Server kommuniziert, klicken Sie auf Benutzer in der Plone-Konfiguration und lassen sich anschließend alle Nutzer anzeigen. Nun sollten Ihnen die Nutzer aus dem LDAP-Server korrekt angezeigt werden.
Anmerkung: Der Nutzername für die Zope-Instanz, z.B. admin
, sollte nicht in Ihrem LDAP-Verzeichnis vorhanden sein.
Die oben gezeigte LDAP-Konfiguration konfiguriert das ldap
-Plugin in acl_users
.
Hier lassen sich die einzelnen Funktionen der Nutzerverwaltung aktivieren bzw. deaktivieren. Beim Klicken in die Funktionen können diese auch konfiguriert werden. Schauen wir uns nun User Adder genauer an:
Auch hier werden die aktiven Plugins von oben nach unten durchsucht, d.h. die Nutzereigenschaften werden zunächst mit dem ldap
-Plugin und dann erst mit dem source_users
-Plugin gesucht.
Sollen in Plone angelegte Nutzer nicht in das LDAP-Repository übernommen werden, muss die Reihenfolge der beiden Plugins geändert werden.
Sollen neue Plone-Nutzer nicht in das LDAP-Repository übernommen werden, verschieben Sie das ldap
-Plugin in die Available Plugins.
Three tier architecture:
ldapy
Low-level Python-Bibliothek, die libldap via cffi undasynchrone Aufrufe via Generatoren unterstützt.
entsprechend sqlalchemy modelliert zur Unterstützung von Session-Management mit Connection-Pools und Abfrage der LDAP-Einträge mit Attributnamen und Typ-Mapping.
pas.plugins.ldapalchemy
PAS-Plugin, das unter Verwendung von ldapalchemy mit dem LDAP redet
Mit vs.genericsetup.ldap haben wir ein Produkt entwickelt, das den Im- und Export der LDAP- oder AD-Konfiguration von Plone-Sites erlaubt.
vs.genericsetup.ldap
nutzt das Generic Setup Tool, um die Konfiguration einer LDAP- oder Active Directory-Anbindung aus einer Plone-Site exportieren und in einer anderen Plone-Site wieder importieren zu können. Damit werden reproduzierbare und programmatische LDAP- und AD-Anbindungen möglich.
Um vs.genericsetup.ldap
zu installieren, wird in der buildout.cfg-Datei
folgendes eingetragen:
[buildout]
eggs =
...
vs.genericsetup.ldap
[instance]
...
zcml =
...
collective.genericsetup.ldap
Nach der Installation von vs.genericsetup.ldap
können Sie in das Generic Setup Tool Ihrer Plone-Site gehen und die Konfiguration Ihrer LDAP- oder AD-Anbindung exportieren, indem Sie im Export-Reiter LDAP Settings Export auswählen und anschließend auf Export selected steps klicken. Anschließend können Sie die ldap_plugin.xml
-Datei in src/vs.policy/vs/policy/profile/default/
kopieren. Beim Installieren des vs.policy
-Produkts wird das Plone LDAP plugin oder ActiveDirectory Multi Plugin automatisch im Pluggable Auth Service erstellt. Ist vs.policy
bereits für Ihre Website installiert, können Sie eines der beiden Plugins auch nachträglich hinzufügen, indem Sie im Import-Reiter des Generic Setup Tool den LDAP Settings Import auswählen und anschließend auf Import selected steps klicken.
Um eine lauffähige Kerberos-Umgebung sowie eine Plone bzw. Zope-Website mit Single Sign On zu erhalten, sind Anpassungen auf mehreren Ebenen notwendig:
Einrichtung eines zentralen Kerberos-Servers (KDC, Key Distribution Center)
Einrichtung des Betriebssystems des vorgelagerten Webservers, sodass Kerberos-Tickets vom KDC bezogen werden können
Einrichtung des Betriebssystems des Website-Benutzers, sodass es beim Maschinen-Login Kerberos-Tickets vom KDC bezieht
Einrichtung des vorgelagerten Webservers, in diesem Falle Apache
Installation eines Zope-Zusatzproduktes, damit der User Folder die vom Webserver zusätzlich zur Verfügung gestellten Authentifizierungsdaten auswertet
Dieses Dokument befasst sich lediglich mit der serverseitigen Einrichtung. Das Betriebssystem des Benutzers der Website muss also bereits so konfiguriert sein, dass der Logon für den Benutzer bereits über Kerberos abgewickelt wird.
Unser Beispielsystem setzt auf einem Linux-Server auf, der als Kerberos-Server konfiguriert wird. Der Apache-Server läuft ebenfalls unter Linux und agiert als Kerberos-Client. Beim Anmeldevorgang wird automatisch ein Kerberos-Ticket bezogen, welches die spätere Abwicklung der Kerberos-Authentifizierung zur Website ermöglicht. Das Einrichten einer solchen Konstellation ist auf dem Internet vielfach beschrieben, siehe auch die Weiterführenden Links am Ende des Dokumentes.
mod_auth_kerb
-Authentication-Modul¶Für Apache wird das Zusatzmodul mod_auth_kerb
benötigt, welches unter Linux
mit den Bordmitteln des Betriebssystems installiert werden kann, wie z.B.
apt-get
unter Debian und Ubuntu oder yum
unter RedHat-Systemen:
# apt-get install libapache2-mod-auth-kerb
oder:
# yum install mod_auth_kerb
Falls das Modul anschließend nicht automatisch geladen wird, kann dies manuelle geschehen mit:
# LoadModule auth_kerb_module /usr/lib/apache2/modules/mod_auth_kerb.so
Je nach OS muss der Pfadname des Moduls noch angepasst werden.
Da die Header der HTTP-Anfrage von Apache modifiziert werden, muss auch
mod_headers
installiert sein. In unserer Beispielkonfiguration wird Apache
als Proxy für Plone verwendet, womit auch mod_rewrite
und mod_proxy
aktiv sein müssen.
Der Service Principal kann erstellt werden mit folgendem kadmin-Befehl:
# kadmin -p bofh/admin -q "addprinc -randkey HTTP/www.example.com"
keytab
)¶Ein keytab ist eine Datei zum Speichern der Schlüssel für einen oder mehrere
Kerberos-Prinzipals. mod_auth_kerb
benötigt diese Tabelle um den oben
erstellten Service Principal nutzen zu können.
Die Schlüsseltabelle kann mit kadmin
erstellt werden:
# kadmin -p bofh / admin -q "ktadd -k /etc/apache2/http.keytab HTTP / www.example.com"
oder auf RedHat-basierten Systemen im Pfad /etc/httpd/http.keytab
.
Die -k
-Option spezifiziert den Pfadnamen der keytab
-Datei, die
erstellt wird sofern sie noch nicht existiert.
Anschließend muss der Eigentümer so geändert werden, dass der Apache-Prozess darauf zugreifen kann:
# chown www-data /etc/apache2/http.keytab
oder auf RedHat-basierten Systemen:
# chown apache /etc/httpd/http.keytab
Um zu überprüfen, ob der Schlüssel korrekt in die keytab
eingetragen
wurde, sollten wir uns als Service Principal authentifizieren und uns
anschließend das resultierende Granting-Ticket mit klist
anzeigen lassen:
# kinit -k -t /etc/apache2/http.keytab HTTP/www.example.com
# klist
In der unten gezeigten Apache-Konfiguration lauscht die Zope-Instanz auf IP
10.0.0.2
am Port 8080
, und Apache auf 10.0.0.1
am Port 80
. Das
Plone-Portal befindet sich in der Zope-Instanz unter dem Pfad /portal
. In
Kerberos wird der Realm-Wert MYDOMAIN
verwendet:
<VirtualHost 10.0.0.1:80>
ServerAdmin webmaster@mydomain.com
ServerName intranet.mydomain.com
<Location />
AuthName "Intranet"
AuthType Kerberos
KrbAuthoritative on
KrbAuthRealms MYDOMAIN
KrbServiceName HTTP
Krb5Keytab /etc/krb5.keytab
KrbMethodNegotiate on
KrbMethodK5Passwd off
KrbSaveCredentials on
require valid-user
RequestHeader set X_REMOTE_USER %{remoteUser}e
</Location>
RewriteEngine On
RewriteRule ^/(.*) http://10.0.0.2:8080/VirtualHostBase/http/intranet.mydomain.com:80/portal/VirtualHostRoot/$1 [L,P,E=remoteUser:%{LA-U:REMOTE_USER}]
</VirtualHost>
Um diese Konfiguration nun verwenden zu können, muss der Apache-Webserver neu gestartet werden mit:
# service apache2 force-reload
Wie man erkennen kann, reicht das Einfügen der mod_auth_kerb
-Direktiven in
eine Location
-Direktive. Kerberos wird als Authentifizierungsmechanismus
festgelegt, und es wird eine positive Identifikation des Benutzers zum Zugriff
erfordert (require valid-user). Wichtig hierbei ist das Abschalten von
KrbMethodK5Passwd
, um eine Abfrage und Übertragung des Kerberos-Logins
zwischen Browser und Apache zu verhindern. Es wird ausschliesslich die
Negotiate-Methode zugelassen (KrbMethodNegotiate
), bei der keine Logins über
das Netz geschickt werden, sondern nur Kerberos-Ticket-Informationen.
Die Plone-Instanz selber muss kein Kerberos verstehen. Wie man in der
Apache-Konfiguration ersehen kann, wird der ermittelte Benutzername in einen
zusätzlichen HTTP-Header X_REMOTE_USER
geschrieben und so weitergeleitet.
Beim Einsatz von mod_auth_kerb
in Apache muss man beachten, dass man
Kerberos-Authentifizierung nicht mit anderen Authentifizierungen kombinieren
kann. Es ist nicht möglich, bei erfolgloser Kerberos-Authentifizierung auf z.B.
normale Basic Auth zurückzufallen. Ferner ist es nicht möglich, diese
Authentifizierung optional zu gestalten, sodass auch bei erfolglosem Kerberos-
Versuch der Besuch der Website gestattet wird. Das heisst, man kann auf einem
für Kerberos-Authentifizierung eingerichteten Hostnamen keine Besucher bedienen,
die anonym durchgelassen werden sollen oder die auf andere Weise authentifiziert
werden können.
Auf der Plone-Seite reicht die Installation und Konfiguration eines Zusatzproduktes, welches den von Apache gesetzten zusätzlichen HTTP-Header versteht und auswertet. Für unser Beispiel benutzen wir Products.WebserverAuth (siehe auch Weiterführende Links unten). Das Produkt kann als Python Egg einfach in einen bestehenden Plone-Buildout eingebunden werden:
[instance]
…
eggs =
…
Products.WebServerAuth
Nachdem das Buildout-Skript durchlaufen und die Instanz neu gestartet wurde, sollte in portal-url → site setup → Add-on Products WebServerAuth aktiviert werden können.
Damit wird im PluggableAuthService
des Portals das
WebServerAuth
-Plugin zur Verfügung gestellt.
Von der Standardkonfiguration auf dem Reiter Options wurde nur in einem
Punkt abgewichen: Wir haben die Option "Only users with a pre-existing
Plone account"
gewählt, um nur solche Kerberos-Benutzer durchzulassen, die
auch in der Plone-Instanz bekannt sind.
Zudem muss das neue Plugin in unserer Beispielkonfiguration nur für zwei
Dienste aktiviert werden, nämlich für Authentication
und Extraction
.
Extraction
ist für das Ermitteln von Login-Daten aus der hereinkommenden HTTP-Anfrage zuständig. Da jeder Zugriff über Apache automatisch den vom neuen Plugin ausgewerteten HTTP-Header enthält und die Verarbeitung dieses Headers schnell und einfach ist, setzen wir das neue Plugin als erstes aktives Extraction-Plugin ein.
Authentication
nimmt die im ersten Schritt ermittelten Login-Daten und prüft, ob ein Benutzer mit diesen Login-Daten bekannt ist und angemeldet werden kann. Da in unserem Beispielszenario die Benutzer in ActiveDirectory vorgehalten werden und auch Plone dort die Benutzerdaten sucht, ist das neue Plugin als letztes aktives Authentication-Plugin geführt. Somit wird am bisherigen Verfahren vor dem Einsatz von Kerberos am wenigsten geändert.
Der Abmelden-Link des Plone-Portal (ZMI → Plone-Portal → portal_actions → Benutzer → Logout) sollte auf eine spezielle Logout-Seite umleiten, die z.B. den folgenden Inhalt trägt:
»Sorry, Sie müssen Ihren Web-Browser schließen um sich von diesem Portal abzumelden.«
Hierzu können Sie auch das logged_out
-Template entsprechend anpassen.
Das Login-Portlet sollte nicht angezeigt werden.
Der Password ändern-Link (z.B. in ZMI → Plone-Portal → portal_controlpanel) sollte ebenfalls nicht mehr angezeigt werden.
Nach den oben genannten Konfigurationsschritten sollte ein korrekt auf Windows angemeldeter und in Plone bekannter Benutzer bei Besuch der Plone-Instanz sofort und ohne Umweg über die Login-Maske angemeldet sein. Das ist schnell erkennbar daran, dass z.B. kein Login-Link mehr angeboten wird, wohl aber eines auf die eigenen Inhalte und Präferenzen.
Bei Problemen ist besonders das Dokument Using mod_auth_kern and Windows 2000/2003 as KDC hilfreich. Es erklärt in kleinen Schritten, wie die Konfiguration geprüft und Fehler ausgemerzt werden können.
Auf der Plone-Seite kann man die korrekte Weitergabe der Login-Informationen
sehr einfach mit einer simplen DTML-Methode testen, die die Werte des
REQUEST
ausgibt:
<dtml-var REQUEST>
Dort muss im Bereich environ ein Header namens HTTP_X_REMOTE_USER
sichtbar
sein, der den vollen Kerberos-Login-Namen des Benutzers enthält. Ist er es
nicht, wurde der Login nicht korrekt von Apache weitergegeben.
Bevor Sie eine Plone-Site aktualisieren, sollten Sie verschiedenes vorbereiten.
Lesen Sie What’s new in… und die release notes der für Sie relevanten Plone-Version. Sie finden diese im CMFPlone-Ordner Ihrer Distribution.
Überprüfen Sie die Abhängigkeiten
Die Abhängigkeiten werden in den release notes angegeben:
Welche Python-Version?
Welche Zope-Version?
Welche Python-Bibliotheken?
Achten Sie darauf, dass alle der von Ihnen verwendeten Zusatzprodukte die neue Plone-Version unterstützen.
Einen Überblick über alle Artikel einer Plone 2.5- oder 3.x-Site erhalten Sie mit mr.inquisition. Dabei erhalten Sie Informationen
zur Art der Artikeltypen
zur Anzahl der Artikel je Artikeltyp
zum Ort der Artikel
zu Artikeln, die mit deinstallierten Produkten erstellt wurden
Installiert werden kann mr.inquisition
mit:
[buildout]
...
eggs =
...
mr.inquisition
[instance]
...
zcml =
mr.inquisition
Nach dem Durchlaufen des Buildout-Skripts und dem
Starten der Instanz erhalten Sie z.B. unter
http://localhost:8080/Plone/@@inquisition
einen
Überblick über die verfügbaren Ansichten zur Analyse
der ZODB.
Falls die neue Version von Plone auf einer aktuelleren Zope-Version basiert, sollten Sie diese vor dem Plone-Update installieren.
Achtung: Zope hat seine eigenen Migrationsrichtlinien, die in den release notes derjenigen Zope-Version zu finden sind, auf die Sie migrieren möchten. Im allgemeinen aktualisiert Plone jedoch mit seinen Migrationsskripten auch die Zope-Version.
Lesen Sie die folgenden Dateien in Ihrem CMFPlone-Ordner:
README.txt
INSTALL.txt
UPGRADE.txt
Diese Dateien können wichtige last minute Informationen und spezifische Anweisungen enthalten.
Ein schneller Überblick, wie für eine Plone-Site ein Upgrade durchgeführt werden kann.
Erstellen Sie eine neue Zope-Instanz mit Buildout.
Geben Sie im [productdistros]
-Abschnitt Ihrer buildout.cfg
-Datei alle erforderlichen Zusatzprodukte an.
Ihre eigenen aktualisierten Produkte können Sie in das Products
-Verzeichnis Ihres Buildout-Projekts kopieren.
Fahren Sie Ihre alte Zope-Instanz herunter.
Kopieren Sie die var/Data.fs
-Datei aus Ihrer alten Instanz in das var/filestorage
-Verzeichnis Ihres Buildout-Projekts.
Konfigurieren Sie Ihre neue Zope-Instanz, s.a. Buildout-Konfiguration.
Starten Sie Ihre neue Zope-Instanz.
Gehen Sie in das Zope Management Interface (ZMI) Ihrer Plone-Site und anschließend zum Plone Migrations Tool (portal_migrations
).
Nachdem Sie den Upgrade-Reiter gewählt haben, erhalten Sie eine Angabe wie diese:
Instance version: 4.3.18
File system version: 5.2
Klicken Sie die Upgrade-Taste.
zc.buildout
¶Üblicherweise verwendet bootstrap.py
nun zc.buildout
in Version 1.5.2 und nicht mehr 1.4.4. Die aktuelle Version von zc.buildout
referenziert jedoch nicht mehr die globalen Python Site Packages, weshalb eigene Pakete nun in das lokale Build übernommen werden müssen.
In einem bestehenden Buildout-Projekt können Sie zc.buildout
aktualisieren mit:
$ python2.7 bootstrap --version 1.5.2
Die Suche wurde in Plone 4.2 grundlegend überarbeitet, sodass frühere Anpassungen der Suche voraussichtlich nicht mehr funktionieren werden.
Um nun Ihre Änderungen für Plone 4.2 zu übernehmen, verwenden Sie am besten z3c.jbot.
Anschließend kopieren Sie plone.app.search/plone/app/search/search.pt
in Ihr Produkt, z.B. nach vs.theme/vs/theme/overrides/plone.app.search.search.pt
.
Nun starten Sie Ihre Instanz um zu überprüfen, ob dies fehlerfrei geschieht.
Schließlich können Sie Ihre gewünschten Änderungen am Suchformular vornehmen.
Beim Upgrade der Plone-Site bleiben die alten Kollektionen weiterhin erhalten als Collection (old-style); sie werden nicht migriert.
Die neuen Kollektionen implementieren die queryCatalog
-Methode in plone.app.collection.collection
, sodass in vielen Fällen voraussichtlich nur das Interface und die Referenzen auf den portal_type
geändert werden müssen.
Um diese Änderungen rückwärtskompatibel implementieren zu können, empfiehlt sich, diese Methode nur in Views zu verwenden, die ausschließlich den neuen Kollektionen vorbehalten sind. Dies lässt sich mit Conditionally run ZCML realisieren, also z.B. mit:
<browser:page
zcml:condition="installed plone.app.collection"
name="mycollectionview"
for="plone.app.collection.interfaces.ICollection"
class=".views.MyCollectionView"
permission="zope2.View"/>
Analog lassen sich auch Interfaces nur für neue Kollektionen registrieren, z.B.:
<class class="plone.app.collection.collection.Collection"
zcml:condition="installed plone.app.collection">
<implements interface=".interfaces.IMyCollectionInterface" />
</class>
getRawQuery
-Methode¶Die getRawQuery
-Methode können Sie selbst verwenden mit:
from plone.app.querystring import queryparser
query = queryparser.parseFormquery(collectionobj, collectionobj.getRawQuery())
Die Abhängigkeiten von Plone 4.3 sind deutlich verringert worden. Dadurch wird der Speicherverbrauch von Plone verringert und Importe beschleunigt. Es kann jedoch sein, dass Plone-Zusatzprodukte entsprechend angepasst werden müssen.
Importierte zope.*
-Pakete haben in neueren
Versionen weniger Abhängigkeiten
Zusätzliche Abhängigkeiten können in der
setup.py
-Datei angegeben werden.
Weitere Informationen hierzu erhalten Sie in Upgrade von Zusatzprodukten.
Im Folgenden eine Liste derjenigen Zusatzprodukte, die zwar in Plone 4.2, nicht jedoch in Plone 4.3 enthalten sind:
stattdessen wird nun lxml verwendet
kann z.B. für das Inline Editing zusätzlich installiert werden.
Einen Überblick über die gebräuchlichsten Importe und deren neue Orte erhalten Sie in Updating package dependencies.
ZODB selbst ist mit Python 3 kompatibel, eine in Python 2.7 erstellte Datenbank kann jedoch nicht ohne vorherige Migration in Python 3 verwendet werden. Hierfür müssen folgende Schritte ausgeführt werden:
Aktualisieren der Site auf Plone 5.2 mit Python 2, s.a. Upgrading Plone 5.1 to 5.2.
Stellt sicher, dass der Code aller von euch verwendeten Add-Ons in Python 3 funktioniert, s.a. Python 3-Migration.
Im Buildout mit py2env
und zodbverify überprüfen, ob die Integrität eurer
Datenbank gewährleistet ist:
Installation von zodbverify
. Hierfür wird zunächst die devel.cfg
geändert und anschließend das bin/buildout
aufgerufen:
[instance]
eggs +=
zodbverify
...
Anschließend kann die Datenbank überprüft werden mit:
$ bin/zodbupdate --convert-py3 --file=var/filestorage/Data.fs --encoding utf8
...
Updating magic marker for var/filestorage/Data.fs
Ignoring index for /Users/pbauer/workspace/projectx/var/filestorage/Data.fs
Loaded 2 decode rules from AccessControl:decodes
Loaded 12 decode rules from OFS:decodes
Loaded 2 decode rules from Products.PythonScripts:decodes
Loaded 1 decode rules from Products.ZopeVersionControl:decodes
Committing changes (#1).
Zusätzlich können noch weitere Optionen angegeben werden, z.B.:
-D
falls defekte Pickles entdekckt werden, können diese direkt debugged werden.
--encoding-fallback
falls UnicodeDecodeError
auftreten, ist die Instanz vermutlich nicht
einheitlich utf-8
kodiert. Hier empfiehlt es sich, als Fallback
latin1
zu verwenden, da dies die frühere Standardkodierung von Zope
war, also --encoding-fallback latin1
.
Falls Integritätsprobleme, auftreten, müssen diese vor der Migration gelöst werden. Weitere typische Probleme sind:
defekter Data.fs.index
, z.B.:
$ ./bin/zodbupdate --convert-py3 --file=var/filestorage/Data.fs --encoding=utf8
Updating magic marker for var/filestorage/Data.fs
loading index
Traceback (most recent call last):
File "/home/erral/downloads/eggs/ZODB-5.5.1-py3.6.egg/ZODB/FileStorage/FileStorage.py", line 465, in _restore_index
info = fsIndex.load(index_name)
File "/home/erral/downloads/eggs/ZODB-5.5.1-py3.6.egg/ZODB/fsIndex.py", line 134, in load
v = unpickler.load()
UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 249: ordinal not in range(128)
Diesen Fehler solltet ihr beheben können indem ihr vor der Migration die Datei
Data.fs.index
löscht.
fehlerhafte Suche
In diesem Fall solltet ihr den Katalog neu erstellen. Hierzu geht ihr in eurer
Plone-Site in den portal_catalog
, wählt den Advanced-Reiter und klickt
dann auf Clear and Rebuild.
Kopiert die Datenbank nun in ein Buildout mit py3env
, startet jedoch nicht
die Instanz.
Migriert die Datenbank mit zodbupdate.
Zunächst wird zodbupdate
mit Buildout installiert:
[buildout]
parts =+
zodbupdate
[zodbupdate]
recipe = zc.recipe.egg
eggs =
zodbupdate
${buildout:eggs}
Anschließend kann die ZODB aktualisiert werden mit:
$ bin/zodbupdate -f var/filestorage/Data.fs
Überprüft die Integrität eurer Datenbank mit zodbverify
. Wenn Probleme
auftreten, behebt diese und wiederholt die Migration.
Startet die Instanz und überprüft manuell ob alles wie erwartet funktioniert.
Das ATContentTypes-Produkt wird mit einem eigenen Migration Framework ausgeliefert.
ATContentTypes verwendet eine Registry für Migrationen, die im ATCT Tool (portal_atct
) verwaltet werden. Dies ermöglicht, Migrationen in einem einfach zu bedienenden Web-Interface durchzuführen. In vielen Fällen reicht jedoch eine manuell erstellte External Method vollkommen aus, da Migrationen meist nur einmal aufgerufen werden müssen.
Zu einem früheren Zeitpunkt wurde ein CMF-Inhaltstyp PhotoAlbum erstellt und dieser soll nun in einen einfachen Archetypes Folder zurückverwandelt werden. Hierzu wird, sofern noch nicht vorhanden, in unserem Produkt src/vs.photo/vs/photo/upgrades.py
und folgendem Inhalt angelegt:
from Products.CMFCore.utils import getToolByName
from StringIO import StringIO
from Products.ATContentTypes.migration.walker import CatalogWalker
from Products.ATContentTypes.migration.migrator import CMFFolderMigrator
class PhotoAlbumMigrator(CMFFolderMigrator):
"""Base class to migrate PhotoAlbum to Folder.
"""
walkerClass = CatalogWalker
src_meta_type = 'PhotoAlbum'
src_portal_type = 'PhotoAlbum'
dst_meta_type = 'ATFolder'
dst_portal_type = 'Folder'
def migrate(self):
"""Run the migration"""
out = StringIO()
print >> out, "Starting migration"
portal_url = getToolByName(self, 'portal_url')
portal = portal_url.getPortalObject()
migrators = (PhotoAlbumMigrator,)
for migrator in migrators:
walker = migrator.walkerClass(portal, migrator)
walker.go(out=out)
print >> out, walker.getOutput()
print >> out, "Migration finished"
return out.getvalue()
Nun muss nur noch im Wurzelverzeichnis der Site eine neue External Method erstellt werden mit
migrateTypes
vs.photo.migrate
migrate
Um die externe Methode auszuführen, müssen Sie nur noch in den «Test»-Reiter klicken 1.
In dem oben genannten Beispiel wird in der migrate()
-Funktion ein Migrator auf alle mit einem Walker gefundenen Objekte angewendet.
Sie werden zum Finden der zu migrierenden Inhalte verwendet. Der einfachste Migrator ist der CatalogWalker in walker.py, der eine Katalogabfrage für alle Inhalte eines bestimmten Typs durchführt.
Sie sind einfache Klassen, die die gegebenen Inhaltstypen migrieren. Das Framework enthält Basisklassen, die das Schreiben von Migrators stark vereinfachen.
Ein Migrator ist meist eine von CMFFolderMigrator
oder CMFItemMigrator
abgeleitete Klasse. Diese beiden und weitere Basis-Migrationsklassen sind in migrator.py definiert:
CMFItemMigrator
migriert einen CMF-Typ einschließlich seiner Meta-Angaben, lokalen Rollen etc.
CMFFolderMigrator
gewährleistet darüberhinaus, dass auch die Inhalte migriert werden.
Es gibt drei Arten von Migrationen:
Jede Methode in einer Klasse, die mit migrate_
beginnt, wird automatisch aufgerufen.
Wenn Sie sich z.B. die BaseMigrator
-Klasse anschauen, sehen Sie eine Reihe solcher Methoden, migrate_properties
, migrate_owner
, etc.
Auch die Reihenfolge, in der die Methoden aufgerufen werden, sind durch Präfixe definiert:
beforeChange_
Methoden, wie z.B. beforeChange_storeDates
oder beforeChange_storeSubojects
, die vor der Migration angewandt werden.
last_migrate_
Methoden, wie last_migrate_date
, die aufgerufen werden, bevor der Migrator die Migration eines Objekts beendet.
Die Methode custom()
wird nach den migrate_
- aber vor den last_migrate_
-Methoden aufgerufen. Die Standardimplementierung ist leer und dient nur dazu,von einer eigenen Migrationsmethode überschrieben zu werden. Hier ein Beispiel aus atctmigrator.py:
class FileMigrator(CMFItemMigrator):
walkerClass = CatalogWalker
...
def custom(self):
ctype = self.old.getContentType()
file = str(self.old)
self.new.setFile(file, mimetype = ctype)
Schließlich noch die einfachste Methode mit der map class
-Variablen, die eine Zuordnung von Attributen und/oder Methoden erlaubt. Auch hier wieder ein Beispiel aus atctmigrator.py:
class LinkMigrator(CMFItemMigrator):
walkerClass = CatalogWalker
map = {'remote_url' : 'setRemoteUrl'}
Warnung: Migrationen können normalerweise nur selten rückgängig gemacht werden da sie meist mehrere Transaktionen zugleich umfassen. Daher sollten Sie unbedingt vor der Migration eine Sicherungskopie Ihrer Plone-Site erstellen.
Das contentmigration-Produkt wird für Migrationen verschiedener Versionen desselben Typs genutzt.
Häufiger als die Migration von einem Inhaltstyp zu einem anderen ist die Änderung der internen Organisation eines Inhaltstyps. Das Archetypes-Tool bietet einige Hilfsmittel zur Durchführung von solchen Migrationen über den Update Schema-Reiter. Diese synchronisieren die ZODB-Schemata Ihrer Inhaltstypen mit dem auf Dateisystem-Ebene definierten Inhaltstyp. Beachten Sie jedoch, dass Sie dabei Daten verlieren können. Wenn Sie z.B. ein Feld umbenannt haben, führt Update Schema für bestehende Objekte zu leeren oder Standardwerten für das neue Feld. Um die alten Daten zu erhalten, ist eine Migration nötig.
Für solche Fälle wurde das contentmigration-Produkt geschreiben. Sie können contentmigration installieren, indem Sie in buildout.cfg
folgendes eintragen:
[buildout]
...
eggs =
...
Products.contentmigration
Anschließend kann es von Ihren eigenen Migrationsmethoden aufgerufen werden, z.B.:
from Products.contentmigration.walker import CustomQueryWalker
from Products.contentmigration.migrator import FieldActionMigrator
from Products.Archetypes.public import *
class MyMigrator(FieldActionMigrator):
src_portal_type = 'MyType'
src_meta_type = 'MyType'
fieldActions = ({'fieldName' : 'someField',
'storage' : AttributeStorage(),
'newFieldName' : 'renamedField',
'newStorage' : AnnotationStorage(),
'transform' : lambda obj, val, **kw: val + 10,
})
contentmigration erweitert die ATContentTypes-Migrator um folgende Methoden:
Der CustomQueryWalker
spezifiziert eine Katalogabfrage, z.B. kann die Migration auf Teilinhalte Ihrer Site beschränkt werden mit:
walker = CustomQueryWalker(portal, migrator,
query = {'path' : '/some/path'})
walker.go()
Gegebenenfalls können src_portal_type
und src_meta_type
aus dem Migrator in die Anfrage eingefügt werden.
Der CustomQueryWalker
kann von jedem Migrator genutzt werden.
Der BaseInlineMigrator
ist dem BaseMigrator
ähnlich; während jedoch der BaseMigrator
ein altes Objekt temporär kopiert, ein neues Objekt erstellt und dann die Migrationsmethoden anwendet, werden beim BaseInlineMigrator
die Migrationsmethoden auf der Stelle angewendet. Dies vereinfacht den Code deutlich, da Attribute, lokale Rollen etc. nicht kopiert werden müssen. Er eignet sich daher für Migrationen, in denen sich die Felder bestimmter Objekte ändern.
Folgende Methoden können verwendet werden: migrate_
, beforeChange_
last_migrate_
und custom()
, nicht jedoch das map class
-Attribut.
Achtung: Sie können nur self.obj
verwenden, nicht self.old
und self.new
.
Auch BaseInlineMigrator
kann von jedem Walker verwendet werden.
Der FieldActionMigrator
ist eine Erweiterung von BaseInlineMigrator
um Aktionen, die auf Felder angewendet werden können. Dafür wird eine Liste der zu migrierenden Attribute und der auszuführenden Aktion angegeben. Detaillierte Informationen zu diesen Aktionen erhält man direkt in der field.py-Datei. Beispiele finden sich in testATFieldMigration.py.
plone.app.contenttypes kann alle Standard-Plone-Artikeltypen zu Dexterity migrieren. Hierzu gehören auch Topics, die früheren Kollektionen. Die Migration schließt auch die meisten Features ein, wie Portlets, Kommentare, Contentrules, lokale Rollen und lokale Workflows.
Warnung
Frühere Versionen der Inhalte bleiben bei der Migration nicht erhalten.
Mit @@pac_installer
gibt es eine Ansicht, die die Installation von plone.app.contenttypes
erlaubt ohne dass die alten Archetypes-basierten
Artikeltypen ersetzt werden. Anschließend wird auf das Migrationsformular
weitergeleitet und die zu migrierenden Artikeltypen können ausgewählt werden.
Dies erlaubt ihnen, nur bestimmte Archetypes-Artikeltypen zu migrieren.
Topics unterstützten sog. Subtopics, ein Feature, das in Kollektionen nicht
mehr exisitert. Daher müssen Kollektionen zunächst folderish werden bevor eine
Migration von Subtopics durchgeführt werden kann. Hierfür muss die Basisklasse
von plone.dexterity.content.Container
abgeleitet werden und nicht von
plone.dexterity.content.Item
:
from plone.app.contenttypes.behaviors.collection import ICollection
from plone.dexterity.content import Container
from zope.interface import implementer
@implementer(ICollection)
class FolderishCollection(Container):
pass
Falls die bestehende Kollektion überschrieben werden soll, kann die
Collection.xml
verwendet werden:
<?xml version="1.0"?>
<object name="Collection" meta_type="Dexterity FTI">
<property name="klass">my.package.content.FolderishCollection</property>
</object>
Und falls die Suche der übergeordneten Kollektion vererbt werden soll, sind die Änderungen aus acquire query erforderlich.
Da LinguaPlone die Dexterity-Artikeltypen nicht unterstützt, muss zunächst von LinguaPlone zu plone.app.multilingual migriert werden. Weitere Hinweise finden Sie in LinguaPlone-Migration.
collective.contentleadimage
¶collective.contentleadimage erweiterte die Standard-Plone-Artikeltypen um ein Bild. Dieses Bild bleibt bei der Migration erhalten sofern der zugehörige Dexterity-Artikeltyp das Behavior «Lead Image» hat. Dabei informiert zunächst das Navigationsformular mit dem Kommentar extended fields: ‘leadImage’, ‘leadImage_caption’ und auch das Migrationsformular zeigt für jeden Dexterity-Typ an, ob er das Behavior hat.
Während der Migration der Standard-Artikeltypen werden die eigenen Artikeltypen
nicht migriert. plone.app.contenttypes
enthält jedoch auch ein
Migrationsformular für solche Artikeltypen: @@custom_migration
. Dabei muss
der Dexterity-Artikeltyp, zu dem migriert werden soll existieren und die Klasse
des alten Artikeltyps noch existieren. Der alte Artikeltyp muss hingegen nicht
in portal_types
registriert sein – er kann dort auch bereits durch den
Dexterity-Typ ersetzt worden sein.
Im View @@custom_migration
kann für jeden Archetypes- der entsprechende
Dexterity-Typ ausgewählt werden. Anschließend können die Felder aufeinander
abgebildet werden oder auch Felder ignoriert werden.
Anschließend wird die Konfiguration überprüft indem ein migrierter Artikel aufgerufen wird: Geschieht dies fehlerfrei, ist der Test bestanden. Anschließend werden die Änderungen wieder zurückgerollt.
Häufig sind mit einer Aktualisierung von Plone auch die verwendetenZusatzprodukte zu aktualisieren. Dies ist nur eine generelle Anleitung, die keine Rücksicht auf produktspezifische Upgrade-Prozeduren nimmt.
Haben Sie weitere Produkte in Ihrer alten Plone-Site verwendet, müssen für diese ebenfalls aktuelle Versionen in der neuen Zope-Instanz installiert werden.
Starten Sie Ihre neue Zope-Instanz und überprüfen anschließend, ob im Product Management-Ordner des Zope Management Interface (ZMI) (INSTANCE_URL/Control_Panel/Products/manage_main
) alle Produkte korrekt installiert wurden.
Navigieren Sie anschließend im ZMI zu Ihrer Site und dort in das Plone QuickInstaller Tool (portal_quickinstaller
). Führen Sie ein Upgrade oder eine Neuinstallation der jeweiligen Produkte durch.
Für die Migration von Plone3 auf Plone4 sollten die folgenden Änderungen beachtet werden.
Viele globale Definitionen aus global_defines.pt
werden nicht mehr vollständig im main_template.pt
eingebunden, da dies die Performance deutlich beeinträchtigt. Einige globale Variablen wie z.B. context
, view
und template
bleiben jedoch erhalten. Nicht erhalten geblieben sind jedoch die Definitionen aus der _initializeData
-Klasse im @@plone
-View von Plone3: Products/CMFPlone/browser/ploneview.py
Es empfiehlt sich daher, alle Seiten Ihres Produkts sich in einer Plone4-Site anzuschauen und dann ggf. die globalen Definitionen selbst einzubinden, also z.B.:
tal:attributes="action string:$here_url/${template/getId}"
ersetzen durch:
tal:attributes="action string:${context/@@plone_context_state/object_url}/${template/getId}"
exists
verwenden z.B.::tal:condition=“python:exists(‚portal/mystyle.css‘)“
Diese Bedingung führt zu keinem Fehler, sondern die Überprüfung schlägt auch fehl, wenn portal
nicht definiert ist. Daher sollten Sie alle Templates nach exists
durchsuchen und überprüfen, ob die verwendeten globalen Definitionen auch tatsächlich vorhanden sind.
Produkte, die Icons für CMF-Aktionen am Action Icons Tool regsitrierten, sollten zukünftig die icon_expr
-Anweisung verwenden um Icons am ‚Actions Tool* oder im Control Panel Tool zu registrieren. So wird beispielsweise in Plone4 das Icon für Document in Products/CMFPlone/profiles/default/types/Document.xml
so angegeben:
<property name="icon_expr">string:${portal_url}/document_icon.png</property>
Zope2 vor Version 2.12.0 unterstützte zwei verschiedene Arten von Interfaces, die Zope2- und die Zope3-Implementierung:
Zope2:
from Interface import Interface
class MyInterface(Interface):
pass
class MyClass(object):
__implements__ = (MyInterface,)
Zope3:
from zope.interface import Interface
class MyInterface(Interface):
pass
class MyClass(object):
implements(MyInterface)
In Zope2.12 werden Zope3-Interfaces unterstützt.
Bei einer Zope2-Implementierung von Intervace wird dann folgender Fehler ausgegeben:
ImportError: No module named Interface
Diverse import-Methoden sind verschoben worden. Früher bereits als deprecated gekennzeichnete Methoden wurden entfernt.
Folgende Methoden wurden verschoben:
alt |
neu |
---|---|
Products.ATContentTypes.content.folder.ATFolder |
plone.app.folder.folder.ATFolder |
Products.ATContentTypes.content.folder.ATFolderSchema |
plone.app.folder.folder.ATFolderSchema |
Products.CMFPlone.browser.navtree.SitemapNavtreeStrategy.icon |
Products.CMFPlone.browser.navtree.SitemapNavtreeStrategy.item_icon |
Products.CMFPlone.browser.plone |
Products.CMFPlone.browser.ploneview |
Products.CMFPlone.browser.ploneview.cache_decorator |
plone.memoize.instance.memoize |
Products.CMFPlone.browser.ploneview.IndexIterator |
Products.CMFPlone.utils.IndexIterator |
Products.CMFPlone.browser.ploneview.Plone.isRightToLeft |
@@plone_portal_state/is_rtl |
Products.CMFPlone.browser.ploneview.Plone.keyFilteredActions |
@@plone_context_state/keyed_actions |
Products.CMFPlone.browser.portlets |
plone.app.portlets.portlets |
Products.CMFPlone.interfaces.OrderedContainer.IOrderedContainer |
OFS.interfaces.IOrderedContainer |
Products.CMFPlone.utils.BrowserView |
Products.Five.BrowserView |
Products.CMFPlone.utils.getGlobalTranslationService |
Products.PageTemplates.GlobalTranslationService.getGlobalTranslationService |
Products.CMFPlone.utils.scale_image |
Products.PlonePAS.utils.scale_image |
Products.CMFPlone.utils.utranslate |
zope.i18n.translate |
Products.PageTemplates.GlobalTranslationService.getGlobalTranslationService |
zope.i18n |
Products.CMFPlone.utils.ulocalized_time |
Products.CMFPlone.i18nl10n.ulocalized_time |
zope.app.cache.interfaces.ram.IRAMCache |
zope.ramcache.interfaces.ram.IRAMCache |
Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget.ReferenceBrowserWidget |
archetypes.referencebrowserwidget.ReferenceBrowserWidget |
Products.CMFPlone.CatalogTool.registerIndexableAttribute
Stattdessen sollte plone.indexer
verwendet werden.
Products.CMFPlone.PloneTool.setDefaultSkin
Products.CMFPlone.PloneTool.setCurrentSkin
Products.CMFPlone.PortalContent
Favorite-Artikeltyp
use_folder_tabs
aus den site_properties
keyed_actions
Sollte durch die actions
-Methode aus @@plone_context_state
ersetzt werden, die nun als einzigen Parameter eine action category benötigt.
Auch Validatoren benötigen nun Zope3-Interfaces da ansonsten beim Starten der Instanz folgender Fehler ausgegeben wird:
Products.validation.exceptions.FalseValidatorError:
<vs.registration.validators.ProjectIdValidator instance at 0xa92082c>
Die Zeile:
__implements__= (IValidator,)
sollte ersetzt werden durch:
from zope.interface import implements
...
try:
# Plone 4 and higher
import plone.app.upgrade
USE_BBB_VALIDATORS = False
except ImportError:
# BBB Plone 3
USE_BBB_VALIDATORS = True
...
if USE_BBB_VALIDATORS:
__implements__ = (ivalidator,)
else:
implements(IValidator)
translate
-Methode¶Folgende Module stehen nicht mehr zur Verfügung:
Products.CMFPlone.utils.utranslate
Products.PageTemplates.GlobalTranslationService.getGlobalTranslationService
Stattdessen sollte zope.i18n.translate
verwendet werden.
Und mit zope.i18n.translate
ändert sich dann auch der Aufruf gegenüber utranslate
:
msgid
ist nun das erste und nicht mehr erst das zweite Argument dieses Aufrufs.
domain
ist nun optional.
In Plone 4 kann jeder Artikeltyp im Portal Types Tool eine zusätzliche Eigenschaft für die Ansicht beim Hinzufügen haben. Diese Eigenschaft wird als TALES
-Ausdruck für eine URL angegeben werden. Ein Link mit dieser URL wird Nutzern dann im Hinzufügen-Menü von Plone angezeigt.
Diese Eigenschaft hat den Titel Add view URL (expression) und die interne ID add_view_expr
This property has the title Add view URL (expression) and the internal id add_view_expr
.
Auf diese Weise lässt sich z.B. für ein selbst-entwickeltes Hinzufügen-Formular folgender Ausdruck angeben:
string:${folder_url}/@@add-my-content
Beachten Sie hierbei, dass der View für den folder
-Artikeltyp registriert wird und nicht für den zu erstellenden Artikeltyp.
send
statt secureSend
¶Mit der send
-Methode ändern sich auch weitere Angaben:
Nun wird der vollständige MIME type als msg_type
angegeben und nicht mehr nur der subtype-Parameter, also z.B.:
msg_type='text/plain'
statt:
subtype='plain'
Um eigene Headers-Angaben für eine Nachricht anzugeben, kann z.B. folgendes angegeben werden:
from email import message_from_string
from email.Header import Header
my_message = message_from_string(message_body.encode('utf-8'))
my_message.set_charset('utf-8')
my_message['CC']= Header('someone@example.com')
my_message['BCC']= Header('secret@example.com')
my_message['X-Custom'] = Header(u'Some Custom Parameter', 'utf-8')
mailhost.send(my_message, mto, mfrom, subject)
In Plone 3 wird ein Portlet an einen bestimmten Portlet-Manager gebunden mit der Anweisung:
for="plone.app.portlets.interfaces.IColumn"
In Plone 4 erfolgt dies nun mit:
<for interface="plone.app.portlets.interfaces.IColumn" />
Somit lassen sich auch mehrere Werte im for
-Feld angeben:
<for interface="plone.app.portlets.interfaces.IColumn" />
<for interface="plone.app.portlets.interfaces.IDashboard" />
Für die Migration auf Plone 4.1 sollten die folgenden Änderungen beachtet werden.
Plone
zu Products.CMFPlone
¶setup.py
-Datei¶In der setup.py
-Datei sollte bisher folgendes stehen:
install_requires=[
'setuptools',
'Plone',
],
Stattdessen sollte in Plone 4.1 folgendes verwendet werden:
install_requires=[
'setuptools',
'Products.CMFPlone',
],
Auch sollten hier die anderen Pakete aufgelistet werden, von denen das Paket abhängt, z.B. Products.Archetypes
oder plone.app.portlets
.
Sofern pages
etc. in einer zcml
-Datei mit ener CMF-Core-Permission geschützt sind, muss die configure.zcml
-Datei ergänzt werden um:
<include package="Products.CMFCore" file="permissions.zcml"
xmlns:zcml="http://namespaces.zope.org/zcml"
zcml:condition="have plone-41" />
Einige ältere Import-Aliase funktionieren in Plone 4.1 nicht mehr und müssen ersetzt werden:
Frühere Plone-Versionen |
Plone 4.1 |
---|---|
|
|
|
|
|
|
Ab Plone 4.1 kann die Konfiguration zur Versionierung eigener Artikeltypen im Profil repositorytool.xml
angegeben werden. Genaue Angaben hierzu finden Sie in
Wenn Sie von Plone 4.0 aktualisieren, erhalten Sie vermutlich folgende Fehlermeldung:
ImportError: cannot import name DEFAULT_POLICIES
Um die Kompatibilität Ihres Produkts mit Plone 4.1 wiederherzustellen, muss für Ihren bisherigen Code in der setuphandlers.py
-Datei eine Bedingung angegeben werden, also z.B. statt:
from Products.CMFEditions.setuphandlers import DEFAULT_POLICIES
sollte folgendes angegeben werden:
try:
from Products.CMFEditions.setuphandlers import DEFAULT_POLICIES
# we're on plone < 4.1, configure versionable types manually
setVersionedTypes(portal)
except ImportError:
# repositorytool.xml will be used
pass
Für Plone 4.1 wird die Konfiguration zur Versionierung der Artikel im Profil repositorytool.xml
angegeben:
<?xml version="1.0"?>
<repositorytool>
<policymap>
<type name="MyType">
<policy name="at_edit_autoversion"/>
<policy name="version_on_revert"/>
</type>
<type name="AnotherType">
<policy name="at_edit_autoversion"/>
<policy name="version_on_revert"/>
</type>
</policymap>
</repositorytool>
Falls ein EXTENSION-Profil zum Installieren eines Produkts verwendet wurde, können Upgrade steps des Generic Setup Tools verwendet werden, um Migrationsskripte zu verwalten.
Dabei kann das Generic Setup Tool die Versionsnummer entweder aus der version.txt
-Datei im Wurzelverzeichnis des Produkts auslesen oder aus einer metadata.xml
-Datei im Installationsprofil. Entsprechend wurde das Default
-Profil von vs.policy
ergänzt um src/vs.policy/vs/policy/profiles/default/metadata.xml
:
<?xml version="1.0"?>
<metadata>
<description>Policies for www.veit-schiele.de</description>
<version>1.3</version>
</metadata>
Die Upgrade steps werden dann in src/vs.policy/vs/policy/configure.zcml
registriert:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="vs.policy">
...
<!-- Upgrade step for the migration -->
<genericsetup:upgradeStep
sortkey="1"
source="1.0"
destination="1.1"
title="Upgrade from 1.0 to 1.1"
description="Fixes the front page title"
profile="vs.policy:default"
handler=".upgrades.v1_0_to_v1_1"
/>
...
</configure>
Hier wird die Aktualisierung von Version 1.0 auf 1.1 definiert, wobei die Funktion v1_0_to_v1_1()
aufgerufen wird.
Die Funktion ist definiert in src/vs.policy/vs/policy/upgrades.py
:
from Products.CMFCore.utils import getToolByName
def v1_0_to_v1_1(portal_setup):
portal_url = getToolByName(portal_setup, 'portal_url')
portal = portal_url.getPortalObject()
front_page = portal['front-page']
front_page.setTitle('Welcome to Veit Schiele communication design')
Die Aktualisierung von 1.1 auf 1.2 teilt sich in zwei Teile auf, v1_1_to_v1_2a
und v1_1_to_v1_2b
. Dabei sorgt sortkey
für die richtige Reihenfolge bei der Aktualisierung über mehrere Versionen:
<genericsetup:upgradeSteps
sortkey="2"
source="1.1"
destination="1.2"
profile="vs.policy:default"
>
<genericsetup:upgradeStep
title="Upgrade titles"
description="Fix all other titles"
handler=".upgrades.v1_1_to_v1_2a"
/>
<genericsetup:upgradeStep
title="Upgrade site title"
description="Fixes the portal title"
handler=".upgrades.v1_1_to_v1_2b"
/>
</genericsetup:upgradeSteps>
Falls eine große Anzahl von Migrationen durchlaufen werden soll, kann sich ein spezielles Extension
-Profil empfehlen:
<genericsetup:registerProfile
name="1.2_to_1.3"
title="Migration profile for veit-schiele.de 1.2 to 1.3"
description=""
directory="profiles/migrations/v1_2_to_v1_3"
for="Products.CMFPlone.interfaces.IMigratingPloneSiteRoot"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
<genericsetup:upgradeStep
sortkey="3"
source="1.2"
destination="1.3"
title="Upgrade from 1.2 to 1.3"
description="Runs a migration profile"
profile="vs.policy:default"
handler=".upgrades.v1_2_to_v1_3"
/>
Zunächst wird das Extension
-Profil in profiles/migrations/v1_2_to_v1_3
für das Interface Products.CMFPlone.interfaces.IMigratingPloneSiteRoot
registriert, wodurch das Profil nicht beim Erstellen der Site oder in Plone’s Website-Konfiguration zum Hinzufügen/Entfernen von Produkten angezeigt wird. Anschließend wird noch ein upgrade step registriert, der das Profil angibt.
Schließlich muss noch das Verzeichnis src/vs.policy/vs/policy/profiles/migrations/v1_2_to_v1_3
und darin die entsprechenden XML-Dateien erzeugt werden.
Alle Third-Party-Add-Ons sollten sowohl Python 2 wie auch Python 3 unterstützen.
Eine Übersicht, welche Add-Ons in Collective bereits auf Python 3 aktualisiert wurden, findet ihr in Python 3 porting state for Plone add-ons.
Zur Python-3-Migration verwenden wir six und modernize.
sie können installiert werden mit:
$ python3 -m venv py3env
$ cd py3env
$ ./bin/pip install modernize six
precompiler
¶Zusätzlich verwenden wir plone.recipe.precompiler um Syntaxfehler zu finden. Er
kann mit Buildout installiert werden, indem in der py3.cfg
-Datei folgendes
angegeben wird:
parts += precompiler
...
[precompiler]
recipe = plone.recipe.precompiler
eggs = ${instance:eggs}
compile-mo-files = true
precompile
wird jedes Mal ausgeführt, wenn ihr Bbuildout ausführt. Wenn ihr nur precompile
ausführen möchtet, könnt ihr dies mit:
$ bin/buildout -c py3.cfg install precompiler
python-modernize
¶python-modernize
bereitet Python-2-Code automatisch für die Python-3-Portierung
vor. Dabei weist euch python-modernize
auf Probleme hin, die nicht automatisch
gelöst werden können.
Mit bin/python-modernize -x libmodernize.fixes.fix_import src/my.package
könnt
ihr euch anzeigen lassen, welche Änderungen modernize
an eurem Plone-Add-on
my.package
vornehmen würde.
Bemerkung
Ihr könnt python-modernize
u.a. mit folgenden Optionen aufrufen:
-x
schließt bestimmte Fixers aus.
-l
listet euch alle verfügbaren Fixers auf.
Bemerkung
Im Cheat Sheet Writing Python 2-3 compatible code erhaltet ihr einen Überblick, wie sich die Syntax von Python 2 zu Python 3 ändert.
Wir verwenden die py3.cfg
aus dem Plone-5.2-Branch von vs_buildut:
$ bin/buildout -c py3.cfg
$ bin/wsgi.py
Häufige Probleme beim Starten sind:
Class Advice
Relative Imports
Syntax Error beim Import von async
Neben dem manuellen Testen solltet ihr automatisiert Testen mit:
$ bin/test --all -s my.package
Alternativ könnt ihr den Testrunner automatisch den Python-Debugger starten lassen mit:
$ bin/test -s my.package -D
Aktualisiert die classifiers
in setup.py
aktualisiert:
classifiers=[
...
"Framework :: Plone :: 5.2",
...
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
...
],
Meist wird in Plone Text verwendet und nur in sehr seltenen Fällen Bytes.
Versucht den Code so zu ändern, dass ihr mit Beenden der Python 2-Unterstützung einfach nur das``if``-Statement löschen müsst, z.B.:
if six.PY2 and isinstance(value, six.text_type):
value = value.encode('utf8')
do_something(value)
Dabei könnt Ihr Hilfsmethoden verwenden wie safe_text
, safe_bytes
, safe_unicode
und safe_encode
, z.B.:
from Products.CMFPlone.utils import safe_unicode
...
obj = self.context.unrestrictedTraverse(
safe_unicode(item['_path'].lstrip('/')), None)
python-modernize
ändert ebenfalls nicht from StringIO import StringIO
obwohl
der Import nur in Python-2 funktioniert. Für Python-3 müsst ihr überprüfen, ob es
sich um Text- oder Binärdaten handelt und die import-Anweisung entsprechend
schreiben:
from six import StringIO
oder:
from six import BytesIO
Weitere Informationen findet ihr im The Conservative Python 3 Porting Guide.
Prozeduren beim Auftreten von Problemen während des Plone-Upgrades.
Fährt die Zope-Instanz nicht mit ZOPE_INSTANCE/bin/zopectl start
hoch, erhalten Sie mit ZOPE_INSTANCE/bin/zopectl fg
Hinweise, an welcher Stelle Zope hängen bleibt.
Überprüfen der Log-Dateien
Wenn ein Fehler auf der aktualisierten Plone-Site ausgegeben wird, gibt es möglicherweise eine informative Fehlermeldung in den Log-Dateien der Zope-Instanz. Gehen Sie zu ZOPE_INSTANCE/logs/event.log
und suchen dort nach error
, exception
oder traceback
.
Detailliertere Informationen zu den Fehlermeldungen finden Sie in
Testen der Anpassungen
Wenn Sie Page Templates und Python-Skripte angepasst haben, mögen diese Änderungen mit der neuen Plone-Versionen interferieren. Entfernen Sie temporär diese Anpassungen indem Sie die Layer aus dem von Ihnen verwendeten Skin entfernen.
Testen der Zusatzprodukte
Um Bugs oder Kompatibilitätsprobleme in den installierten Zusatzprodukten herauszufinden, deinstallieren Sie alle Produkte, die nicht zusammen mit Plone ausgeliefert werden, in Site Setup, Add/Remove Products. Anschließend entfernen Sie diese Produkte aus dem Produktverzeichnis Ihrer Zope-Instanz.
Falls das Problem hiermit beseitigt wird, überprüfen Sie für jedes der betreffenden Produkte:
ob dessen Version auch mit den aktuellen Versionen von Plone, Zope und Python kompatibel ist?
ob dieses Produkt zusätzliche Upgrade-Prozeduren benötigt?
ob das Produkt korrekt installiert ist? Installieren Sie es erneut und überprüfen Sie die Ausgabe beim Starten der Zope-Instanz mit ZOPE_INSTANCE/bin/zopectl fg
.
Liegt ein Produkt nicht für die aktuelle Plone-Version vor und hinterlässt beim Deinstallieren »verwaiste« Inhalte, so können Sie sich mit folgendem Skript solche Inhalte auflisten lassen:
portal_types = context.portal_types.objectIds()
print "Orphaned items:"
print
for i in context.portal_catalog.uniqueValuesFor('portal_type'):
if i in portal_types: continue
print i
results = context.portal_catalog(portal_type=i)
for i in results:
print i.getURL()
print
return printed
Um das Skript zu verwenden, erstellen Sie im ZMI des Wurzelverzeichnisses Ihrer Plone-Site ein Python-Skript, kopieren den Code in das Formular und sichern es anschließend. Zum Ausführen des Skripts müssen Sie nun nur noch in den «Test»-Reiter klicken.
Wie Sie Upgrade-Skripte für Ihre eigenen Produkte schreiben können, erfahren Sie hier: Eigene Upgrade-Skripte schreiben.
Testen Sie in einer neu aufgesetzten Plone-Site.
Falls das Problem nicht in einer neu aufgesetzten Plone-Site auftritt, bedeutet dies, dass die Ursache zu suchen ist
in einer Anpassung
einem Zusatzprodukt
oder in Inhalten, die nicht sauber migriert wurden.
Machen Sie das Problem reproduzierbar
Bevor Sie andere um Hilfe bitten, sollten Sie das Problem so beschreiben können, dass es für andere in deren Systemumgebung nachvollziehbar ist. Schränken Sie hierzu die Fehlerquellen so weit wie möglich ein um es anderen zu erleichtern, Ihr Problem nachvollziehen zu können.
Häufig werden Local Persistent Utilities beim Deinstallieren eines Produkts nicht ebenfalls entfernt.
Bemerkung
Mit wildcard.fixpersistentutilities steht nun ein Modul zur Verfügung, mit dem sich Local Utilities, Subscribers, Adapters und Portal Tools auf der Web-Oberfläche entfernen lassen.
Nach dem Deinstallieren solcher Produkte kann es z.B. folgende Fehlermeldungen geben:
AttributeError: type object 'IQueue' has no attribute '__iro__'
oder:
AttributeError: type object 'ISalt' has no attribute '__iro__'
Starten der Instanz im Debug-Modus:
$ ./bin/instance debug
Anschließend holen wir uns den site manager der Site Plone
. àpp`
referenziert dabei auf das Zope-Root-Objekt:
sm = app.Plone.getSiteManager()
Nun importieren wir das Interface des Utility. Anschließend melden wir es ab und löschen es schließlich. Dies sieht z.B. für Singing & Dancing so aus:
from collective.singing.interfaces import ISalt
from collective.singing.async import IQueue
util_obj = sm.getUtility(ISalt)
sm.unregisterUtility(provided=ISalt)
del util_obj
sm.utilities.unsubscribe((), ISalt)
del sm.utilities.__dict__['_provided'][ISalt]
del sm.utilities._subscribers[0][ISalt]
util = sm.queryUtility(IQueue, name='collective.dancing.jobs')
sm.unregisterUtility(util, IQueue, name='collective.dancing.jobs')
del util
del sm.utilities._subscribers[0][IQueue]
Dabei unterscheidet sich das Vorgehen, da für ISalt
ein unnamed utility und für IQueue
ein named utility registriert sind.
Anschließend müssen die Änderungen noch an der ZODB commited werden:
import transaction
transaction.commit()
app._p_jar.sync()
sm = app.Plone.getSiteManager() adapters = sm.utilities._adapters for x in adapters[0].keys(): if x.__module__.find("my.package") != -1: print "deleting %s" % x del adapters[0][x] sm.utilities._adapters = adapters subscribers = sm.utilities._subscribers for x in subscribers[0].keys(): if x.__module__.find("my.package") != -1: print "deleting %s" % x del subscribers[0][x] sm.utilities._subscribers = subscribers provided = sm.utilities._provided for x in provided.keys(): if x.__module__.find("my.package") != -1: print "deleting %s" % x del provided[x] sm.utilities._provided = provided from transaction import commit commit() app._p_jar.sync()
Plone
Die ID der Site
my.package
Das Paket, aus dem Subscriber, Adapter und Provider kommen
Nach dem Entfernen von lokalen persistenten Komponenten muss ggf. auch noch das Portal Setup Tool bereinigt werden:
setup_tool = app.Plone.portal_setup
toolset = setup_tool.getToolsetRegistry()
if 'my.package' in toolset._required.keys():
del toolset._required['my.package']
setup_tool._toolset_registry = toolset
from transaction import commit
commit()
app._p_jar.sync()
wildcard.fixpersistentutilities ist ein Python-Package, das das einfache Entfernen von Local Persistent Utilities ermöglicht. Damit lassen sich p4a, Singing & Dancing, LinguaPlone etc. einfach beseitigen.
wildcard.fixpersistentutilities stellt folgende Features bereit:
Entfernen von adapters
Entfernen von subscribers
Entfernen von provided interfaces
Entfernen von provided interfaces
Dies ist z.B. hilfreich beim Entfernen von collective.flowplayer.
Das Paket sollte nie in produktiven Umgebungen eingesetzt werden. Auch sollten Sie vorher immer eine Sicherungskopie Ihrer ZODB erstellt haben. Um das Paket dann nutzen zu können, fügen Sie einfach der URL Ihrer Plone-Site-Root @@fix-persistent-utilities
hinzu. Anschließend können Sie alle registrierten Utilities Ihrer Website durchsuchen und ggf. entfernen.
Bemerkung
Mit wildcard.fixpersistentutilities steht nun ein Modul zur Verfügung, mit dem sich Local Utilities, Subscribers, Adapters und Portal Tools auf der Web-Oberfläche entfernen lassen.
Ist in einer Instanz jemals ein Plone 4 Artists (p4a)-Produkt installiert worden, lässt sich deren Utilities und Interfaces nicht mehr einfach entfernen.
Wir haben nun ein Skript entwickelt, mit dem sich diese Utilities und Interfaces löschen lassen: fixinterfaces.py.
Dieses Skript sollte im Wurzelverzeichnis des Buildout-Projekts abgelegt werden. Anschließend kann die Instanz im Debug-Modus gestartet werden:
$ ./bin/instance debug
Sofern unsere Site nun die ID mysite
hat, werden die Utilities und
Interfaces gelöscht.
Falls die Site eine andere ID enthält, lasst sich das Skript einfach ändern. Auch für andere Zusatzprodukte kann das Skript leicht modifiziert werden.
GSXML nutzt GenericSetup um ATCT-basierte Inhalte als XML-Daten im- und exportieren zu können.
GSXML benötigt
lxml kann folgendermaßen installiert werden:
Zunächst wird folgende Änderung in der buildout.cfg
-Datei vorgenommen:
[buildout]
...
extends =
...
lxml.cfg
Die lxml.cfg
-Datei sieht dann folgendermaßen aus:
[lxml]
parts =
staticlxml
pylxml
[pylxml]
recipe=zc.recipe.egg
interpreter=pylxml
eggs=
lxml
[staticlxml]
recipe = z3c.recipe.staticlxml
egg = lxml
Nun sollte Buildout problemlos durchlaufen und die Instanz neu gestartet werden können:
$ ./bin/buildout
$ ./bin/instance fg
GSXML kann einfach als Egg installiert werden:
[buildout]
...
eggs =
elementtree
collective.plone.gsxml
...
Anschließend wird das Egg noch im [instance]
-Abschnitt angegeben:
[instance]
...
zcml=
collective.plone.gsxml
...
Nachdem das Buildout-Skript erneut durchlaufen und die Instanz gestartet wurde kann GSXML einfach mit dem Quickinstaller für die Plone-Site installiert werden. Anschließend erscheinen im Aktionen-Menü die Einträge Export und Import:
Für die Migration einer Plone-Site mit LinguaPlone zu plone.app.multilingual sind zunächst einmal beide
Produkte zu installieren. Hierfür wird die Buildout-Konfiguration der Plone-
4.3-Site entsprechend geändert, z.B. in der devel.cfg
:
[instance-base]
eggs +=
...
Products.LinguaPlone
plone.app.multilingual
… und in der versions.cfg
:
[versions]
...
Products.LinguaPlone = 4.1.3
plone.app.multilingual = 2.0.0
Siehe hierfür auch github.com/veit/vs_buildout/commit.
Anschließend wird das Buildout-Skript aufgerufen und die Instanz neu gestartet. Nun
kann in der Plone-4.3-Site plone.app.multilingual
aktiviert und LinguaPlone
deaktiviert werden. Anschließend sollten im language controlpanel im Migration-
Reiter die folgenden vier Schritte ausgeführt werden:
Reindexieren des Sprachindex (optional)
Die Migration von LinguaPlone basiert auf einem aktuellen Sprachindex.
Zuordnen der Inhalte zum passenden root language-Ordner
In diesem Schritt werden die Inhalte in die entsprechenden root language- Ordner verschoben.
Übertragen der Referenzen aus dem LinguaPlone-Katalog in den
plone.app.multilingual
-Katalog
Aufräumen nach der Migration Dieser Schritt sucht und repariert verlorene Verknüpfungen.
Dieser Schritt ist nur erforderlich wenn LinguaPlone nicht installiert ist. Daher wird dieser Schritt auch nur dann angezeigt.
Tipps zu Migrationen und zukunftsfähigeren Produkten.
Starten Sie Ihre Zope-Instanz im Debug-Modus (ZOPE_INSTANCE/bin/zopectl fg
) und nutzen anschließend Ihr Produkt. Überprüfen Sie, ob auf der Konsole deprecation warnings ausgegeben werden.
Entfernen Sie den plone_deprecated
-Skin-Layer und überprüfen, ob Ihre Anwendung anschließend noch problemlos funktioniert.
Auf dem QuickInstaller basierende Installationen sollten stattdessen GenericSetup-Profile verwenden.
Ab CMF 2.1.0 steht addWorkflowFactory
nicht mehr zur Verfügung. Workflows können von da an nur noch mit dem GenericSetup-Tool erstellt werden. Zur Migration installieren Sie einfach Ihr Produkt in einer Plone2.5-Site und exportieren den Workflow anschließend mit dem GenericSetup-Tool. Bevor Sie schließlich die alte Python-basierte Workflow-Definition aus dem Extensions-Ordner löschen, achten Sie darauf, dass ggf. die workflow scripts erhalten bleiben.
manage_
-Methoden sollten durch events
ersetzt werden.
Um die Permissions aus dem CMFCore-Produkt zu importieren, sollte in der __init__.py
Datei folgender (rückwärtskompatibler) Code verwendet werden:
try: # for Plone 2.5 and above
from Products.CMFCore import permissions as CMFCorePermissions
except: # for Plone 2.1
from Products.CMFCore import CMFCorePermissions
Archetypes 1.5, das zusammen mit Plone 3.0 ausgeliefert wird, enthält kein Transaction
-Modul mehr, statt:
from Products.Archetypes import transaction
kann nun einfach folgendes verwendet werden:
import transaction
Zope hat die Syntax, um Transaktionen zu importieren geändert. Ab Zope 2.10.x wird die bisherige Syntax:
get_transaction().commit(1)
durch folgende ersetzt:
transaction.commit(1)
transaction
muss selbstverständlich zunächst importiert werden.
CMF hat seit längerer Zeit ContentFactoryMetadata
durch FactoryTypeInformation
ersetzt. Der Aufruf:
from Products.CMFCore.TypesTool import ContentFactoryMetadata
sollte also ersetzt werden durch:
from Products.CMFCore.TypesTool import FactoryTypeInformation
Berechtigungen wie z.B. cmf.ModifyPortalContent
müssen in zcml
angegeben werden:
<include package="Products.CMFCore" file="permissions.zcml"
xmlns:zcml="http://namespaces.zope.org/zcml"
zcml:condition="have plone-41" />
Dabei wird zcml:condition
in Plone 4.1.3 definiert in der meta.zcml
-Datei:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:meta="http://namespaces.zope.org/meta">
<meta:provides feature="plone-4" />
<meta:provides feature="plone-41" />
</configure>
Mit collective.transmogrifier lassen sich sog. Pipelines für den Ex- und Import von Webinhalten konfigurieren. Und zum Crawlen und Parsen einer statischen Website wird funnelweb verwendet.
Am Beispiel der auf dem Documentation-Server Sphinx basierenden Website des Plone-Nutzerhandbuch zeige ich exemplarisch, wie eine solche Migration aussehen kann: https://dev.veit-schiele.de/svn/plone-nutzerhandbuch/trunk/
Erstellen des migration
-Skripts:
$ ./bin/buildout -c migration.cfg
Aufrufen des Skripts mit:
$ ./bin/migration --ploneupload:target=http://admin:secret@localhost:8080/Plone/documentation/manual/plone-nutzerhandbuch
Anmerkung 1: In userem Fall wird das Plone-Nutzerhandbuch in eine lokale Plone-Site mit der ID Plone importiert wobei das Plone Help Center die ID documentation und das reference Manual die ID plone-nutzerhandbuch hat. Falls Sie die Dokumentation in eine andere Plone-Site mit anderen HTTP Basic Auth-Credentials importieren möchten, können Sie diese Zeile selbstverständlich entsprechend abändern.
Folgende Schritte werden während der Migration ausgeführt:
Aus der Sphinx-Dokumentation werden Titel, Beschreibung und Haupttext von jeder Seite extrahiert.
Anschließend werden die Inhalte für das Plone Help Center mit XML-RPC erzeugt.
Veröffentlichen der Artikel sofern notwendig und Verstecken des Ordners, der die Bilder enthält in der Navigation.
Anmerkung 2: Das Skript zum Hochladen der Dateien überschreibt momentan noch nicht bereits vorhandene Artikel. Falls Seiten umbenannt oder verschoben wurden, sollte zunächst die gesamte Dokumentation gelöscht werden bevor die Dateien erneut hochgeladen werden.
migration.cfg
¶Das Skript bin/migration
wird erstellt mit der in der migration.cfg
angegebenen Konfiguration:
[buildout]
...
migration
[migration]
recipe = funnelweb
crawler-url=file://${buildout:directory}/docs/html
crawler-ignore=
cgi-bin
javascript:
_static
_sources
genindex\.html
search\.html
saesrchindex\.js
cache-output =
template1-title = text //div[@class='body']//h1[1]
template1-_permalink = text //div[@class='body']//a[@class='headerlink']
template1-text = html //div[@class='body']
template1-_label = optional //p[contains(@class,'admonition-title')]
template1-description = optional //div[contains(@class,'admonition-description')]/p[@class='last']/text()
template1-_remove_useless_links = optional //div[@id = 'indices-and-tables']
templateauto-condition = python:False
titleguess-condition = python:True
indexguess-condition = python:True
hideguess-condition = python:item.get("_path","").startswith('_images') and item.get('_type')=='Folder'
changetype-value=python:{'Folder':'HelpCenterReferenceManualSection','Document':'HelpCenterLeafPage'}.get(item['_type'],item['_type'])
ploneprune-condition=python:item.get('_type') in ['HelpCenterReferenceManualSection','HelpCenterReferenceManual'] or item['_path'] == ''
recipe
Verwendet wird funnelweb. Weitere Informationen zu diesem Abschnitt erhalten Sie auf der Homepage.
crawler-url
legt die zu migrierende Website fest, in unserem Fall file://${buildout:directory}/docs/html
. Es könnte jedoch auch eine URL wie z.B. http://www.plone-nutzerhandbuch.de
angegeben werden.
crawler-ignore
Links, denen nicht gefolgt werden soll, können mit regulären Ausdrücken angegeben werden.
In Falle des Documentation Servers Sphinx werden u.a. die Verzeichnisse _static
und _sources
ignoriert.
cache-output
Da unsere Inhalte aus dem Dateisystem kommen, ist hier kein lokaler Cache nötig.
titleguess-condition
, indexguess-condition
Bilder erhalten ihren Titel aus dem Backlink-Text
hideguess-condition
Bilder werden nicht in der Navigation angezeigt
changetype-value
Statt Ordnern werden HelpCenterLeafPage- und statt Seiten HelpCenterLeafPage-Artikeltypen angelegt
ploneprune-condition
Die Inhalt aller ordnerähnlichen Artikel wird daraufhin überprüft, ob sie Inhalte enthalten, die noch nicht lokal gespeichert sind.
pipeline.cfg
¶Die pipeline.cfg
definiert dann die Reihenfolge, in der die Anweisungen abgearbeitet werden. Ein Beispiel finden Sie wieder für das Plone-Nutzerhandbuch: pipeline.cfg
drop-resources
filtert Ressourcen wie css-, Javascript- und Anwendungen aus
drop-unneeded-html
filtert nicht benötigten HTML-Code aus
treeserializer
muss vor dem localconstructor ausgeführt werden
templatefinder
extrahiert Titel, Beschreibung und Haupttext aus den von Sphinx generierten Seiten.
Beachten Sie bitte, dass Note-Leerzeichen in XPaths als  
angegeben werden müssen.
Weitere Infos zu XPath erhalten Sie unter
mark-container-remote-content-type
erstellt einen Hinweis, sodass Verzeichnisse beim Hochladen als HelpCenterReferenceManualSection angelegt werden
mark-page-remote-content-type
erstellt einen Hinweis, sodass HTML-Dateien beim Hochladen als HelpCenterLeafPage-Artikel angelegt werden
mark-image-folders-to-navigation-exclusion
versteckt den images-Ordner in der Navigation.
mark-remote-folder-to-be-cleaned
überprüft, ob ein Ordner Objekte enthält, die noch nicht lokal gespeichert wurden.
mark-remote-root-to-be-cleaned
löscht alle Inhalte aus dem ausgewählten Reference Manual
topublish
erstellt einen Hinweis, damit nach dem Hochladen der Workflow-Status auf veröffentlicht gesetzt wird.
Dabei wird berücksichtigt, dass Bilder keinen Workflow unterliegen.
ploneuploader
erstellt die Artikel in der Plone-Site
schemaupdater
aktualisiert die Inhalte der Plone-Site mit den extrahierten Inhalten aus der Sphinx-Dokumentation
set-folder-default-page
setzt index.html als Standardseite eines Ordners
publish
veröffentlicht die hochgeladene Dokumentation sofern sie noch nicht veröffentlicht ist
excludefromnavigation
versteckt Artikel in der Navigation
cleanremotefolder
löscht Objekte, die auf der Plone-Site sind, jedoch nicht in der lokalen Kopie
JSON (JavaScript Object Notation) ist gut geeignet um die Inhalte aus Plone im- und exportieren zu können. collective.jsonmigrator kann ggf. diese Azfgabe erleichtern.
Alternativ lässt sich auch einfach ein eigener View schreiben zum Export im JSON-Format:
import os
import base64
try:
import json
except ImportError:
# Python 2.5/Plone 3.3 use simplejson
import simplejson as json
from Products.Five.browser import BrowserView
from Products.CMFCore.interfaces import IFolderish
from DateTime import DateTime
# Private attributes for the export list
EXPORT_ATTRIBUTES = ["portal_type", "id"]
class ExportFolderAsJSON(BrowserView):
"""
Exports the current context folder as JSON.
"""
def convert(self, value):
"""
Convert value to more JSON friendly format.
"""
if isinstance(value, DateTime):
# Zope DateTime
return value.ISO8601()
elif hasattr(value, "isBinary") and value.isBinary():
return None
# FileField and ImageField payloads are binary
# as OFS.Image.File object
data = getattr(value.data, "data", None)
if not data:
return None
return base64.b64encode(data)
else:
return value
def grabData(self, obj):
"""
Export schema data as dictionary object.
Binary fields are encoded as BASE64.
"""
data = {}
for field in obj.Schema().fields():
name = field.getName()
value = field.getRaw(obj)
print "%s" % (value.__class__)
data[name] = self.convert(value)
return data
def grabAttributes(self, obj):
data = {}
for key in EXPORT_ATTRIBUTES:
data[key] = self.convert(getattr(obj, key, None))
return data
def export(self, folder, recursive=False):
"""
Export content items.
Possible to do recursively nesting into the children.
:return: list of dictionaries
"""
array = []
for obj in folder.listFolderContents():
data = self.grabData(obj)
data.update(self.grabAttributes(obj))
if recursive:
if IFolderish.providedBy(obj):
data["children"] = self.export(obj, True)
array.append(data)
return array
def __call__(self):
"""
"""
folder = self.context.aq_inner
data = self.export(folder)
pretty = json.dumps(data, sort_keys=True, indent=' ')
self.request.response.setHeader("Content-type", "application/json")
return pretty
Siehe auch
Deliverance wird zur Gestaltung von HTML verwendet wobei ein konsistenter Stil auf verschiedene Anwendungen und statische Dateien angewendet werden kann.
Mit Deliverance kann eine Plone-Site ähnlich wie mit
collective.xdv über XSLT-Regeln gestaltet werden. Daher teilt
Deliverance
mit collective.xdv
folgende Vorteile:
Web-Designer müssen kein Vorwissen in Bezug auf Plone und Python mitbringen;
Standardbibliotheken und -werkzeuge können verwendet werden;
Auch eine bereits existierende Webgestaltung, z.B. von Open Source Web Design, kann einfach verwendet werden.
Deliverance
bietet darüberhinaus noch folgende Vorteile:
Die Transformationsregeln lassen sich an Bedingungen knüpfen und gehen damit über XSLT hinaus.
Deliverance
ist nicht Plone-spezifisch, sodass das Theme auch für weitere Webanwendungen wie Trac, Mailman, Wordpress etc. genutzt werden kann.
Mit Deliverance
lassen sich einfach Mashups verschiedener Webinhalte darstellen.
Deliverance
lässt sich einfach mit Buildout installieren. Hierzu erstellen Sie eine deliverance.cfg
-Datei mit folgendem Inhalt:
[buildout]
parts =
lxml
server
# Change the number here to change the version of Plone being used
extends =
versions = versions
# Add additional egg download sources here. dist.plone.org 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 =
[versions]
Deliverance = 0.6.0
WebOb = 0.9.8
lxml = 2.2.4
[lxml]
recipe = z3c.recipe.staticlxml
egg = lxml
force = false
[server]
recipe = zc.recipe.egg
eggs =
lxml
PasteScript
Deliverance
interpreter = py
Nun sollte Buildout problemlos durchlaufen werden:
$ ./bin/buildout -c deliverance.cfg
...
Generated script '/home/veit/deliverance_buildout/bin/paster'.
Generated script '/home/veit/deliverance_buildout/bin/deliverance-proxy'.
Generated interpreter '/home/veit/deliverance_buildout/bin/py'.
Nun definieren wir in rules.xml
die Konfiguration für den Deliverance-Server:
<server-settings>
<server>localhost:8000</server>
<execute-pyref>true</execute-pyref>
<dev-allow>localhost</dev-allow>
<dev-user username="admin" password="secret" />
</server-settings>
<server>
gibt den verwendeten Host und Port an.
Wird localhost
oder 127.0.0.1
angegeben, sind nur lokale Verbindungen zulässig.
Der Standardwert ist localhost:8080
<execute-pyref>
Hiermit können diejenigen, die Transformationsregeln schreiben dürfen, mit jeder Anfrage beliebigen Code ausgeführen.
Der Standardwert ist true
.
Für weitere Informationen schauen Sie in die pyref
-Python-Referenz
<dev-allow>
Liste der IP-Adressen, die Zugang erhalten.
<dev-user>
Die Angabe von Nutzername und Passwort ist nur sinnvoll, wenn <dev-allow>
sehr resktrikiv gehandhabt wird.
Weitere Informationen zur Server-Konfiguration erhalten Sie in server-settings.
In derselben Datei werden auch die Pfade für den Proxy-Server angegeben:
<proxy path="/static" class="static" editable="1">
<dest href="{here}/static/" />
</proxy>
<proxy path="/" class="plone">
<dest href="http://localhost:8080/VirtualHostBase/http/localhost:8000/Plone/VirtualHostRoot/" />
</proxy>
<proxy path>
Dies liefert bei einer Anfrage an /static
die Dateien aus {here}/static
aus, wobei {here}
dasselbe Verzeichnis ist, in dem auch die rules.xml
-Datei liegt.
<dest>
gibt das Ziel der Anfrage an.
Es kann sowohl als href-URL (http://…
) als auch als Datei-URL (file:///…
) angegeben werden.
Sie können nun den Server starten mit:
$ ./bin/deliverance-proxy rules.xml
Soll die Ausgabe nicht im Terminal ausgegeben werden, kann der Deliverance-Proxy gestartet werden mit:
$ /usr/bin/nohup ./bin/deliverance-proxy rules.xml &
Anschließend können Sie sich unter http://localhost:8000/.deliverance/login
anmelden und nun jeder URL ?deliv_log
anhängen um das Deliverance-Log am unteren Rand der Seite zu erhalten. Das Log beschreibt detailliert, wie die Seite transformiert wurde einschließlich der Informationen über den Proxy und jede Anfrage. Zudem können Sie sich den Quellcode anschauen, die Regeln bearbeiten etc. Diese Angaben erleichtern das Schreiben der Transformationsregeln ungemein.
Damit das static
-Verzeichnis nicht über die Deliverance-Port-Nummer ausgeliefert wird, genügt es nicht, eine entsprechende Apache RewriteRule
zu schreiben. Der Apache muss darüberhinaus konfiguriert werden mit ProxyPreserveHost On
. Dies sendet den Host-Namen, den der Apache vom Client erhält, an Deliverance weiter. Und sobald der Deliverance-Proxy den Host-Namen erhalten hat, verhält er sich wie gewünscht.
<rule class="static" />
<rule class="plone" suppress-standard="true">
<rule>
definiert eine Reihe von Transformtionen.
Es werden sowohl page classes als auch request/response matching unterstützt.
Mit page classes lässt sich eine rule class
einem bestimmten patch
, einer bestimmten domain
oder einem bestimmten response-header
zuweisen, z.B:
<rule class="news-section">
<theme href="/static/news.html" />
<match path="regex:^/news" class="news-section" />
Weitere Informationen hierzu erhalten Sie im Abschnitt match and page classe.
suppress-standard="true"
Deliverance kommt üblicherweise mit einer Reihe von Aktionen, die das Kopieren des Titels oder eines Skripts erlauben. Diese sind:
<rule>
<replace content="children:/html/head/title"
theme="children:/html/head/title" nocontent="ignore" />
<append content="elements:/html/head/link"
theme="children:/html/head" nocontent="ignore" />
<append content="elements:/html/head/script"
theme="children:/html/head" nocontent="ignore" />
<append content="elements:/html/head/style"
theme="children:/html/head" nocontent="ignore" />
</rule>
Diese Regeln können unterbunden werden mit dem suppress-standard="true"
-Attribut.
<theme href="/static/index.html" />
<theme>
definiert das Thema, das Sie verwenden in Form einer URL.
<replace>
ersetzt ein Element aus theme
durch ein Element aus content
.
Die folgende Aktion ersetzt z.B. den Titel des Themes durch denjenigen aus Plone:
<replace content='/html/head/title' theme='/html/head/title' />
<append>
fügt ein Element aus content
am Ende eines Elements aus theme
ein.
Die folgende Aktion hängt z.B. die base
-Url aus Plone an die head
-Angaben des Theme. Dies gewährleistet, dass die Links aus Plone weiterhin funktionieren:
<append content='/html/head/base' theme='children:/html/head' />
<prepend>
fügt ein Element aus content
am Anfang eines Elements aus theme
ein.
Die folgende Aktion stellt z.B. das Navigationsportlet aus Plone an den Anfang der rechten Spalte des Theme:
<prepend content='dl.portletNavigationTree' theme='children:#rightbar' />
<drop>
entfernt das Element aus dem content
oder theme
.
Die folgende Aktion entfernt z.B. das User-Icon von Plone:
<drop content='#user-name img' />
Jede Aktion beruht auf der Auswahl der Elemente des Theme und des Inhalts. Die einfachste Auswahl kann anhand von CSS-Selektoren erfolgen.
Es können auch XPath-Angaben als Selektoren verwendet werden. Diese beginnen immer mit /
.
Diese beiden Selektoren verweisen immer auf Elemente. Um spezifischere Aktionen ausführen zu können, wurden daher noch die folgenden Selektoren eingeführt:
elements
Der Standardselektor.
children
erlaubt, Regeln auf Kindelemente der ausgewählten Elemente anzuwenden. Hiermit lassen sich auch Aktionen auf Textinhalte anwenden.
attributes
Hiermit lassen sich Aktionen nur auf bestimmte Attribute der ausgewählten Elemente anwenden.
tag
Dieser Selektor erlaubt, Aktionen nur auf einen Tag, nicht jedoch auf dessen Kindelemente anzuwenden.
||
-Operator¶Der ||
-Operator nimmt die Ergebnisse des ersten Selektors, sofern vorhanden. Andernfalls nimmt er die Ergebnisse des zweiten Selektrors. So verwendet z.B. die folgende Aktion alle Elemente der ID content
; sind in content
jedoch keine Elemente vorhanden, werden die Kindelemente von <body>
verwendet:
content="#content || children:body"
if-content
¶Alle Aktionen können das Attribut if-content
erhalten womit die Aktion nur ausgeführt wird wenn die Bedingung erfüllt ist, z.B.:
<replace if-content='body.section-news' content='children:dl.portletEvents dt.portletHeader a' theme='children:#rightbar h2' />
Dem zu überprüfenden Wert kann auch not:
vorangestellt werden.
Deliverance erlaubt auch das Einbinden von externen Quellen. Hierzu wird das href
-Attribut für eine Aktion verwendet, z.B.:
<append href="http://twitter.com/plone"
content="#timeline"
theme='#rightbar' />
Somit ist Deliverance nicht nur für das Theming beliebiger Webanwendungen geeignet, es kann auch das Mashup verschiedener Inhalte von Webanwendungen übernehmen.
plone.app.theming integriert den Diazo-XSLT-Proxy in Plone. Einige Vorteile von plone.app.theming
sind:
Web-Designer müssen kein Vorwissen in bezug auf Plone und Python mitbringen;
Standardbibliotheken und -werkzeuge können verwendet werden;
Auch eine bereits existierende Webgestaltung, z.B. von Open Source Web Design, kann einfach verwendet werden.
Im Vergleich zu Deliverance weist plone.app.theming
folgende Unterschiede auf:
Es wird nur das XSLT-Subset von Deliverance verwendet.
Die Antwortzeiten von Diazo sind schneller, da nicht erst zur Laufzeit eine XSLT-Datei kompiliert wird.
Es gibt nur XPath- und CSS3-Selektoren:
Diazo |
Deliverance |
---|---|
|
|
|
|
|
|
|
|
|
|
oder
|
|
|
|
|
|
Es lassen sich Bedingungen für die Aktionen angeben, z.B.:
<drop if-content="not(//*[@class='portlet'])" css:theme="#portlet-wrapper" />
Darüber hinaus kann eine Bedingung auch auf mehrere Aktionen angewendet werden, z.B.:
<rules css:if-content="#personal-bar">
<append css:theme="#header-box" css:content="#user-prefs"/>
<append css:theme="#header-box" css:content="#logout"/>
</rules>
Und auch verschachtelte Bedingungen sind mögich, z.B.:
<rules if-content="condition1">
<rules if-content="condition2">
<copy if-content="condition3" css:theme="#a" css:content="#b"/>
</rules>
</rules>
oder:
<copy if-content="(condition1) and (condition2) and (condition3)" css:theme="#a" css:content="#b"/>
Auch mehrere Themes
lassen sich durch Bedingungen unterscheiden, z.B.:
<theme href="theme.html"/>
<theme href="news.html" css:if-content="body.section-news"/>
<theme href="members.html" css:if-content="body.section-members"/>
oder:
<rules css:if-content="body.section-news">
<theme href="news.html"/>
<copy css:content="h2.articleheading" css:theme="h1"/>
</rules>
Es können auch Pfadbedingungen angegeben werden, z.B.:
<theme href="news.html" if-path="/news"/>
Endet die Pfadbedingung mit /
, so wird die Regel nur angewendet, wenn die URL an dieser Stelle endet, also z.B.:
<theme href="news.html" if-path="/news/"/>
wird auf /Plone/news
oder /Plone/news/
angewendet, nicht jedoch auf /Plone/news/news1.html
.
Auch externe Inhalte können eingebunden werden, z.B. in einem Mashup:
<append css:content="#timeline"
href="http://www.heise.de/newsticker/classic/"
css:theme="#rightbar" />
Die Regeln werden nicht nacheinander abgearbeitet sondern in folgender festgelegter Reihenfolge:
<before />
<drop />
<replace />
<prepend />
<after />
Dies führt meines Erachtens zu einer unnötigen Komplexität bei der Analyse, welche Regeln in welcher Reihenfolge abgearbeitet werden.
Zudem bringt Diazo keine Debug-Konsole mit, wodurch diese Analyse deutlich erschwert wird.
plone.app.theming
lässt sich einfach mit Buildout installieren. Hierzu nehmen Sie folgende Änderungen in der buildout.cfg
-Datei vor:
[buildout]
...
extends =
http://dist.plone.org/release/4.1b2/versions.cfg
http://good-py.appspot.com/release/plone.app.theming/1.0b5
find-links =
http://dist.plone.org/release/4.1b2/
...
versions = versions
...
eggs =
...
plone.app.theming
Nun sollte Buildout problemlos durchlaufen und die Instanz neu gestartet werden können:
$ ./bin/buildout
...
$ ./bin/instance fg
Anmerkung: Während der Entwicklung empfiehlt sich, Zope im Debug-Modus laufen zu lassen da dann die Änderungen an theme
oder rules
sofort übernommen werden.
Wir können nun unser neues Theme erstellen indem wir in unserem Buildout-Projekt einen Ordner static
erstellen und in diesem das Invention-Theme von Open Source Web Design bereitstellen:
$ cd plone.app.theming_buildout
$ curl -O http://www.oswd.org/files/designs/3293/Invention.zip
$ unzip Invention.zip
Nun erstellen wir in unserem Invention
-Ordner noch die rules.xml-Datei mit den Angaben für die XSLT-Transformationen.
plone.app.theming
¶Setzen Sie nun eine neue Plone-Site mit dem Extension-Profile Diazo theme support
auf und wählen anschließend in deren Plone Control Panel die Konfiguration für das Diazo theme-Zusatzprodukt aus:
ändert die plone.app.theming-Transformation.
Aktivieren Sie diese Option.
Wählen Sie ein Theme aus
Hier können Sie eine eigene Regeldatei für Diazo, den Pfad zu den statischen Dateien oder die URL zu einem entfernten Server angeben. den angeben.
Der Pfad zu einer XML-Datei, die die Regeln für die Transformation enthält, also z.B.:
Invention/rules.xml
Es lassen sich auch Python-Pfade angeben, z.B.:
python://vs.theme/static/rules.xml
Verwendet Ihr Theme relative Pfade zu Bildern, CSS-Dateien oder anderen Ressourcen, kann hier ein Präfix eingegeben werden, der gewährleistet, dass diese Ressourcen an jeder Stelle der Plone-Site verfügbar sind.
erlaubt die Verwendung von Regeln und Themes von entfernten Servern.
Sofern es Namen von Hosts gibt, die nicht gestaltet werden sollen, können diese hier zeilenweise aufgelistet werden. Dies ist zumindest während der Entwicklung des Themes sinnvoll um die Ausgaben der ungestaltetem Website mit der gestalteten zu vergleichen.
Der Standardwert ist 127.0.0.1
Hier können Parameter definiert werden, die in den Regeln des Themes verwendet werden können, z.B. mit $name
. Diese Parameter werden mittels TALES-Ausdrücken definiert, die entweder string
, number
, boolean
oder None
ausgeben sollten. Verfügbare Variablen sind:
context
request
portal
portal_state
context_state
Je Zeile kann eine Variable definiert werden im Format name = expression
.
Schauen wir uns nun unsere einfache Transformationsregeln nochmals genauer an:
<replace theme='//*[@id="leftbar"]' content='//*[@id="content"]' />
Dabei mag Ihnen die Syntax von XPath zur Adressierung der Knoten des DOM zunächst kompliziert erscheinen. Erfreulicherweise gibt es jedoch Werkzeuge wie z.B. Firebug, die Ihnen diese Arbeit abnehmen:
Die aus Firebug kopierten XPath-Angaben können z.B. so aussehen:
//*[@id="content"]
oder so, falls es sich nicht um eine id handelt sondern einen h2
-Tag innerhalb des zweiten div
-Tags, der wiedrum von einem div
-Tag auf oberster Ebene umgeben ist:
/html/body/div/div[2]/h2
Neben id
und Anzahl können Knoten auch anhand ihres Namens adressiert werden.
Weitere Informationen erhalten Sie in XPath.
Alternativ zu Firebug können Sie auch CSS to XPath verwenden um sich die XPath-Ausdrücke anzeigen zu lassen.
Es gibt lediglich fünf verschiedene Arten von Regeln für die Diazo-Transformationen:
<replace>
ersetzt ein Element des Theme durch den Inhalt der Site.
Eine übliche Anwendung ist z.B., die <title>
und <base>
-Tags aus Plone zu übernehmen:
<replace theme="/html/head/title" content="/html/head/title" />
<replace theme="/html/head/base" content="/html/head/base" />
<append>
, <prepend>
fügt Inhalte der Website am Anfang oder ende des angegebenen Elements ein.
Hiermit können z.B. die von Plone verwalteten CSS- und Javascript-Dateien dem Theme hinzugefügt werden:
<append theme='/html/head' content='/html/head/script' />
<append theme="/html/head" content="/html/head/link | /html/head/style" />
Eine weitere, häufig verwendete Möglichkeit ist die Verwendung der id
- und class
-Elemente des body
-Tags um verschiedene Bereiche der Website unterschiedlich zu gestalten:
<prepend theme="/html/body" content="/html/body/@class" />
An dem folgenden Beispiel, in dem die linke und rechte Spalte von Plone in einer Spalte zusammengefasst werden soll, wird deutlich, dass mit der Verwendung von <append> und <prepend> verhindert werden kann, dass eine Regel die Inhalte einer anderen Regel überschreibt:
<append content='//*[@id="portal-column-one"]/div' theme='//*[@id="rightbar"]' />
<append content='//*[@id="portal-column-two"]/div' theme='//*[@id="rightbar"]' />
<before>
, <after>
Diese sind äquivalent zu <append />
und <prepend />
, aber stellen den betreffenden Inhalt vor oder nach dem betreffenden Element des theme dar.
<copy>
kopiert HTML-Knoten aus der Plone-Site innerhalb des Theme-Tags:
<copy content='//*[@id="portal-globalnav"]/li' theme='//*[@id="main-nav"]' />
Beachten Sie hierbei, dass jedes <li>
-Element innerhalb von id="portal-globalnav"
der Plone-Site in den Knoten mit der id="main-nav"
des Themes kopiert wird.
<drop>
entfernt das angegebene Element.
Diese Regel unterscheidet sich insofern von den anderen, da sie aus nur einem content-value
-Paar besteht.
Hiermit kann die Ausgabe von Inhalten der Plone-Site unterbunden werden, z.B.das Icon des user-name-Knotens:
<drop content='//*[@id="user-name"]/img' />
Genaugenommen wird jedes <img>
-Tag innerhalb des Elements mit der ID user-name
entfernt.
Häufig sollen die Resourcen in einem Python-Paket verwaltet werden.
Ein solches Python-Paket lässt sich einfach erstellen mit:
$ cd src/
$ paster create -t plone vs.diazotheme
...
Register Profile (Should this package register a GS Profile) [False]: True
...
Hiermit erstellen wir das Python Egg vs.diazotheme
aus dem Template plone
mit einem Profil für das Generic Setup Tool.
Damit dieses Egg auch der Instanz zur Verfügung steht, ändern Sie Ihre buildout.cfg
-Datei folgendermaßen ab:
[buildout]
...
develop =
...
src/vs.diazotheme
...
[instance]
...
eggs =
...
vs.diazotheme
zcml =
...
vs.diazotheme
Rufen Sie anschließend Ihr ./bin/buildout
-Skript erneut auf.
Zunächst wird hierzu in vs.diazotheme/vs/diazotheme/configure.zcml
das Verzeichnis Invention
als Theme registriert:
<plone:static
type="theme"
directory="Invention" />
Erstellen Sie hierin ein eigenes Theme oder laden eins von Open Source Web Design herunter, z.B. Invention:
$ cd vs.diazotheme/vs/diazotheme/
$ curl -O http://www.oswd.org/files/designs/3293/Invention.zip
$ unzip -v Invention.zip
Anschließend können Sie in diesem Theme die Datei manifest.cfg
erstellen mit Titel, Beschreibung, Verweis auf die rules.xml
-Datei etc.:
[theme]
title = vs.diazotheme
description = A diazo theme for veit schiele communications
rules = /++theme++vs.diazotheme/rules.xml
[theme:parameters]
ajax_load = python: 'ajax_load' in request.form
Für die XSLT-Transformationsregeln erstellen Sie anschließend in Invention
die rules.xml-Datei.
Damit Plone die Regeldatei mit den XSLT-Transformationen auch liest, wird ein Profil profiles/default/registry.xml
erstellt, das die Werte im Formular Diazo theme settings festlegt:
<registry>
<!-- plone.app.theming settings -->
<record field="enabled" interface="plone.app.theming.interfaces.IThemeSettings">
<value>True</value>
</record>
<record field="rules" interface="plone.app.theming.interfaces.IThemeSettings">
<value>/++theme++vs.diazotheme/rules.xml</value>
</record>
<record field="absolutePrefix" interface="plone.app.theming.interfaces.IThemeSettings">
<value>/++theme++vs.diazotheme</value>
</record>
</registry>
rules
Statt des Verweises auf die rules.xml
-Datei in der theme
-Ressource mit /++theme++vs.diazotheme
könnte hier auch ein Python-Aufruf stehen:
python://vs.diazotheme/Invention/rules.xml
absolutePrefix
Auch hier wurde wieder auf die registrierte Theme-Ressource verwiesen:
/++resource++vs.diazotheme
Die Angabe konvertiert relative URLs zu absoluten unter Verwendung dieses Präfixes.
Hierzu erstellen wir die Datei profiles/default/cssregistry.xml
mit folgendem Inhalt:
<?xml version="1.0"?>
<object name="portal_css">
<!-- Set conditions on stylesheets we don't want to pull in -->
<stylesheet
expression="not:request/HTTP_X_THEME_ENABLED | nothing"
id="public.css"
/>
<!-- Add new stylesheets -->
<stylesheet title="" authenticated="False" cacheable="True"
compression="safe" conditionalcomment="" cookable="True" enabled="on"
expression="request/HTTP_X_THEME_ENABLED | nothing"
id="++theme++vs.diazotheme/css/style.css" media="" rel="stylesheet"
rendering="link"
applyPrefix="True"
/>
</object>
not:request/HTTP_X_THEME_ENABLED | nothing
sorgt dafür, dass die public.css
-Datei nicht ausgeliefert wird wenn
in der HTML-Anfrage HTTP_X_THEME_ENABLED
enthalten ist, also die
Plone-Site über Diazo ausgeliefert wird.
request/HTTP_X_THEME_ENABLED | nothing
würde umgekehrt eine Datei
nur ausliefern, wenn die Anfrage durch Diazo gestellt wird.
++theme++vs.diazotheme/styles.css
registriert unsere styles.css
-Datei an Plones Stylesheets
Registry.
applyPrefix
In Plone 4 kann eine Stylesheetdatei auch mit relativen URLs geparst werden.
Schließlich erstellen wir noch die Datei profiles/default/metadata.xml
um mit unserem vs.diazotheme
-Produkt auch gleichzeitig das benötigte plone.app.theming
mitzuinstallieren:
<metadata>
<version>1</version>
<dependencies>
<dependency>profile-plone.app.theming:default</dependency>
</dependencies>
</metadata>
Wenn Sie nun das Buildout-Skript erneut aufrufen, anschließend die Instanz starten und eine neue Plone-Site mit dem Profil vs.diazotheme
erstellen, sollte die Plone-Site mit dem neuen Theme erscheinen.
Wenn Zope im Entwicklungsmodus gestartet wird, z.B. in der Konsole mit
./bin/instance fg
im Vordergrund läuft, wird das Thema bei jeder
Anfrage neu kompiliert. Üblicherweise wird jedoch nur beim ersten Aufruf
kompiliert und anschließend nur noch bei Änderungen im Control Panel.
Soll auch in Entwicklungsmodus nicht bei jedem Aufruf neu kompiliert
werden, so sollten Sie die Umgebungsvariable DIAZO_ALWAYS_CACHE_RULES
aktivieren, also z.B. die Instanz starten mit:
$ DIAZO_ALWAYS_CACHE_RULES=1 ./bin/instance fg
Auch im Entwicklungsmodus ist es möglich, vorübergehend das Thema zu deaktivieren indem Sie den Query String Parameter diazo.off=1
anhängen,
also z.B.:
http://localhost:8080/mysite/frontpage?diazo.off=1
Schließlich können Sie sich in einem Overlay Ihre Diazo-Regeln anzeigen lassen, wobei grün bedeutet, dass die Regel angewendet wird, bei rot hingegen die Regel nicht angewendet wird.
Um diese Anscicht zu erhalten, ergänzen Sie die URL im Browser um
?diazo.debug=1
, also beispielsweise:
http://localhost:8080/mysite/front-page?diazo.debug=1
Der Parameter wird in Nicht-Entwicklungsmodus ignoriert.
xsl:message
¶Der Debugger untersucht jede Regel und Zustand isoliert. Wird eine Regel
jedoch durch eine andere Regel außer Kraft gesetzt, zeigt er dies nicht an.
Wird also z.B. wenn eine drop
-Regel ein Element entfernt, dem eine
àppend`-Regel etwas hinzufügen möchte, also z.B.:
<rules css:if-content="#visual-portal-wrapper xml:id="r0">
<drop css:theme="content" xml:id="r1"/>
<append css:theme="content" xml:id="r2">
<xsl:message>Some content</xsl:message>
</append>
...
</rules>
Dabei fügt <xsl: message>
nichts in das Dokument selbst ein, sondern
nur in das error_log
, das ebenfalls im Overlay angezeigt wird.
plone.app.theming ist eine Weiterentwicklung von collective.xdv so wie Diazo eine Weiterentwicklung von XDV ist.
Die Syntax der Diazo-Regeln ist denen von XDV sehr ähnlich. Zunächst einmal haben sich die Namespaces geändert. Während diese in XDV noch angegeben wurden mit:
<rules
xmlns="http://namespaces.plone.org/xdv"
xmlns:css="http://namespaces.plone.org/xdv+css"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
...
</rules>
lauten diese für Diazo nun:
<rules
xmlns="http://namespaces.plone.org/diazo"
xmlns:css="http://namespaces.plone.org/diazo/css"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
...
</rules>
Zusätzlich haben sich einige Regeln vereinfacht, z.B.:
<copy />
sollte nur zum Kopieren von Attributen verwendet werden, zum Ersetzen bestehender Attribute sollte <replace />
mit theme-children
verwendet werden
<prepend />
wurde ersetzt durch <before />``mit ``theme-children
.
<append />
wurde ersetzt durch <after />
mit theme-children
.
In der Diazo-Dokumentation finden Sie weitere Hinweise über verfügbare Rwgeln.
Zum Aktualisieren einer Website, die mit collective.xdv gestaltet wurde, sind die folgenden Schritte nötig:
Deinstallieren Sie das XDV-Theme-Paket im Quickinstaller Tool.
Stoppen Sie die Instanz und entfernen collective.xdv aus Ihrem Buildout (entweder in buildout.cfg
, einer ähnlichen Konfigurationsdatei oder in der setup.py
-Datei unter install_requires
).
Installieren Sie plone.app.theming
und ändern Ihre Regeln wie oben beschrieben.
collective.xdv
ist ein neuer Ansatz um eine Plone-Website zu gestalten Einige Vorteile von collective.xdv
sind:
Web-Designer müssen kein Vorwissen in bezug auf Plone und Python mitbringen;
Standardbibliotheken und -werkzeuge können verwendet werden;
Auch eine bereits existierende Webgestaltung, z.B. von Open Source Web Design, kann einfach verwendet werden.
Im Vergleich zu Deliverance weist collective.xdv
folgende Unterschiede auf:
Es wird nur das XSLT-Subset von Deliverance verwendet.
Die Antwortzeiten von XDV sind schneller, da nicht erst zur Laufzeit eine XSLT-Datei kompiliert wird.
Es gibt nur XPath- und CSS3-Selektoren:
XDV |
Deliverance |
---|---|
|
|
|
|
|
|
|
|
|
|
oder
|
|
|
|
|
|
Es lassen sich Bedingungen für die Aktionen angeben, z.B.:
<drop if-content="not(//*[@class='portlet'])" css:theme="#portlet-wrapper" />
Darüber hinaus kann eine Bedingung auch auf mehrere Aktionen angewendet werden, z.B.:
<rules css:if-content="#personal-bar">
<append css:theme="#header-box" css:content="#user-prefs"/>
<append css:theme="#header-box" css:content="#logout"/>
</rules>
Und auch verschachtelte Bedingungen sind mögich, z.B.:
<rules if-content="condition1">
<rules if-content="condition2">
<copy if-content="condition3" css:theme="#a" css:content="#b"/>
</rules>
</rules>
oder:
<copy if-content="(condition1) and (condition2) and (condition3)" css:theme="#a" css:content="#b"/>
Auch mehrere Themes
lassen sich durch Bedingungen unterscheiden, z.B.:
<theme href="theme.html"/>
<theme href="news.html" css:if-content="body.section-news"/>
<theme href="members.html" css:if-content="body.section-members"/>
oder:
<rules css:if-content="body.section-news">
<theme href="news.html"/>
<copy css:content="h2.articleheading" css:theme="h1"/>
</rules>
Es können auch Pfadbedingungen angegeben werden, z.B.:
<theme href="news.html" if-path="/news"/>
Endet die Pfadbedingung mit /
, so wird die Regel nur angewendet, wenn die URL an dieser Stelle endet, also z.B.:
<theme href="news.html" if-path="/news/"/>
wird auf /Plone/news
oder /Plone/news/
angewendet, nicht jedoch auf /Plone/news/news1.html
.
Auch externe Inhalte können eingebunden werden, z.B. in einem Mashup:
<append css:content="#timeline"
href="/http://twitter.com/plone"
css:theme="#rightbar" />
Die Regeln werden nicht nacheinander abgearbeitet sondern in folgender festgelegter Reihenfolge:
<before />
<drop />
<replace />
<prepend />
<after />
Dies führt meines Erachtens zu einer unnötigen Komplexität bei der Analyse, welche Regeln in welcher Reihenfolge abgearbeitet werden. Zudem bringt XDV keine Debug-Konsole mit, wodurch diese Analyse deutlich erschwert wird.
collective.xdv
lässt sich einfach mit Buildout installieren. Hierzu nehmen Sie folgende Änderungen in der buildout.cfg
-Datei vor:
[buildout]
...
extends =
http://dist.plone.org/release/3.3.5/versions.cfg
lxml.cfg
http://good-py.appspot.com/release/collective.xdv/1.0?plone=3.3.5
versions = versions
...
eggs =
...
collective.xdv [Zope2.10]
[versions]
zope.i18n = 3.7.2
Bemerkung
Beachten Sie bitte, dass in der URL durch 1.0?plone=3.3.5
die Version von collective.xdv auf die Version 1.0 und Plone auf die Version 3.3.5 festgeschrieben wird.
Bemerkung
[Zope2.10]
wird für Plone 3.3 oder früher benötigt um mit
ZPublisherEventsBackport
die publication events aus Zope 2.12 auch
Zope 2.10 zur Verfügung zu stellen. Verwenden Sie Zope 2.12 und Plone 4,
genügt Ihnen einfach:
eggs =
...
collective.xdv
Die lxml.cfg
-Datei sieht dann folgendermaßen aus:
[lxml]
parts =
staticlxml
pylxml
[pylxml]
recipe=zc.recipe.egg
interpreter=pylxml
eggs=
lxml
[staticlxml]
recipe = z3c.recipe.staticlxml
egg = lxml
Nun sollte Buildout problemlos durchlaufen und die Instanz neu gestartet werden können:
$ ./bin/buildout
...
We have the distribution that satisfies 'collective.xdv[zope2.10]==1.0rc11'.
Getting required 'collective.directoryresourcepatch==1.0'
We have the distribution that satisfies 'collective.directoryresourcepatch==1.0'.
Getting required 'ZPublisherEventsBackport==1.1'
We have the distribution that satisfies 'ZPublisherEventsBackport==1.1'.
Getting required 'five.globalrequest==1.0'
We have the distribution that satisfies 'five.globalrequest==1.0'.
Getting required 'repoze.xmliter==0.1'
We have the distribution that satisfies 'repoze.xmliter==0.1'.
Getting required 'plone.transformchain==1.0b1'
We have the distribution that satisfies 'plone.transformchain==1.0b1'.
Getting required 'plone.subrequest==1.3'
We have the distribution that satisfies 'plone.subrequest==1.3'.
Getting required 'plone.app.registry==1.0b2'
We have the distribution that satisfies 'plone.app.registry==1.0b2'.
Getting required 'lxml==2.2.4'
We have the distribution that satisfies 'lxml==2.2.4'.
Getting required 'xdv==0.4b2'
We have the distribution that satisfies 'xdv==0.4b2'.
...
$ ./bin/instance fg
Anmerkung: Während der Entwicklung empfiehlt sich, Zope im Debug-Modus laufen zu lassen da dann die Änderungen an theme
oder rules
sofort übernommen werden.
Wir können nun unser neues Theme erstellen indem wir in unserem Buildout-Projekt einen Ordner static
erstellen und in diesem das Invention-Theme von Open Source Web Design bereitstellen:
$ cd collective-xdv_buildout
$ curl -O http://www.oswd.org/files/designs/3293/Invention.zip
$ unzip Invention.zip
Nun erstellen wir in unserem Invention
-Ordner noch die rules.xml
-Datei mit den Angaben für die XSLT-Transformationen.
collective.xdv
¶Setzen Sie nun eine neue Plone-Site mit dem Extension-Profile XDV theme support
auf und wählen anschließend in deren Plone Control Panel die Konfiguration für das XDV theme-Zusatzprodukt aus:
Enabled
ändert die XDV-Theme-Transformation.
Aktivieren Sie diese Option.
Domains
Auf welche Domäne soll die Transformation angewendet werden?
Dabei ist zu berücksichtigen, dass auf 127.0.0.1
aus Sicherheitsgründen nie eine Theme-Transformation angewendet werden kann, sodass Sie immer wieder zu Ihrer Site zurückkehren können auch wenn die Transformationsregeln unbrauchbar werden sollten. Dieses Verhalten kann auch dazu genutzt werden, die CSS3-Selektoren und XPath-Anweisungen für content
auszulesen.
Der Standardwert ist localhost:8080
Theme template
Der Pfad zu einer statischen HTML-Datei. Eine relative Pfadangabe ist immer relativ zum Buildout-Verzeichnis, also z.B.:
Invention/index.html
Neben Pfadangaben im Dateisystem lassen sich hier auch Python-Pfade angeben, z.B.:
python://vs.xdvtheme/static/index.html
Rules file
Der Pfad zu einer XML-Datei, die die Regeln für die Transformation enthält, also z.B.:
Invention/rules.xml
Auch hier lassen sich wieder Python-Pfade angeben, z.B.:
python://vs.xdvtheme/static/rules.xml
Häufig sind an dieser Stelle keine weiteren Angaben zu machen. Daher speichern wir die Einträge ab und sollten anschließend unsere einfachen XSLT-Transformationen betrachten können:
Im folgenden noch kurz die weiteren Optionen für die XDV theme-Konfiguration:
Alternate themes
Hier können alternative themes
- und rules
-Dateien angegeben werden. Die Angabe erfolgt in der Form path|theme|rules
.
path
Regulärer Ausdruck
theme
Dateipfad oder URL
path
Pfad zu der Datei, die die XDV-Transformationsregeln bereitstellt.
Unstyled paths
Pfade, die nicht gestaltet werden sollen wobei die Pfadangaben aus regulären Ausdrücken bestehen sollen, z.B.:
^.*/manage$
Absolute URL prefix
konvertiert relative URLs in der theme
-Datei zu absouten Pfaden unter Verwendung dieses Präfixes.
XSLT extension file
XDV kann erweitert werden um eine eigene XSLT-Datei. Diese kann hier als Pfadangabe oder URL angegeben werden.
Read network
Ist diese Option aktiviert, können URLs für die Angabe der rules
- und config
-Dateien angegeben werden.
Schauen wir uns nun unsere einfache Transformationsregeln nochmals genauer an:
<replace theme='//*[@id="leftbar"]' content='//*[@id="content"]' />
Dabei mag Ihnen die Syntax von XPath zur Adressierung der Knoten des DOM zunächst kompliziert erscheinen. Erfreulicherweise gibt es jedoch Werkzeuge wie z.B. Firebug, die Ihnen diese Arbeit abnehmen:
Die aus Firebug kopierten XPath-Angaben können z.B. so aussehen:
//*[@id="content"]
oder so, falls es sich nicht um eine id handelt sondern einen h2
-Tag innerhalb des zweiten div
-Tags, der wiedrum von einem div
-Tag auf oberster Ebene umgeben ist:
/html/body/div/div[2]/h2
Neben id
und Anzahl können Knoten auch anhand ihres Namens adressiert werden.
Weitere Informationen erhalten Sie in XPath.
Alternativ zu Firebug können Sie auch CSS to XPath verwenden um sich die XPath-Ausdrücke anzeigen zu lassen.
Es gibt lediglich fünf verschiedene Arten von Regeln für die XDV-Transformationen:
<replace>
ersetzt ein Element des Theme durch den Inhalt der Site.
Eine übliche Anwendung ist z.B., die <title>
und <base>
-Tags aus Plone zu übernehmen:
<replace theme="/html/head/title" content="/html/head/title" />
<replace theme="/html/head/base" content="/html/head/base" />
<append>
, <prepend>
fügt Inhalte der Website am Anfang oder ende des angegebenen Elements ein.
Hiermit können z.B. die von Plone verwalteten CSS- und Javascript-Dateien dem Theme hinzugefügt werden:
<append theme='/html/head' content='/html/head/script' />
<append theme="/html/head" content="/html/head/link | /html/head/style" />
Eine weitere, häufig verwendete Möglichkeit ist die Verwendung der id
- und class
-Elemente des body
-Tags um verschiedene Bereiche der Website unterschiedlich zu gestalten:
<prepend theme="/html/body" content="/html/body/@class" />
An dem folgenden Beispiel, in dem die linke und rechte Spalte von Plone in einer Spalte zusammengefasst werden soll, wird deutlich, dass mit der Verwendung von <append> und <prepend> verhindert werden kann, dass eine Regel die Inhalte einer anderen Regel überschreibt:
<append content='//*[@id="portal-column-one"]/div' theme='//*[@id="rightbar"]' />
<append content='//*[@id="portal-column-two"]/div' theme='//*[@id="rightbar"]' />
<before>
, <after>
Diese sind äquivalent zu <append />
und <prepend />
, aber stellen den betreffenden Inhalt vor oder nach dem betreffenden Element des theme dar.
<copy>
kopiert HTML-Knoten aus der Plone-Site innerhalb des Theme-Tags:
<copy content='//*[@id="portal-globalnav"]/li' theme='//*[@id="main-nav"]' />
Beachten Sie hierbei, dass jedes <li>
-Element innerhalb von id="portal-globalnav"
der Plone-Site in den Knoten mit der id="main-nav"
des Themes kopiert wird.
<drop>
entfernt das angegebene Element.
Diese Regel unterscheidet sich insofern von den anderen, da sie aus nur einem content-value
-Paar besteht.
Hiermit kann die Ausgabe von Inhalten der Plone-Site unterbunden werden, z.B.das Icon des user-name-Knotens:
<drop content='//*[@id="user-name"]/img' />
Genaugenommen wird jedes <img>
-Tag innerhalb des Elements mit der ID user-name
entfernt.
Häufig sollen die Resourcen in einem Python-Paket verwaltet werden.
Ein solches Python-Paket lässt sich einfach erstellen mit:
$ cd src/
$ paster create -t plone vs.xdvtheme
...
Register Profile (Should this package register a GS Profile) [False]: True
...
Hiermit erstellen wir das Python Egg vs.xdvtheme
aus dem Template plone
mit einem Profil für das Generic Setup Tool.
Damit dieses Egg auch der Instanz zur Verfügung steht, ändern Sie Ihre buildout.cfg
-Datei folgendermaßen ab:
[buildout]
...
eggs =
...
vs.xdvtheme
develop =
...
src/vs.xdvtheme
...
[instance]
...
zcml =
vs.xdvtheme
Rufen Sie anschließend Ihr ./bin/buildout
-Skript erneut auf.
Zunächst wird hierzu in vs.xdvtheme/vs/xdvtheme/configure.zcml
das Verzeichnis Invention
als Ressource registriert:
<browser:resourceDirectory
name="vs.xdvtheme" directory="Invention" />
Erstellen Sie hierin ein eigenes Theme oder laden eins von Open Source Web Design herunter, z.B. Invention:
$ cd vs.xdvtheme/vs/xdvtheme/
$ curl -O http://www.oswd.org/files/designs/3293/Invention.zip
$ unzip -v Invention.zip
Für die XSLT-Transformationsregeln erstellen Sie anschließend in Invention
die rules.xml-Datei.
Damit Plone die Regeldatei mit den XSLT-Transformationen auch liest, wird ein Profil profiles/default/registry.xml
erstellt, das die Werte im Formular XDV theme settings festlegt:
<registry>
<!-- collective.xdv settings -->
<record field="enabled" interface="collective.xdv.interfaces.ITransformSettings">
<field type="plone.registry.field.Bool">
<title>Enabled</title>
</field>
<value>True</value>
</record>
<record interface="collective.xdv.interfaces.ITransformSettings" field="domains">
<value>
<element>localhost:8080</element>
</value>
</record>
<record interface="collective.xdv.interfaces.ITransformSettings" field="rules">
<value>python://vs.xdvtheme/Invention/rules.xml</value>
</record>
<record interface="collective.xdv.interfaces.ITransformSettings" field="theme">
<value>python://vs.xdvtheme/Invention/index.html</value>
</record>
<record interface="collective.xdv.interfaces.ITransformSettings" field="absolute_prefix">
<value>/++resource++vs.xdvtheme</value>
</record>
</registry>
python://vs.xdvtheme/Invention/rules.xml
Die Ressourcen können nicht nur als Dateipfad angegeben werden sondern auch durch einen Python-Aufruf
/++resource++vs.xdvtheme
Die Angabe im Feld absolute_prefix
konvertiert relative URLs zu absoluten unter Verwendung dieses Präfixes.
Hierzu erstellen wir die Datei profiles/default/cssregistry.xml
mit folgendem Inhalt:
<?xml version="1.0"?>
<object name="portal_css">
<!-- Set conditions on stylesheets we don't want to pull in -->
<stylesheet
expression="not:request/HTTP_X_XDV | nothing"
id="public.css"
/>
<!-- Add new stylesheets -->
<stylesheet title="" authenticated="False" cacheable="True"
compression="safe" conditionalcomment="" cookable="True" enabled="on"
expression="request/HTTP_X_XDV | nothing"
id="++resource++vs.xdvtheme/css/style.css" media="" rel="stylesheet"
rendering="link"
applyPrefix="True"
/>
</object>
not:request/HTTP_X_XDV | nothing
sorgt dafür, dass die public.css
-Datei nicht ausgeliefert wird wenn in der HTML-Anfrage HTTP_X_XDV
enthalten ist, also die Plone-Site über xdv ausgeliefert wird.
request/HTTP_X_XDV | nothing
würde umgekehrt eine Datei nur ausliefern, wenn die Anfrage durch xdv gestellt wird.
++resource++vs.xdvtheme/styles.css
registriert unsere styles.css
-Datei an Plones Stylesheets Registry.
applyPrefix
In Plone 4 kann eine Stylesheetdatei auch mit relativen URLs geparst werden.
Schließlich erstellen wir noch die Datei profiles/default/metadata.xml
um mit unserem vs.xdvtheme
-Produkt auch gleichzeitig das benötigte collective.xdv
mitzuinstallieren:
<metadata>
<version>1</version>
<dependencies>
<dependency>profile-collective.xdv:default</dependency>
</dependencies>
</metadata>
Wenn Sie nun das Buildout-Skript erneut aufrufen, anschließend die Instanz starten und eine neue Plone-Site mit dem Profil vs.xdvtheme
erstellen, sollte die Plone-Site mit dem neuen Theme erscheinen.
Zum Entwickeln Ihres Themes Sollen Sie die Zope-Instanz im debug
-Modus starten, da Änderungen am Template oder den XSLT-Regeln dann sofort sichtbar werden. Und wenn die Stylesheets Registry die css-Dateien ebenfalls im Debug-Modus ausliefert, werden auch die Änderungen in diesen Dateien sofort sichtbar.
Diazo ist eine Weiterentwicklung von XDV und teilt mit Deliverance die folgenden Vorteile:
Diazo ist im Gegensatz zu plone.app.theming nicht Plone-spezifisch, sodass das Theme auch für weitere Webanwendungen wie Trac, Mailman, Wordpress etc. genutzt werden kann.
Mit Diazo lassen sich auch einfach Mashups verschiedener Webinhalte darstellen.
Diazo hat gegenüber Deliverance die folgenden Vorteile:
Die Regeln sind einfacher
Die Entwicklung wird von der Plone-Community getragen.
Diazo lässt sich auf zweierlei Arten aufsetzen:
als einfacher XSLT-Proxy
zusammen mit WSGI-Middleware-Filtern; dann sollte beim Installieren des Diazo-Eggs zusätzlich [wsgi]
angegeben werden.
Erstellen eines Buildout-Verzeichnisses:
$ mkdir diazo
Herunterladen der bootstrap.py
-Datei:
$ cd diazo
$ curl -O http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py
Erstellen der buildout.cfg
-Datei:
[buildout]
# Adjust the version number as required. See
# http://good-py.appspot.com/release/diazo for a full list
extends = http://good-py.appspot.com/release/diazo/1.0rc4
versions = versions
parts =
lxml
diazo
[diazo]
recipe = zc.recipe.egg
eggs =
diazo [wsgi]
PasteScript
[lxml]
recipe = z3c.recipe.staticlxml
egg = lxml
Bootstrapping des Buildout-Projekts:
$ python2.6 bootstrap.py
Erstellen des Buildout-Projekts:
$ bin/buildout
Dies sollte die drei Skripte ./bin/paster
, ./bin/diazocompiler
und ./bin/diazorun
erstellen.
Die Konfigurationsdatei diazo/proxy.ini
für den Proxy-Server nutzt Paste Deploy um eine WSGI-Anwendung zu erstellen:
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 8000
[composite:main]
use = egg:Paste#urlmap
/static = static
/ = default
[app:static]
use = egg:Paste#static
document_root = %(here)s/theme
[pipeline:default]
pipeline = theme
content
[filter:theme]
use = egg:diazo
rules = %(here)s/rules.xml
prefix = /static
debug = true
[app:content]
use = egg:Paste#proxy
address = http://127.0.0.1:8080/VirtualHostBase/http/127.0.0.1:8000/Plone
[server:main]
Server, der mit ./bin/paster serve proxy.ini
aufgerufen werden kann.
[composite:main]
definiert das grundlegende URL-Mapping.
paster
liefert alles aus /static
mit [app:static]
aus und alles andere mit [app:default]
.
[app:static]
liefert das Theme unter /static
aus dem static
-Verzeichnis aus.
[pipeline:default]
liefert die durch Diazo transformierten Inhalte aus theme
und content
als default
.
[filter:theme]
Hier wird der Pfad auf die rules.xml
-Datei und der Präfix für alle relativen Pfade (z.B. auf CSS-Dateien) angegeben.
debug = true
Hiermit wird das Theme bei jeder Anfrage neu erstellt, sodass die Entwicklung deutlich leichter fällt. Für den produktiven Betrieb sollte jedoch debug = false
gesetzt werden um die Performance zu verbessern.
[app:content]
liefert die Inhalte aus http://127.0.0.1:8000/Plone
Schließlich sind noch die Transformationsregeln in diazo/rules.xml
-Datei anzugeben. In dem hier abgebildeten Beispiel werden jedoch nur einige grundlegende Transformationen ausgeführt:
<rules
xmlns="http://namespaces.plone.org/diazo"
xmlns:css="http://namespaces.plone.org/diazo/css"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<theme href="theme/theme.html" />
<prepend theme="//head" content="//head/base"
nocontent="ignore" />
<prepend theme="//head" content="//head/link"
nocontent="ignore" />
<prepend theme="//head" content="//head/style"
nocontent="ignore" />
<append theme="//head" content="//head/script"
nocontent="ignore" />
<append theme="//head" content="//head/meta"
nocontent="ignore" />
<replace css:theme="title"
css:content="title"
nocontent="ignore" />
<copy css:theme="div.container"
css:content="body > *"
nocontent="ignore" />
</rules>
Wesentlicher Bestandteil eines Themes ist eine HTML-Datei, theme/theme.html
:
<!DOCTYPE html>
<html>
<head>
<title>Dummy title</title>
<link rel="stylesheet"
href="./screen.css"
type="text/css"
media="screen, projection" />
<link rel="stylesheet"
href="./print.css"
type="text/css"
media="print" />
<!--[if IE]>
{% compress css %}
<link rel="stylesheet"
href="./ie.css"
type="text/css"
media="screen, projection" />
{% endcompress %}
<![endif]-->
</head>
<body>
<div class="container">
<h1>Dummy Headline</h1>
<p>Sample content</p>
</div>
</body>
</html>
Schließlich sollten Sie den Diazo-Server starten können mit:
$ ./bin/paster serve --reload proxy.ini
<theme />
spezifiziert eine theme
-Datei, z.B.:
<theme href="theme/theme.html" />
href
erforderlicher Verweis auf eine HTML-Datei als relative oder absolute URL
if
spezifiziert eine mögliche Bedingung für diesen Verweis. Dies ermöglicht auch die Verwendung mehrerer Themes
if-path
spezifiziert einen URL-Pfad, der mit dem aktuellen Request erfüllt sein muss damit die Regel angewendet wird.
<notheme />
unterdrückt das Theming, z.B.:
<theme href="theme/theme.html" />
<notheme css:if-content="body.template-file_view" />
Es können auch mehrere <notheme />
-Elemente angegeben werden.
Es stehen die Attribute if
, if-path
, if-content
und css:if-content
zur Verfügung.
<replace>
ersetzt ein Element des Theme durch den Inhalt der Site.
Eine übliche Anwendung ist z.B., die <title>
und <base>
-Tags aus Plone zu übernehmen:
<replace theme="/html/head/title" content="/html/head/title" />
<replace theme="/html/head/base" content="/html/head/base" />
<append>
, <prepend>
fügt Inhalte der Website am Anfang oder Ende des angegebenen Elements ein.
Hiermit können z.B. die von Plone verwalteten CSS- und Javascript-Dateien dem Theme hinzugefügt werden:
<append theme='/html/head' content='/html/head/script' />
<append theme="/html/head" content="/html/head/link | /html/head/style" />
Eine weitere, häufig verwendete Möglichkeit ist die Verwendung der id
- und class
-Elemente des body
-Tags um verschiedene Bereiche der Website unterschiedlich zu gestalten:
<prepend theme="/html/body" content="/html/body/@class" />
An dem folgenden Beispiel, in dem die linke und rechte Spalte von Plone in einer Spalte zusammengefasst werden soll, wird deutlich, dass mit der Verwendung von <append> und <prepend> verhindert werden kann, dass eine Regel die Inhalte einer anderen Regel überschreibt:
<append content='//*[@id="portal-column-one"]/div' theme='//*[@id="rightbar"]' />
<append content='//*[@id="portal-column-two"]/div' theme='//*[@id="rightbar"]' />
<before>
, <after>
Diese sind äquivalent zu <append />
und <prepend />
, aber stellen den betreffenden Inhalt vor oder nach dem betreffenden Element des theme dar.
<copy>
kopiert HTML-Knoten aus der Plone-Site innerhalb des Theme-Tags:
<copy content='//*[@id="portal-globalnav"]/li' theme='//*[@id="main-nav"]' />
Beachten Sie hierbei, dass jedes <li>
-Element innerhalb von id="portal-globalnav"
der Plone-Site in den Knoten mit der id="main-nav"
des Themes kopiert wird.
<drop>
entfernt das angegebene Element.
Diese Regel unterscheidet sich insofern von den anderen, da sie aus nur einem content-value
-Paar besteht.
Hiermit kann die Ausgabe von Inhalten der Plone-Site unterbunden werden, z.B.das Icon des user-name-Knotens:
<drop content='//*[@id="user-name"]/img' />
Genaugenommen wird jedes <img>
-Tag innerhalb des Elements mit der ID user-name
entfernt.
<strip>
entfernt das angegebene Element aus theme
oder content
wobei die Kindelemente erhalten bleiben.
Beispiele:
<strip css:theme="#content" />
Das Element mit der ID content
wird entfernt nicht jedoch dessen Kindelemente:
<strip css:content="#main-area .wrapper" />
<replace css:theme="#content-area" css:content="#main-area" />
Zunächst wird das Element in theme
mit der ID content-area
durch das Element in content
mit der ID main-area
ersetzt und dann werden alle Kindelemente von #main-area
mit der Klasse wrapper
abgezogen.
<merge>
fügt die Werte von Elementen aus theme
und content
desselben Attributs zusammen, also z.B.:
<merge attributes="class" css:theme="body" css:content="body" />
Die Regeln werden nicht nacheinander abgearbeitet sondern in folgender festgelegter Reihenfolge:
<before />
-Regeln, die theme
verwenden, nicht jedoch theme-children
.
<drop />
<replace />
-Regeln, die theme
verwenden, nicht jedoch theme-children
.
Soll <drop />
auf demselben Knoten verwendet werden, muss method="raw"
verwendet werden, also z.B.:
<drop css:content="#viewlet-social-like"/>
<replace method="raw"
css:theme-children="#viewlet-social-like"
css:content="#viewlet-social-like"
/>
<strip />
Regeln mit attributes
, also z.B.:
<merge attributes="class" css:theme="body" css:content="body" />
theme-children
von <before />
-, <replace />
- und <after />
-Regeln sofern keine <replace />
-Regel für theme
für denselben Knoten bereits früher ausgeführt wurde.
<after />
-Regeln, die theme
verwenden, nicht jedoch theme-children
.
Die folgende Grafik veranschaulicht die Reihenfolge, in der die Regeln angewendet werden:
Diese festgelegte Reihenfolge führt meines Erachtens zu einer unnötigen Komplexität bei der Analyse, welche Regeln in welcher Reihenfolge abgearbeitet werden. Zudem bringt Diazo zum aktuellen Zeitpunkt noch keinen Debugger mit, wodurch die Analyse zusätzlich erschwert wird.
Falls keine Regel auf ein bestimmtes Element des theme
zutrifft, so wird dieses Element ignoriert.
Eine <replace>
-Regel, die auf ein Element in theme
, jedoch nicht auf ein Element in content
zutrifft, wird im theme
entfernt.
Eine <copy>
-Regel, die auf ein bestimmtes Element des theme
zutrifft, entfernt ebenfalls dieses Element wenn es kein entsprechendes Element in content
gibt.
Sollen die Elemente nicht entfernt sondern ein Platzhalter erhalten bleiben, kann eine bedingte Regel mit if-content
angegeben werden, z.B.:
<replace css:theme="#header" content="#header-element" if-content="" />
if-content
oder css:if-content
spezifiziert ein Element im content
if-not-content
oder css:if-not-content
kehrt die Bedingung für ein Element im content
um, z.B.:
<drop css:theme="#portlet-wrapper" css:if-not-content=".portlet"/>
if-not-path
spezifiziert einen URL-Pfad, der mit dem aktuellen Request nicht erfüllt sein darf damit die Regel angewendet wird, z.B.:
<drop css:theme="#news-box" if-not-path="/news"/>
if-path
oder css:if-path
spezifiziert einen Pfad in content
Soll der Pfad z.B. beginnen mit somewhere
, sieht die Regel folgendermaßen aus:
<copy
if-path="/somewhere"
css:theme="#content"
css:content="body > *"
/>
Angabe eines exakter Pfad mit:
if-path="/somewhere/"
Angabe des Pfadende mit:
if-path="somewhere/"
Angabe eines Pfadbestandteils mit:
if-path="somewhere"
if-not-path
oder css:if-not-path
kehrt die Bedingung für einen Pfad im content
um
if="$mode=''"
spezifiziert einen Knoten, der vorhanden sein muss, damit eine Regel oder ein Theme angewendet werden:
<drop css:theme=".warning" if="$mode = 'anon-personalbar'" />
if-not="$mode=''"
spezifiziert einen Knoten, der nicht vorhanden sein darf, damit eine Regel oder ein Theme ausgeführt werden
Gruppierung von Bedingungen:
<rules xmlns="http://namespaces.plone.org/diazo"
xmlns:css="http://namespaces.plone.org/diazo/css"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<rules css:if-content="#personal-bar">
<after css:theme-children="#header-box" css:content="#user-prefs"/>
<after css:theme-children="#header-box" css:content="#logout"/>
</rules>
…
</rules>
Verschachtelung von Bedingungen:
<rules if="condition1">
<rules if="condition2">
<copy if="condition3" css:theme="#a" css:content="#b"/>
</rules>
</rules>
entspricht:
<copy if="(condition1) and (condition2) and (condition3)" css:theme="#a" css:content="#b"/>
<theme href="theme.html"/>
<theme href="news.html" css:if-content="body.section-news"/>
<theme href="members.html" css:if-content="body.section-members"/>
Mit Inline-XSLT-Anweisungen lassen sich z.B. die Leerzeichen zwischen ELementen entfernen und automatische Einrückungen vornehmen:
<xsl:strip-space elements="*" />
<xsl:output indent="yes"/>
Inline XSLT-Anweisungen werden direkt innerhalb des <rules>
-Tag angegeben und ohne Bedingungen ausgeführt.
<after theme-children="/html/head">
<style type="text/css">
/* From the rules */
body > h1 { color: red; }
</style>
</after>
<replace css:theme="#details">
<dl id="details">
<xsl:for-each css:select="table#details > tr">
<dt><xsl:copy-of select="td[1]/text()"/></dt>
<dd><xsl:copy-of select="td[2]/node()"/></dd>
</xsl:for-each>
</dl>
</replace>
Mit <replace>
lässt sich auch der Inhalt modifizieren, so kann z.B. das input
-Element mit der Klasse searchButton
ersetzt werden durch ein button
-Element vom Typ submit
:
<replace css:content="div#portal-searchbox input.searchButton">
<button type="submit">
<img src="images/search.png" alt="Search" />
</button>
</replace>
Ein Absatz ohne Inhalte lässt sich z.B. so entfernen:
<drop content="p[not(*) and (not(normalize-space()) or text() = ' ')]"/>
Tags lassen sich z.B. am Beginn oder Ende eines Inhaltsbereichs einfügen:
<replace css:theme="#account a.dropdown-toggle"
css:content="#portal-personaltools li#anon-personalbar a" />
<before css:theme-children="#account a.dropdown-toggle"
method="raw">
<i class="icon-user"></i>
</before>
Etwas aufwändiger wird es, wenn Tags innerhalb von Inhaltselementen eingefügt werden sollen:
<replace css:content-children="#content" css:theme-children="#content"/>
<before css:theme-children="#content">
<div id="wrapper">
<xsl:apply-templates css:select="#title" mode="raw"/>
<xsl:apply-templates css:select="#description" mode="raw"/>
</div>
</before>
<drop css:content="#title"/>
<drop css:content="#description"/>
Auch die Attribute eines Tags lassen sich ändern. So kann z.B. eine css- Klasse hinzugefügt werden mit:
<xsl:template match="ul[@id='portal-globalnav']/li/@class[contains(., 'selected')]">
<xsl:attribute name="class"><xsl:value-of select="." /> current-menu-item</xsl:attribute>
</xsl:template>
Auch Bilder in content
lassen sich hiermit in einer bestimmten Größe anzeigen mit:
<replace css:theme="#content" css:content="#content" />
<xsl:template match="img/@src[not(contains(., '@@'))]">
<xsl:attribute name="src"><xsl:value-of select="." />/@@/images/image/thumb</xsl:attribute>
</xsl:template>
Dies ändert z.B.:
<img src="smiley.gif" class="myimage" />
in:
<img src="smiley.gif/@@/images/image/thumb" class="myimage" />
Mit xsl:copy
lassen sich Texte im Inhalt ergänzen, z.B.:
<replace css:theme="#content"
css:content="#content" />
<xsl:template match="h2/text()">
<xsl:copy /> – Extra text
</xsl:template>
rules
-Dateien¶Mit dem XInclude
-Protokoll lassen sich andere rules
-Dateien einschließen, z.B.:
<rules
…
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="base.xml" />
</rules>
<replace css:theme-children="#navigation ul.dropdown-menu li a"
css:content-children=".navTreeLevel2 > li > div"
href="/sitemap" />
Um entfernte Inhalte einbinden zu können, muss Diazo folgendermaßen konfiguriert werden:
[filter:theme]
use = egg:collective.diazo.readheaders
#You can use any other Diazo middleware options here, too!
read_network = True
Da der von Diazo verwendete libxml2-HTMLParser Namespace-Präfixe herauskürzt, kann z.B. der FaceBook Like-Button <fb:like></fb:like>
nicht integriert werden mit //*[local-name()="like"]
. Stattdessen kann z.B. folgende XSL-Transformation verwendet werden:
<xsl:template match="activity|add-profile-tab|bookmark|comments|friendpile|like|like-box|live-stream|login-button|pronoun|recommendations|serverFbml|profile-pic|user-status">
<xsl:element name="fb:{local-name()}" xmlns:fb="http://www.facebook.com/2008/fbml">
<xsl:apply-templates select="@*|node()"/>
</xsl:element>
</xsl:template>
Üblicherweise gibt Diazo den HTML-Seiten den Doctype XHTML 1.0 Transitional
. Um Strict
anzugeben, sollte folgende XSLT angegeben werden:
<xsl:output
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"/>
Es ist nicht möglich, den HTML5-Doctype mit XSLT zu setzen. Stattdessen sollte dann <!DOCTYPE html>
gesetzt werden.
Siehe auch
Englische Diazo-Dokumentation
Die Tests von Diazo enthalten viele gebräuchliche Regeln
Snippets vor allem zur Nutzung von Bootstrap und Foundation
Damit die Pop-ups für Kontakt- und Login-Formulare etc. funktionieren, sollte im Template ein div
-Tag mit der ID content
vorhanden sein. Dies ist notwendig da popupforms.js
ansonsten eine Fehlermeldung ausgibt:
var common_content_filter = '#content>*:not(div.configlet),dl.portalMessage.error,dl.portalMessage.info';
Für das Deployment wird ein Proxy-Web-Server benötigt, der die XSL-Transformationen ausführen kann.
Um das Diazo-Theme über Nginx auszuliefern, muss Nginx Moment in einer speziellen Version des html-xslt-Projekts kompiliert werden. Hierzu geben Sie folgendes an:
$ ./configure --with-http_xslt_module
Falls libxml2 und libxslt nicht an den erwarteten Stellen installiert wurden, müssen mit --with-libxml2=<path>
und --with-libxslt=<path>
die passenden Pfade angegeben werden.
Anschließend wird die Site entpsrechend konfiguriert:
location / {
xslt_stylesheet /etc/nginx/theme.xsl
path='$uri'
;
xslt_html_parser on;
xslt_types text/html;
rewrite ^(.*)$ /VirtualHostBase/http/localhost/Plone/VirtualHostRoot$1 break;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Diazo "true";
proxy_set_header Accept-Encoding "";
}
Um Edge Side Includes (ESI) in Varnish zu ermöglichen, fügen wir in der Varnish-Konfigurationsdatei folgendes hinzu:
sub vcl_fetch {
if (obj.http.Content-Type ~ "text/html") {
esi;
}
}
Apache erfordert mod_transform
mit html parsing-Unterstützung. Dann kann die Konfiguration folgendermaßen aussehen:
NameVirtualHost *
LoadModule transform_module /usr/lib/apache2/modules/mod_transform.so
<VirtualHost *>
FilterDeclare THEME
FilterProvider THEME XSLT resp=Content-Type $text/html
TransformOptions +ApacheFS +HTML +HideParseErrors
TransformSet /theme.xsl
TransformCache /theme.xsl /etc/apache2/theme.xsl
<LocationMatch "/">
FilterChain THEME
</LocationMatch>
</VirtualHost>
Bemerkung
Apache ist zum aktuellen Zeitpunkt leider nicht in der Lage, Error-Responses durch die WSGI-Pipeline zu schleusen. Daher lassen sich mit Apache zum aktuellen Zeitpunkt z.B. nicht 404 Not Found-Seiten gestalten.
Das Ausführen von XSLT für ein Diazo-Theme ist gewöhnlich deutlich schneller als die Auslieferung einer Plone-Seite selbst. Dabei sollte eine Anfrage weniger als eine Sekunde benötigen. 10–50ms je Anfrage an Diazo, je nach Komplexität des Themes sind dabei vollkommen normal. Wenn Sie jedoch feststellen, dass das Rendering von XSLT viel mehr Zeit verbraucht, haben Sie folgende Möglichkeiten um den Fehler genauer analysieren zu können:
Einfache binäre Suche
Entfernen Sie Schritt für Schritt eine weitere Hälfte Ihres Themes und wiederholen anschließend Ihre Messung. So können Sie ggf. den zeitaufwendigen XSLT-Anweisungen auf die Spur kommen.
Dabei werden Sie entdecken, dass Regeln wie z.B. die folgende sehr aufwändig sind:
<before css:content="head link" css:theme="head link" />
Diese Diazo-Regel trifft auf mehrere Knoten in einem Dokument zu. Diese Regel
fügt eine Kopie aller link
-Tags aus content
vor jeden link
-Tag im
Theme. Stattdessen wird jedoch vermutlich das Folgende benötigt:
<before css:content="head link" theme="/head/link[1]" />
Kompilieren Sie Ihr Theme mit dem Diazo-Server
Anschließend können Sie folgende Messungen durchführen:
$ bin/diazocompiler -r rules.xml -o compiled.xsl
$ xsltproc --timing --repeat --html --noout compiled.xsl mypage.html
Parsing stylesheet compiled.xsl took 1 ms
Parsing document mypage.html took 26 ms
Applying stylesheet 20 times took 197 ms
Dabei erhalten wir mit xsltproc --profile
jedoch nur Statistiken über
einzelne Templates. Dies führt leider nicht immer zum gewünschten Ergebnis.
Das Extensible Messaging and Presence Protocol (XMPP) ist ein offener Standard für ein Kommunikationsprotokoll für Nachrichten, der auf der Extensible Markup Language (XML) basiert.
Das Protokoll wurde ursprünglich Jabber genannt und wurde von der Jabber Open-Source-Community 1999 als Protokoll für Instant Messaging (IM) entwickelt. Das Protokoll war jedoch von Anfang an auf Erweiterungen ausgelegt, sodass heute auch andere Anwendungen wie Voice over Internet Protocol (VOIP) und File transfer signaling* darüber realisiert werden können.
Siehe auch
ejabberd ist ein XMPP-Applikationsserver, der vorwiegend in Erlang geschrieben ist.
ejabberd setzt neben GNU Make und GCC mindestens auch Expat und Erlang voraus:
# apt-get install libexpat1-dev erlang
Dies erfolgt im Buildout-Abschnitt [ejabberd]
:
[ejabberd]
recipe = rod.recipe.ejabberd
erlang-path = /usr/bin
url = http://www.process-one.net/downloads/ejabberd/2.1.8/ejabberd-2.1.8.tar.gz
Auch die Konfiguration erfolgt mit Buildout:
[ejabberd.cfg]
recipe = collective.recipe.template
input = templates/ejabberd.cfg.in
output = ${buildout:directory}/etc/ejabberd.cfg
xmppdomain = localhost
pubsub_max_items_node = 1000
admin_userid = admin
collaboration_allowed_subnet = 0,0,0,0
collaboration_port = 5347
component_password = secret
Das Template finden Sie hier: templates/ejabberd.cfg.in.
Die pcre
-Bibliothek wird benötigt für reguläre Ausdrücke in der location
-Direktive und für das ngx_http_rewrite_module
:
# apt-get install pcre-devel
Herunterladen und Installation erfolgen mit Buildout:
[nginx]
recipe = zc.recipe.cmmi
url = http://nginx.org/download/nginx-1.0.8.tar.gz
md5sum = 1049e5fc6e80339f6ba8668fadfb75f9
Die Konfiguration erfolgt ebenfalls in Buildout:
[nginx-conf]
recipe = gocept.nginx
configuration =
worker_processes 1;
daemon off;
events {
worker_connections 1024;
}
http {
proxy_read_timeout 400;
server {
listen 8080;
server_name localhost;
location ~ ^/http-bind {
proxy_pass http://localhost:5280;
}
location / {
proxy_pass http://localhost:8081/VirtualHostBase/http/localhost:8080/Plone/VirtualHostRoot/;
}
}
}
Nach der Installation und Konfiguration können die Diente gestartet werden:
$ ./bin/ejabberdctl restart
$ ./bin/nginx start
$ ./bin/instance start
Mit supervisord lässt sich das Starten und Stoppen der Dienste automatisieren. Zur Installation von supervisord wird folgendes in der Buildout-Konfiguration eingetragen:
[supervisor]
recipe = zc.recipe.egg
eggs = supervisor
[supervisor-conf]
recipe = collective.recipe.template
input = ${buildout:directory}/templates/supervisord.conf.in
output = ${buildout:directory}/etc/supervisord.conf
Die Vorlage für die Konfigurationsdatei sieht dann folgendermaßen aus: supervisord.conf.in.
Anschließend lassen sich die Dienste starten mit:
$ ./bin/supervisord
Überprüfen Sie, ob ejabberd
, nginx
und der ZEO-Cluster gestartet
sind.
Erstellen Sie eine neue Plone-Site mit collective.xmpp.chat.
Gehen Sie zu den Konfigurationseinträgen unter http://localhost:8082/Plone/portal_registry
und editieren die collective.xmpp.*
-Einträge.
Starten Sie anschließend die Instanz neu.
Melden Sie sich als Administrator an der Site an.
Erstellen Sie die gewünschten Nutzer.
Erstellen Sie die benötigten PubSub-Knoten durch Aufrufen des
@@setup-xmpp
-View in Ihrer Plone-Site.
Strophe.js ist eine Javascript-Bibliothek die bidirektionales Streaming über HTTP-Verbindungen (BOSH) erlaubt.
function chatMessageReceived {
alert(Reviewved a message stanza);
return true;
}
connection.addHandler(chatMessageReceived, null, 'message', 'chat');
var message = $msg({
to: "someone@jabber.plone.org",
type: "chat"
}).c("body").t("Welcome to the plone chat");
ergibt z.B. folgende Stanza:
<message to="someone@jabber.plone.org"
type="chat">
<body>Welcome to the plone chat</body>
</message>
Hier nur einige der wesentlichen Plugins:
Backbone.js erlaubt strukturiertes Javascript mit
Models
Collections
Views
ChatBox = Backbone.Modeol.extend({
initialize: function () {
this.set ({
'jid' : Strophe.getNodeFromJid(this.get('jid')),
'box_id' : this.get('id'),
'fullname' : this.get('fullname'),
});
}
});
var box = new ChatBox({'id': hex_sha1(jid), 'jid': jid, 'fullname': name});
Die offensichtlichsten Views für collective.xmpp.chat
sind definiert in collective.xmpp.chat.browser.javascripts.converse.js:
ControlBoxView
ChatRoomView
ChatBoxView
ChatBoxView = Backbone.View.extend({
tagName: 'div',
className: 'chatbox',
events: {'keypress textareea.chat-textarea': keyPressed'};
template: _.template(
'<div class="chat-title"> {{fullname}} </div>' +
'<div class="chat-content"></div>' +
'<form class="sendXMPPMessage" method="post">' +
'<textarea type="text" class="chat-area" />' +
'</form>'),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
keyPressed: function (ev) {
...
}
});
Repoze vereinigt verschiedene Technologien um WSGI und Zope zu verbinden.
Python-Standard (PEP 333), der die Kommunikation zwischen Web-Servern und Web-Anwendungen spezifiziert.
akzeptieren Anfragen eines Browser/Client und reichen die Daten an die Anwendungen weiter.
Sie antworten auf Anfragen, wobei sie die von Anwendungen zurückgelieferten Daten verwenden.
geben Antworten zurück.
Anwendung, die die nächste Anwendung aufruft, wobei die funktionale Anordnung eine sog. Pipeline bildet.
ermöglicht Zope in einer WSGI-Umgebung zu nutzen oder umgekehrt anderen WSGI-Anwendungen Zope-Technologien als Middleware bereitzustellen.
Dabei besteht Repoze einerseits aus einer Reimplementierung von Zope-Funktionalitäten als Python-Bibliotheken und WSGI-Middleware, andererseits aus bestehender WSGI-Middleware (Paste).
[buildout]
extends = http://dist.plone.org/release/4.1-latest/versions.cfg
parts =
instance
paster
wsgiconfig
[instance]
recipe = plone.recipe.zope2instance
eggs =
Plone
PIL
user = admin:admin
[paster]
recipe = zc.recipe.egg
eggs =
${instance:eggs}
Paste
PasteScript
repoze.tm2
repoze.retry
script = paster
[wsgiconfig]
recipe = collective.recipe.template
input = inline:
[app:zope]
use = egg:Zope2#main
zope_conf = ${buildout:directory}/parts/instance/etc/zope.conf
[pipeline:main]
pipeline =
egg:paste#evalerror
egg:repoze.retry#retry
egg:repoze.tm2#tm
zope
[server:main]
use = egg:paste#http
host = localhost
port = 8000
output = ${buildout:directory}/zope2.ini
Die Buildout-Konfiguration nutzt umfangreich Ian Bicking’s Paste, speziell PasteDeploy, das eine deklarative Syntax zum Konfigurieren von WSGI-Pipelines bereitstellt.
Zope2 kann nun einfach gestartet werden mit:
$ ./bin/paster serve zope2.ini
Anmerkung 1: infrae.wsgi scheint eine sauberere und besser dokumentierte Lösung zu sein im Vergleich zu repoze.zope2
und dem neuen Zope2 WSGI publisher. Um ìnfrae.wsgi
zu verwenden, tragen Sie einfach im [wsgiconfig]
-Abschnitt stattdessen folgendes ein:
use = egg:infrae.wsgi#zope2
Anmerkung 2: Soll ein anderer Web-Server wie z.B. gunicorn verwendet werden, so kann einfach im Abschnitt [server:main]
stattdessen folgendes eingetragen werden:
use = egg:gunicorn#main
Siehe auch
bin/
Verzeichnis, das alle Skripte dieses Projekts enthält, einschließlich der Skripte von Paste, Repoze, Zope, ZODB und ZEO.
Die Repoze-Skripte sind:
addzope2user
fügt einen Nutzer mit Manage-Rechten in Zopes acl_users
-Ordner hinzu; äquivalent zu zopectl adduser
.
debugzope2
startet einen Python-Interpreter wobei das Zope-Objekt an app
gebunden wird.
runzope2script
startet ein Python-Skript wobei das Zope-Objekt an app
gebunden wird; äquivalent zu zopectl run
.
etc/
Verzeichnis, das die Konfigurationsdateien enthält, z.B.
zope2.ini
Paste-Konfigurationsdatei,
zope.conf
und site.zcml
Zope-Konfigurationsdatei
apache2.conf
mod_wsgi-Konfiguration für den Apache-Webserver.
import/
Verzeichnis, um die .zexp
-Dateien importieren zu können.
Products/
Verzeichnis, das klassische Zope2-Produkte enthalten kann.
var/
Verzeichnis mit den Daten der Zope-Instanz.
Die Konfigurationsdatei des Repoze-Projekts etc/zope2.ini
gliedert sich in folgende Abschnitte:
[Default]
Dieser Abschnitt enthält globale Angaben. Zunächst ist nur folgendes angegeben:
debug = True
Hiermit wird das Repoze-Projekt üblicherweise im Debug-Modus gestartet, wobei hiervon nicht nur der Zope-Server, sondern auch weitere Paste-Middleware beeinflusst werden kann.
[app:zope2]
In diesem Abschnitt wird die zope2
-WSGI-Anwendung definiert.
paste.app_factory
Paste-spezifische Angabe für den Aufruf einer WSGI-Anwendung.
Die weiteren Angaben dieses Abschnitt sind Konfigurationen von obob
, einem Object Publishing Framework für Repoze.
zope.conf
gibt den Ort der Zope-Konfigurationsdatei an, wobei %(here)s
eine Paste-Konvention für das Verzeichnis ist, in der die Paste-Konfigurationsdateien liegen.
[pipeline]
Abschnitt, der jeweils eine WSGI-Pipeline definiert. Eine Pipeline kann aus keiner oder mehr Middleware und einer Anwendung bestehen:
[pipeline:main]
pipeline = egg:Paste#cgitb
egg:Paste#httpexceptions
# egg:Paste#translogger
egg:repoze.retry#retry
egg:repoze.tm#tm
egg:repoze.vhm#vhm_xheaders
errorlog
zope2
In dieser Konfiguration steht zope2
am Ende und verweist auf die im Abschnitt [app:zope2]
definierte Zope-WSGI-Anwendung.
egg:Paste#cgitb
Exception handler, der die Ausgabe des tracebacks coloriert. Es können auch andere exception handler, wie z.B. evalerror, verwendet werden:
egg:Paste#evalerror
egg:Paste#httpexceptions
gibt für bestimmte Python-Exceptions entsprechende HTTP-Exceptions aus, z.B. 404 Not Found
, 302 Redirect
, 401 Unauthorized
.
egg:Paste#translogger
Wird der translogger
eingeschaltet, wird das Access-Log in der Konsole ausgegeben.
egg:repoze.retry#retry
Implementierung einer retry policy, wobei konfiguriert werden kann, bei welchen Fehler eine erneute Anfrage erfolgt und wie oft eine solche Anfrage wiederholt wird.
egg:repoze.tm#tm
Implementierung der ZODB transaction management policy. repoze.tm
kann auch verwendet werden um Transaktionen z.B. von relationalen Datenbanken oder von Dateisystem-Operationen zu steuern.
egg:repoze.vhm#vhm_xheaders
Zope’s Virtual Host Monster entsprechende WSGI-Middleware. Die Schreibweise unterscheidet sich zwar, die Wirkung ist jedoch dieselbe (s.a. README.txt).
errorlog
Ersatz für Zope2’s error_log
.
[server:main]
Dieser Abschnitt definiert, welcher HTTP-Server in welcher Konfiguration verwendet wird. Statt des ZServers könnte hier auch Paste, WSGIUtils oder cherrypy angegeben werden. Einen Überblick über verschiedene Server-Konfigurationen finden Sie hier: In the Fitting Room: Trying on WSGI Servers.
Deliverance erlaubt Gestaltungen auf Inhalte nach bestimmten Regeln anzuwenden.
Deliverance kann einfach in einem Repoze-Projekt installiert werden mit:
$ sudo easy_install Deliverance
Anschließend wird in etc/zope2.ini
im Abschnitt pipeline:main
deliverance hinzugefügt:
[pipeline:main]
pipeline = egg:Paste#cgitb
egg:Paste#httpexceptions
# egg:Paste#translogger
egg:repoze.retry#retry
egg:repoze.tm#tm
egg:repoze.vhm#vhm_xheaders
errorlog
deliverance
zope2
Dann wird ein neuer Abschnitt eingefügt:
[filter:deliverance]
paste.filter_app_factory = deliverance.wsgimiddleware:make_filter
theme_uri = http://www.veit-schiele.de/frontpage
rule_uri = file:///%(here)s/rules.xml
Schließlich wird die Datei etc/rules.xml
mit folgendem Inhalt erstellt:
<?xml version="1.0" encoding="UTF-8"?>
<rules xmlns:xi="http://www.w3.org/2001/XInclude" xmlns="http://www.plone.org/deliverance">
<prepend theme="//head" content="//head/link" nocontent="ignore" />
<prepend theme="//head" content="//head/style" nocontent="ignore" />
<append theme="//head" content="//head/script" nocontent="ignore" />
<append theme="//head" content="//head/meta" nocontent="ignore" />
<append-or-replace theme="//head"
content="//head/title"
nocontent="ignore" />
<replace theme="//body//div[@id='content']"
content="//body//div[@id='content']"
nocontents="ignore" />
</rules>
Nach dem Neustart der Zope-Instanz mit:
$ ./bin/paster serve etc/zope2.ini
sollte die Plone-Site nun das Motiv meiner Website übernommen haben:
Ab repoze.zope2 0.2.6
kann man sich das error log mit /__error_log__
anzeigen lassen (das Zope error_log
-Objekt funktioniert nicht in Repoze-Projekten).
Apache Solr ist eine OpenSource Suchmaschine, die auf Sites wie Twitter Apple- and iTunes-Stores, Wikipedia und vielen anderen eingesetzt wird.
Apache Solr erlaubt nicht nur, die Inhalte verschiedener Systeme zu durchsuchen, es bietet auch weitere umfangreiche Suchfunktionen:
erlaubt die zunehmende Verfeinerung der Suche
anhand von Geodaten
Anhand der von Ihnen gemachten Eingaben werden Ihnen die am häufigsten gesuchten Phrasen vorgeschlagen
Falls Sie sich vertippt haben sollten, schlägt Solr Ihnen eine korrekte Schreibweise vor.
Hierzu gehören z.B. auch PDFs und MS-Office-Dokumente.
Darüberhinaus kann ein Cluster für Solr erstellt werden, wodurch sich die Last deutlich verteilen lässt.
Solr lässt sich einfach mit Buildout installieren. Hierzu kann das Paket collective.recipe.solrinstance verwendet werden.
Hierzu erstellen wir die Datei solr.cfg
, die z.B. so aussehen kann:
[buildout]
parts =
solr-download
solr-instance
[versions]
collective.recipe.solrinstance = 3.5
gocept.download = 0.9.5
[solr-download]
recipe = hexagonit.recipe.download
download-directory = parts/solr-download
strip-top-level-dir = true
url = http://ftp-stud.hs-esslingen.de/pub/Mirrors/ftp.apache.org/dist//lucene/solr/3.4.0/apache-solr-3.4.0.tgz
ignore-existing = true
solr-instance]
recipe = collective.recipe.solrinstance
solr-location = ${solr-download:location}
host = 83.223.91.163
port = 8983
basepath = search
solr-location
Pfad zur Installation von Solr. In unserem Fall ist dies
${solr-download:location}
host
Name oder IP-Adresse des Solr-Servers.
Der Standardwert ist localhost
:
port
Der Port, an dem Solr auf Anfragen lauscht.
Der Standardwert ist 8983
.
basepath
Pfad zum Solr-Service auf dem Server. Hieruas wird die endgülitge URL für den Solr-Service generiert:
$host:$port/$basepath
Der Standardwert ist solr
.
Der Solr-Server kann nun einfach gestartet werden mit:
$ ./bin/solr-instance fg
oder:
$ ./bin/solr-instance start
logdir = ${buildout:directory}/var/solr
logging-template = ${buildout:directory}/templates/logging.properties.tmpl
logdir
${buildout:directory}/var/solr
logging-template
${buildout:directory}/templates/logging.properties.tmpl
Das logging.properties.tmpl
kann dann z.B. so aussehen:
# Default global logging level:
.level= INFO
max-num-results = 500
section-name = SOLR
unique-key = id
default-search-field = text
filter =
text solr.StopFilterFactory ignoreCase="true" words="stopwords.txt"
text solr.WordDelimiterFilterFactory generateWordParts="1" generateNumberParts="1" catenateWords="0" catencatenateAll="0"
text solr.LowerCaseFilterFactory
text solr.RemoveDuplicatesTokenFilterFactory
index =
name:text type:text stored:true
name:title type:text stored:true
name:created type:date stored:true required:true
name:modified type:date stored:true
name:filesize type:integer stored:true
name:mimetype type:string stored:true
name:id type:string stored:true required:true
name:relpath type:string stored:true
name:fullpath type:string stored:true
name:renderurl type:string stored:true
name:tag type:string stored:true
max-num-results
Die maximale Anzahl von Ergebnissen, die der Solr-Server ausliefern soll.
Der Standardwert ist 500
.
section-name
Name des Abschnitts für die Produktkonfiguration, die für die zope.conf
generiert wird.
Der Standardwert ist solr
.
unique-key
beschreibt ein Feld als eindeutig für alle Dokumente. Weitere Informationen hierzu erhalten Sie unter SchemaXml.
Der Standardwert ist uid
.
default-search-field
konfiguriert das Standardsuchfeld sofern kein Feld explizit angegeben wurde.
filter
konfiguriert zusätzliche Filter für den Standard-Feldtyp. Jeder Filter wird in einer neuen Zeile aus einem Index und Parametern definiert. Dabei kann einer der verfügbaren Indextypen angegeben werden und als Parameter Schlüssel-Wert-Paare. Einen Überblick über die verfügbaren Filter erhalten Sie in TokenFilterFactories.
Weitere Konfigurationsmöglichkeiten von collective.recipe.solrinstance
erhalten Sie in Supported options.
SolrIndex ist ein ZCatalog Multi-Index, der Solr verwendet. Er ersetzt den Standard Volltext-Index von Plone und ermöglicht damit u.a.
die unterschiedliche Gewichtung von Feldern
die Verwendung von Stopwords
die Einbeziehung von Synonymen
SolrIndex lässt sich so erweitern um
Dies erlaubt die zunehmende Verfeinerung der Suche
Anhand der von Ihnen gemachten Eingaben werden Ihnen die am häufigsten gesuchten Phrasen vorgeschlagen
Falls Sie sich vertippt haben sollten, schlägt Solr Ihnen eine korrekte Schreibweise vor.
SolrIndex kann einfach mit Buildout installiert werden:
[instance]
…
eggs =
…
…alm.solrindex
Anschließend sollte SolrIndex ein SolrIndex auf der Plone-Site hinzugefügt werden. Ein solcher SolrIndex kann dann mehrere ZCatalog-Indexe enthalten.
bg.solr stellt Views für Plone bereit, mit denen sich in Solr suchen lässt und die Ergebnisse in Plone angezeigt werden.
Falls Solr nicht über Deliverance oder Diazo angezeigt werden soll, können die Ansichten für die Solr-Suche auch einfach mit bg.solr in Plone integriert werden.
Die Installation kann in Buildout einfach erfolgen mit:
[buildout]
…
extensions =
mr.developer
sources = sources
auto-checkout =
bg.solr
[sources]
bg.solr = git https://github.com/zopyx/bg.solr
[instance]
…
eggs =
…
bg.solr
bg.crawler ermöglicht die Indexierung von Dateien und Dateibäumen im Dateisystem durch Solr.
bg.crawler erlaubt auf der Komandozeile (Command line interface, CLI), einzelne Dateien oder Dateibäume von Solr indizieren zu lassen.
In einer virtualenv
-Umgebung lässt sich bg.crawler
einfach installieren mit:
$ easy_install bg.crawler
Innerhalb dieser virtuellen Umgebung lässt sich bg.crawler
einfach aufrufen mit:
$ ./bin/solr-crawler --help
Folgende Parameter stehen Ihnen zur Verfügung:
--solr-url
definiert die URL des Solr-Servers.
--render-base-url
Basis-URL, die den ersten Teil von Solrs renderurl
bildet.
--max-depth
begrenzt die Tiefe der Ordnerhierarchie bis zu der Dateien indiziert werden sollen.
--commit-after
Die Anzahl der Dokumente, die mit einem commit an Solr übergeben werden.
--tag
Die importierten Dokumente werden mit einer bestimmten Zeichenkette getagt.
So lassen sich unterschiedliche Datenquellen auch bei einer späteren Suchanfrage in Solr noch durch unterschiedliche Tags unterscheiden.
--clear-all
leert den Solr-Index vollständig bevor die Daten neu importiert werden.
--clear-tag
entfernt alle Dokumente aus dem Solr-Index, die einen bestimmten Tag enthalten, bevor die Daten neu importiert werden.
--verbose
ermöglicht ein besseres Logging.
--no-type-check
Falls diese Option gewählt wird, wird nicht nach bestimmten Dateitypen gefiltert.
Weitere Informationen zu bg.crawler
erhalten sie unter bg.crawler documentation.
archetypes.schemaextender erlaubt das Erweitern, Ändern und Löschen von Feldern eines Archetypes-Schema.
archetypes.schemaextender erlaubt, Archetypes- Schemas dynamisch mit Adaptern zu erweitern. Dies kann verwendet werden um neue Felder hinzuzufügen, Felder oder Fieldsets neu anzuordnen etc.
So sind dann auch drei verschiedene Adapter verfügbar:
ISchemaExtender
erlaubt das Hinzufügen neuer Felder zu einem Schema.
IOrderableSchemaExtender
erlaubt neue Felder hinzuzufügen und Felder neu anzuordnen, ist jedoch
deutlich kostspieliger als ISchemaExtender
.
IBrowserLayerAwareExtender
verwendet plone.browserlayer
sodass der Extender nur verfügbar ist sofern
ein Layer registriert wurde. Damit lässt sich die Schemaerweiterung von Plone-
Artikeltypen auf eine Site begrenzen.
ISchemaModifier
erlaubt auf niedrigschwelligem Niveau die Manipulation von Schemas.
Zunächst wird archetypes.schemaextender
als Abhängigkeit unseres Produkts
in vs/registration/configure.setup.py
registriert:
install_requires=[
'setuptools',
...
'archetypes.schemaextender',
],
Anschließend wird in vs/registration/interfaces.py
das entsprechende
Interface definiert:
from plone.theme.interfaces import IDefaultPloneLayer
class IVSRegistrationExtenderLayer(IDefaultPloneLayer):
"""A Layer Specific to VSRegistrationExtender"""
Dieser Layer wird nun registriert in
vs/registration/profiles/default/browserlayer.xml
mit:
<layers>
<layer name="vs.registration"
interface="vs.registration.interfaces.IVSRegistrationExtenderLayer" />
</layers>
Dann wird ein neues Paket hinzugefügt:
$ mkdir extender
$ touch extender/__init__.py
Anschließend wird es in die Konfiguration eingeschlossen indem in vs/registration/configure.zcml
folgende Zeile hinzugefügt wird:
<include package=".extender" />
Nun wird der Extender selbst registriert in
vs/registration/extender/configure.zcml
:
<include package="archetypes.schemaextender" />
<adapter factory=".teaser.TeaserExtender"
provides="archetypes.schemaextender.interfaces.ISchemaExtender"
for="Products.ATContentTypes.interfaces.IATEvent" />
Nun wird die Klasse TeaserExtender
in vs/registration/extender/teaser.py
erstellt:
class TeaserExtender(object):
""" teaser fields """
implements(ISchemaExtender, IBrowserLayerAwareExtender)
# bind this extender to the browser layer
layer = VSRegistrationExtenderLayer
fields = [TeaserField('teaserImage',
default=False,
storage = AnnotationStorage(migrate=True),
swallowResizeExceptions=zconf.swallowImageResizeExceptions.enable,
pil_quality=zconf.pil_config.quality,
pil_resize_algo=zconf.pil_config.resize_algo,
max_size=config.TEASER_MAX_DIMENSION,
sizes=config.TEASER_SIZES,
widget=atapi.ImageWidget(
label=u"Teaser image",
label_msgid='label_teaser_image',
i18n_domain='plone',
),
schemata='Teaser',
),
]
def __init__(self, context):
self.context = context
def getFields(self):
return self.fields
Die vollständige Datei können Sie sich hier anschauen: teaser.py
.
Der hierfür notwendige Adapter wird in
vs/registration/extender/configure.zcml
konfiguriert:
<adapter
factory=".extender.VSRegistrationExtender"
provides="archetypes.schemaextender.interfaces.IOrderableSchemaExtender" />
Nun wird die Klasse TeaserExtender
in vs/registration/extender/teaser.py
erweitert:
from archetypes.schemaextender.interfaces import IOrderableSchemaExtender, IBrowserLayerAwareExtender
...
class TeaserExtender(object):
implements(IOrderableSchemaExtender, IBrowserLayerAwareExtender)
...
def fiddle(object, schema):
def getOrder(self, order):
do = order['default']
# place teaser at the very top
do.remove('teaser')
do.insert(0, 'teaser')
return order
Ein bestehendes Schema lässt sich ändern, indem zunächst ein Adapter
konfiguriert wird in vs/registration/extender/configure.zcml
:
<adapter
factory=".teaser.TeaserExtender"
provides="archetypes.schemaextender.interfaces.ISchemaModifier" />
Nun wird die Klasse TeaserExtender
in vs/registration/extender/teaser.py
erweitert:
from archetypes.schemaextender.interfaces import IOrderableSchemaExtender, IBrowserLayerAwareExtender, ISchemaModifier
...
def fiddle(object, schema):
schema['image'].widget.visible = {'edit':'invisible','view':'invisible'}
return schema
plone.indexer vereinfacht die Erstellung und Verwaltung eigener Indizees in Plone.
plone.indexer erlaubt das Erstellen von Adaptern zum Indizieren des ZCatalog.
Zunächst wird ein Adapter-Paket erstellt:
$ mkdir vs.theme/vs/theme/adapters
$ touch vs.theme/vs/theme/adapters/__init__.py
Anschließend wird es in die Konfiguration eingeschlossen in vs.theme/vs/theme/configure.zcml
:
<include package=".adapters" />
Ein einzelner Adapter wird dann konfiguriert in vs.theme/vs/theme/adapters/configure.zcml
:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:cmf="http://namespaces.zope.org/cmf"
xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="vs.theme">
<adapter name="hasTeaserImage" factory=".indexer.hasTeaserImageDocument" />
</configure>
Die Klasse hasTeaserImageDocument
in vs.theme/vs/theme/adapters/indexer.py
sieht dann so aus:
from plone.indexer import indexer
from Products.ATContentTypes.interfaces import ITextContent
def _hasTeaserImage(obj, fieldname):
""" generic wrapper """
field = obj.getField(fieldname)
if field is None:
return False
img = field.get(obj)
img_data = str(img.data)
return len(img_data) > 0
@indexer(ITextContent)
def hasTeaserImageDocument(obj):
""" Returns True/False if an teaser image exists or not """
return _hasTeaserImage(obj, 'teaserImage')
Mit z3c.jbot lassen sich Views und alle Objekte des Skins Tool, wie Page Templates, CSS- und Javascript-Dateie einfach überschreiben oder erweitern.
z3c.jbot
in vs.theme/setup.py
ein:
...
install_requires=[
'setuptools',
'z3c.jbot',
...
],
...
Anschließend wird folgende Direktive in vs.theme/vs/theme/configure.zcml
angegeben:
<configure
...
xmlns:browser="http://namespaces.zope.org/browser">
...
<include package="z3c.jbot" file="meta.zcml" />
...
<browser:jbot
directory="overrides"
layer=".interfaces.IThemeSpecific"
/>
Der Layer wird nun konfiguriert in
vs.theme/vs/theme/profiles/default/browserlayer.xml
:
<?xml version="1.0"?>
<layers>
<layer name="vs.theme"
interface="vs.theme.interfaces.IThemeSpecific" />
</layers>
Der Layer erwartet das Interface IThemeSpecific
in der Datei
vs.theme/vs/theme/interfaces.py
:
from plone.theme.interfaces import IDefaultPloneLayer
from plone.portlets.interfaces import IPortletManager
class IThemeSpecific(IDefaultPloneLayer):
"""Marker interface that defines a Zope 3 browser layer.
"""
Schließlich wird der Ordner overrides
angelegt und darn z.B. eine Kopie
von atct_topic_view.pt
als vs.theme/vs/theme/overrides/Products.ATContentTypes.skins.ATContentTypes.atct_topic_view.pt
.
Dieses PageTemplate lässt sich dann z.B. um ein Teaser-Element erweitern:
<td>
<img tal:condition="obj/hasTeaserImage"
tal:attributes="src string:${obj/getURL}/@@teaserImage?scale=teaser"
class="teaserImage"
/>
<div class="teaserText"
tal:condition="obj/teaserText"
tal:content="structure obj/teaserText" />
</td>
Mit z3c.jbot lassen sich auch einfach neue Templates aus bestehenden erstellen.
Hierzu wird nun in vs.theme/vs/theme/overrides/Products.ATContentTypes.skins.ATContentTypes.atct_topic_view.pt
eine Bedingung für die Tabellenzeile mit dem Teaser eingefügt:
<table class="listing"
summary="Content listing"
i18n:attributes="summary summary_content_listing;">
<thead>
<tr tal:condition="options/with_teaser | request/with_teaser | nothing" >
...
<td tal:condition="options/with_teaser | request/with_teaser | nothing">
<tal:if condition="obj/hasTeaserImage">
<a tal:attributes="href obj/getURL">
<img tal:condition="obj/hasTeaserImage"
tal:attributes="src string:${obj/getURL}/@@teaserImage?scale=teaser"
class="teaserImage"
/>
</a>
</tal:if>
<tal:if condition="not: obj/hasTeaserImage">
<div class="title" tal:content="obj/Title" />
<div class="description" tal:content="obj/Description" />
<a tal:attributes="href obj/getURL"
i18n:translate="label_more">
more
</a>
</tal:if>
</td>
Nun wird with_teaser
definiert in vs.theme/vs/theme/browser/topic.py
:
from Products.Five.browser import BrowserView
class TopicTeaserView(BrowserView):
""" Topic table view with teaser """
def __call__(self):
view = self.context.restrictedTraverse('atct_topic_view')
return view(with_teaser=True)
hasTeaserImage
wird aus dem Index abgefragt. Sehen Sie hierzu plone.indexer.
Anschließend wird in vs.theme/vs/theme/browser/configure.zcml
die neue Ansicht registriert:
<browser:page
name="atct_topic_teaser_view"
for="Products.ATContentTypes.interfaces.topic.IATTopic"
permission="zope2.View"
class=".topic.TopicTeaserView"
/>
Schließlich wird dieser View in derselben zcml
-Datei noch für das Hinzufügen-Menü konfiguriert:
<include package="plone.app.contentmenu" />
...
<browser:menuItem
for="Products.ATContentTypes.interfaces.topic.IATTopic"
menu="plone_displayviews"
title="Collection with teaser"
action="atct_topic_teaser_view"
description="Collection table view with teaser"
/>
Dieie Angabe der Bildgröße erfolgt hierbei nicht in der Schemadefinition sondern im Page Template:
<img tal:define="scale context/@@images"
tal:replace="structure python: scale.scale('image',
width=260, height=160, direction='down').tag()" />
Die Berechnung der Bildgröße erfolgt dann beim Aufrufen der Seite.
Folgende Parameter sind verfügbar:
up
skaliert die kürzere Seite auf die gewünschte Größe und beschneidet die andere Seite falls nötig.
down
skaliert die längere Seite auf die gewünschte Größe und beschneidet die andere Seite falls nötig.
thumbnail
skaliert das Bild auf die gewünschte Größe ohne es zu beschneiden.
Die Proportionen des Bildes können sich hierdurch verändern.
Diese Option erfordert die Angabe sowohl der Höhe als auch der Breite.
quality
Qualität des resultierenden Bildes.
Die Breite der Klassen lässt sich dann einfach berechnen mit6.25*n - 2.25)%. Dies führt dann z.B. zu folgenden Klassen:
width-full
Die volle Breite beträgt 97.75%, da links und rechts jeweils eine Margin von 1,125% erhalten bleiben.
width-1:2
Die Breite beträgt 47.75%.
width-3:4
Die Breite beträgt 72.75%.
Die Positionierung der Klassen wird vom rechten Rand aus vorgenommen mit
margin-left: -100 + (6.25*n) + 1.125
. Dies führt z.B. zu folgenden Klassen:
position-0
{margin-left: -73.875%;}
position-1:4
{margin-left: -73.875%;}
position-1:2
{margin-left: -48.875%;}
position-3:4
{margin-left: -23.875%;}
Sollen nun beispielsweise die beiden Portlet-Spalten links vom Inhalt positioniert werden, so sind hierfür nur geringe Änderungen notwendig:
Zunächst wird der bestehende sunburstview überschrieben in
vs.theme/vs/theme/browser/configure.zcml
:
<browser:page
for="*"
name="sunburstview"
class=".sunburstview.SunburstView"
layer=".interfaces.IThemeSpecific"
permission="zope.Public"
allowed_interface="plonetheme.sunburst.browser.interfaces.ISunburstView"
/>
Anschließend kopieren wir sunburstview.py in unser browser-Package und ändern darin die Berechnung der css-Klasse für denjenigen div-Tag, der den Inhaltsbereich enthält:
Sofern beide Portlet-Spalten angezeigt werden, soll der Inhaltsbereich 2 von 4 Spalten breit sein und ab der Hälfte der Seite beginnen, also:
elif sl and sr:
return "cell width-1:2 position-1:2"
Sofern nur die rechte Portlet-Spalte, nicht jedoch die linke angezeigt werden soll, bleibt die rechte Spalte an ihrer Position stehen und auch in diesem Fall wird der Inhalt 2 Spalten breit sein und ab der Hälfte der Seite beginnen:
elif (sr and not sl) and (not portal_state.is_rtl()):
return "cell width-1:2 position-1:2"
Schließlich müssen wir noch die main_template.pt-Datei anpassen um die rechte Portlet-Spalte immer an zweiter Stelle im Raster-layout anzuzeigen. Hierfür wird für den div-Tag mit der ID portal-column-two die position-Klasse geändert in:
<div id="portal-column-two"
class="cell width-1:4 position-1:4"
...
tal:attributes="class python:isRTL and 'cell width-1:4 position-0' or 'cell width-1:4 position-1:4'">
Das Erstellen von Dummy-Inhalten, seien es nun Seiten, Bilder oder Dateien, ist sehr nützlich um mit geringem Aufwand einen besseren Eindruck vom Look & Feel der neuen Website zu erhalten. Hierzu haben wir die setup.py
-Datei erweitert, sodass Texte mit dem loremipsum-Modul generiert werden. Das loremipsum-Modul kann so konfiguriert werden, dass es einzelne Sätze oder mehrere Absätze generieren kann:
import loremipsum
def gen_paragraphs(num=3):
return u'/'.join([p[2] for p in loremipsum.Generator().generate_paragraphs(num)])
def gen_sentence():
return loremipsum.Generator().generate_sentence()[-1]
def gen_sentences(length=80):
return u'/'.join([s[2] for s in loremipsum.Generator().generate_sentences(length)])
Das folgende Glossar kann dann einfach so erzeugt werden:
def installGlossary(self, site):
service = site.restrictedTraverse('deutschland/de/service')
glossary = invokeFactory(service, 'PloneGlossary', 'Glossar')
for i in range(100):
term = invokeFactory(glossary, 'PloneGlossaryDefinition')
term.setDefinition(gen_paragraphs(2))
term.reindexObject()
Auch Bilder lassen sich erstellen, wobei die lorempixel-Website verwendet wird. Der Quellcode zur unten abgebildeten Gallerie sieht z.B. so aus:
import random
import urllib2
def random_image(width, height):
url = 'http://lorempixel.com/%d/%d/' % (width, height)
return urllib2.urlopen(url).read()
def installAssets(self, site):
service = site.restrictedTraverse('de/service')
assets = invokeFactory(service, 'Folder', 'Assets')
for width,height in ((200,200), (400,400), (600, 400), (800, 600), (800,800), (1024, 768)):
imagefolder_id = '%sx%s' % (width, height)
images = invokeFactory(assets, 'Folder', imagefolder_id)
for i in range(20):
img = invokeFactory(images, 'Image')
img.setImage(random_image(width, height))
img.reindexObject()
Mit zopyx.ipsumplone hat unser Partner Andreas Jung nun ein eigenständiges Produkt zum Erstellen von Dummy-Inhalten entwickelt. Nach der Installation dieses Produkts muss innerhalb einer Plone-Site nur der View @@demo-content
aufgerufen werden um eine Reihe von Ordnern mit Bilder, Nachrichten, Terminen und Dateien zu erzeugen.
Buildout erlaubt, identische Entwicklungsumgebungen einfach aufzusetzen. Hierzu nutzt Buildout die Fähigkeit der setuptools, automatisch Abhängigkeiten aufzulösen und Aktualisierungen durchzuführen.
Buildout-Konfigurationsdateien erlauben verschiedene Ersetzungen von Variablen, z.B.:
[buildout]
parts =
variables
source
[variables]
var = ${source:path}/var
logs = ${variables:var}/logs
[source]
path = instance
Die Namen von Abschnitten und Optionen in Variablenersetzungen dürfen nur alphanumerische Zeichen, Bindestriche, Punkte und Leerzeichen enthalten.
Der Name des Abschnitts kann im selben Abschnitt weggelassen werden, soll jedoch der aktuelle Name des Abschnitts ermittelt werden, so ist dies mit _buildout_section_name_
möglich, also z.B.:
[variables]
...
base_variables = ${:_buildout_section_name_}
Ein Abschnitt kann ein oder mehrere Abschnitte erweitern wobei die Optionen des referenzierten Abschnitts zunächst kopiert und anschließend die Variablen substituiert werden. Dies ermöglicht die Verwendung von Abschnitten als Makros.
Beispiel:
[instance-base]
recipe = plone.recipe.zope2instance
...
[instance1]
<=instance-base
http-address = 8081
[instance2]
<=instance
http-address = 8082
Attributen lassen sich Werte hinzufügen und entfernen mit den Operatoren +
und -
. Folgendes Beispiel kann dies illustrieren:
[instance-debug]
<=instance-base
eggs +=
Products.PDBDebugMode
z3c.deadlockdebugger
oder umgekehrt:
[instance1]
<=instance-base
eggs -=
Products.PDBDebugMode
Eine Buildout-Konfigurationsdatei kann eine andere erweitern. Dabei werden die Optionen der erweiterten Konfigurationsdatei gelesen sofern sie nicht bereits definiert sind:
[buildout]
extends = base.cfg
mr.bob ist ein Dateisystem-Template-Renderer.
mr.bob ermöglicht, aus einer Vorlage eine Verzeichnisstruktur zu erstellen, die das Erstellen von Python-Paketen deutlich vereinfacht.
Im bobtemplate
-Namespace sind u.a. folgende Pakete zu finden:
erstellt Python-Pakete für Plone, ggf. auch mit nested Namespaces.
liefer Vorlagen zum Erstellen von Buildout-Projekten, zum Erstellen eigener bobtemplates und Python-Namespace-Paketen, optional mit nose, coverage und Sphinx-Dokumentationsvorlagen.
Pyramid und Python- Basis-Paket.
Plone- und Pyramid-Vorlagen
mr.bob``und ``bobtemplates
-Pakete lassen sich einfach mit Buildout installieren,
z.B.:
[buildout]
parts =
...
mrbob
[mrbob]
recipe = zc.recipe.egg
eggs =
mr.bob
bobtemplates.plone
Mit dem folgenden Aufruf können Antworten für zukünftige Pakete gespeichert werden:
$ mrbob --remember-answers -O vs.policy bobtemplates:plone_addon
...
Anschließend kann diese Konfiguration immer wieder verwendet werden, z.B. mit:
$ ../bin/mrbob --config .mrbob.ini -O vs.theme bobtemplates:plone_addon
Eine solche Konfigurationsdatei kann auch mit einer URL angespreochen werden, also z.B.:
$ ../bin/mrbob --config https://raw.github.com/veit/dotfiles/master/.mrbob.ini bobtemplates:plone_addon
Alternativ kann auch eine globale Konfigurationsdatei erstellt werden in
~/.mrbob.ini
, z.B.:
[mr.bob]
verbose = True
[variables]
package.namespace = vs
author.name = Veit Schiele
author.email = kontakt@veit-schiele.de
author.github.user = veit
author.irc = irc.freenode.org#veit
Siehe auch
bobtemplates.plone liefert Vorlagen für mr.bob template um Pakete für Plone-Projekte zu erstellen.
Um ein Paket zu erstellen wie vs.policy
, geben Sie folgendes im Terminal ein:
$ cd src/
$ ../bin/mrbob -O vs.policy bobtemplates:plone_addon
Es können auch Pakete mit verschachtelten Namensräumen erstellt werden, z.B.:
$ ../bin/mrbob -O vs.bootstrap.tinymce bobtemplates:plone_addon
Anschließend müssen Sie die folgenden Optionen beantworten:
Optionen sind
Basic
Dexterity
Theme
Der Standardwert ist Basic
.
Hier sollte Ihr Name angegeben werden
Ihre E-Mail-Adresse
Ihr Account-Name bei github
Einzeilige Beschreibung des Pakets.
Der Standardwert ist An add-on for Plone
Für welche Plone-Version wird das Paket entwickelt?
Basic
¶Die Basic
-Vorlage liefert die folgende Dateistruktur:
vs.policy/
├── bootstrap-buildout.py
├── buildout.cfg
├── CHANGES.rst
├── CONTRIBUTORS.rst
├── docs
│ ├── index.rst
│ ├── LICENSE.GPL
│ └── LICENSE.rst
├── MANIFEST.in
├── README.rst
├── setup.py
├── src
│ └── vs
│ ├── __init__.py
│ └── policy
│ ├── browser
│ │ ├── configure.zcml
│ │ ├── __init__.py
│ │ ├── overrides
│ │ └── static
│ ├── configure.zcml
│ ├── __init__.py
│ ├── interfaces.py
│ ├── locales
│ │ ├── update.sh
│ │ └── vs.policy.pot
│ ├── profiles
│ │ ├── default
│ │ │ ├── browserlayer.xml
│ │ │ ├── metadata.xml
│ │ │ └── vspolicy_default.txt
│ │ └── uninstall
│ │ ├── browserlayer.xml
│ │ └── vspolicy_uninstall.txt
│ ├── setuphandlers.py
│ ├── testing.py
│ └── tests
│ ├── __init__.py
│ ├── robot
│ │ └── test_example.robot
│ ├── test_robot.py
│ └── test_setup.py
└── travis.cfg
buildout.cfg
Das Paket enthält eine Buildout-Konfigurationsdatei, die z.B. zum Testen verwendet werden kann.
src/vs/policy/tests/
Das Paket kommt mit einem Test-Setup und Beispieltests zur Installation des Pakets.
Es enthält außerdem in src/vs/policy/tests/robot/test_example.robot
einen
Robot-Test für das Anmelden an der Plone-Site.
Schließlich enthält das Paket mit travis.cfg
auch eine Konfigurationsdatei,
das das Testen des Pakets mit Travis erlaubt.
src/vs/policy/profile
Das Paket enthält ein Generic Setup-Profil, das einen Browserlayer installiert.
Für Plone 5 wird daneben noch ein uninstall
-Profil installiert.
src/vs/policy/Locales
Das Paket registriert ein Verzeichnis für die Übersetzungsdateien.
src/vs/policy/browser/overrides
Das Paket registriert einen Ordner, in dem Templates etc. mit z3c.jbot überschrieben werden können.
src/vs/policy/setuphandlers.py
Diese Datei kann verwendet werden um Code hinzuzufügen, der beim Installieren eines Pakets ausgeführt werden soll.
Für Plone 5 gibt es innerhalb dieser Datei auch eine Methode, die beim Deinstallieren aufgerufen wird.
setup.py
In install_requires
werden zusätzlich die folgenden zwei Pakete angegeben:
plone.app.theming
plone.app.themingplugins
src/vs/theme/configure.zcml
Hier wird der Ordner mit dem Theme konfiguriert:
<configure
...
xmlns:plone="http://namespaces.plone.org/plone"
...
<plone:static
directory="theme"
type="theme"
name="vs.theme"
/>
src/vs/theme/profiles/default/metadata.xml
Hier wird als Abhängigkeit plone.app.theming
angegeben:
<dependency>profile-plone.app.theming:default</dependency>
Dexterity
¶setup.py
In install_requires
wird zusätzlich plone.app.dexterit
angegeben
src/vs/task/interfaces.py
Für den Dexterity-Artikeltyp wird ein Interface angegeben, in unserem Fall:
from vs.task import _
from zope import schema
from zope.interface import Interface
class ITask(Interface):
title = schema.TextLine(
title=_(u"Title"),
required=True,
)
description = schema.Text(
title=_(u"Description"),
required=False,
)
src/vs/task/profiles/default/metadata.xml
Hier wird als Abhängigkeit plone.app.dexterity
angegeben:
<dependency>profile-plone.app.dexterity:default</dependency>
bobtemplates.plone liefert Vorlagen für mr.bob template um Pakete für Plone-Projekte zu erstellen.
Um ein Paket zu erstellen wie vs.policy
, geben Sie folgendes im Terminal ein:
$ cd src/
$ ../bin/mrbob -O vs.policy bobtemplates:plone_addon
Es können auch Pakete mit verschachtelten Namensräumen erstellt werden, z.B.:
$ ../bin/mrbob -O vs.bootstrap.tinymce bobtemplates:plone_addon
Anschließend müssen Sie die folgenden Optionen beantworten:
Optionen sind
Basic
Dexterity
Theme
Der Standardwert ist Basic
.
Hier sollte Ihr Name angegeben werden
Ihre E-Mail-Adresse
Ihr Account-Name bei github
Einzeilige Beschreibung des Pakets.
Der Standardwert ist An add-on for Plone
Für welche Plone-Version wird das Paket entwickelt?
Basic
¶Die Basic
-Vorlage liefert die folgende Dateistruktur:
vs.policy/
├── bootstrap-buildout.py
├── buildout.cfg
├── CHANGES.rst
├── CONTRIBUTORS.rst
├── docs
│ ├── index.rst
│ ├── LICENSE.GPL
│ └── LICENSE.rst
├── MANIFEST.in
├── README.rst
├── setup.py
├── src
│ └── vs
│ ├── __init__.py
│ └── policy
│ ├── browser
│ │ ├── configure.zcml
│ │ ├── __init__.py
│ │ ├── overrides
│ │ └── static
│ ├── configure.zcml
│ ├── __init__.py
│ ├── interfaces.py
│ ├── locales
│ │ ├── update.sh
│ │ └── vs.policy.pot
│ ├── profiles
│ │ ├── default
│ │ │ ├── browserlayer.xml
│ │ │ ├── metadata.xml
│ │ │ └── vspolicy_default.txt
│ │ └── uninstall
│ │ ├── browserlayer.xml
│ │ └── vspolicy_uninstall.txt
│ ├── setuphandlers.py
│ ├── testing.py
│ └── tests
│ ├── __init__.py
│ ├── robot
│ │ └── test_example.robot
│ ├── test_robot.py
│ └── test_setup.py
└── travis.cfg
buildout.cfg
Das Paket enthält eine Buildout-Konfigurationsdatei, die z.B. zum Testen verwendet werden kann.
src/vs/policy/tests/
Das Paket kommt mit einem Test-Setup und Beispieltests zur Installation des Pakets.
Es enthält außerdem in src/vs/policy/tests/robot/test_example.robot
einen
Robot-Test für das Anmelden an der Plone-Site.
Schließlich enthält das Paket mit travis.cfg
auch eine Konfigurationsdatei,
das das Testen des Pakets mit Travis erlaubt.
src/vs/policy/profile
Das Paket enthält ein Generic Setup-Profil, das einen Browserlayer installiert.
Für Plone 5 wird daneben noch ein uninstall
-Profil installiert.
src/vs/policy/Locales
Das Paket registriert ein Verzeichnis für die Übersetzungsdateien.
src/vs/policy/browser/overrides
Das Paket registriert einen Ordner, in dem Templates etc. mit z3c.jbot überschrieben werden können.
src/vs/policy/setuphandlers.py
Diese Datei kann verwendet werden um Code hinzuzufügen, der beim Installieren eines Pakets ausgeführt werden soll.
Für Plone 5 gibt es innerhalb dieser Datei auch eine Methode, die beim Deinstallieren aufgerufen wird.
setup.py
In install_requires
werden zusätzlich die folgenden zwei Pakete angegeben:
plone.app.theming
plone.app.themingplugins
src/vs/theme/configure.zcml
Hier wird der Ordner mit dem Theme konfiguriert:
<configure
...
xmlns:plone="http://namespaces.plone.org/plone"
...
<plone:static
directory="theme"
type="theme"
name="vs.theme"
/>
src/vs/theme/profiles/default/metadata.xml
Hier wird als Abhängigkeit plone.app.theming
angegeben:
<dependency>profile-plone.app.theming:default</dependency>
Dexterity
¶setup.py
In install_requires
wird zusätzlich plone.app.dexterit
angegeben
src/vs/task/interfaces.py
Für den Dexterity-Artikeltyp wird ein Interface angegeben, in unserem Fall:
from vs.task import _
from zope import schema
from zope.interface import Interface
class ITask(Interface):
title = schema.TextLine(
title=_(u"Title"),
required=True,
)
description = schema.Text(
title=_(u"Description"),
required=False,
)
src/vs/task/profiles/default/metadata.xml
Hier wird als Abhängigkeit plone.app.dexterity
angegeben:
<dependency>profile-plone.app.dexterity:default</dependency>
ZopeSkel ist eine Sammlung von Vorlagen, mit denen sich schnell Buildout-Projekte und Plone-Erweiterungen erstellen lassen.
Bemerkung
In der Vergangenheit war ZopeSkel ein einziges großes
Paket, das nun ab Version 3.0 in mehrere kleine Pakete
aufgeteilt wurde, die unter dem templer
-Namespace erschienen.
Falls Sie ältere Vorlagen benötigen, sollten Sie eine Version
von ZopeSkel < 3.0
verwenden.
Im templer
-Namespace sind u.a. folgende Pakete zu finden:
stellt Ihnen basic_namespace
und nested_namespace
zum Erstellen von Python-Namespace- und verschachtelten Python-Namespace-Paketen zur Verfügung.
stellt Ihnen basic_buildout
und recipe
zum Erstellen von Buildout-Projekten und Rezepten zum Erweitern des Buildout-Systems zur Verfügung.
stellt Ihnen zope2_basic
und zope2_nested
zum Erstellen von Zope-Namespace- und verschachtelten Zope-Namespace-Paketen zur Verfügung.
stellt Ihnen archetype
, plone_basic
und plone_nested
zum Erstellen von Paketen für Plone.
stellt Ihnen sog. local commands zur Verfügung und zwar für die folgenden Vorlagen:
archetype
contenttype
Ein Gerüst für einen Archetypes-Artikeltyp
schema_field
Ein iterativer Generator für Archetypes-Felder.
plone_basic
browserview
Eine Zope-BrowserView
-Klasse mit Interface und Template
browserlayer
Ein Zope-BrowserLayer
-Interface und dessen GenericSetup-Registrierung
ZopeSkel kann einfach mit buildout installiert werden:
parts =
...
zopeskel
[zopeskel]
recipe = zc.recipe.egg
unzip = true
eggs =
Paste
ZopeSkel
templer.plone
templer.plone.localcommands
Beim Erstellen des buildout-Projekts werden im bin/
-Verzeichnis die Skripte zopeskel
und paster
erstellt.
bin/zopeskel --list
gibt eine Liste der verfügbaren Vorlagen aus.
bin/zopeskel --help
stellt Ihnen eine vollständige Hilfe für ZopeSkel zur Verfügung.
Um eine Liste mit allen verfügbaren Vorlagen (Templates) und ausführlichen Beschreibungen zu erhalten, geben sie folgendes ein:
$ ./bin/zopeskel --list
Plone Development
-----------------
archetype: A Plone project that uses Archetypes content types
This creates a Plone project that uses Archetypes content types. It
has local commands that will allow you to add content types and to
add fields to your new content types.
...
Um nun ein Projekt aus einer dieser Vorlagen zu erstellen, wird ZopeSkel folgendermaßen aufgerufen:
$ ./bin/zopeskel <template> <output-name>
also z.B.:
$ cd src/
$ ../bin/zopeskel archetype vs.registration
Es können auch noch weitere Variablen neben dem Projektnamen mitgegeben werden, z.B.:
$ ../bin/zopeskel archetype vs.registration author_email=kontakt@veit-schiele.de
Dies ist gut geeignet sofern Pakete skriptgesteuert erstellt werden sollen. Eine vollständige Liste der Variablen erhalten Sie mit:
$ ./bin/paster create -t <template-name> --list-variables
Hierzu können Sie die Datei .zopeskel
in Ihrem Home-Verzeichnis anlegen, z.B. mit:
$ ./bin/zopeskel --make-config-file > ~/.zopeskel
Die .zopeskel
-Datei kann dann z.B. so aussehen:
[DEFAULT]
author_email = kontakt@veit-schiele.de
license_name = BSD
master_keywords = Web Python Zope
[[plone_basic]]
expert_mode = all
namespace_package = vs
add_profile = True
keywords = %(master_keywords)s Plone
url = https://github.com/veit/
Sie können im [DEFAULT]
-Abschnitt bestimmte Werte angeben, die für eine spezifische Vorlage wieder überschrieben werden können. So ist z.B. im [DEFAULT]
-Abschnitt als Lizenz BSD
angegeben, diese wird jedoch im [plone3_theme]
-Abschnitt für diese Vorlage wieder überschrieben.
Sie können auch Angaben aus dem [DEFAULT]
-Abschnitt in spezifischen Vorlagen erweitern unter Verweis auf die master
-Liste.
Bemerkung
Im Gegensatz zum paster create
-Aufruf kann das zopeskel
-Skript nicht mit dem
Argument --svn-repository
aufgerufen werden um ein Paket an einer bestimmten Stelle
eines SVN-Repository zusammen mit der Verzeichnishierarchie trunk/
, tags/
und
branches/
zu erstellen.
Neben den Vorlagen für Projekte liefert ZopeSkel auch Vorlagen, die nur in bestimmten Kontexten zur Verfügung stehen, sog. local commands. Mit diesen können Sie bestehende ZopeSkel-Projekte erweitern.
Bemerkung
Local commands können aktuell nur mit dem paster
-Skript aufgerufen werden.
Die Installation erfolgt in Buildout mit:
[buildout]
parts =
...
paster
zopeskel
...
[paster]
recipe = zc.recipe.egg
eggs =
ZopeSkel
PasteScript
PasteDeploy
Wenn Sie z.B. ein Archetypes-Paket erstellt haben mit:
$ cd src
$ ../bin/zopeskel archetype vs.registration
dann können Sie als nächstes die local commands hierfür installieren mit:
$ cd vs.registration/
$ python setup.py egg_info
Schließlich können Sie in das src/
-Verzechnis dieses Pakets wechseln und dort
einen Artikeltyp erstellen:
$ cd src/
$ ../../../bin/paster add contenttype Registrant
Einen Überblick über alle im Kontext verfügbaren local commands erhalten Sie mit:
$ ../../../bin/paster add --list
Available templates:
browserlayer: A Plone browserlayer
browserview: A browser view skeleton
contenttype: A content type skeleton
schema_field: A handy AT schema builder
Siehe auch
Erstellen eigener ZopeSkel- und local commands-Templates.
Die Python-Skripte zum Erstellen eines local commands-Templates entsprechen weitgehend denen eines normalen ZopeSkel-Templates. Wenn wir uns die Portlet
-Klasse in zopeskel/localcommands/archetype.py
genauer anschauen, stellen wir fest, dass sie von ArchetypeSubTemplate
abgeleitet wird und diese wiederum von ZopeSkelLocalTemplate
:
import os
from templer.core.vars import var
from templer.localcommands import TemplerLocalTemplate
from Cheetah.Template import Template as cheetah_template
class ArchetypeSubTemplate(TemplerLocalTemplate):
use_cheetah = True
parent_templates = ['archetype']
class ContentType(ArchetypeSubTemplate):
_template_dir = 'templates/archetype/contenttype'
summary = "A content type skeleton"
...
use_cheetah
Für local commands muss der Wert auf True
gesetzt werden.
parent_templates
Liste der ZopeSkel-Templates, die dieses local command-Template aufrufen können.
_template_dir
Verzeichnis mit den Template-Dateien.
summary
Zusammenfassende Beschreibung des Templates.
Die anschließend folgende pre
-Methode ermittelt die Variablen contenttype_classname
, contenttype_classname
, contenttype_classname
, contenttype_name
und add_permission_name
des übergeordneten Pakets.
Auch die Template-Struktur entspricht weitgehend der von normalen ZopeSkel-Templates mit dem Unterschied, dass alle Dateien mit _insert
enden. Betrachten wir uns z.B. das portlet
-Template genauer, entdecken wir folgende Struktur:
$ tree ~/.buildout/eggs/templer.plone.localcommands-1.0b1-py2.7.egg/templer/plone/localcommands/templates/archetype/contenttype/
/home/veit/.buildout/eggs/templer.plone.localcommands-1.0b1-py2.7.egg/templer/plone/localcommands/templates/archetype/contenttype/
├── config.py_insert
├── content
│ ├── configure.zcml_insert
│ └── +content_class_filename+.py_tmpl
├── interfaces
│ ├── +content_class_filename+.py_tmpl
│ └── __init__.py_insert
├── profiles
│ └── default
│ ├── factorytool.xml_insert
│ ├── rolemap.xml_insert
│ ├── types
│ │ └── +types_xml_filename+.xml_tmpl
│ └── types.xml_insert
└── README.txt_insert
5 directories, 10 files
Die mit _tmpl
endenden Dateien werden wie normale ZopeSkel-Templates behandelt. Speziell für local command-Templates sind die auf _insert
endenden Dateien. Der Inhalt dieser Dateien wird in die korrespondierenden Dateien des bereits bestehenden Projekts eingefügt. Schauen wir uns nun z.B. profiles/default/rolemap.xml_insert
genauer an:
#<?xml version="1.0"?>
#<rolemap>
# <permissions>
# <!-- -*- extra stuff goes here -*- -->
<permission name="$add_permission_name" acquire="False">
<role name="Manager" />
<role name="Contributor" />
</permission>
# </permissions>
#</rolemap>
Existiert in dem Projekt bereits eine Datei profiles/default/rolemap.xml
, dann werden nur die Zeilen hinzugefügt, die nicht mit #
beginnen.
Existiert noch keine profiles/default/rolemap.xml
-Datei, wird die Datei aus dem Template ohne die mit #
beginnenden Zeilen geschrieben.
Hier eine Übersicht über verschiedene Arten von Tests, deren konkrete Unterscheidung jedoch schwierig werden kann.
werden aus der Programmierperspektive geschrieben. Sie testen isoliert eine einzelne Methode oder Funktion.
untersuchen die Abhängigkeit von Methoden und Komponenten während Unit Tests meist mit möglichst wenig Abhängigkeiten geschrieben werden. Meist verwenden Unit Tests und Integrationstests jedoch dasselbe Framework.
beschreiben meist Nutzungsfälle (Use Cases) und deren Abläufe. Werden sie aus Nutzersicht geschrieben und beziehen sich nur auf die an der Oberfläche angebotenen Eingabemöglichkeiten, werden sie auch Akzeptanztests genannt.
Auch Systemtests werden aus Nutzersicht geschrieben, jedoch ohne Kenntnis des Systems. Systemtests sollen Nutzer mit ihren üblichen Verhaltensmustern simulieren.
Tests liefern häufig eine gute Beschreibung, wie einzelne Komponenten verwendet werden sollen, welche Schnittstellen und Zustände sie aufweisen können. Jim Fulton hat für den Zope-3-Entwicklungsprozess auf docstrings basierende Unit Tests eingeführt, die sich zunehmend auch in anderen Python-Projekten etablieren.
Siehe hierzu auch Tim Peters, Jim Fulton: Literate unit testing: Unit Testing with Doctest.
testet eine einzelnes Szenario.
ist eine konsistente Testumgebung.
ist eine Sammlung mehrerer Test Cases.
durchläuft eine Test Suite und stellt die Ergebnisse dar.
Unit Tests sind kleine, sich selbst enthaltende Testmethoden, die unabhängig von anderen Methoden ausgeführt werden und sich nicht gegenseitig beeinflussen.
Folgende Regeln für unit tests sind zu beachten:
Schreiben Sie mindestens einen Test für jede Methode.
Schreiben Sie zunächst die Interface- und/oder stub-Methoden, dann die Tests. Vergewissern Sie sich, dass die Tests nicht bestanden werden (da der Code ja noch nicht geschrieben ist).
Erst jetzt sollte das neue Feature implementiert werden mit dem Ziel, den Test zu bestehen.
Wenn Sie nach einem Release einen Bug entdecken, beheben Sie ihn nicht einfach, sondern
schreiben Sie zunächst einen Test, der den Fehler demonstriert,
dann erst beseitigen Sie den Bug.
Unit tests erlauben Ihnen,
Ihre Software auch in entfernten Umgebungen überprüfen zu können;
beim Implementieren neuer Features nicht bereits bestehende zu kompromitieren;
bereits behobene Bugs nicht wieder einzuführen;
Zeit bei der Entwicklung einzusparen, da die Chancen fehlerhaften Code schnell zu erkennen, deutlich steigen;
Code schreiben und testen in derselben Umgebung auszuführen;
die Test-Abdeckung immer weiter zu erhöhen.
Unit Tests im Zope-2/Plone-Kontext basieren meist auf ZopeTestCase, der das Python unittest-Modul verwendet. Dabei laufen die Unit Tests meistens in einer Sandbox (auch Test-Fixture genannt) ab.
PloneTestCase basiert auf ZopeTestCase, ist jedoch eher ein Integrationstest, der die Integration Ihrer und der zugrundeliegenden Komponenten wie ZODB und ZPublisher überprüft. PloneTestCase erstellt eine leere Zope-Instanz mit einer einzelnen Plone Site, einem Nutzer und dem Standard Mitglieder-Ordner. Ist der Test beendet, wird die Transaktion abgebrochen, so dass keine der durch den Test vorgenommenen Änderungen der Plone-Site erhalten bleibt.
Es gibt verschiedene Möglichkeiten, Tests in Zope ablaufen zu lassen.
In den Versionen Zope 2.9 bis 2.11 lässt sich der TestRunner folgendermaßen aufrufen um ein ganzes Paket zu testen:
$ ./bin/test -s vs.policy
-s my.package
, --package my.package
, --dir my.package
durchsucht die angegebenen Verzeichnisse nach Tests.
-m test_setup
, --module test_setup
spezifiziert ein Testmodul als regulären Ausdruck, z.B.:
$ ./bin/test -s my.package -m 'test_setup'
-t '.*installed.*'
, --test test_theme_installed
spezifiziert einen Testfilter als regulären Ausdruck, z.B.:
$ ./bin/test -s vs.policy -m '.*setup.*' -t '.*installed.*'
Hiermit werden im Paket vs.policy
alle, mit installed
endenden,
Methoden in allen Testmodulen, die auf setup
enden, durchlaufen.
-u
, --unit
durchläuft ausschließlich Unit tests und ignoriert andere layer
-
Optionen.
-f
, --non-unit
durchläuft alle Tests, die keine Unit Tests sind
-v
, --verbose
führt zu ausführlicherer Ausgabe
--ndiff
falls ein Doctest fehlschlägt, wird ndiff.py
zur Darstellung der Unterschiede verwendet
--udiff
falls ein Doctest fehlschlägt, wird Unified Diff zur Darstellung der Unterschiede verwendet
--cdiff
falls ein Doctest fehlschlägt, wird Context Diff zur Darstellung der Unterschiede verwendet
-d
, post-mortem
stoppt die Ausführung nach dem ersten nicht-bestandenen Test und ermöglicht post-mortem-Debugging, d.h. die Debug-Session wird nur gestartet, wenn ein Test fehlschlägt.
--path src/my.package
fügt einen Pfad zu Pythons Suchpfad hinzu, wobei die Option mehrfach angegeben werden kann.
Diese erhalten Sie mit:
$ ./bin/test --help
Wenn die relevanten Tests erfolgreich verliefen, sollten schließlich noch alle Tests durchgeführt werden um sicherzustellen, dass nicht an anderer Stelle etwas gebrochen ist. Wenn alle Tests erfolgreich durchlaufen wurden, erscheint eine Meldung:
Ran 10 tests with 0 failures and 0 errors in 4.830 seconds.
Falls nicht alle Tests erfolgreich durchlaufen wurden, ändert sich die Meldung:
Ran 10 tests with 2 failures and 3 errors in 9.688 seconds.
Dabei wurden dann zwei Tests nicht bestanden und drei Tests enthielten Fehler.
roadrunner ist ein Testrunner, der die testgetriebene Entwicklung deutlich beschleunigen kann, da er vorab das Standard-Zope- und Plone-Environment für PloneTestCase läd. zur Installation wird einfach folgendes in die buildout.cfg
-Datei eingetragen:
[buildout]
parts =
…
roadrunner
[roadrunner]
recipe = roadrunner:plone
packages-under-test = vs.policy
Anschließend kann es wie der reguläre Zope-Testrunner aufgerufen werden:
$ ./bin/roadrunner -s vs.policy
Mit zc.recipe.testrunner
steht ein Buildout-Rezept zum Erstellen eigener TestRunner für mehrere Eggs zur Verfügung. Die Buildout-Konfiguration für das Testen aller in einer Instanz verwendete Eggs kann z.B. so aussehen:
[buildout]
...
parts =
...
instance
test
[test]
recipe = zc.recipe.testrunner
defaults = ['--auto-color', '--auto-progress']
eggs =
${instance:eggs}
defaults = ['--auto-color', '--auto-progress', '-q', '--module', '^vs[.]']
initialization =
import warnings
warnings.simplefilter('ignore', DeprecationWarning)
eggs
Liste der zu testenden Eggs wobei jedes Egg in einer neuen Zeile stehen sollte.
defaults
Standardoptionen, die üblicherweise als Python list literal angegeben werden.
--ndiff
Wenn ein Doctest fehlschlägt, wird das ndiff.py
-Utility zum Anezigen der Unterschiede verwendet. Alternativen zu dieser Angabe sind:
--udiff
für Unified Diffs
--cdiff
für Context Diffs.
Weitere Informationen erhalten Sie in der Dokumentation auf PyPI zu zc.recipe.testrunner.
plone.recipe.alltests erlaubt das Testen aller Eggs eines Buildout-Projekts. bin/alltests
durchläuft alle Tests aller Abhängigkeiten des Hauptprodukts. Hierzu sind lediglich folgende drei Zeilen in der buildout.cfg
-Datei hinzuzufügen:
[buildout]
...
parts =
...
instance
test
alltests
...
[alltests]
recipe = plone.recipe.alltests
Darüberhinaus können noch folgende Optionen angegeben werden:
eggs
Eine Liste von Paketen, die getestet werden sollen.
Der Standardwert sind die im [tests]
-Abschnitt angegebenen Eggs.
test-script
Der Ort im Dateisystem von zc.recipe.testrunner
.
Der Standardwert ist bin/test
exclude
Eine Liste von Eggs, die aus dem Testen ausgeschlossen werden sollen. Als Werte können reguläre Ausdrücke angegeben werden, z.B.:
[alltests]
recipe = plone.recipe.alltests
exclude =
repoze.*
groups
Ein Buildout-Abschnitt mit einem Mapping von Gruppen- zu Paketnamen, z.B.:
[alltests]
recipe = plone.recipe.alltests
groups = test-groups
[test-groups]
Zope2 =
Acquisition
DateTime
ExtensionClass
Persistence
ZODB =
transaction
zc.lockfile
chameleon =
chameleon.core
cmf.pt
z3c.pt
package-map
Ein Buildout-Abschnitt mit einem Mapping von Distributions- zu Paketnamen, z.B.:
[alltests]
recipe = plone.recipe.alltests
groups = test-groups
[package-map]
Plone = Products.CMFPlone
Der Zope Test Runner ermöglicht auch, die Testabdeckung eines Produkts zu ermitteln:
./bin/test -s vs.registration --coverage=coverage
Für eine schnelle Statusübersicht erhalten Sie folgendes Ergebnis:
Ran 19 tests with 0 failures and 0 errors in 43.924 seconds.
Tearing down left over layers:
Tear down Products.PloneTestCase.layer.PloneSite in 2.009 seconds.
Tear down Products.PloneTestCase.layer.ZCML in 0.012 seconds.
lines cov% module (path)
14 57% vs.registration.__init__ (/home/veit/myproject/src/vs.registration/vs/registration/__init__.py)
66 63% vs.registration.browser.enquiry (/home/veit/myproject/src/vs.registration/vs/registration/browser/enquiry.py)
7 100% vs.registration.browser.registrant (/home/veit/myproject/src/vs.registration/vs/registration/browser/registrant.py)
26 76% vs.registration.browser.registration (/home/veit/myproject/src/vs.registration/vs/registration/browser/registration.py)
37 100% vs.registration.content.registrant (/home/veit/myproject/src/vs.registration/vs/registration/content/registrant.py)
44 100% vs.registration.content.registration (/home/veit/myproject/src/vs.registration/vs/registration/content/registration.py)
22 100% vs.registration.interfaces (/home/veit/myproject/src/vs.registration/vs/registration/interfaces.py)
64 100% vs.registration.portlets.registrants (/home/veit/myproject/src/vs.registration/vs/registration/portlets/registrants.py)
25 96% vs.registration.setuphandlers (/home/veit/myproject/src/vs.registration/vs/registration/setuphandlers.py)
17 100% vs.registration.tests.base (/home/veit/myproject/src/vs.registration/vs/registration/tests/base.py)
12 91% vs.registration.tests.test_doctest (/home/veit/myproject/src/vs.registration/vs/registration/tests/test_doctest.py)
74 100% vs.registration.tests.test_portlet_registrants (/home/veit/myproject/src/vs.registration/vs/registration/tests/test_portlet_registrants.py)
22 100% vs.registration.tests.test_setup (/home/veit/myproject/src/vs.registration/vs/registration/tests/test_setup.py)
Ausführlichere Informationen für jede Testdatei sind im Verzeichnis coverage
enthalten, z.B. für vs.registration.browser.registration
in coverage/vs.registration.browser.registration.cover
:
"""Define a browser view for the Registration content type. In the FTI
configured in profiles/default/types/*.xml, this is being set as the default
view of that content type.
1: """
1: from Acquisition import aq_inner
1: from Products.Five.browser import BrowserView
1: from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
1: from Products.CMFCore.utils import getToolByName
1: from vs.registration.interfaces import IRegistration
1: from vs.registration.interfaces import IRegistrant
1: from plone.memoize.instance import memoize
2: class RegistrationView(BrowserView):
"""Default view of a registration
"""
# This template will be used to render the view. An implicit variable
# 'view' will be available in this template, referring to an instance
# of this class. The variable 'context' will refer to the registration
# being rendered.
1: __call__ = ViewPageTemplateFile('registration.pt')
1: @memoize
def registrations(self):
>>>>>> context = aq_inner(self.context)
>>>>>> catalog = getToolByName(context, 'portal_catalog')
>>>>>> return [ dict(url=registration.getURL(),
title=registration.Title,
description=registration.Description,)
for registration in
catalog(object_provides=IRegistration.__identifier__,
>>>>>> path=dict(query='/'.join(context.getPhysicalPath()),
>>>>>> depth=1),
>>>>>> sort_on='sortable_title')
]
...
Dies entspricht exakt der Datei, jedoch ist den meisten Zeilen eine Zahl vorangestellt:
n
diese Zeile wurde n
-mal aufgerufen.
>>>>>>
diese Zeile wurde nicht aufgerufen.
Die Anzahl der durch den Test nicht aufgerufenen Zeilen durch die gesamte Anzahl der Zeilen ergibt die Testabdeckung in Prozent.
Auch für mehrere Eggs lässt sich die Testabdeckung mit zc.recipe.testrunner
angeben:
[buildout]
...
parts =
...
test
coverage
[test]
recipe = zc.recipe.testrunner
eggs =
${instance:eggs}
[coverage]
recipe = zc.recipe.egg
eggs = coverage
initialization =
include = '--source=${buildout:directory}/src'
sys.argv = sys.argv[:] + ['run', include, 'bin/test', '--all']
coverage
erstellt einen übersichtlichen Report über die Testabdeckung. Um einen solchen Report zu erhalten, kann einfach ein entsprechender Abschnitt in der buildout.cfg
-Datei eingetragen werden:
[buildout]
...
parts =
...
report
...
[report]
recipe = zc.recipe.egg
eggs = coverage
scripts = coverage=report
initialization =
sys.argv = sys.argv[:] + ['html', '-i']
Anschließend können Sie file:///home/veit/vs_buildout/htmlcov/index.html
in Ihrem Browser öffnen und erhalten einen übersichtlichen Report:
Statt der bereits angelegten Datei src/vs.policy/vs/policy/tests.py
erstellen wir ein eigenes tests
-Modul:
$ rm -rf src/vs.policy/vs/policy/tests.py
$ mkdir src/vs.policy/vs/policy/tests
$ touch src/vs.policy/vs/policy/tests/__init__.py
Anschließend definieren wir im neu erstellten tests
-Ordner zunächst ein
Test-Fixture, eine gleichbleibende Testumgebung mit der Basisklasse TestCase
, die an den Layer VS_POLICY_INTEGRATION
gebunden wird. Hierzu
erstellen wir im tests
-Verzeichnis die Datei base.py
mit folgendem
Inhalt:
import unittest2 as unittest
from plone.testing import z2
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD
from vs.policy.tests import layer
def get_browser(app, loggedIn=True):
browser = z2.Browser(app)
if loggedIn:
auth = 'Basic %s:%s' % (TEST_USER_NAME, TEST_USER_PASSWORD)
browser.addHeader('Authorization', auth)
return browser
class TestCase(unittest.TestCase):
layer = layer.VS_POLICY_INTEGRATION
class FunctionalTestCase(unittest.TestCase):
layer = layer.VS_POLICY_FUNCTIONAL
In layer.py
werden anschließend die Test-Layer VS_POLICY_INTEGRATION
und
VS_POLICY_FUNCTIONAL
definiert, die beide auf VS_POLICY_LAYER
basieren:
from plone.app.testing import applyProfile
from plone.app.testing import PloneFixture
from plone.app.testing import PloneSandboxLayer
from plone.app.testing import PloneTestLifecycle
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.testing import z2
from zope.configuration import xmlconfig
class VsPolicyFixture(PloneFixture):
# No sunburst please
extensionProfiles = ()
VS_POLICY_FIXTURE = VsPolicyFixture()
class VsPolicyTestLifecycle(PloneTestLifecycle):
defaultBases = (VS_POLICY_FIXTURE, )
class IntegrationTesting(VsPolicyTestLifecycle, z2.IntegrationTesting):
pass
class FunctionalTesting(VsPolicyTestLifecycle, z2.FunctionalTesting):
pass
class VsPolicyLayer(PloneSandboxLayer):
defaultBases = (VS_POLICY_FIXTURE, )
def setUpZope(self, app, configurationContext):
import vs.policy
xmlconfig.file("configure.zcml", vs.policy,
context=configurationContext)
z2.installProduct(app, 'vs.policy')
def tearDownZope(self, app):
z2.uninstallProduct(app, 'vs.policy')
def setUpPloneSite(self, portal):
applyProfile(portal, 'vs.policy:default')
setRoles(portal, TEST_USER_ID, ['Manager'])
portal.invokeFactory('Folder', 'test-folder')
setRoles(portal, TEST_USER_ID, ['Member'])
VS_POLICY_LAYER = VsPolicyLayer()
VS_POLICY_INTEGRATION = IntegrationTesting(
bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Integration")
VS_POLICY_FUNCTIONAL = FunctionalTesting(
bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Functional")
Die eigentlichen Tests werden in der Datei test_test.py
definiert:
from vs.policy.tests.base import FunctionalTestCase
class TestTest(FunctionalTestCase):
def test_test(self):
self.assertTrue(True)
Unit Tests, die auf dem Python unittest-Modul, ZopeTestCase und PloneTestCase basieren, müssen sich an einige Namenskonventionen halten:
Alle Testdateien müssen mit test
beginnen, z.B. test_setup.py
.
In den Testdateien werden Klassen für Testfälle definiert, die ein oder
mehrere Testmethoden enthalten können, die ebenfalls mit test
beginnen
müssen, z.B. test_portal_title
.
Zunächst wird die Basisklasse importiert, dann die Klassen für die Testfälle und schließlich die Test Suite selbst definiert.
Jede Testsuite kann aus mehreren Testklassen bestehen. Wird die Testsuite ausgeführt, werden alle Testmethoden aller Testklassen der Test-Suite ausgeführt.
Innerhalb einer Testklasse kann die afterSetUp()
-Methode unmittelbar vor
jedem Test aufgerufen werden um Testdaten für diesen Test anzugeben. Nachdem
der Test durchgeführt wurde, werden die Transaktionen zurückgenommen, so dass
normalerweise keine Artefakte zurückbleiben.
Werden jedoch Änderungen außerhalb von Zope vorgenommen, müssen diese mit der
Methode beforeTearDown()
aufgeräumt werden.
Die in einer Testklasse verwendeten Methoden wie self.assertEqual()
oder
self.failUnless()
sind Assertion-Methoden, und wenn eine von ihnen
fehlschlägt, gilt der ganze Test als fehlgeschlagen.
Testmethoden überprüfen, ob etwas wahr oder falsch ist. Daher kann aus den Tests auch herausgelesen werden, wie sich Ihr Produkt verhalten soll, welche Fähigkeiten es enthält. Die Liste der Testmethoden ist ausführlich in der Python-Dokumentation für unittest.TestCaseObjects enthalten. Die häufigsten sind:
failUnless(expr)
stellt sicher, dass der Ausdruck expr
wahr ist.
assertEqual(expr1, expr2)
stellt sicher,dass expr1
gleich expr2
ist.
assertRaises(exception, callable, ...)
stellt sicher, dass beim Aufruf von callable
die Fehlermeldung
exception
ausgegeben wird.
Bemerkung
callable
sollte der Name einer Methode oder ein aufrufbares
Objekt sein, nicht ein aktueller Aufruf, z.B.:
self.assertRaises(AttributeError, myObject.myMethod, someParameter)
fail()
Dies ist sinnvoll, wenn ein Test noch nicht fertiggestellt ist oder in
einem if
-Statement, das deutlich macht, dass der Test fehlgeschlagen
ist.
ZopeTestCase und PloneTestCase fügen zu den Assertion-Methoden noch weitere hilfreiche Methoden und Variablen hinzu, die mit Zope interagieren. Hier nur kurz die wesentlichen Variablen:
self.portal
Die PloneSite, in der der Test ausgeführt wird.
self.folder
Der member
-Ordner des Mitglieds, als der die Tests ausgeführt werden.
Und hier die wesentlichen Hilfsmethoden:
self.logout()
abmelden, d.i. die Rolle anonymous
bekommen;
self.login()
sich erneut anmelden; wird ein Nutzername mit übergeben, erfolgt die Anmeldung als dieser Nutzer.
self.setRoles(roles)
durchläuft eine Liste von Rollen, die angenommen werden sollen.
self.setRoles((Manager,))
lässt Sie beispielsweise die Rolle des
Managers für eine bestimmte Zeit annehmen.
self.setPermissions(permissions)
analog können auch Berechtigungen für den Testnutzer in self.folder
angegeben werden;
self.setGroups(groups)
eine Liste von Gruppen, der der aktuelle Nutzer angehören soll.
Mehr über Unit Tests in Python erfahren Sie in der unittest-Python- Dokumentation.
Der Testrunner kann nun gestartet werden mit:
$ ./bin/test -s vs.policy
Wären die Tests geschrieben worden, bevor die Profile erstellt wurden, hätten beide Tests fehlschlagen müssen und der Testrunner folgendes ausgeben:
AssertionError:"Welcome to Veit Schiele != ''
...
AssertionError:'Veit Schiele != 'Plone site'
Ran 2 tests with 2 failures and 0 errors
Nachdem die Profile angelegt wurden, sollte jedoch keiner der Tests fehlschlagen:
Ran 2 tests with 0 failures and 0 errors.
-s my.package
, --package my.package
, --dir my.package
durchsucht die angegebenen Verzeichnisse nach Tests.
-m test_setup
, --module test_setup
spezifiziert ein Testmodul als regulären Ausdruck, z.B.:
$ ./bin/test -s my.package -m 'test_setup'
-t '.*installed.*'
, --test test_theme_installed
spezifiziert einen Testfilter als regulären Ausdruck, z.B.:
$ ./bin/test -s vs.policy -m '.*setup.*' -t '.*installed.*'
Hiermit werden im Paket vs.policy
alle, mit installed
endenden,
Methoden in allen Testmodulen, die auf setup
enden, durchlaufen.
-u
, --unit
durchläuft ausschließlich Unit tests und ignoriert andere layer
-
Optionen.
-f
, --non-unit
durchläuft alle Tests, die keine Unit Tests sind
-v
, --verbose
führt zu ausführlicherer Ausgabe
--ndiff
falls ein Doctest fehlschlägt, wird ndiff.py
zur Darstellung der
Unterschiede verwendet
--udiff
When there is a doctest failure, show it as a unified diff. falls ein Doctest fehlschlägt, wird Unified Diff zur Darstellung der Unterschiede verwendet
--cdiff
falls ein Doctest fehlschlägt, wird Context Diff zur Darstellung der Unterschiede verwendet
-d
, post-mortem
stoppt die Ausführung nach dem ersten nicht-bestandenen Test und ermöglicht post-mortem-Debugging, d.h. die Debug-Session wird nur gestartet, wenn ein Test fehlschlägt.
--path src/my.package
fügt einen Pfad zu Pythons Suchpfad hinzu, wobei die Option mehrfach angegeben werden kann.
Diese erhalten Sie mit:
$ ./bin/test --help
Wenn die relevanten Tests erfolgreich verliefen, sollten schließlich noch alle Tests durchgeführt werden um sicherzustellen, dass nicht an anderer Stelle etwas gebrochen ist. Wenn alle Tests erfolgreich durchlaufen wurden, erscheint eine Meldung:
Ran 10 tests with 0 failures and 0 errors in 4.830 seconds.
Falls nicht alle Tests erfolgreich durchlaufen wurden, ändert sich die Meldung:
Ran 10 tests with 2 failures and 3 errors in 9.688 seconds.
Dabei wurden dann zwei Tests nicht bestanden und drei Tests enthielten Fehler.
roadrunner ist ein Testrunner, der die testgetriebene Entwicklung deutlich
beschleunigen kann, da er vorab das Standard-Zope- und Plone-Environment für
PloneTestCase läd. zur Installation wird einfach folgendes in die
buildout.cfg
-Datei eingetragen:
[buildout]
parts =
...
roadrunner
[roadrunner]
recipe = roadrunner:plone
packages-under-test = vs.policy
Anschließend kann es wie der reguläre Zope-Testrunner aufgerufen werden:
$ ./bin/roadrunner -s vs.policy
Übernehmen Sie Tests z.B. aus Plone wenn diese Ihren eigenen Absichten entsprechen.
Dummy-Implementierungen sind häufig der einzige Weg um bestimmte Funktionen zu testen. Siehe auch CMFPlone/tests/dummy.py für einige Dummy-Objekt-Beispiele.
Tests können auch verwendet werden um Dinge auszuprobieren – sie sind eine sichere Umgebung.
Während des Debugging können print
-Statements in den Test eingefügt werden
um nachvollziehbare Hinweise im Terminal zu erhalten.
Es kann jedoch auch gleich der Python-Debugger in die Testmethoden importiert werden mit:
import pdb; pdb.set_trace()
Anschließend können Sie mit r
schrittweise durch den Testkode gehen.
Mehr zum Python-Debugger erfahren Sie in Debugging und in der Python-Dokumentation.
DocTests bieten eine interessante Möglichkeit, um Python-Code zu testen indem die Geschichte einer Methode oder Klasse erzählt wird. In Zope 3 sind DocTests weit verbreitet und werden oft für Unit Tests verwendet. Zudem sind DocTests oft eine gute Möglichkeit, gleichzeitig Dokumentationen und Tests zu schreiben.
Meist werden DocTests in eine einzige Datei geschrieben. Dabei ist jedoch zu beachten, dass nicht strikt zwischen den einzelnen Schritten getrennt wird und daher keine sauberen Unit Tests durchgeführt werden. Alternativ kann auch für jede Methode oder Klasse ein DocTest in deren docstring geschrieben werden. Die Syntax der DocTests ist zwar identisch, aber jeder docstring wird als eigene Test Fixture durchgeführt, die eine saubere Trennung der Tests erlaubt.
Hier nun ein einfaches Beispiel aus /Products/CMFPlone/PloneTool.py
:
def normalizeString(self, text, relaxed=False):
"""Normalizes a title to an id.
normalizeString() converts a whole string to a normalized form that
should be safe to use as in a url, as a css id, etc.
If relaxed=True, only those characters that are illegal as URLs and
leading or trailing whitespace is stripped.
>>> ptool = self.portal.plone_utils
>>> ptool.normalizeString("Foo bar")
'foo-bar'
...
"""
return utils.normalizeString(text, context=self, relaxed=relaxed)
Dabei ist zu beachten, dass der docstring der Methode sowohl den einfachen Text enthält, der beschreibt, was die Methode tut, als auch Python-Statements, wie man sie aus einem interaktiven Python-Interpreter kennt.
Und tatsächlich führt der DocTest Runner jede Zeile, die mit >>>
beginnt, im Python-Interpreter aus. Folgt diesem Statement eine Zeile, die genausoweit eingerückt ist, nicht leer ist und nicht mit >>>
beginnt, so wird dies als Ergebnis des Statements erwartet. Stimmen sie nicht überein, gibt doctest eine Fehlermeldung aus.
Wird kein Ausgabewert angegeben, wird von der Methode keine Ausgabe erwartet. Gibt die Methode dennoch etwas aus, wirft doctest eine Fehlermeldung aus.
...
bedeutet »beliebig viele Zeichen«. Dies ist sinnvoll für Werte, die nicht vorhergesagt werden können wie automatisch generierte IDs, die auf dem aktuellen Datum oder zufälligen Zahlen beruhen.
DocTests sind ein Feature von Python 2 und daher kann Python’s unittest
-Library für einfache Doctests verwendet werden. Zope 3 erweitert die Funktionalität noch um das zope.testing
-Modul:
import unittest
from zope.testing import doctest
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('README.txt'),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Wird die Datei z.B. als test_doctests.py
im tests
-Verzeichnis Ihres Produkts gespeichert, kann es mit den üblichen Aufrufen gestartet werden. Wenn Sie sich Zope3-DocTests anschauen, können Sie häufig feststellen, dass in den ersten Zeilen die Komponentenarchitektur oder einzelne Komponenten explizit geladen werden.
Um die Testumgebung für DocTests anzugeben, können Sie z.B. folgendes eingeben:
import unittest
import doctest
from zope.testing import doctestunit
from zope.component import testing, eventtesting
from Testing import ZopeTestCase as ztc
from vs.registration.tests import base
def test_suite():
return unittest.TestSuite([
# Demonstrate the main content types
ztc.ZopeDocFileSuite(
'README.txt', package='vs.registration',
test_class=base.RegistrationFunctionalTestCase,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE | doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS),
])
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Dieser Test ist aus vs.registration/tests/test_doctest.py
entnommen. Er führt dabei die README.txt
-Datei aus vs.registration/vs/registration/
aus.
Mit diesem Setup referenziert die Variable self
auf eine PloneTestCase-Instanz. Demzufolge können Sie z.B. folgendes angeben:
>>> self.portal.invokeFactory('Registration', 'my-first-registration')
um ein Dokument im Wurzelverzeichnis Ihrer Site anzulegen. Auch alle anderen Methoden aus PloneTestCase und ZopeTestCase sollten innerhalb von DocTests laufen.
In optionflags
lassen sich Optionen und Anweisungen für docteststs
angeben, u.a.:
doctest.NORMALIZE_WHITESPACE
Wenn angegeben, werden alle Abfolgen von Leerzeichen und/oder Zeilenumbrüchen als gleich betrachtet.
doctest.ELLIPSIS
Wenn angegeben, kann eine Ellipse ...
in der erwarteten Ausgabe auf jede beliebige Zeichenfolge passen.
doctest.REPORT_ONLY_FIRST_FAILURE
Wenn angegeben, wird der erste fehlgeschlagene Test angezeigt, nicht jedoch der Ausgang der weiteren Tests. Hiermit wird verhindert, dass doctest
fehlgeschlagene Tests aufgrund von vorangehend gescheiterten Tests ausgibt.
Eine vollständige Übersicht über alle Optionen finden Sie in Option Flags and Directives
Das DocTest-Modul kommt mit einer umfangreichen Dokumentation.
Sie können z.B. auf Hilfsmethoden in Ihrem Produkt verweisen, angenommen Ihr Produkt enthält die Methode reset(self)
in my.package.tests.utils
, so kann diese Methode mit DocTest aufgerufen werden:
>>> from my.package.tests.utils import reset
>>> reset()
Möchten Sie z.B. ein Produkt in obigem Beispiel verfügbar machen, müssen Sie nur ZopeTestCase.installProduct()
in der testsuit-Datei Ihres Produkts aufrufen.
Wenn Sie import pdb; pdb.set_trace()
in Ihren DocTest einfügen, können Sie zwar nicht schrittweise durch Ihren Kode gehen, aber Variablen und der Status der Test Fixture kann mit print
ausgegeben werden.
Dabei sollten Sie jedoch beachten, dass sich locals
auf Interna von doctest
bezieht:
(Pdb) locals()
{'__return__': None, 'self': <zope.testing.doctest._OutputRedirectingPdb instance at 0x5a7c8f0>}
Das gewohnte Verhalten von pdb
erhalten Sie indem Sie im Stack eine Ebene nach oben gehen:
(Pdb) up
> /Users/veit/vs_buildout/src/Products.PloneGetPaid/Products/PloneGetPaid/notifications.py(22)__call__()
-> import pdb ; pdb.seT_trace()
(Pdb) locals()
{'settings': <Products.PloneGetPaid.preferences.StoreSettings object at 0x5f631b0>, 'store_url': 'http://nohost/plone', 'self': <Products.PloneGetPaid.notifications.MerchantOrderNotificationMessage object at 0x56c30d0>, 'order_contents': u'11 pz @84.00 total: US$924.00\n22 ph @59.00 total: US$1298.00\n12 pf @98.00 total: US$1176.00\n23 pX @95.00 total: US$2185.00\n3 pM @89.00 total: US$267.00\n22 po @60.00 total: US$1320.00\n23 pj @39.00 total: US$897.00\n15 po @34.00 total: US$510.00\n5 pS @76.00 total: US$380.00\n1 pm @70.00 total: US$70.00', 'template': u'To: ${to_email}\nFrom: "${from_name}" <${from_email}>\nSubject: New Order Notification\n\nA New Order has been created\n\nTotal Cost: ${total_price}\n\nTo continue processing the order follow this link:\n${store_url}/@@admin-manage-order/${order_id}/@@admin\n\nOrder Contents\n\n${order_contents}\n\nShipping Cost: ${shipping_cost}\n\n', 'pdb': <module 'pdb' from '/Users/moo/code/python-macosx/parts/opt/lib/python2.4/pdb.pyc'>}
(Pdb)
Weitere Informationen zum Debugging erhalten Sie in der Python Dokumentation.
Interlude erlaubt die Verwendung einer interaktiven Shell innerhalb von Doctests, wobei die oben beschriebene Besonderheit nicht auftritt:
>>> from interlude import interact
>>> interact(locals())
Wenn der testrunner
nun interact
durchläuft, erhalten Sie eine interaktive Python-Konsole.
Folgender Code gibt Exceptions aus:
>>> try:
... someOperation()
... except:
... import pdb; pdb.set_trace()
>>> # continue as normal
DocTests als Unit Tests haben den Vorteil, dass sie direkt in docstrings beim auszuführenden Code stehen. Darüberhinaus bietet die Syntax von DocTests viele Annehmlichkeiten, die das Schreiben von Tests angenehmer und schneller machen.
Im folgenden zeige ich ein einfaches Beispiel aus Archetypes:
[...]
class ReferenceSource(object):
"""A base implementation of an IReferenceSource adapter that
relies on a IReferenceQuery local utility to look up targets and
on an IReferenceStorage adapter to add references.
Basic setup:
>>> import archetypes.testing
>>> archetypes.testing.setupPortal()
>>> archetypes.testing.wireUp()
We create an object that we adapt to our ReferenceSource:
>>> obj = archetypes.testing.FakeObject('sourceobj')
>>> source = ReferenceSource(obj)
We don't have any targets or relationships at this point:
>>> source.getTargets()
[]
>>> source.getRelationships()
set([])
[...]
Teardown:
>>> archetypes.testing.teardownPortal()
"""
[...]
Der Test Runner schaut nach DocTests innerhalb von docstrings, und führt jeden von ihnen als seine eigene Test Fixture, d.h. unabhängig von anderen Tests, aus. In diesem Beispiel werden Tests für jede Klasse geschrieben, die zugleich dokumentieren, wie die Klasse verwendet wird.
Um docstring-DocTests ausführen zu können, muss noch eine Test Suite hinzugefügt werden, die dem Test Runner sagt, dass er nach diesen docstrings suchen soll. In den meisten Fällen genügt eine einfache Test Suite, die das Zope-3-Framework nutzt, wie z.B. test_doctests.py, die auf die o.g. base.py
referenziert:
import unittest
from zope.testing import doctest
optionflags = doctest.REPORT_ONLY_FIRST_FAILURE | doctest.ELLIPSIS
def test_suite():
from zope.testing.doctestunit import DocTestSuite
return unittest.TestSuite((
DocTestSuite('archetypes.reference.base',
optionflags=optionflags),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Wenn Sie PloneTestCase benötigen, sieht Ihre Test Suite z.B. so aus:
import unittest
from zope.testing import doctest
from Testing.ZopeTestCase import ZopeDocTestSuite
from Products.pluggablecatalog.tests import common
common.setupPloneSite()
from Products.PloneTestCase import PloneTestCase
optionflags = (doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_ONLY_FIRST_FAILURE)
def test_suite():
return unittest.TestSuite(
[ZopeDocTestSuite(module,
test_class=PloneTestCase.PloneTestCase,
optionflags=optionflags)
for module in ('Products.pluggablecatalog.tool',)]
)
Diese Test Suite ist testDoctests des pluggablecatalog-Produkts entnommen.
Und common
enthält einfach:
def setupPloneSite():
from Products.PloneTestCase import PloneTestCase
PloneTestCase.installProduct('pluggablecatalog')
PloneTestCase.setupPloneSite()
Falls Ihnen die Tests zu umfangreich erscheinen oder Sie sie gern in einer separaten Datei hätten, können Dummy-Methoden in einem normalen Unit Test erstellt werden, die docstrings mit DocTests enthalten. Diese DocTests sollten beschreibenden Text und die entsprechenden Testmethoden enthalten.
Das folgende Beispiel ist aus der i18n-Architektur von Plone 3 entnommen: test_countries.py. Es bezieht sich stark auf die Zope 3-Komponentenarchitektur, lädt ZCML-Dateien und bezieht das Test-Setup von zope.component.testing
:
import unittest
[...]
from zope.component.testing import setUp, tearDown
from zope.configuration.xmlconfig import XMLConfig
from zope.testing import doctest
from zope.testing.doctestunit import DocTestSuite
def configurationSetUp(self):
setUp()
XMLConfig('meta.zcml', zope.component)()
XMLConfig('meta.zcml', zope.app.publisher.browser)()
XMLConfig('configure.zcml', plone.i18n.locales)()
def testAvailableCountries():
"""
>>> util = queryUtility(ICountryAvailability)
>>> util
<plone.i18n.locales.countries.CountryAvailability object at ...>
>>> countrycodes = util.getAvailableCountries()
>>> len(countrycodes)
243
>>> 'de' in countrycodes
True
>>> countries = util.getCountries()
>>> len(countries)
243
>>> de = countries['de']
>>> de['name']
'Germany'
>>> de['flag']
'/@@/country-flags/de.gif'
"""
def test_suite():
return unittest.TestSuite((
DocTestSuite('plone.i18n.locales.countries'),
DocTestSuite(setUp=configurationSetUp,
tearDown=tearDown,
optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE),
))
if __name__ == '__main__':
unittest.main(defaultTest="test_suite")
Während unit tests und DocTests die Gültigkeit von einzelnen Methoden und Modulen überprüfen, überprüfen funktionale Tests die Anwendungen als Ganzes. Häufig nehmen sie dabei die Sicht des Nutzers ein und üblicherweise orientieren sie sich an den Nutzungsfällen (Use cases) der Anwendung. Systemtests hingegen testen die Anwendung als Blackbox.
Mit Zope 3 kommt die Bibliothek zope.testbrowser
, die es ermöglicht,
DocTests zu schreiben, die sich wie ein Webbrowser verhalten. Sie können URLs
öffnen, Links anklicken, Formularfelder ausfüllen und abschicken und dann die
zurückgelieferten HTTP headers, URLs und Seiteninhalte überprüfen.
Bemerkung
Da der testbrowser
momentan kein JavaScript unterstützt, empfiehlt sich
zum Testen dynamischer User Interfaces selenium. Selenium wird auch vom
Robot Test Automatisation Framework
verwendet.
Funktionale Tests sind kein Ersatz für Unit tests sondern überprüfen den funktionalen Ablauf, wie ihn der Nutzer wahrnimmt. Ein funktionaler Test überprüft z.B., ob ein Delete-Button vorhanden ist und wie erwartet funktioniert. Um die Tests überschaubar zu halten, wird meist nur überprüft, ob die entsprechenden Templates vorhanden sind und für Nutzer mit verschiedenen Rollen und Rechten wie erwartet funktioniert.
Im folgenden nun ein Auszug aus einem zope.testbrowser
-Test aus
vs.registration
:
Setting up and logging in
-------------------------
>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> portal_url = self.portal.absolute_url()
[...
>>> from Products.PloneTestCase.setup import portal_owner, default_password
>>> browser.open(portal_url + '/login_form?came_from=' + portal_url)
>>> browser.getControl(name='__ac_name').value = portal_owner
>>> browser.getControl(name='__ac_password').value = default_password
>>> browser.getControl(name='submit').click()
zope.testbrowser
wird verwendet um die Interaktion mit einem Browser zu
simulieren. Dies sind keine reinen funktionalen Tests, da auch der Zustand der
ZODB überprüft und verändert wird.
Anschließend meldet sich der Test als Eigentümer des Portals im login_form
-
Formular an.
Alle Aktionen finden im browser
-Objekt statt. Dies simuliert einen
Webbrowser mit einer Schnittstelle zum Finden und Ausführen von Form controls
und Links. Mit den Variablen browser.url
und browser.contents
lassen
sich sowohl die URL als auch die Inhalte überprüfen. zope.testbrowser
bietet
eine umfangreiche Dokumentation in seiner README.txt
<http://svn.zope.org/Zope3/trunk/src/zope/testbrowser/README.txt?view=auto>-Datei. Die bedeutendsten Methoden des IBrowser interface sind:
open(url)
öffnet eine gegebene URL.
reload()
lädt die aktuelle Seite erneut.
goBack(count=1)
simuliert, wie häufig der zurück-Button gedrückt wird.
getLink(text=None, url=None, id=None)
sucht nach einen Link entweder mit dem in <a>
-Tags angegebenen Inhalt,
der URL im href
-Attribut oder der ID des Links.
Mit click()
kann dieser Link ausgeführt werden.
getControl(label=None, name=None, index=None)
gibt den inhalt eines Formularfeldes aus, das entweder durch sein Label oder
seinen Namen gefunden wird. Das index
-Argument wird verwendet, wenn
mehrere form controls vorliegen (index=0
ist z.B. die erste form
control).
Und auch hier kann mit click()
das Klicken auf das Kontroll-Objekt
ausgeführt werden.
Das IBrowser-Interface bietet einige Eigenschaften (Properties), die zum Abfragen des Zustands der aktuellen Seite verwendet werden können:
url
die vollständige URL der aktuellen Seite.
contents
Den vollständigen Inhalt der aktuellen Seite als string (normalerweise mit HTML-Tags).
headers
die HTTP headers.
Detailliertere Angaben zu weiteren Methoden, Attributen, Schnittstellen und Beispielen für die verschiedenen Arten von Links und Controls erhalten Sie in Interfaces und der README.txt-Datei.
Da ein Test mit dem testbrowser
ein normaler DocTest ist, kann er mit
üblichem testrunner und Testsuite-Setup laufen.
Bemerkung
Die Site mit dem zope.testbrowser
aufzusetzen wäre unnötig kompliziert,
zumal damit nur Plone getestet würde und nicht vs.registration
.
Wenn der testbrowser
zu einer Zope-Fehlermeldung führt, kann es schwierig
werden, die Ursache zu ermitteln. Zwei Änderungen vereinfachen dies deutlich.
Zunächst sollten Sie sicherstellen, dass die Fehler auch angezeigt werden:
>>> browser.handleErrors = False
Damit werdenIhnen die vollständigen Zope-Exceptions angezeigt.
Und wenn PloneTestCase verwendet wird, kann auch Plone’s Error log verwendet werden:
>>> self.portal.error_log._ignored_exceptions = ()
Damit werden Fehler wie NotFound und Unauthorized im Error log angezeigt werden. Zudem kann es sinnvoll sein, Verbose Security zu aktivieren.
Anschließend kann statt einer Zeile, die zu einem Fehler führte, z.B. folgender Kode eingegeben werden:
>>> try:
... browser.getControl('Save').click()
... except:
... print self.portal.error_log.getLogEntries()[0]['tb_text']
... import pdb; pdb.set_trace()
>>> # continue as normal
Damit wird der letzte Eintrag in das Error log ausgegeben und ein PDB break point gestzt.
Ein Systemtest überprüft ein System als sog. Blackbox. Ein funktionaler Test konzentriert sich auf die geforderten Funktionsabläufe, die meist in Nutzungsfällen (Use Cases) beschrieben sind.
Für einen funktionalen Test mag es akzeptabel sein, Annahmen auf einem
festgelegten Status einer Site, der Testsuite, zu machen. Der Systemtest macht
hingegen keine solchen Annahmen. Daher benötigt ein zope.testbrowser
-Test
idealerweise nicht das PloneTestCase-Test fixture:
import unittest
from zope.testing import doctest
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('TestSystem.txt'),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Abgesehen davon bleiben die verwendeten Methoden für einen Systemtest dieselben.
Der Zope-Testrecorder zeichnet funktionale Tests im Browser auf und speichert sie als ausführbare Tests. Funktionale Tests, die den Zope-Testbrowser nutzen, ersparen uns das erneute Herumklicken im Browser zum Testen des User Interfaces. Der Zope-Testrecorder kann auch Selenium-Tests, eine andere Form von funktionalen Tests, die in Ihrem Webbrowser direkt vor Ihren Augen ablaufen und daher auch JavaScript unterstützen.
Hierzu müssen Sie zope.testrecorder
nur in der buildout.cfg
-Datei eintragen:
[buildout]
...
eggs =
...
zope.testrecorder
...
[instance]
...
zcml =
...
zope.testrecorder
Anschließend wird das Buildout-Skript aufgerufen und die Instanz neu gesteartet.
Nun sollte der zope.testrecorder
verfügbar sein unter folgender URL:
http://localhost:8080/++resource++recorder/index.html
Sie sollten nun folgende Seite sehen können:
Geben Sie nun die Adresse Ihrer Plone-Site ein, z.B. http://localhost:8080/mysite
, und klicken auf Go.
Wollen Sie freien Text in Ihr DocTest-Dokument schreiben, klicken Sie auf die Add comment-Taste.
Wenn Sie überprüfen wollen, ob bestimmter Text auf Ihrer Seite erscheint, markieren Sie diesen Text, klicken dann bei gedrückter Umschalt-Taste auf diesen Text und wählen Check text appears on page.
Schließlich können Sie die Aufzeichnung beenden und sie als Python DocTest abspeichern. Dann erhalten Sie ungefähr dies:
Create the browser object we'll be using.
>>> from zope.testbrowser import Browser
>>> browser = Browser()
>>> browser.open('http://localhost:8080/mysite')
A test comment.
>>> 'start writing' in browser.contents
True
Anschließend können Sie diesen Text in eine DocTest-Datei schreiben und gegebenenfalls Änderungen vornehmen.
Lesen Sie INSTALL.txt
für weitere Anweisungen.
Sie sollten ein Skript erstellen bevor Sie mit dem Aufzeichnen von Tests beginnen. Kommentieren Sie Ihre Tests um festzuhalten, was getestet werden soll.
Überlegen Sie sich, welche Aktionen die allgemeingültigsten und zuverlässigsten Tests bieten.
Wie beim Zope-Testbrowser, sollte auch beim Zope-Testrecorder zunächst eine Site aufgesetzt werden, bevor Sie mit dem Aufzeichnen der Tests beginnen.
Bevor Sie das Schreiben von DocTests mit dem Zope-Testrecorder beenden, sollten Sie den Kode noch von Textstellen befreien, deren Konsistenz nicht gewährleistet werden kann, wie z.B.
aus dem Datum generierte IDs von Inhaltstypen
und identische oder nahezu identische Textstellen, die in Ihrem DocTest erscheinen.
Jasmine ist ein Behavior-Driven-Development-Framework zum Testen von JavaScript-Code. Dabei benötigt es kein DOM und mit der enfachen Syntax lassen sich leicht Tests erstellen.
Eine Test-Suite beginnt mit dem Aufruf der globalen Jasmine-Funktion und zwei Paremetern:
Der Name oder Titel einer spec
-Suite
Die Funktion, die die Suite implementiert
Spezifikationen (specs) sind definiert durch die globale Jasmine-Funktion ˚ìt` mit den Parametern Titel und Funktion.
Dabei kann die Funktion sowohl eine spec sein als auch ein Test. Eine spec enthält eine oder mehrere Erwartungen (expectations), deren Überprüfung entweder wahr oder falsch sein können.
Bewahrheiten sich alle Erwartungen einer spec so wird diese bestanden.
Da describe
und it
Funktionen sind, können
sie jeden ausführbaren Code enthalten, der zur
Implementierung eines Tests erforderlich ist. Dabei
lassen sich in describe
definierte Variablen in
jedem ìt`-Block innerhalb der Suite verwenden.
Jasmine unterstützt auch das Testen asynchroner Operationen. Entsprechende Specs können mit einer Reihe von Blöcken mit runs
-Aufrufen geschrieben werden, die üblicherweise asynchron abgearbeitet werden.
waitsFor
wird verwendet mit einer Latch-Funktion, einer Fehlermeldung und einem Timeout.
Die Latch-Funktion stellt Anfragen bis es
true
zurückerhält oder der Timeout überschritten
ist. Wenn der Timeout überschritten wird, ist die
Specs fehlgeschlagen und gibt die Fehlermeldung
aus.
describe
¶Mit der describe
-Funktion lassen sich einfach specs gruppieren wobei die Namen der specs sich idealerweise zu einem Satz aneinanderreihen lassen.
Jasmine stellt die globalen Funktionen beforeEach
und afterEach
bereit, die vor bzw. nach jeder
spec in describe
aufgerufen werden. So lässt
sich der Initialisierungscode in die beforeEach
-
Funktion verschieben und das Zurücksetzen der Variablen in die afterEach
-Funktion.
describe
-Blöcken¶describe
-Blöcke können verschachtelt werden sodass
eine Test-Suite als Funktionsbaum zusammengestellt
werden kann. Bevor eine Spec ausgeführt wird, läuft
Jasmine durch den Baum und führt alle beforeEach
-
Funktionen in der Reihenfolge aus. Umgekehrt werden, nachdem die Spec ausgeführt wurde, alle afterEach
-Funktionen durchlaufen.
Suites und Specs können deaktiviert werden mit den xdescribe
und xit
-Funktionen.
Matchers implementieren einen boolschen Vergleich zwischen aktuellen und erwarteten Werten und teilen anschließend Jasmine mit, ob die Erwartungen erfüllt wurden oder nicht.
toBe
entspricht ===
toEqual
für einfache Literale, Variablen und Objekte
toMatch
für reguläre Ausrücke
toBeDefined
vergleicht mit undefined
toBeUndefined
vergleicht mit undefined
toBeNull
vergleicht mit Null
toBeTruthy
vergleicht mit boolscher Wahrscheinlichkeit
toBeFalsy
vergleicht mit boolscher Wahrscheinlichkeit
toContain
vergleicht, ob ein Item in einem Array enthalten ist
toBeLessThan`
mathematischer Vergleich
toBeGreaterThan
mathematischer Vergleich
toBeCloseTo
Präzision des mathematischen Vergleichs
toThrow
überprüft, ob eine Funktion eine Fehlermeldung ausgibt.
Jeder Matcher kann auch eine negative Annahme
überprüfen indem bei èxpect
ein not
dem
matcher vorangestellt wird.
Zwar bringt Jasmine bereits eine ganze Reihe von Matchers mit, es gibt jedoch auch die Möglichkeit, eigene Matchers für spezifische Annahmen zu schreiben.
Jasmine’s Test-Doubles werden Spy genannt. Wesentlich sind momentan sog. Test stubs möglich, für die Jasmine spezielle Matchers bereitstellt:
toHaveBeenCalled
ist wahr wenn der Spy aufgerufen wird
toHaveBeenCalledWith
ist wahr wenn die Liste der Argumente übereinstimmt mit den aufgezeichneten Aufrufen des Spy.
andCallThrough
lässt den Spy alle Aufrufe nachverfolgen und zusätzlich an die aktuelle Implementierung übertragen
andReturn
gibt einen spezifischen Wert beim Aufruf der Funktion aus
andCallFake
gibt die Aufrufe an die angebotene Funktion weiter
createSpy
jasmine.createSpy
kann einen minimalen Spy
erzeugen, der Aufrufe und Argumente nachverfolgt etc.
createSpyObj
jasmine.createSpyObj
erzeugt ein Mock-Objekt mit
mehreren Spies
jasmine.any
¶jasmine.any
ist wahr, wenn der Name des Konstruktors oder der Klasse dem erwarteten Wert entspricht.
Zum Testen von Timeouts und Intervallen kann
setTimeout
- und setInterval
verwendet werden.
Jasmine ist in JavaScript geschrieben und muss daher in eine JS-Umgebung eingebunden werden.
Hierfür wird eine HTML-Seite geschrieben, die die
Javascript-Dateien mit dem <script>
-Tag
einbindet sodass alle Specs mit Jasmine
durchlaufen und aufgezeichnet werden. Daher ist
diese HTML-Seite der Test-Runner. Sehen Sie
hierzu SpecRunner.html.
Dabei werden folgende Schritte durchlaufen:
Zunächst wird ein HTMLReporter
erstellt um
die Ergebnisse jeder Spec und jeder Test-Suite
aufzuzeichnen. Der Reporter ist auch für die
spätere Darstellung der Ergebnisse zuständig.
Auswählen einzelner Test-Suites oder Specs, die Durchlaufen werden sollen.
Durchlaufen aller ausgewählten Tests.
Diese Seite sollte im tests
-Modul unseres Pakets unter dem Namen testRunner.html
abgespeichert werden.
Anschließend passen wir die Verweise auf die
Quelldateien an, da die von uns zu testenden
Javascript-Dateien nicht im tests
-Modul selbst
sondern im browser
- oder skins
-Modul liegen
werden.
Nun kopieren wir noch die folgenden Dateien in
tests/jasmine
und passen die Pfade in
testRunner.html
entsprechend an:
Schließlich können wir noch unsere Specs
schreiben wobei sich bewährt hat, die Javascript-
Dateinamen im tests
-Modul beizubehalten.
gocept.jslint integriert jsHint in das Python-Unittest-Modul.
Es bietet eine JSLint
-TestCase-Klasse, die konfigurierbar alle Javascript-Dateien einsammelt und dynamisch für jede dieser Dateien eine Testmethode generiert. Diese Methoden können einfach verwendet werden, z.B. mit:
class MyJSLintTest(gocept.jslint.TestCase):
include = ('my.package.browser:js',
'my.package.browser:js/lib')
options = (gocept.jslint.TestCase.options +
('browser', 'jquery',))
include
ist eine Liste von Pfasen zu Ressourcen, ausgehend
von packagename:path
.
options
ist eine Liste von Optinen für jsHint, s.a. Enforcing Options.
predefined
Liste globaler Variablen, die mit der
undef
-Option verwendet werden können.
exclude
Liste von Dateinamen, die nicht getestet werden sollen.
Das Robot Framework ist ein generisches Framework zur Durchführung von automatisierten Softwaretests, v.a. Akzeptanztests.
Robot verwendet für das Keyword- Driven Testing eine tabellenartige Struktur zur Verwaltung der Testdaten.
Selenium2Library ermöglicht, die Selenium 2 (WebDriver)-Bibliothek im Robot-Framework zu verwenden.
Weitere Informationen zur Selenium2Library finden Sie im Wiki.
Beim Erstellen eines neuen Pakets sollte darauf geachtet werden, dass bei der
Frage nach robot tests
mit true
geantwortet wird:
$ ../bin/zopeskel plone_basic vs.registration
...
Expert Mode? (What question mode would you like? (easy/expert/all)?) ['easy']: all
...
robot tests (should the default robot test be included) [false]: true
Bei einem bestehenden Paket sollte folgendes in die setup.py
-Datei
eingetragen werden:
extras_require={
'test': ['plone.app.testing[robot]>=4.2.2']
},
Damit wird neben plone.app.testing noch die robotsuite und die robotframework- selenium2library zum Testen installiert.
Zudem ist in vs_buildout/src/vs.registration/src/vs/registration/tests/
eine test_robot.py
-Datei angelegt worden mit:
from vs.registration.testing import VS_REGISTRATION_FUNCTIONAL_TESTING
from plone.testing import layered
import robotsuite
import unittest
def test_suite():
suite = unittest.TestSuite()
suite.addTests([
layered(robotsuite.RobotTestSuite("robot_test.txt"),
layer=VS_REGISTRATION_FUNCTIONAL_TESTING)
])
return suite
Die zugehörige robot_test.txt
-Datei sieht dann so aus:
*** Settings ***
Library Selenium2Library timeout=10 implicit_wait=0.5
Suite Setup Start browser
Suite Teardown Close All Browsers
*** Variables ***
${BROWSER} = firefox
*** Test Cases ***
Plone site
[Tags] start
Go to http://localhost:55001/plone/
Page should contain Plone site
*** Keywords ***
Start browser
Open browser http://localhost:55001/plone/ browser=${BROWSER}
Schließlich kann der Test aufgerufen werden mit:
$ cd vs_buildout/src/vs.registration
$ python bootstrap.py
$ ./bin/buildout
$ ./bin/test
Running vs.registration.testing.VsregistrationLayer:Functional tests:
Set up plone.testing.zca.LayerCleanup in 0.000 seconds.
Set up plone.testing.z2.Startup in 0.394 seconds.
Set up plone.app.testing.layers.PloneFixture in 10.463 seconds.
Set up vs.registration.testing.VsregistrationLayer in 0.464 seconds.
Set up plone.testing.z2.ZServer in 0.503 seconds.
Set up vs.registration.testing.VsregistrationLayer:Functional in 0.000 seconds.
Running:
Ran 1 tests with 0 failures and 0 errors in 3.026 seconds.
Running vs.registration.testing.VsregistrationLayer:Integration tests:
Tear down vs.registration.testing.VsregistrationLayer:Functional in 0.000 seconds.
Tear down plone.testing.z2.ZServer in 5.152 seconds.
Set up vs.registration.testing.VsregistrationLayer:Integration in 0.000 seconds.
Running:
Ran 1 tests with 0 failures and 0 errors in 0.004 seconds.
Tearing down left over layers:
Tear down vs.registration.testing.VsregistrationLayer:Integration in 0.000 seconds.
Tear down vs.registration.testing.VsregistrationLayer in 0.002 seconds.
Tear down plone.app.testing.layers.PloneFixture in 0.092 seconds.
Tear down plone.testing.z2.Startup in 0.007 seconds.
Tear down plone.testing.zca.LayerCleanup in 0.004 seconds.
Total: 2 tests, 0 failures, 0 errors in 20.510 seconds.
Daneben werden noch Log-Dateien erstellt in parts/test/
, z.B. robot_log.html
:
Siehe auch
Asko Soukka: Getting started with Robot Framework and plone.app.testing
Beispiele:
plone.act Robot-Framework-Ressourcen zum Testen von Plone
Sphinx- und ReadTheDocs-Unterstützung für das Robot-Framework.
sphinxcontrib-robotdoc ist eine Erweiterung von Sphinx, zur Übernahme der Robot-Framework-Tests.
Dabei werden zwei neue Docutils-
Direktiven eingeführt: robot_tests
und robot_keywords
. Beide
Direktiven erlauben die folgenden Angaben:
Filter in Form regulärer Ausdrücke
Pfadangabe zu den Robot-Framework-Testdaten und -Ressourcen.
Auswahl der Tests in Form einer Komma-separierten Tag-Liste
Die folgenden Beispiele können z.B. eingebunden werden in vs_buildout/src/vs.registration/docs/index.rst
.
Einbinden aller Tests einer Testsuite:
.. robot_tests::
:source: ../src/vs/registration/tests/my_suite.txt
Alles Tests einer Testsuite, die mit Log
beginnen:
.. robot_tests:: Log.*
:source: ../src/vs/registration/tests/my_suite.txt
Einbinden aller Tests, die mit login
oder logout
getaggt sind:
.. robot_tests::
:source: ../src/vs/registration/tests/my_suite.txt
:tags: login, logout
Einbinden aller user keywords eines Tests oder eine Ressource:
.. robot_keywords::
:source: ../src/my_package/tests/acceptance/my_suite.txt
Einbinden aller user keywords, die mit Log
beginnen:
.. robot_keywords:: Log.*
:source: ../src/my_package/tests/acceptance/my_suite.txt
ReadTheDocs unterstützt eigene Sphinx-Plugins:
Zunächst wird das Plugin in der Sphinx-Konfigurationsdatei (conf.py
)
in die Liste der èxtensions` eingetragen:
extensions = ['sphinxcontrib_robotdoc']
Das Plugin sollte auf PyPI veröffentlicht worden sein, siehe sphinxcontrib-robotdoc.
Desweiteren soll das ReadTheDocs-Project mit virtualenv
erstellt
werden:
Use virtualenv
[x] Install your project inside a virtualenv using setup.py install
Es muss eine pip requirements-Datei geben, das das Sphinx- Plugin (und ggf. die mindestens erforderliche Version) enthält:
sphinxcontrib-robotdoc>=0.3.4
Ggf. kann die requirements
-Datei nur für ReadTheDocs bereitgestellt
werden indem sie in einem Unterverzeichnis erstellt wird, z.B. in
./docs/requirements.txt
.
Schließlich geben Sie im ReadTheDocs-Dashboard den Pfad zu Ihrer
requirements
-Datei an:
Requirements file:
docs/requirements.txt
WAVE ist ein Werkzeug zum Analysieren der Barrierefreiheit:
Die WAVE Toolbar ist eine Offline-Version dieses Dienstes als Firefox-Plugin:
robotframework-wavelibrary ist eine Bibliothek für das Robot-Framework, die die WAVE-Analyse Robot-Framework-Test durchführt.
In die Buildout-Konfiguration wird der Abschnitt pybot
mit dem Python-Egg
robotframework-wavelibrary
hinzugefügt:
[buildout]
parts =
...
pybot
[pybot]
recipe = zc.recipe.egg
eggs =
robotframework
robotframework-wavelibrary
Anschließend wird die Bibliothek installiert mit:
$ python bootstrap.py
$ ./bin/buildout
Hierfür können wir z.B. eine Datei plone.robot
mit folgendem Inhalt
schreiben:
*** Settings ***
Library WAVELibrary
Resource plone/app/robotframework/server.robot
Suite Setup Setup
Suite Teardown Teardown
*** Variables ***
${START_URL} about:
*** Keywords ***
Setup
Setup Plone site plone.app.robotframework.testing.AUTOLOGIN_ROBOT_TESTING
Import library Remote ${PLONE_URL}/RobotRemote
Enable autologin as Site Administrator
Set autologin username test-user-1
Teardown
Teardown Plone Site
*** Test Cases ***
Test new page form tabs
[Template] Check new page tabs for accessibility errors
default
categorization
dates
creators
settings
*** Keywords ***
Check new page tabs for accessibility errors
[Arguments] ${fieldset}
Go to ${PLONE_URL}/createObject?type_name=Document
${location} = Get location
Go to ${PLONE_URL}
Go to ${location}#fieldsetlegend-${fieldset}
Check accessibility errors
Eine vollständige Liste der WAVE-Library-Keywords erhalten Sie unter Keywords. Daneben können auch weiterhin die robot-Keywords und die Selenium-Leywords verwendet werden.
Dieser Test kann nun aufgerufen werden mit:
$ ./bin/pybot plone.robot
Mit einem Continuous Integration Server können periodisch Unit- und Integrationstests durchlaufen werden.
Der Jenkins Continuous Integration Server kann Sie zudem sofort informieren, wenn ein Test fehlschlägt. Dies wird vor allem dann bedeutsam, wenn Sie mit anderen an derselben Code-Basis arbeiten. Dabei informiert Sie Jenkins, welches Checkin fehlschlug mit Revisionsnummer, Cheickin-Nachricht und Autor.
Mit collective.xmltestreport kommt ein Test-Runner, dessen XML-Ausgabe von Jenkins zur Erstellung von Grafiken und Trends verwendet werden kann.
Bemerkung
Eine Anleitung zum Installieren von Jenkins finden Sie in Installing Jenkins. Falls jedoch keine dieser Möglichkeiten zutreffend sein sollte, können Sie den Jenkins Continous Integration Server auch einfach mit Buildout und dem Rezept jarn.jenkins installieren.
[buildout]
[buildout]
parts =
jetty-download
jenkins-download
jenkins
[jetty-download]
recipe = hexagonit.recipe.download
url = http://ftp.halifax.rwth-aachen.de/eclipse//jetty/stable-9/dist/jetty-distribution-9.2.2.v20140723.tar.gz
strip-top-level-dir = true
[jenkins-download]
recipe = hexagonit.recipe.download
url = http://mirrors.jenkins-ci.org/war/latest/jenkins.war
download-only = true
[jenkins]
recipe = jarn.jenkins
jetty-location = ${jetty-download:location}
jenkins-location = ${jenkins-download:location}
host = localhost
port = 8070
Hiermit wird sowohl jetty als auch Jenkins heruntergeladen und ein ausführbare Jetty-Umgebung in parts/jenkins
erstellt. Außerdem wird mit bin/jenkins
ein Skript erstellt, mit dem sich der Jenkins-Server starten und Stoppen lässt.
Um die Installation zu testen können Sie einfach folgendes angeben:
$ ./bin/jenkins fg
Damit wird der Jetty-Server am Port 8070
gestartet. Die Jenkins-Instanz ist dann erreichbar unter http://127.0.0.1:8070/jenkins/
.
Diese wird von Jenkins schreibt nach var/jenkins/log
geschrieben.
Die Konfiguration einschließlich der auszuführenden Jobs wird von Jenkins nach var/jenkins/data
geschrieben. Dabei entspricht der Verzeichnisname in var/
dem Namen des Abschnitts, das das Rezept jarn.jenkins
verwendet.
Verwenden Sie in Jenkins die Free Style-Vorlage um einen neuen Job zu erstellen.
Alte Builds verwerfen
Wie lange sollen Builds aufbewahrt werden? Hiermit lässt sich der Festplattenverbrauch von Jenkins steuern. Jenkins bietet hierzu zwei Strategien an:
Jenkins löscht Aufzeichnungen, sobald sie ein bestimmtes Alter erreichen, z.B. 7 Tage alt sind.
Jenkins bewahrt nur die N
neuesten Builds auf. Wenn ein neuer Build gestartet wird, löscht Jenkins den ältesten.
Jenkins erlaubt darüberhinaus, dass einzelne Builds markiert werden mit*Dieses Protokoll für immer aufbewahren*, sodass wichtige Builds von der automatischen Löschung ausgeschlossen werden.
Erweiterte Projekteinstellungen
Hier können Sie die Anzahl der Wiederholungen bei fehlgeschlagenen Checkouts angeben.
Source-Code-Management (SCM)
Hier können Sie z.B. die URL Ihres Subversion-Repository, die Check-Out-Strategie und den Repository-Browser angeben.
Build-Auslöser
Hier können Sie die Zeitpläne angeben, zu denen die Builds gestartet werden sollen.
Buildverfahren
Als Buildverfahren wählen Sie ein Shell-Skript, das z.B. folgenden Inhalte haben kann:
cd /home/veit/my_buildout
./bin/develop up
./bin/buildout
./bin/test --xml -s vs.registration
./bin/develop up
aktualisiert die Quellen von mr.developer
.
./bin/test --xml -s vs.registration
Das Buildout hat einen [test]
-Abschnitt, der folgendermaßen aussieht:
[test]
recipe = collective.xmltestreport
eggs =
vs.registration
extra-paths = ${zope2:location}/lib/python
defaults = ['--exit-with-status', '--auto-color', '--auto-progress']
Das Rezept collective.xmltestreport
ist eine spezielle Version von zc.recipe.testrunner
um Testreports im XML-Format zu schreiben wie es von JUnit/Ant verwendet wird. Dies erlaubt es Jenkins, die Testergebnisse zu analysieren.
Post-Build-Aktionen
Es sind reguläre Ausdrücke wie z.B. parts/test-jenkins/testreports/*.xml
erlaubt. Das Ausgangsverzeichnis ist der Arbeitsbereich.
Mit dem Plot plugin können Sie sich Trends grafisch darstellen lassen.
Hier können Sie eine Liste der Empfänger angeben, die bei jedem fehlgeschlagenen Build informiert werden sollen.
Darüberhinaus können auch diejenigen informiert werden, die einen Build fehlschlagen ließen.
Auf virtualenv basierende Automatisierung von Tests.
tox kann verwendet werden
zur Überprüfung, ob Ihr Paket mit verschiedenen Python-Versionen und Interpretern installiert werden kann
zum Ausführen der Tests in verschiedenen Environments und mit verschiedenen Testwerkzeugen
als Frontend für Continous Integration Server, das unnötige Wiederholungen vermeidet sowie CI- und Shell-basierte Tests verbindet.
detox erlaubt das verteilte Aufrufen von
tox
, sodass Tests parallel ausgeführt werden können. Die Optionen und die
Konfigurationsmöglichkeiten entsprechen denen von tox
.
Weitere Informationen erhalten Sie in der Dokumentation zum tox automation project. Dort ist auch die Verwendung zusammen mit dem Jenkins Integration Server beschrieben: Using Tox with the Jenkins Integration Server.
Travice CI ist ein hosted Continuous-Integration-Service.
Travis CI ist ein gehosteter Continous Integration Service, mit dem sich Plone-Zusatzprodukte einfach testen lassen.
Travis lässt sich mit Ruby Gems installieren:
$ gem install travis
Damit unser Paket getestet werden kann, muss zunächst die setup.py
-
Datei dieses Produkts erweitert werden:
...
install_requires=[
'setuptools',
'Products.CMFPlone>=4.2',
],
extras_require={
'test': ['plone.app.testing'],
},
...
Aufsetzen von Travis
Zunächst melden Sie sich einfach mit Ihrem Github-Account an: App Authorization.
Anschließend konfigurieren Sie Ihren Travis CI-Server mit einer travis.yml
-Datei im Wurzelverzeichnis
Ihres Repository:
---
language: python
python: '2.7'
install:
- mkdir -p buildout-cache/eggs
- mkdir -p buildout-cache/downloads
- python bootstrap.py -c travis.cfg
- ./bin/buildout -N -t 3 -c travis.cfg
script: ./bin/test
Weitere Konfigurationsmöglichkeiten erhalten Sie in Travis CI-Konfiguration.
Die Datei travis.cfg
sieht dann z.B. so aus:
[buildout]
extends =
http://svn.plone.org/svn/collective/buildout/plonetest/test-4.x.cfg
parts = test
package-name = vs.registration
package-extras = [test]
#test-eggs = Pillow
allow-hosts +=
code.google.com
robotframework.googlecode.com
[environment]
ZSERVER_HOST = 0.0.0.0
ROBOT_ZOPE_HOST = 0.0.0.0
[test]
environment = environment
[test]
eggs =
${buildout:package-name} ${buildout:package-extras}
${buildout:test-eggs}
test-eggs = Pillow
Diese Zeile sollte auskommentiert werden sofern PIL für die Tests benötigt wird.
Schließlich können Sie noch ein Status-Bild in Ihre README.txt
-Datei
einfügen:
.. image:: https://secure.travis-ci.org/collective/vs.registration.png
:target: http://travis-ci.org/collective/vs.registration
Travice CI bietet umfangreiche Konfigurationsmöglichkeiten.
Die Konfiguration des Travis CI-Server erfolgt in der travis.yml
-Datei im
Wurzelverzeichnis Ihres Repository.
Hier stehen Ihnen u.a. zusätzliche Konfigurationsmöglichkeiten zur Verfügung, u.a.:
language
gibt nicht nur die Sprache an, sondern ggf. auch die Sprachversionen, mit denen getestet werden soll:
language: python
python:
- 2.6
- 2.7
env
Umgebungen, in der die Tests ausgeführt werden sollen, also z.B.:
env:
- PLONE_VERSION=4.2
- PLONE_VERSION=4.3
Ein vollständiges Beispiel hierfür finden Sie in https://github.com/plone/plone.api/blob/master/.travis.yml.
services
Dienste, die vor dem Testen bereitstehen sollen, z.B.:
- riak # will start riak
- rabbitmq # will start rabbitmq-server
- memcache # will start memcached
Eine vollständige Liste der zur Verfügung stehenden Services finden Sie in Configure Your Projects to Use Services in Tests.
before_install
Aufruf, der vor der Installation ausgeführt werden soll, z.B. um ein GUI headless testen zu können:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
Weitere Informationen hierzu erhalten Sie in GUI & Headless browser testing on travis-ci.org.
install
Installation der Testumgebung, z.B.:
- unzip Sauce-Connect-latest.zip
- java -jar Sauce-Connect.jar $SAUCE_USERNAME $SAUCE_ACCESS_KEY -i $TRAVIS_JOB_ID -f CONNECTED &
- JAVA_PID=$!
before_script
Aufruf, der unmittelbar vor einem Test aufgerufen werden soll, z.B.:
bash -c "while [ ! -f CONNECTED ]; do sleep 2; done"
test
Testaufruf, z.B.:
./bin/test
after_script
Aufruf, der unmittelbar nach dem Test durchgeführt wird, z.B.:
kill $JAVA_PID
branches
Blacklist und Whitelist von zu testenden Branches, z.B.:
# blacklist
branches:
except:
- legacy
- experimental
# whitelist
branches:
only:
- master
- stable
notifications
Benachrichtigungen, , z.B.:
notifications:
irc:
email:
- "kontakt@veit-schiele.de"
channels:
- "irc.freenode.org#sprint"
on_success: never
on_failure: always
template:
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
- "Change view : %{compare_url}"
- "Build details : %{build_url}"
Falls nach einem push
kein neuer Travis-Build erstellt werden soll, z.B. bei Änderungen in der Dokumentation, so kann dies einfach beim Commit angegeben werden:
$ git commit -m 'Typo in README.rst [ci skip]'
Die Nutzung von Sauce Labs zusammen mit dem Robot-
Framework ist ähnlich einem eigenen Selenium-Grid. Vor allem erfordert es, dass
die Browser Passwörter eingeben können. Dies kann mit einigen wenigen Variablen
in vs_buildout/src/vs.registration/src/vs/registration/tests/robot_test.txt
konfiguriert werden:
*** Settings ***
Library Selenium2Library timeout=10 implicit_wait=0.5
Suite Setup Start browser
Suite Teardown Close All Browsers
*** Variables ***
${ZOPE_HOST} = localhost
${ZOPE_PORT} = 55001
${ZOPE_URL} = http://${ZOPE_HOST}:${ZOPE_PORT}
${PLONE_SITE_ID} = plone
${PLONE_URL} = ${ZOPE_URL}/${PLONE_SITE_ID}
${BROWSER} = Firefox
${REMOTE_URL} =
${DESIRED_CAPABILITIES} = platform:Linux
${BUILD_NUMBER} = manual
*** Test Cases ***
Plone site
[Tags] start
Go to ${PLONE_URL}
Page should contain Plone site
*** Keywords ***
Start browser
${BUILD_INFO} = Set variable
... build:${BUILD_NUMBER},name:${SUITE_NAME} | ${TEST_NAME}
Open browser ${PLONE_URL} ${BROWSER}
... remote_url=${REMOTE_URL}
... desired_capabilities=${DESIRED_CAPABILITIES},${BUILD_INFO}
Die Variablen bedeuten im Einzelnen:
ZOPE_HOST
Angabe for den Host des ZServer.
Dar Standardwert ist localhost
. For Tests mit dem Internet Explorer ist
jedoch die Angabe 0.0.0.0
erforderlich.
ZOPE_PORT
Angabe des Ports, an dem der ZServer lauscht.
Der Standardwert ist 55001
.
ZOPE_URL
Root-Variable für die URL der Zope-Anwendung.
PLONE_SITE_ID
ID der Plone-Site.
PLONE_URL
URL der Plone-Site.
BROWSER
Browser, mt dem der Test durchgeführt werden soll.
REMOTE_URL
URL des zu verwendenden Selenium-Hubs.
DESIRED_CAPABILITIES
spezifiziert verschiedene Parameter des Selenium-Hubs, z.B. die Browser- Version.
BUILD_NUMBER
Travis-CI-Build auf Sauce Labs.
Nun wird eine .travis.yml
-Datei erstellt um Travis-CI mitzuteilen, welches
Envirinment verwendet und welche Tests ausgeführt werden sollen:
---
language: python
python: '2.7'
install:
- mkdir -p buildout-cache/eggs
- mkdir -p buildout-cache/downloads
- python bootstrap.py -c travis.cfg
- ./bin/buildout -N -t 3 -c travis.cfg
- curl -O http://saucelabs.com/downloads/Sauce-Connect-latest.zip
- unzip Sauce-Connect-latest.zip
- java -jar Sauce-Connect.jar $SAUCE_USERNAME $SAUCE_ACCESS_KEY -i $TRAVIS_JOB_ID -f CONNECTED &
- JAVA_PID=$!
before_script:
- bash -c "while [ ! -f CONNECTED ]; do sleep 2; done"
script: ./bin/test
after_script:
- kill $JAVA_PID
env:
global:
- ROBOT_BUILD_NUMBER=travis-$TRAVIS_BUILD_NUMBER
- ROBOT_REMOTE_URL=http://$SAUCE_USERNAME:$SAUCE_ACCESS_KEY@ondemand.saucelabs.com:80/wd/hub
matrix:
- ROBOT_BROWSER=firefox ROBOT_DESIRED_CAPABILITIES=tunnel-identifier:$TRAVIS_JOB_ID
- ROBOT_BROWSER=chrome ROBOT_DESIRED_CAPABILITIES=tunnel-identifier:$TRAVIS_JOB_ID
- ROBOT_BROWSER=internetexplorer ROBOT_DESIRED_CAPABILITIES=tunnel-identifier:$TRAVIS_JOB_ID
SAUCE_USERNAME
und SAUCE_ACCESS_KEY
Nutzername und Passwort verschlüsselt als Umgebungsvariable.
travis encrypt
schreibt die verschlüsselten Werte direkt in die .travis.yml
-Datei:
$ travis encrypt SAUCE_USERNAME=myusername -r mygithubname/example.product --add env.global
$ travis encrypt SAUCE_ACCESS_KEY=myaccesskey -r mygithubname/example.product --add env.global
matrix
Aktuell erlaubt Sauce Labs drei gleichzeitige Verbindungen für Open-Source- Projekte, z.B. für drei verschiedene Browser.
Achten Sie bei Open-Source-Projekten darauf, dass Sie nicht Ihren privaten Zugang nutzen sondern denjenigen des Projekts. Hierfür ist die öffentliche URL des Repository erforderlich.
Schließlich sollten die Travis-CI-Tests für Ihr Produkt auf Travis-CI.org oder GitHub eingerichtet werden.
Das Generic Setup Tool vereinfacht die Vorkonfigurierung einer Site.
Jede konfigurierbare Komponente stellt Handler zum Im- und Export von Profilen bereit.
Profile sind Konfigurationsdateien für bestimmte Komponenten einer Website. Diese können z.B. Rollen, Berechtigungen, Skin-Layer und vieles mehr festlegen. Dabei werden im wesentlichen zwei Arten von Profilen unterschieden:
Profil für die Basiskonfiguration einer Site. Products.CMFPlone
bringt ein solches Profil mit, das die Standardkonfiguration einer Plone-Site enthält.
Profil, das auf einem base profile aufbaut und an einigen Stellen die Standardkonfiguration ändert und neue Im- und Export-Schritte bereitstellen kann.
GenericSetup-Profile können einfach mit ZCML registriert werden, z.B. in der configure.zcml
-Datei von vs.theme
:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="vs.theme">
<genericsetup:registerProfile
name="default"
title="Theme for the Websites of Veit Schiele Communications"
directory="profiles/default"
description="Default profile for vs.theme"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
</configure>
name
Bestandteil der ID des GenericSetup-Profils. Die vollständige ID lautet profile-<package_name>:<profile_name>
, in unserem Fall also profile-vs.theme:default
.
title
Der Titel des Profils, der Ihnen im Generic Setup Tool beim Import angezeigt wird und im Quickinstaller beim Aktivieren eines Pakets.
directory
Relative Pfadangabe zum Verzeichnis mit den Profilinformationen. Meist entspricht der Verzeichnisname dem Profilnamen.
description
Die Beschreibung des Profils sollte eine kurze Zusammenfassung für die Verwendung des Profils geben.
provides
Die Art des Profils, also EXTENSION
oder BASE
.
Häufig ist die einfachste Möglichkeit ein Profil zu schreiben diejenige, in einer Site Änderungen an der Konfiguration vorzunehmen und anschließend die Profile derjenigen Tools zu exportieren, deren Konfiguration geändert wurde.
Anschließend sollte dieses Profil jedoch nicht unmittelbar übernommen werden sondern nur diejenigen Teile, die auch tatsächlich geändert wurden.
Üblicherweise ersetzen die Werte von ÈXTENSION
-Profilen die bereits bestehenden Werte. Mit purge="False"
kann dieses Verhalten jedoch geändert werden, z.B.:
<?xml version="1.0"?>
<object name="portal_properties">
<object name="navtree_properties">
<property name="metaTypesNotToList" type="lines" purge="False">
<element value="Registration"/>
</property>
</object>
</object>
Hierdurch wird
Registration
der Liste der Artikeltypen hinzugefügt. Ohnepurge="False"
würde nur derRegistration
-Artikeltyp nicht in der Navigation angezeigt werden.
Das Profil in metadata.xml
kann z.B. so aussehen:
<?xml version="1.0"?>
<metadata>
<description>Policy for the Website of Veit Schiele Communications</description>
<version>1.0dev $LastChangedRevision$ </version>
<dependencies>
<dependency>profile-vs.theme:default</dependency>
<dependency>profile-Products.LinguaPlone:LinguaPlone</dependency>
</dependencies>
</metadata>
description
Kurze Erläuterung des Profils
version
Die Versionsnummer des Profils
Diese wird auch verwendet um Upgrade-Schritte durchzuführen. Upgrades können immer nur zwischen definierten Versionsnummern durchgeführt werden.
dependencies
Voraussetzungen für dieses Profil. Profile, die hier genannt werden, werden beim Import zuerst ausgeführt.
Siehe auch
Content rules lassen sich in neueren Plone-Versionen mit einem Generic Setup-Profil erstellen. Wie ein solches Profil aussehen kann, sehen Sie in der Datei contentrules.xml
.
In diesem Profil ist eine Regel definiert:
<rule
name="rule-1"
title="Mail notification"
description=""
enabled="True"
event="Products.CMFCore.interfaces.IActionSucceededEvent"
stop-after="False">
name
Name (ID) der Regel
title
Titel der Regel
description
Beschreibung der Regel
enabled
Ist die Regel aktiv?
event
Welches Ereignis löst die Regel aus.
stop-after
Sollen weitere Regeln nach dieser Regel ausgeführt werden?
Anschließend folgen drei weitere Abschnitte:
Eine Liste von Bedingungen (conditions
) für diese Regel
Eine Liste von Aktionen (actions
) für diese Regel
Die Zuweisung von Regeln einem bestimmten Kontext
conditions
)¶plone.app.contentrules
kommt mit den folgenden Bedingungen:
plone.conditions.PortalType
)Mit dieser Bedingung legen Sie fest, dass eine Aktion nur bei bestimmten Artikeltypen ausgeführt wird.
plone.conditions.FileExtension
)Mit dieser Bedingung können Sie festlegen, dass eine Aktion nur bei bestimmten Dateiendungen ausgeführt wird.
plone.conditions.WorkflowState
)Mit dieser Bedingung legen Sie fest, dass eine Aktion nur bei Artikeln angewendet wird, die sich in einem bestimmten Status befinden.
plone.conditions.WorkflowTransition
)Mit dieser Bedingung legen Sie fest, dass eine Aktion nur bei bestimmten Workflow-Übergängen (transitions
) angewendet wird.
plone.conditions.Group
)Mit dieser Bedingung legen Sie fest, dass eine Aktion nur ausgeführt wird, wenn der aktuelle Benutzer Mitglieder in einer bestimmten Gruppe ist.
plone.conditions.Role
)Mit dieser Bedingung legen Sie fest, dass eine Aktion nur ausgeführt wird, wenn der Benutzer eine bestimmte Rolle hat.
actions
)¶plone.actions.Logger
)protokolliert ein bestimmtes Ereignis
plone.actions.Notify
)gibt eine Nachricht im Browser des Nutzers aus.
plone.actions.Copy
)kopiert den Artikel in einen bestimmten Ordner.
plone.actions.Move
)verschiebt den Artikel in einen bestimmten Ordner.
plone.actions.Delete
)löscht den Artikel.
plone.actions.Workflow
)ändert den Status des Artikels.
plone.actions.Mail
)Versenden einer E-Mail unter Angabe von Betreff, Absender, Empfänger und Nachrichtentext.
Dabei können Sie für diese Felder folgende Variablen verwenden:
${absolute_url}
URL des Artikels
${user_email}
E-Mail-Adresse des Nutzers
${user_fullname}
Name des Nutzers
${user_id}
Id des Nutzers
${contributors}
Beteiligte
${created}
Erstellungsdatum
${creators}
Ersteller
${description}
Beschreibung
${effective}
Veröffentlichungsdatum
${expires}
Ablaufdatum
${format}
Format
${identifier}
Identifier (URI)
${keywords}
Betreff
${language}
Sprache
${modified}
Änderungsdatum
${rights}
Veröffentlichungsrechte
${subject}
Betreff
${title}
Titel
${type}
Artikeltyp
${manager_emails}
E-Mails an Verwalter
${member_emails}
E-Mail an Mitglieder
${owner_emails}
E-Mail an Eigentümer
${reviewer_emails}
E-Mail an Redakteure
${change_authorid}
Geänderter Name des Autors
${change_comment}
Kommentar
${change_title}
Geänderter Titel
${change_type}
Geänderter Artikeltyp
${review_state}
Geänderter Status
assignment
)¶<assignment
location="/news"
name="rule-1"
enabled="True"
bubbles="False"
insert-before="*"
/>
location
(erforderlich)Der Ort,an dem die Regel greifen soll. Üblicherweise sind dies in Plone Ordner, die mit dem IRuleAssignable
-Interface markiert werden.
Hier wird eine Pfadangabe relativ zu portal root erwartet.
name
(erforderlich)Der Name der Regel, die zugewiesen werden soll.
enabled
(optional)Soll die Regel an dem angegebenen Ort aktiv sein?
bubbles
(optional)Soll die Regel auch in Unterordnern zugewiesen werden?
Der Standardwert ist False
wodurch die Regel nicht auf passende Events in Unterordnern angewendet wird.
insert-before
(optional)Dies kann verwendet werden um die Reihenfolge, in der die Zuweisungen für einen bestimmten Ort ausgeführt werden sollen, zu beeinflussen.
Wird hier nichts angegeben, wird die Regel nach allen anderen Regeln an diesem Ort ausgeführt.
*
bewirkt, dass die Regel als erste ausgeführt wird.
Ab Plone 4.1 lässt sich das Repositorytool über ein Generic-Setup-Profil konfigurieren.
Die repositorytool.xml
-Datei kann z.B. so aussehen:
<?xml version="1.0"?>
<repositorytool>
<policymap>
<type name="MyType">
<policy name="at_edit_autoversion"/>
<policy name="version_on_revert"/>
</type>
<type name="AnotherType">
<policy name="at_edit_autoversion"/>
<policy name="version_on_revert"/>
</type>
</policymap>
</repositorytool>
Soll das Produkt auch in Plone < 4.1 funktionieren, sollte in der setuphandlers.py
-Datei eine Bedingung angegeben werden. Sehen Sie hierzu Generic-Setup-Profil für die Versionierung.
Mit der toolset.xml
-Datei lassen sich Portal Tools instantiieren oder entfernen, z.B.:
<?xml version="1.0"?>
<tool-setup>
<required tool_id="portal_foo" class="dotted.path.to.Foo" />
<forbidden tool_id="portal_bar" />
</tool-setup>
In diesem Beispiel wird portal_foo
instantiiert mit der Klasse Foo
. Zudem wird das portal_bar
-Tool entfernt sofern vorhanden.
Bemerkung
Die toolset.xml
-Datei kann nicht nur im Paket verwendet werden, das das jeweilige Tool bereitstellt sondern ist vor allem auch für Policy-Pakete gedacht.
Referenz zu den Dexterity Feldtypen und -eigenschaften, Widgets, Behaviors, Form-Directives sowie Eigenschaften und Methoden der Inhaltsobjekte.
Die gebräuchlichsten Feldtypen und -eigenschaften, die in Dexterity verwendet werden.
Eine Anleitung zum Erstellen eines Schemas erhalten Sie in Schema Interfaces.
Interface |
Eigenschaft |
Typ |
Beschreibung |
---|---|---|---|
|
|
unicode |
Der Titel des Feldes, der im Widget verwendet wird. |
|
unicode |
Die Beschreibung des Feldes, die im Widget verwendet wird. |
|
|
bool |
Es wird überprüft, ob das Feld eine Angabe enthält.
Der Standarwert ist |
|
|
bool |
Ist der Wert |
|
|
Der Standardwert eines Feldes. Dieser Wert kann ggf. auch als
Fallback verwendet werden, falls keine Angabe gemacht wurde.
Dieser Wert muss eine valide Angabe für dieses Feld sein.
Der Standardwert ist |
||
|
Ein wert, der verdeutlicht, dass dieses Feld nicht ausgefüllt
wurde. Dieser Wert wird bei der Validierung des Formulars
verwendet. Der Standarwert ist |
||
|
|
int |
Die minimale Länge der Eingabe.
Wird für |
|
int |
Die maximale Länge der Eingabe.
Wird für |
|
|
|
Der minimal erlaubte Wert. Dies muss ein valider Wert für
dieses Feld sein. Der Standardwert ist |
|
|
Der maximal erlaubte Wert. Dies muss ein valider Wert für
dieses Feld sein. Der Standardwert ist |
||
|
|
Erlaubte Werte einer Liste, eines Tuples oder einer anderen
Sammlung. Muss für jedes |
|
|
|
Ob die Werte in der Kollektion eindeutig sein müssen
oder nicht. Wird meist nicht direkt gesetzt, sondern es wird
ein |
|
|
|
Beschreibt die erlaubten Schlüssel in einem Dictionary.
Ähnlich dem |
|
|
Beschreibt die erlaubten Werte in einem Dictionary.
Ähnlich |
||
|
|
|
Ein Interface, das von jedem Objekt, das in diesem Feld gespeichert wird bereitgestellt werden muss. |
|
|
|
Standard-MIME-Typ für den Eingabetext eines Rich Text-Felds.
Der Standard ist |
|
|
Standard-MIME-Typ für den transformierten Text eines
Rich Text-Felds.
Der Standard ist |
|
|
|
Eine Liste aller erlaubten MIME-Typen für die Eingabe.
Der Standardwert ist |
Die folgende Tabelle listet die am häufigsten verwendeten Feltypen auf, sortiert nach dem Modul, von dem sie importiert werden können.
zope.schema
¶plone.namedfile.field
¶Weitere Informationen erhalten Sie unter plone.namedfile und plone.formwidget.namedfile.
Name |
Typ |
Beschreibung |
Typ |
---|---|---|---|
|
|
Eine hochzuladende Binärdatei. Üblicherweise wird das Widget
aus |
|
|
|
Ein hochzuladendeis Bild. Üblicherweise wird das Widget
aus |
|
|
|
Eine hochzuladende Binärdatei, die als ZODB-BLOB gespeichert
wird. Üblicherweise wird das Widget aus
|
|
|
|
Ein hochzuladendes Bild, das als ZODB-BLOB gespeichert
wird. Üblicherweise wird das Widget aus
|
|
z3c.relationfield.schema
¶Weitere Informationen erhalten Sie unter z3c.relationfield.
Name |
Typ |
Beschreibung |
Typ |
---|---|---|---|
|
|
Speichert den Wert einer einzelnen Relation. |
|
|
|
List-Feld für |
Siehe |
|
|
Choice-Feld für |
Siehe |
plone.app.textfield
¶Weitere Informationen erhalten Sie unter plone.app.textfield.
Name |
Typ |
Beschreibung |
Typ |
---|---|---|---|
|
|
Speichert einen |
|
Standard-Widgets und Third-Party-Widgets.
Die gebräuchlichsten Widgets werden von z3c.form
bereitgestellt. Weitere Informationen zu diesen Widgets erhalten Sie in der z3c.form-Dokumentation.
Die Tabelle führt die häufig verwendeten Widgets auf:
Widget |
Importiert von |
Feld |
Beschreibung |
---|---|---|---|
|
|
|
Hiermit erhalten Sie den Standard-WYSIWYG-Editor von Plone für dieses Feld. |
|
|
|
Dieses Feld erlaubt neben dem Standard-WYSIWYG-Editor auch Text-basiertes Markup wie bei reStructuredText. |
|
|
|
Autocpmplete-Widget, das auf jQuery-Autocomplete basiert.
Erfordert ein Choice-Feld mit Angabe von |
|
|
|
Multi-Select-Version für Tuple, Listen,Sets oder Frozensets
mit dem WErtetyp |
|
|
|
Content-Browser. Erfordert eine Quelle, die nach Objekten angefragt werden kann. |
|
|
|
Content-Browser. Erfordert eine Quelle, die nach Objekten angefragt werden kann. |
|
|
|
Ein Widget zum Hochladen von Dateien. |
|
|
|
Ein Widget zum Hochladen von Bildern. |
|
|
Collection` |
Listen-Eintrag für List, Tuple, Set oder Frozenset-Felder.
Erfordert als Wertetyp |
|
|
|
Checkbox für wahr/falsch. |
|
|
|
Ein Set von Checkboxen. Wird verwendet für Set- oder
Frozenset-Felder mit |
Eine Liste der gebräuchlichsten Dexterity-Behaviors.
Dexterity kommt mit einer Reihe von Behaviors. Die folgende Tabelle liefert die Interfaces, die in den Factory Type Information (FTI) angegeben werden um die jeweiligen Behaviors zu verwenden.
Interface |
Beschreibung |
---|---|
|
Fügt die Standardfelder Titel und Beschreibung hinzu. |
|
Fügt das Fieldset Kategorisierung mit dessen Feldern hinzu |
|
Fügt das Datum-Fieldset und dessen Felder hinzu. |
|
Fügt das Urheber-Fieldset und dessen Felder hinzu. |
|
Fügt alle DublinCore-Felder der oben genannten Behaviors hinzu. |
|
Berechnet den Namen aus dem Titel-Attribut. |
|
Fügt ein Related Items-Feld zum Kategorisierung-Fieldset hinzu. |
Directives ermöglichen die Konfiguration der Formulare aus den Schemata.
Dexterity verwendet plone.autoform um die auf z3c.form basierenden Hinzufügen- und Bearbeiten-Formulare zu konfigurieren. Somit lassen sich Schema mit Anmerkungen versehen, die das Formular konfigurieren.
Diese Anmerkungen können einfach mit Direktiven aus plone.directives.form und plone.directives.dexterity einem mit plone.directives.form.Schema
erstellten Schema hinzugefügt werden.
Ein einfaches Beispiel kann so aussehen:
from zope import interface, schema
from plone.directives import form
…
class IExcludeFromNavigation(form.Schema):
"""Interface to exclude items from navigation.
"""
form.fieldset('settings', label=u"Settings",
fields=['exclude_from_nav'])
exclude_from_nav = schema.Bool(
title=_(u'label_exclude_from_nav', default=u'Exclude from navigation'),
description=_(u'help_exclude_from_nav', default=u'If selected, this item will not appear in the navigation tree'),
default=False
)
form.omitted('exclude_from_nav')
Im folgenden eine Übersicht über alle Form-Directives aus plone.directives.form:
Name |
Beschreibung |
---|---|
|
Spezifiziert ein alternatives Widget für ein Feld. Dabei wird der Feldname als Key und der Widget-Name als Value angegeben. Das Widget kann entweder eine Instanz aus z3c.form.widget sein oder ein Dotted Name eines Widgets. |
|
Spart ein oder mehrere Felder aus einem Formular aus. Als Parameter kann eine Sequenz der Feldnamen angegeben werden. |
|
Folgende Modi sind möglich: |
|
Spezifiziert, dass das betreffende Feld vor einem anderen
gerendert werden soll. Ist das Feld in einem zusätzlichen
Schema definiert (z.B. in einem Behavior), ist der Name z.B.
|
|
Spezifiziert, dass das betreffende Feld nach einem anderen
gerendert werden soll. Ist das Feld in einem zusätzlichen
Schema definiert (z.B. in einem Behavior), ist der Name z.B.
|
|
Markiert ein bestimmtes Feld als Primary Field in einem Schema. Dies wird beim Zugriff per WebDAV für das Marshalling des Objekts verwendet. |
|
Erstellt ein Fieldset, das in Plone als Reiter im Bearbeiten- Formular angezeigt wird. |
Im folgenden die Security-Directives aus plone.directives.dexterity
:
Name |
Beschreibung |
---|---|
|
Setzt die Zope3-Permission, die zum Lesen des Feldwerts erforderlich ist. Dabei wird der Feldname als Schlüssel und die Berechtigung als Wert angegeben. |
|
Setzt die Zope3-Permission, die zum Schreiben des Feldwerts erforderlich ist. Dabei wird der Feldname als Schlüssel und die Berechtigung als Wert angegeben. |
Alle Felder von Dexterity Inhaltstypen sind als Eigenschaft eines Objekts verfügbar.
Im folgenden eine Liste der gebräuchlichsten Eigenschaften und Methoden von Dexterity Artikeltypen:
Eigenschaft/Methode |
Typ |
Beschreibung |
---|---|---|
|
|
Der Name (ID) des Objekts in seinem Container. Dies kann eine Unicode-Zeichenkette sein wobei aktuell Zope2 jedoch nur ASCII-Zeichen in URLs erlaubt. |
|
|
Der Name (ID) des Objekts in seinem Container. Dies ist das
ASCII-ENcoding von |
|
|
Gibt den Wert der ID-Eigenschaft aus. |
|
|
|
|
|
Der Artikeltyp dieser Instanz. |
|
|
Zope2-spezifische Art, eine Klasse zu beschreiben |
|
|
Gibt den Wert des Titels aus oder sofern dieser nicht gesetzt ist, die ID-Eigenschaft. |
|
|
Die vollständige URL des Inhaltsobjekts. Berücksichtigt Virtual Hosting und die aktuelle Domain. |
|
|
Eine Sequenz von Pfadelementen ab dem Wurzelverzeichnis der Anwendung. Die Angabe sollte nicht verwendet werden um relative URLs zu konstruieren, da Virtual Hosting nicht berücksichtigt wird. |
|
|
Gibt eine Zeichenkette zurück, die als |
|
|
Titel-Eigenschaft des Objekts. Üblicherweise Teil des Schemas eines Objekts, das durch das ÌBasic`-Behavior`` bereitgestellt wird. |
|
|
DublinCore-Accessor für die Titel-Eigenschaft. Es kann auch ``setTitle()``verwendet werden. |
|
|
Eine Liste von User-IDs, Der erste Ersteller ist üblicherweise
der Eigentümer des Objekts.
Mit |
|
|
Der erste Ersteller, der aus der |
|
|
DublinCore-Accessor für Schlagwörter. Die Liste kann
bearbeitet werden mit der |
|
|
DublinCore-Accessor für die Beschreibung, die üblicherweise
mit dem |
|
|
DublinCore-Accessor für die Liste der an dem Objekt
Beteiligten. Die Beschreibung kann geändert werden mit der
|
|
|
DublinCore-Accessor für das Datum des Artikels im ISO-Format.
Sofern vorhanden wird |
|
|
DublinCore-Accessor für das Erstellungsdatum des Artikels im ISO-Format. |
|
|
DublinCore-Accessor für das Veröffentlichungsdatum des
Artikels im ISO-Format. Das Veröffentlichungsdatum kann
geändert werden mit der |
|
|
DublinCore-Accessor für das Ablaufdatum des Artikels im
ISO-Format. Das Ablaufdatum kann geändert werden mit der
|
|
|
DublinCore-Accessor für das Änderungsdatumdes Artikels im . ISO-Format. |
|
|
DublinCore-Accessor für die Sprache des Artikels. Diese kann
geändert werden mit der |
|
|
DublinCore-Accessor für die Copyright-Angabe. Diese kann
geändert werden mit der |
|
|
Gibt die Zope2-DateTime-Angabe für das Erstellungsdatum zurück.
Falls diese nicht gesetzt ist, wird |
|
|
Gibt die Zope2-DateTime-Angabe für das Änderungsdatum zurück.
Falls diese nicht gesetzt ist, wird |
|
|
Gibt die Zope2-DateTime-Angabe für das Veröffentlichungsdatum
zurück. Falls diese nicht gesetzt ist, wird |
|
|
Gibt die Zope2-DateTime-Angabe für das Ablaufdatum zurück.
Falls diese nicht gesetzt ist, wird |
sind gut geeignet, eine große Anzahl homogener Daten zu verwalten. Sie sind jedoch wenig geeignet um hierarchische Daten abzubilden.
wie SQLAlchemy erlauben ein objektorientiertes Arbeiten wobei die Daten in einer relationalen Datenbank gespeichert werden. Die Restriktionen relationeller Datenmodelle bleiben jedoch auch hier erhalten.
z.B. LDAP oder ein Dateisystem sind sehr viel besser geeignet zur Speicherung flexibler hierarchischer Strukturen, wie sie von Content Management Systemen im allgemeinen gefordert werden. Diese Datenbankenbeherrschen jedoch im allgemeinen jedoch keine transaktionalen Semantiken.
ist transparent bei der persistenten Speicherung von Python-Objektem.
Die ZODB kommt mit einem Transaktionssystem, das Nebenläufigkeit (Concurrency) und Atomarität unterstützt.
Beim Entwickeln für Anwendungen auf Basis der ZODB ist gewährleistet, dass gleichzeitige Anfragen, die zu einem Konflikt führen könnten, weitgehend vermieden und die Daten in der ZODB konsistent gespeichert werden. Gelegentlich auftretende ConflictErrors beim Schreiben können verringert werden durch Datenstrukturen, die eigene Konfliktlösungsstrategien mitbringen wie z.B. B-Trees.
Eine weitere Quelle für ConflictErrors unter hoher Schreiblast sind einige Indextypen, die keine Konfliktlösungsstrategien mitbringen. Eine Möglichkeit ist hier, nicht zu jedem Zeitpunkt den Katalog aktuell zu halten sondern asynchron den Katalog zu aktualisieren. Ein Produkt, das dies erlaubt, ist collective.indexing.
Atomarität bedeutet, dass eine Transaktion entweder erfolgreich abgeschlossen wird oder fehlschlägt, die Daten jedoch nie in einem inkonsistenten Status verbleiben. Falls bei einer Transaktion ein ConflictErrors auftritt, wiederholt Zope üblicherweise bis zu drei Mal den Versuch, diese Transaktion erneut durchzuführen.
Wird mit einem externen System wie z.B. dem SQLAlchemy-Wrapper collective.lead gearbeitet, so sollte dieser mit dem Transaktionssystem der ZODB zusammenarbeiten.
Zwar ist Python beschränkt auf eine CPU, ZEO erlaubt jedoch die Verwendung mehrerer Zope-Applikationsserver, die sich eine Datenbank teilen können. Dabei sollte jedem dieser Zope-Clients eine andere CPU verwenden.
ZODB unterstützt Mount-Points, womit sich Daten über mehrere Storages verteilen lassen. Dabei lassen sich die Memory-Cache-Settings für jede Datenbank getrennt angeben. Wird z.B. der Katalog in eine eigene Datenbank ausgelagert, so kann für diese ein deutlich höherer Memory-Cache angegeben werden um die Performance zu verbessern (siehe auch Katalog in eigener ZODB).
schreibt die Daten in eine einzelne Datei auf dem Dateisystem. Diese Datei ist im wesentlichen ein großes Transaktionslog.
schreibt die Daten in eine relationale Datenbank.
Für jede Revision eines Objekts wird eine eigene Datei angelegt.
bietet inkrementelle Updates einer existierenden Datenbank ohne diese selbst zu aktualisieren.
früher sub-transactions
erlaubt feingranulare Fehlersuche und garbage collections während einer Transaktion.
verringert den Speicherverbrauch
bezeiht sich ausschließlich auf eine einzelne Datenbank. Falls also z.B. der Katalog in eine eigene Datenbank ausgelagert wurde, ist dieser nach einem Undo nicht mehr synchron mit dem Datenbestand.
entfernt alte Revisionen eines Objekts.
Beim Lesen der Data.fs
sollte nicht gleichzeitig geschrieben werden.
Vermeiden Sie setDefault
.
Inplace-Migrationen sollten vermeiden werden.
Verwenden Sie skalierbare Datenstrukturen wie BTrees
.
Siehe auch
Python-Pakete werden üblicherweise auf dem Python Package Index (PyPI) veröffentlicht.
Falls Sie noch nicht am Python Package Index (PyPI) registriert sind, tragen Sie sich bitte zunächst im Registrierungsformular ein. Neben Name, Passwort und E-Mail-Adresse können Sie sich ggf. auch noch mit dem PGP-Key authentifizieren.
Anschließend sollten Sie Username und Passwort in einer .pypirc
-Datei in
Ihrem Home-Verzeichnis speichern, z.B.:
[server-login]
username:veit
password:secret
distutils
benötigt diese Information beim register
-Befehl.
In der setup.py
-Datei müssen bestimmte Metadaten angegeben werden. Beim
Aufruf von python setup.py register
übermittelt das Skript diese Angaben
auf python.org
. Weitere Informationen zu diesen Metainformationen erhalten
Sie in der PEP 241: Metadata for
Python Software Packages.
Einige der Metadaten wie name
und version
werden verwendet um den
Dateinamen für die Distributionen zu erstellen. Andere, wie die Trove
classifiers werden
ausschließlich von PyPI verwendet.
Folgende Metadaten sind notwendig:
Der Name des Pakets
Eine Versionsnummer, z.B. 4.0.1
oder 4.1rc3
.
Eine einzeilige Beschreibung des Pakets.
Die URL der Homepage des Pakets.
Der Name des Autors des Pakets.
Die E-Mail-Adresse des Autors.
PEP 241 nennt die E-Mail-Adresse als eindeutigen Schlüssel für Paket-Kataloge.
Der Name der Lizenz unter der das Paket veröffentlicht wird. Ggf. kann auch eine URL einer Lizenz angegeben werden.
Eine ausführlichere Beschreibung des Pakets.
Laut PEP 241 ist diese Beschreibung optional, doch erleichtert dies anderen das Paket im Python package Index zu finden.
Eine Komma-separierte-Liste unterstützter Plattformen.
In vielen Fällen sollte hier Any
der richtige Eintrag sein.
Außerdem können noch Trove classifiers für die Beschreibung der Software anhand eines vordefinierten Vokabulars angegeben werden. Mit ihnen lassen sich z.B. Audience und Development status des Pakets angeben.
Schauen wir uns als konkretes Beispiel die setup.py-Datei von vs.event an:
from setuptools import setup, find_packages
import os
version = '0.2.19'
setup(name='vs.event',
version=version,
description="An extended event content-type for Plone (and Plone4Artists calendar)",
long_description=open("README.txt").read() + "\n" +
open(os.path.join("docs", "HISTORY.txt")).read(),
# Get more strings from https://www.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
"Framework :: Plone",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules",
],
keywords='Zope Plone Event Recurrence Calendar Plone4Artists',
author='Veit Schiele, Anne Walther, Andreas Jung',
author_email='vs.event@veit-schiele.de',
url='http://svn.plone.org/svn/collective/vs.event',
license='GPL',
packages=find_packages(exclude=['ez_setup']),
namespace_packages=['vs'],
include_package_data=True,
zip_safe=False,
install_requires=[
'setuptools',
'python-dateutil',
'dateable.chronos',
'dateable.kalends',
'collective.calendarwidget',
'Products.DataGridField',
'zope.app.annotation',
# -*- Extra requirements: -*-
],
entry_points="""
# -*- Entry points: -*-
""",
)
Beachten Sie bitte, dass long_description
aus zwei externen Dateien zusammengesetzt wird, nämlich README.txt
und HISTORY.txt
.
include_package_data
Ist der Wert auf True
gesetzt, so fügt setuptools
automatisch alle
Dateien innerhalb des Paketverzeichnisses hinzu, die entweder unter CVS-
oder SVN-Versionskontrolle stehen oder in einer MANIFEST.in
-Datei
beschrieben sind. Für aktuellere Versionen von SVN sowie weitere
Versionsverwaltungen wie git und Mercurial sind jedoch Plugins erforderlich:
Weitere Informationen erhalten Sie in Including Data Files.
PKG-INFO
¶Distutils PKG-INFO
kann verwendet werden umd die Metadaten aus setup.py
zu überprüfen. Beim Aufruf von python setup.py sdist
erstellt distutils einen sog. source tarball im dist
-Verzeichnis. Der tarball enthält eine PKG-INFO
-Datei auf oberster Verzeichnisebene.
Schließlich kann zum Testen kann auch die PyPI-Testing-Site verwendet werden.
Falls Sie einen PyPI-Account haben und die Zugangsdaten eingetragen sind in ~/.pypirc
kann das Paket bei PyPI registriert werden mit:
$ python setup.py register
Rufen Sie mit Ihrem Webbrowser https://www.python.org/pypi auf. Dort sollten Sie Ihr Paket in der Liste der letzten 20 aktualisierten Pakete sehen. Sofern Sie angemeldet sind, erscheint Ihr Paket auch in der linken Navigation unter der Überschrift Your Packages.
Wenn Sie nun auf den Namen Ihres Pakets klicken, wird Ihnen das Paket angezeigt. In dieser Ansicht erhalten Sie auch einen Edit-Link mit dem Sie die generierte Seite korrigieren können.
$ python setup.py sdist upload
pythonpackages.com
¶pythonpackages.com bietet eine alternative
Möglichkeit, Releases Ihrer Python-Packages aus einem Github-Repository zu
erstellen und zu testen. Weitere Informationen hierzu erhalten Sie unter
Introduction.
Darüberhinaus bietet pythonpackages.com
auch einen Test Package Index:
$ cd my.package
$ python setup.py sdist upload -r http://index.pythonpackages.com
$ pip install my.package -i http://index.pythonpackages.com
Einfache Integration von Releases in Buildout-Projekte mit gepinnten Versionen.
erstellt eine Voransicht der Dokumentation eines Pakets, bevor eine Release erstellt wird.
automatisiert die Aktualisierung von Versionsnummer, Änderungshistorie und Tagging.
Plugin für zest.releaser, das das Kopieren eines zuvor erstellten Egg zu einem konfigurierbaren Ziel erlaubt.
Mit sdistmaker gibt es eine einfache Möglichkeit, sdist tarballs in svn-Repositories bereitzustellen.
sdistmaker übernimmt folgende Aufgaben:
#. Es durchsucht die tags
-Verzeichnisse Ihres svn-Repository. Dabei kann
sdistmaker auch auf bestimmte Bereiche Ihres Repository eingeschränkt werden.
#. Für jeden Tag erstellt es eine source distribution mit ``python setup.py
sdist``.
Die source distribution wird anschließend in einem Unterverzeichnis Ihres Projekts gespeichert, ähnlich wie in https://pypi.python.org/simple/.
sdistmaker
lässt sich einfach installieren mit:
$ easy_install sdistmaker
Anschließend stehen Ihnen zwei Skripte zur Verfügung:
make_sdist
ist im wesentlichen für Testzwecke gedacht. Dabei können Sie unter Angabe
der tag
-URL und des Zielverzeichnisses einzelne Releases erstellen.
sdists_from_tags
Es durchsucht die svn-Struktur nach geeigneten tags
-Verzeichnissen und
erstellt aus diesen entsprechende Releases.
Zunächst sollte sdistmaker seine eigene Basiskonfiguration erstellen mit:
$ sdists_from_tags --print-example-defaults
Speichern Sie die Ausgabe in eine Konfigurationsdatei, z.B. defaults.py
.
Anschließend können Sie diese Konfiguration verwenden mit:
$ sdists_from_tags --defaults-file=defaults.py
Schließlich werden Sie sdists_from_tags
regelmäßig aufrufen wollen, entweder
als Cron-Job, svn post-commit-hook etc.
sdistmaker
kann mit Buildout folgendermaßen verwendet werden:
[buildout]
parts = sdists
[sdists]
recipe = zc.recipe.egg
eggs = sdistmaker
scripts = sdists_from_tags
# arguments =
# defaults_file='${buildout:directory}/defaults.py'
Dabei wird die defaults.py
-Datei auf dieselbe Weise erzeugt wie oben
beschrieben.
Üblicherweise kann immer nur ein Index für easy_install
angegeben werden. Um
nun neben dem sdistmaker
-Index auch den von PyPI verwenden zu können, kann
z.B. eine Redirect-Anweisung definiert werden, falls im sdistmaker
-Index
nichts gefunden wird:
# Allow indexing
Options +Indexes
IndexOptions FancyIndexing VersionSort
# Start of rewriterules to use our own var/private/* packages
# when available and to redirect to pypi if not.
RewriteEngine On
# Use our robots.txt:
RewriteRule ^/robots.txt - [L]
# Use our apache's icons:
RewriteRule ^/icons/.* - [L]
# We want OUR index. Specified in a weird way as apache
# searches in a weird way for index.htm index.html index.php etc.
RewriteRule ^/index\..* - [L]
# Use our var/private/PROJECTNAME if available,
# redirect to pypi otherwise:
RewriteCond /path/on/server/var/private/$1 !-f
RewriteCond /path/on/server/var/private/$1 !-d
RewriteRule ^/([^/]+)/?$ http://pypi.python.org/pypi/$1/ [L]
# Use our var/private/PROJECTNAME/project-0.1.tar.gz if available,
# redirect to pypi otherwise:
RewriteCond /path/on/server/var/private/$1 !-d
RewriteRule ^/([^/]+)/([^/]+)$ http://pypi.python.org/pypi/$1/$2 [L]
Dieser Index kann nun sowohl mit EasyInstall als auch mit Buildout aufgerufen werden:
EasyInstall
$ easy_install -i https://packages.veit-schiele.de/ vs.event
Sie können den Index in der Buildout-Konfigurationsdatei angeben:
[buildout]
index = https://packages.veit-schiele.de/
parts =
...
gocept.zestreleaser.customupload ist ein Plugin für zest.releaser, das das Hochladen erstellter Eggs via Secure copy (SCP) zu vorher konfigurierten Zielen erlaubt.
Um es zu verwenden, kann in ~/.pypirc
z.B. folgendes konfiguriert werden:
[gocept.zestreleaser.customupload]
vs = scp://download.veit-schiele.de:/var/www/packages
example = https://dav.veit-schiele.de:/var/www/example
Falls das veröffentlichte Paket mit einem der Schlüsselwörter (vs
,
example
) beginnt, werden Sie gefragt, ob das Egg auf den angegebenen Server
hochgeladen werden soll.
Umfassende Anleitung zur Erstellung eines Python-Pakets.
Damit die Versionen von anderen Werkzeugen wie distutils und setuptools auch in Zukunft automatisiert ausgewertet werden können, sollten das Versionsschema des PEP 386 eingehalten werden.
Das in Python 3.3 verwendete distutils2 wird dieses Metadatenschema unterstützen.
Acquisition ist ein Mechanismus, der es Objekten erlaubt, Attribute aus ihrer Umgebung zu erhalten. Eine ausführliche Beschreibung, wie in Zope Acquisition verwendet werden kann, finden Sie im Zope Book.
In der Zope Component Architecture (ZCA) sind Adapter Komponenten, die aus anderen Komponenten erstellt werden um sie einem bestimmten Interface zur Verfügung zu stellen:
>>> class IPerson(interface.Interface):
... name = interface.Attribute("Name")
>>> class PersonGreeter:
...
... component.adapts(IPerson)
... interface.implements(IGreeter)
...
... def __init__(self, person):
... self.person = person
...
... def greet(self):
... print "Hello", self.person.name
Die Klasse definiert einen Constructor, der ein Argument für jedes adaptierte Objekt nimmt.
component.adapts
deklariert, was angepasst werden soll.
adaptedBy
gibt eine Liste der Objekte aus, die adaptiert werden:
>>> list(component.adaptedBy(PersonGreeter)) == [IPerson]
True
provideAdapter
Sofern nur ein Interface angeboten wird, kann dieses einfach bereitgestellt werden mit:
>>> component.provideAdapter(PersonGreeter)
Ebenso können spezifische Argumente zum Registrieren eines Adapters angegeben werden:
>>> class VeitPersonGreeter(PersonGreeter):
... name = 'Veit'
... def greet(self):
... print "Hello", self.person.name, "my name is", self.name
>>> component.provideAdapter(
... VeitPersonGreeter, [IPerson], IGreeter, 'veit')
oder als keyword arguments:
>>> class ChrisPersonGreeter(VeitPersonGreeter):
... name = "Chris"
>>> component.provideAdapter(
... factory=ChrisPersonGreeter, adapts=[IPerson],
... provides=IGreeter, name='chris')
queryAdapter
oder getAdapter
kann für For named adapters verwendet werden:
>>> component.queryAdapter(Person("Chris"), IGreeter, 'veit').greet()
Hello Chris my name is Veit
>>> component.getAdapter(Person("Chris"), IGreeter, 'veit').greet()
Hello Chris my name is Veit
Falls kein Adapter vorhanden ist, gibt queryAdapter
einen Standardwert zurück wohingegen getAdapter
eine Fehlermeldung ausgibt:
>>> component.queryAdapter(Person("Chris"), IGreeter, 'daniel')
>>> component.getAdapter(Person("Chris"), IGreeter, 'daniel')
...
Traceback (most recent call last):
...
ComponentLookupError: (...Person...>, <...IGreeter>, 'daniel')
queryMultiAdapter
oder getMultiAdapter
gibt die Adapter mehrerer Objekte zurück.
Wenn wir z.B. einen Adapter mit mehreren Objekten erstellen:
>>> class TwoPersonGreeter:
...
... component.adapts(IPerson, IPerson)
... interface.implements(IGreeter)
...
... def __init__(self, person, greeter):
... self.person = person
... self.greeter = greeter
...
... def greet(self):
... print "Hello", self.person.name
... print "my name is", self.greeter.name
können wir diesen Multi-Adapter anfragen mit queryMultiAdapter
oder
getMultiAdapter
:
>>> component.queryMultiAdapter((Person("Chris"), Person("Veit")),
... IGreeter).greet()
Hello Chris
my name is Veit
Asynchronous JavaScript and XML.
Application Programming Interface.
Schnittstelle, die Funktionen eines Programms zugänglich macht.
Archetypes ist ein Framework um neue Artikeltypen in Plone aus Schemadefinitionen zu erstellen. Die Seiten zur Ansicht und zum Editieren lassen sich dabei automatisch generieren.
Mit Archetypes geschriebene Artikeltypen, die zusammen mit Plone ausgeliefert werden.
Browserlayer vereinfachen die Registrierung visueller Elemente wie Views, Viewlets etc. sodass diese Elemente nur in den Sites erscheinen, in denen sie explizit installiert wurden.
Zunächst wird ein Marker-Interface z.B. in vs.theme/vs/theme/browser/interfaces.py
erstellt:
from plone.theme.interfaces import IDefaultPloneLayer
class IThemeSpecific(IDefaultPloneLayer):
"""Marker interface that defines a Zope 3 browser layer.
If you need to register a viewlet only for the
"vs.theme" theme, this interface must be its layer.
"""
Anschließend kann dieses Marker-Interface regsitriert werden in vs.theme/vs/theme/profiles/default/browserlayer.xml
, z.B.:
<layers>
<layer name="vs.theme"
interface="vs.theme.interfaces.IThemeSpecific" />
</layers>
Schließlich können visuelle Komponenten für diesen Browserlayer registriert werden in vs.theme/vs/theme/browser/configure.zcml
, z.B.:
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
name="dashboard"
permission="plone.app.portlets.ManageOwnPortlets"
class="plone.app.layout.dashboard.dashboard.DashboardView"
template="templates/dashboard.pt"
layer=".interfaces.IThemeSpecific"
/>
Buildout erlaubt, identische Entwicklungsumgebungen einfach aufzusetzen. Hierzu nutzt buildout die Fähigkeit der setuptools, automatisch Abhängigkeiten aufzulösen und Aktualisierungen durchzuführen (s.a.: Buildout’s documentation).
Der Katalog ist ein interner Index der Inhalte einer Plone-Site. Dabei kann auf den Catalog auch über das ZMI als portal_catalog
zugegriffen werden.
Collective ist ein Subversion-Repository für die Plone-Community um Zusatzprodukte bereitzustellen.
CSS ist ein Web-Standard zur Darstellung von Inhalten. Der Standard ist beschreiben auf der W3C-Website. Eine Einführung in CSS finden Sie unter
Siehe auch: Einführung in Cascading Style Sheets (CSS)
Ein sog. Wrapper um eine Python-Funktion oder -Klasse, die die Funktion oder Klasse als sein erstes Argument nimmt und ein beliebiges Objekt zurückgibt. In Plone werden verschiedene Decorator verwendet, so z.B. memoize zum Caching der Werte von Funktionen und Methoden und profilehooks für das Erstellen von Profilen einzelner Funktionen
Sehen Sie auch PEP 318.
Eine Distribution besteht in Python aus einem Verzeichnis mit einer
setup.py
-Datei und anderen Ressourcen. Die Metaangaben in der
setup.py
-Datei können u.a. die Versionsnummer, Abhängigkeiten und
Lizenzinformationen enthalten.
Werkzeuge wie Setuptools, Distribute oder auch Buildout können die Metainformationen verwenden um Installationen in verschiedenen Versionen zu erhalten, Abhängigkeiten aufzulösen etc.
DocFinderTab ist ein Produkt, das alle Klassen und Methoden eines Objekts im Zope Management Interface (ZMI) auflistet.
Eine spezielle Syntax zum Schreiben von Tests. Ein Vorteil von Doctests ist, dass sie mit dem Test auch gleich die Dokumentation mitliefern. Als Nachteilig hat sich herausgestellt, dass nicht eine Untermenge der Doctests durchlaufen werden kann. Zudem werden beim Fehlschlagen eines Tests die weiteren Tests nicht mehr durchlaufen. Schließlich wird der Code auf eine besondere Weise ausgeführt, die schwieriger nachzuvollziehen und zu analysieren sind.
DTML ist eine serverseitige Template-Sprache, mit der sich dynamische Inhalte erstellen lassen. Plone verwendet für die Erstellung von HTML jedoch ZPT, sodass DTML nur noch für nicht XML-konforme Inhalte wie SQL-Anfragen, Mail- und CSS-Generierung verwendet wird.
Easy Install ist ein
Python-Modul mit dem der Python Package Index
durchsucht werden kann und das die Pakete in die globale Python-Umgebung
installiert. Neben Buildout werden wir nur noch ZopeSkel mit easy_install
installieren, alle weiteren Eggs werden von Buildout in das lokale
Buildout-Projekt heruntergeladen, unter anderem um Versionskonflikte zu
vermeiden.
Ein binäres Distributionsformat, das von den Setuptools und Distribute verwendet wird. Dabei wird ür jede Plattform und jede Python-Version ein spezifisches Egg erstellt.
Daher können Source-Distributionen, die meist nur ein komprimiertes Archiv des Codes und der Metaangaben sind, flexibler eingesetzt werden. Umgekehrt muss für Paketen, die binäre Abhängigkeiten besitzen (wie z.B. in C geschriebene Erweiterungen) die notwendigen Compiler und Bibliotheken verfügbar sein, um die Source-Distribution installieren zu können.
Die Zope Component Architecture (ZCA) ermöglicht, Events an bestimmte Handler zu schicken.
Im Folgenden erstellen wir zwei Beispielklassen, die zope.component.event
für das Dispatching benötigen:
>>> import zope.component.event
>>> class Event1(object):
... pass
>>> class Event2(Event1):
... pass
Anschließend werden zwei Handler für diese Event-Klassen erstellt:
>>> called = []
>>> import zope.component
>>> @zope.component.adapter(Event1)
... def handler1(event):
... called.append(1)
>>> @zope.component.adapter(Event2)
... def handler2(event):
... called.append(2)
Diese Handler werden nun registriert mit:
>>> zope.component.provideHandler(handler1)
>>> zope.component.provideHandler(handler2)
Nun Überprüfen wir, ob die Handler auch tatsächlich aufgerufen wurden:
>>> from zope.event import notify
>>> notify(Event1())
>>> called
[1]
>>> del called[:]
>>> notify(Event2())
>>> called.sort()
>>> called
[1, 2]
Siehe auch
Ein file descriptor ist ein abstrakter Indikator für den Zugriff auf eine Datei.
Dabei verwendet ein ZEO-Server für jeden Storage je Client-Verbindung 3 dieser Deskriptoren. Bei einem ZEO-Server mit zehn Storages und 36 ZEO-Clients würden also 10×36×3 file sescriptors benötigt. Üblicherweise verwendet der ZEO-Server jedoch nicht die erforderlichen 1080 file descriptors sondern nur 1025. Dies führt dann dazu, dass ZEO-Clients mit der Zeit keine Seiten mehr ausliefern und in deren Log-Dateien ECONNREFUSED
-Meldungen erscheinen.
Die Ursache hierfür ist, dass Python üblicherweise kompiliert wird mit einer maximalen Anzahl von 1024 file descriptors je Prozess. Dies lässt sich ändern indem in /usr/include/bits/typesizes.h
der Wert für define __FD_SETSIZE
hochgesetzt wird, z.B. auf 2048. Nach einem Neukompilieren von Python kann der ZEO-Server dann auch alle 1080 file descriptors verwenden.
Andreas Gabriel hat ein Skript geschrieben, mit dem sich die maximale Anzahl der Verbindungen testen lässt: zeo-check-max-connections.py
Handler sind eine spezifische Form von Subscribern, die nichts bereitstellen und meistens von Events aufgerufen werden.
Beim Aufruf eines Handlers wird kein Rückgabewert erwartet. Auch bieten Handler keine API an. Daher werden Handler meist als Funktion und nicht als Klasse implementiert. Zum Beispiel:
>>> import datetime
>>> def documentCreated(event):
... event.doc.created = datetime.datetime.utcnow()
>>> documentCreated = component.adapter(IDocumentCreated)(documentCreated)
Die letzte Zeile markiert den Handler als Adapter von IDocumentCreated
-Events. Nun wird der Handler noch registriert mit:
>>> component.provideHandler(documentCreated)
Schließlich kann die handle
-Funktion verwendet werden um Handlers, die für einen Event registriert sind, aufzurufen:
>>> component.handle(DocumentCreated(doc))
>>> doc.created.__class__.__name__
'datetime'
Siehe auch
Präparierung des Quellcodes, sodass er ohne weitere Änderung in verschiedene Sprachen übersetzt werden kann. i18n wird durch den ersten und letzten Buchstaben von Internationalization und die Anzahl der dazwischenliegenden Zeichen gebildet.
Die Übersetzungsarbeit selbst wird dann l10n genannt.
Ein Test, ob eine Komponente mit anderen Komponenten zusammen läuft. Die meisten Tests, die für Plone-Produkte geschrieben werden, sind Integrationstests da das gesamte Plone-Framework benötigt wird, um die gewünschten Testergebnisse zu erhalten. Ein Beispiel für einen Integrationstest ist der Test, ob ein Objekt eines neuen Artikeltyps erstellt werden kann nachdem dieses Produkt in einer Plone-Site installiert wurde.
Zope-Interfaces sind Objekte, die das externe Verhalten desjenigen Objekts spezifizieren, das sie bereitstellt. Dies geschieht durch:
Informelle Dokumentationen in Doc-Strings.
Attribut-Definitionen
Invarianten, also Bedingungen für Objekte, die dieses Interface bereitstellen.
Dabei spezifiziert ein Interface die Charakteristiken eines Objekts, sein Verhalten und seine Fähigkeiten.
Interfaces machen Angaben, was ein Objekt bereitstellt, nicht wie es bereitgestellt wird. Sie beruhen auf dem Design By Contract-Modell.
Während in einigen anderen Programmiersprachen Interfaces ein Bestandteil der Sprache selbst sind, werden in Python mit der ZCA Interfaces als Meta-Klasse implementiert, die ererbt werden kann.
Für eine Komponente wird zunächst dessen Interface erstellt. Interface-
Objekte werden üblicherweise mit Python Class Statements erstellt, sind
jedoch selbst keine Klassen sondern Objekte. Ein Interface-Objekt wird nun
als Subclass von zope.interface.Interface
erstellt:
from zope.interface import Interface
class IHello(Interface):
def hello(name):
"""Say hello to somebody"""
Durch das Subclassing von zope.interface.Interface
wird nun das Interface-Objekt IHello
erstellt:
>>> IHello
<InterfaceClass __main__.IHello>
Interfaces können auch verwendet werden um ein bestimmtes Objekt zu einem spezifischen Typ gehört. Ein solches Interface ohne Attribute und Methoden wird Marker-Interface genannt. Ein solches Interface kann z.B. so aussehen:
>>> from zope.interface import Interface
>>> class ISpecialGreeting(Interface):
... """A special greeting"""
Gelegentlich sind Regeln mit einem oder mehreren Attributen für das Interface einer Komponente erforderlich. Solche Regeln werden Invarianten genannt und können mit zope.interface.invariant
erstellt werden.
So kann z.B. für ein person
-Objekt mit den Attributen name
, email
und phone
ein Validator erstellt werden, der überprüft ob entweder email
und phone
angegeben wurden.
Zunächst wird nun ein aufrufbares Objekt entweder als Funktion oder Instanz erstellt:
>>> def contacts_invariant(obj):
...
... if not (obj.email or obj.phone):
... raise Exception(
... "At least one contact info is required")
#. Anschließend wird das Interface des person
-Objekts mit der
zope.interface.invariant
-Funktion definiert:
>>> from zope.interface import Interface
>>> from zope.interface import Attribute
>>> from zope.interface import invariant
>>> class IPerson(Interface):
...
... name = Attribute("Name")
... email = Attribute("Email Address")
... phone = Attribute("Phone Number")
...
... invariant(contacts_invariant)
Schließlich kann die validateInvariants
-Methode verwendet werden:
>>> from zope.interface import implements
>>> class Person(object):
... implements(IPerson)
...
... name = None
... email = None
... phone = None
>>> veit = Person()
>>> veit.email = u"veit@example.org"
>>> IPerson.validateInvariants(veit)
>>> chris = Person()
>>> IPerson.validateInvariants(jill)
Traceback (most recent call last):
...
Exception: At least one contact info is required
Dieses Hello
-Interface kann nun assoziiert werden mit einer konkreten
Klasse, in der das Verhalten definiert wird. In unserem Beispiel:
class HelloComponent:
implements(IHello)
def hello(self, name):
return "Hello %s!" % name
Die neue Klasse HelloComponent
implementiert das Hello
-Interface.
Dabei kann eine solche Klasse auch mehrere Interfaces implementieren. Sollen also Instanzen unserer HelloComponent
zusätzlich ein Other
-Interface implementieren, wird einfach eine Sequenz der Interface-Objekte in der HelloComponent
-Klasse bereitgestellt:
class HelloComponent:
implements(IHello, IOther)
...
Mit implementedBy
kann ein Interface gefragt werden, ob eine bestimmte Klasse oder Instanz dieses Interface implementiert. So sollte z.B. die Überprüfung, ob eine Instanz der HelloComponent
-Klasse Hello``implementiert den Wert ``true
zurückliefern:
IHello.implementedBy(HelloComponent)
Interfaces können einfach erweitert werden, so kann z.B. unser IHello
-Interface um eine Methode lastGreeted
erweitert werden:
class ISmartHello(IHello):
"""A Hello object that remembers who is greeted"""
def lastGreeted(self):
"""Returns the name of the last person greeted."""
getBases
gibt eine Liste der Interfaces aus, die durch dieses Interface erweitert wurden, z.B.:
>>> ISmartHello.getBases()
(<InterfaceClass __main__.IHello>,)
extends
gibt true
oder false`, je nachdem, ob ein Interface ein anderes erweitert oder nicht, z.B.:
>>> ISmartHello.extends(IHello)
True
>>> IOther(Interface):
... pass
>>> ISmartHello.extends(IOther)
False
names
gibt eine Liste der Namen aller Items aus, die durch das Interface beschrieben werden, z.B.:
>>> IUser.names()
['getUserName', 'getPassword']
namesAndDescriptions
gibt eine Liste von Tuples (name, description)
aus, z.B.:
>>> IUser.namesAndDescriptions()
[('getUserName', <zope.interface.interface.Method.Method object at 80f38f0>),
('getPassword', <zope.interface.interface.Method.Method object at 80fded8>)]
JavaScript-Bibliothek, die die Traversierung und das Event-Handling von HTML-Dokumenten vereinfacht. So lässt sich z.B. in einem Einzeiler angeben, dass alle PDFs in einem neuen Fenster geöffnet werden sollen:
jQuery("#content a[ @href $= '.pdf']").attr('target', '_blank');
Weitere Informationen zu jQuery erhalten Sie unter:
Und mit FireQuery gibt es eine Firefox-Extension, die in Firebug integriert ist.
In Plone 3 verwendetes AJAX-Framework.
Kupu ist ein graphischer HTML-Editor, der mit Plone zusammen ausgeliefert wird.
l10n ist die Übersetzung in eine oder mehrere spezifische Sprachen. l10n wird durch den ersten und letzten Buchstaben von Localization und die Anzahl der dazwischenliegenden Zeichen gebildet.
Ein Layer ist eine Sammlung von Templates und Skripten. Dabei bildet ein Stapel von Layern einen Skin. Im ZMI können Sie im Properties-Reiter des portal_skins
-Tool die Definition von Skins über Layer sehen und im Content-Reiter sehen Sie diese Layer als Filesystem Directory View oder Folder.
LDAP beschreibt die Kommunikation zwischen einem sog. LDAP-Client und einem Verzeichnisdienst. EIn solches Verzeichnis kann z.B. ein Adressbuch sein, das Personendaten enthält. Der LDAP-Client kann dann ein E-Mail-Programm sein, das bei der Suche nach einer Adresse eine Anfrage an den LDAP-Server, der diese Adressinformationen bereitstellt, stellt.
LDAP ist spezifiziert in RFC 4511.
Für des Entwickelns stellt Plone einen eigenen Logger bereit: plone_log:
from logging import getLogger
log = getLogger('Plone')
log.info('Debug: %s \n%s', summary, text)
Wie plone_log
verwendet werden kann, finden Sie z.B. in setConstrainTypes.cpy:
...
plone_log=context.plone_log
constrainTypesMode = context.REQUEST.get('constrainTypesMode', [])
currentPrefer = context.REQUEST.get('currentPrefer', [])
currentAllow = context.REQUEST.get('currentAllow', [])
plone_log( "SET: currentAllow=%s, currentPrefer=%s" % ( currentAllow, currentPrefer ) )
...
Rolle, die in Zope alle Berechtigungen erhält bis auf Take Ownership.
Macro Expansion Tag Attribute Language
METAL kann für das Verarbeitung von Macros für HTML und XML verwendet werden. Sie kann zusammen mit TAL und TALES verwendet werden.
Macros erlauben Definitionen in einer Datei, die von einer oder mehreren anderen Dateien verwendet werden können. Dabei werden macros immer in vollem Umfang verwendet.
metal:define-macro
Definieren eines Macros als Element und dessen Teilbaum.
metal:use-macro
Verwenden eines Macros wobei der Ausdruck in Zope immer die Angabe des Pfads ist, der auf ein Macro in einem anderen Template verweist.
metal:define-slot
Definieren eines Slots, der angepasst werden kann.
Wird ein Macro verwendet, so können dessen Slots ersetzt werden um das Macro anzupassen. Slot-Definitionen liefern dann den Standard-Inhalt für diesen Slot, der verwendet wird, sofern das Macro nicht angepasst wird.
Die Anweisung metal:define-slot
muss innerhalb von metal:define-macro
verwendet werden. Darüberhinaus müssen die Slot-Namen innerhalb eines Macros einheitlich sein.
metal:fill-slot
Anpassen eines Macros indem ein Slot dieses Macros ersetzt wird.
Die Anweisung metal:fill-slot``muss innerhalb von ``metal:use-macro
verwendet werden. Darüberhinaus müssen die Slot-Namen innerhalb eines Macros einheitlich sein.
If the named slot does not exist within the macro, the slot contents will be silently dropped.
Ein Monkey Patch erlaubt die Änderung des Verhaltens von Zope oder eines Produkts ohne den Original-Code verändern zu müssen.
Ein Monkey Patch lässt sich einfach mit collective.monkeypatcher erstellen. Hierzu tragen wir in die configure.zcml
-Datei folgendes ein:
<configure
...
xmlns:monkey="http://namespaces.plone.org/monkey">
...
<monkey:patch
description="TinyMCE JSON Folder listing should ignore INavigationRoot"
class="Products.TinyMCE.adapters.JSONFolderListing.JSONFolderListing"
original="getListing"
replacement=".patches.getListing"
/>
Nun erstellen wir unseren Pach, indem wir aus Products.TinyMCE.adapters.JSONFolderListing.JSONFolderListing
die Methode getListing
in die Datei patches.py
kopieren und entsprechend anpassen.
Anmerkung: Mit collective.monkeypatcherpanel wird ein Zope2-Control-Panel angelegt, das die mit collective.monkeypatcher
erstellten Monkey Patches anzeigt.
mr.bob ist ein Dateisystem-Template- Renderer. Er ermöglicht, aus einer Vorlage eine Verzeichnisstruktur zu erstellen, die das Erstellen von Python-Paketen deutlich vereinfacht.
Weitere Informationen zur Installation, den Vorlagen und Standardeinstellungen erhalten Sie im Abschnitt Referenzen des Plone- Entwicklerhandbuchs.
Siehe auch
Plone verwendet verschachtelte Pakete um Namensräume zu bilden, die durch Pfadnamen eindeutig angesprochen werden können und so Kollisionen mit anderen Paketen vermeiden helfen. Dabei nutzt Plone eine Funktion der Setuptools, womit mehrere getrennte Python Packages ausgeliefert werden können, die einen gemeinsamen Top-level-Namespace teilen, z.B. plone.theme
und plone.portlets
.
PAS ist ein Framework zur Authentifizierung in Zope. PAS ist ein Zope-acl_users
-Ordner, das Plugins verwendet um verschiedene Authentifizierungsschnittstellen bereitzustellen.
Paste ist ein WSGI-Entwicklungs- und Deployment-System, das von Ian Bicking entwickelt wurde.
PDB ist ein interaktiver Debugger, mit dem schrittweise durch den Code gegangen werden kann um Probleme aufzufinden.
Um einen einfachen Breakpoint zusetzen, kann folgendes angegeben werden:
import pdb; pdb.set_trace()
Anschließend sollte Zope neu im Vordergrund gestartet werden mit:
$ ./bin/instance fg
Anschließend sollte der Code ausgeführt werden, für den der Breakpoint gesetzt wurde. Das Terminal, in dem die Instanz gestartet wurde, sollte dann eine Debug-Session öffnen mit folgender Angabe:
-> Pdb().set_trace()
(Pdb)
Sie können nun mit r
(Return) den set_trace()
-Aufruf verlassen und so schrittweise den Code untersuchen.
Wenn ein Fehler in einer Methode auftritt, die häufig ausgeführt wird, ist es jedoch nur lästig, sehr häufig Return angeben zu müssen. Daher empfiehlt sich, das sog. post-mortem-Idiom zu verwenden:
try:
[YOUR CODE HERE]
except:
import pdb, sys
e, m, tb = sys.exc_info()
pdb.post_mortem(tb)
Anschließend sollte die Zope-Instanz wieder im Vordergrund gestartet werden. Nun wird pdb
nur noch aufgerufen, wenn ein Fehler im Abschnitt [YOUR CODE HERE]
auftritt.
Um zu gewährleisten, dass derselbe pdb-Breakpoint in einer Deubug-Session nicht mehrfach eine Exception ausgibt, kann die Variable PDB_ACTIVE
auf 1
gesetzt werden:
if not globals().get( 'PDB_ACTIVE', 0 ):
globals()['PDB_ACTIVE'] = 1
import pdb; pdb.set_trace()
Anmerkung: Entfernen Sie bitte wieder die debugging hooks bevor der Code in das Repository eingecheckt wird.
Eine ~/.pdbrc
-Konfigurationsdatei kann verwendet werden um sich einige Shortcuts zum Debuggen zu erstellen, z.B.:
# Print a sorted dictionary.
# %1 is the dict
# %2 is the prefix for the names.
alias p_ for k in sorted(%1.keys()): print "%s%-15s= %-80.80s" % ("%2",k,repr(%1[k]))
# Print the member variables of something
alias pi p_ %1.__dict__ %1.
# Print the member variables of self
alias ps pi self
# Print locals
alias pl p_ locals() local:
# Next list and step list
alias nl n;;l
alias sl s;;l
Um weitere Hilfsfunktionen in pdb nutzen zu können, lassen sich auch externe
Python-Dateien in die ~/.pdbrc
-Datei einbinden – siehe hierzu PdbRcIdea.
Verwendung von pdb
Ausführliche Anleitung für die Verwendung von pdb mit Zope
Eine kurze praktische Einführung in pdb
Eine ausführliche Anleitung mit fortgeschrittenen Beispielen
Vergleichbar mit Pythons PEPs (Python Enhancement Proposals).
Das Plone-Team strukturiert und organisiert mit PLIPs den Entwicklungsprozess von Plone.
Plone ist ein, auf dem freien Webanwendungsserver `Zope`_ aufbauendes Enterprise-Content-Management-System, das in der Programmiersprache Python geschrieben ist.
Es kann für Intranet- und Extranet-Anwendungen, als Dokumentenmanagementsystem und als Groupware eingesetzt werden. Zahlreiche Erweiterungen ermöglichen den Einsatz für weitere Aufgaben, z.B. im eLearning, Webshop oder Bilddatenbank.
Portlets sind frei konfigurierbare Ansichten, die sich an beliebigen Stellen der Website hinzufügen lassen.
Folgende Portets werden mit Plone mitgeliefert:
Calendar portlet
Classic portlet
Collection portlet
Termine
Login
Navigation
Nachrichten
RSS feed
Aktuelle Änderungen
Revisionsliste
Suche
Static text portlet
Die Zuweisung kann über folgende Kategorien erfolgen:
context
content_type
group
Beachten Sie bitte, dass gruppenspezifische Portlets normalerweise unterhalb von kontextabhängigen Portlets angezeigt werden.
user
Diese Angabe ist vermutlich nur für die Dashboard-Portlet-Manager sinnvoll.
Plone wird mit folgenden Portlet Managern ausgeliefert:
plone.leftcolumn
und plone.rightcolumn
für die linke und rechte Spalte
plone.dashboard1
bis plone.dashboard4
für die vier Spalten des Dashboard.
Python ist die Programmiersprache, die von Zope und Plone verwendet wird.
Es wird nicht empfohlen, die systemweite Python-Installation zu verwenden da für Plone häufig Python-Pakete benötigt werden, die nicht oder nicht in der gewünschten Version vorliegen.
Wie Python aus den Sourcen installiert werden kann, ist im Kapitel Entwicklungsumgebung beschrieben.
Python Eggs sind ein Deploymentformat für Python-Packages. Sie enthalten ein setup.py
-Skript mit Metainformationen (Lizenz, Abhängigkeiten, etc.) Mit der Python-Bibliothek Setuptools können solche Abhängigkeiten automatisch nachgeladen werden, wobei in Eggs spezifische Versionen angegeben werden können.
Python-Pakete strukturieren den Namensraum von Python-Modulen so, dass sog. dotted module names verwendet werden können.
Sehen Sie auch in der Python Dokumentation: Packages
Der Python Package Index PyPI unter `pypi.python.org ist ein Index mit tausenden von Python-Paketen. Setuptools, easy_install und buildout nutzen diesen Index, um Python Eggs automatisch zu installieren.
Er ist momentan noch der Standardhost zum Herunterladen von Paketen. Zukünftig wird pypi.org der Standard-Host werden; momentan ist er jedoch noch nicht voll funktionsfähig.
Suchpfad für die Dateien von Modulen eines Python-Interpreters. Das Format entspricht demjenigen von PATH
. Innerhalb von Python ist der PYTHONPATH mit sys.path
verfügbar. So kann z.B. beim Aufruf des bootstrap.py
ein Skript bin/buildout
erzeugt werden mit folgendem Inhalt:
#!/opt/python/2.4.6/bin/python
import sys
sys.path[0:0] = [
'/opt/plone/3.3/eggs/zc.buildout-1.3.1-py2.4.egg',
'/opt/plone/3.3/eggs/setuptools-0.6c9-py2.4.egg',
]
Ein Standard-Unit-Testing-Framework für Python.
Repoze ist eine Sammlung von Technologien um den Webanwendungsserver `Zope`_ mit WSGI-Anwendungen zu verbinden.
Um die Ansicht einer Seite in der Plone-Site zu erhalten, wird ein Request an die Plone-Site gestellt. Diese Anfrage wird in Zope in ein request-Objekt gekapselt, i.a. REQUEST
genannt (oder request
in ZPT).
Plone-Infrastruktur, das CSS- und Javascript-Deklarationen in getrennten Dateien erlaubt. Erst für der Auslieferung werden diese Dateien zusammengeschrieben. Auch muss für das Einbinden einer neuen Datei nicht jedesmal in Zope Page Templates geändert werden um die Datei zu importieren oder auf sie zu verweisen. Die Resource Registries sind im ZMI zu finden unter portal_css
, portal_javascript
und portal_kss
.
roadrunner ist ein Testrunner, der die testgetriebene Entwicklung deutlich beschleunigen kann indem er vorab das Standard-Zope- und Plone-Environment für PloneTestCase läd.
Round-Robin wird bei der Lastverteilung (load balancing) von Varnish verwendet, wobei die Resourcen möglichst gleichmäßig beansprucht werden sollen. Dabei werden die Prozesse in einer Warteschlange verwaltet und der vorderste Prozess erhält eine bestimmte Zeit lang Zugang zu den Resourcen bevor er sich wieder am Ende der Warteschlange einreiht.
Ein Stapel von Layer, die als Suchpfad verwendet werden wenn eine Seite gerendert wird. Skins werden im portal_skins
-Tool definiert, das auch durch das ZMI erreichbar ist.
Subscriber sind eine spezifische Form von Adaptern, die Verwendung findet, wenn alle Adapter eines Objekts zu einem Adapter zusammengefasst werden sollen.
Subscriber lassen sich registrieren mit registerSubscriptionAdapter
:
>>> components.registerSubscriptionAdapter(tests.A1_2)
...
Registered event:
SubscriptionRegistration(<Components comps>, [I1], IA2, u'', A1_2, u'')
Subscriber können bereitgestellt werden mit provideSubscriptionAdapter
.
>>> component.provideSubscriptionAdapter(SingleLineSummary)
>>> component.provideSubscriptionAdapter(AdequateLength)
>>> doc = Document("A\nDocument", "blah")
>>> [adapter.validate()
... for adapter in component.subscribers([doc], IValidate)
... if adapter.validate()]
['Summary should only have one line', 'too short']
Mit subscribers
erhalten Sie die Subscriber der jeweiligen Komponente:
>>> doc = Document("A\nDocument", "blah")
>>> [adapter.validate()
... for adapter in component.subscribers([doc], IValidate)
... if adapter.validate()]
['Summary should only have one line', 'too short']
Der Name und die Factory-Methode eines Subscribers sowie die
Angabe, ob der Subscriber erforderlich ist, erhalten Sie mit
provided
, factory
und required
aus
registeredSubscriptionAdapters
:
>>> for registration in sorted(
... components.registeredSubscriptionAdapters()):
... print registration.required
... print registration.provided, registration.name
... print registration.factory, registration.info
Subscriber lassen sich löschen mit
unregisterSubscriptionAdapter
:
>>> components.unregisterSubscriptionAdapter(tests.A1_2)
...
Unregistered event:
SubscriptionRegistration(<Components comps>, [I1], IA2, u'', A1_2, '')
True
Supervisor ist ein Client/Server-System, das die Prozessüberwachung und -kontrolle auf Unix-Betriebssystemen erlaubt.
Dieses Python-Programm erlaubt start
, stop
und restart
anderer Programme auf UNIX-Systemen wobei es auch abgestürzte Prozesse erneut starten kann.
TAL ist eine Template-Sprache, die zur Generierung von XML-Dokumenten verwendet werden kann und abstrahiert dabei völlig von der eingesetzten Programmiersprache. Erst mit TALES, die die Syntax der Ausdrücke von TAL bescreibt, werden implementierungsabhängige Ausdrücke in Python erlaubt.
Im Folgenden alle TAL-Ausdrücke:
tal:attributes
erlaubt das dynamische Ändern der Attribute eines Elements.
tal:define
definiert Variablen.
tal:condition
testet, ob die angegebenen Bedingungen erfüllt werden.
tal:content
ersetzt den Inhalt eines Elements.
tal:omit-tag
entfernt ein Element.
tal:on-error
beschreibt den Umgang bei einem Fehler.
tal:repeat
wiederholt ein Element.
tal:repeat
-Variable hält folgende Informationen:index
Fortlaufende Zahlen, mit Null beginnend
number
Fortlaufende Zahlen, mit Eins beginnend
even
wahr für mit even-indexierte Wiederholungen (0, 2, 4, …)
odd
wahr für odd-indexierte Wiederholungen (1, 3, 5, …)
start
wahr für die erste Wiederholung
end
wahr für die letzte Wiederholung
first
wahr für den ersten Eintrag der Wiederholung
last
wahr fpr den letzten Eintrag der Wiederholung
length
Länge der Sequenz, d.h. die Gesamtzahl der Einträge einer Wiederholung
letter
Position des Eintrags als Kleinbuchstagen: a-z
, aa-az
, ba-bz
, … za-zz
, aaa-aaz
etc.
Letter
Position des Eintrags als Versalien
roman
Position des Eintrags als römische Zahl in Kleinbuchstaben: i
, ii
, iii
, iv
, v
etc.
Roman
Position des Eintrags als römische Zahl in Versalien.
tal:replace
ersetzt den Inhalt eines Elements.
Erhält ein Element mehrere TAL-Anweisungen, so werden diese in folgender Reihenfolge ausgeführt:
tal:define
tal:condition
tal:repeat
tal:content
oder tal:replace
tal:attributes
tal:omit-tag
TALES beschreibt die Syntax der Ausdrücke der Template Attribute Language (TAL) und der Macro Expansion Template Attribute Language (METAL).
TALES stellt mehrere Methoden für Ausdrücke zur Verfügung, die in TAL- und METAL-Attributen durch ein Präfix unterschieden werden können.
path:
Der Präfix ist optional, d.h., wird kein Präfix angegeben, so wird ein Pfad-Ausdruck erwartet.
Solche Ausdrücke referenzieren Objekte, um deren Methoden oder Attribute aufzurufen.
string:
odd
wahr für odd-indexierte Wiederholungen (1, 3, 5, …)
start
wahr für die erste Wiederholung
end
wahr für die letzte Wiederholung
first
wahr für den ersten Eintrag der Wiederholung
last
wahr fpr den letzten Eintrag der Wiederholung
length
Länge der Sequenz, d.h. die Gesamtzahl der Einträge einer Wiederholung
letter
Position des Eintrags als Kleinbuchstagen: a-z
, aa-az
, ba-bz
, … za-zz
, aaa-aaz
etc.
Letter
Position des Eintrags als Versalien
roman
Position des Eintrags als römische Zahl in Kleinbuchstaben: i
, ii
, iii
, iv
, v
etc.
Roman
Position des Eintrags als römische Zahl in Versalien.
tal:replace
ersetzt den Inhalt eines Elements.
Erhält ein Element mehrere TAL-Anweisungen, so werden diese in folgender Reihenfolge ausgeführt:
tal:define
tal:condition
tal:repeat
tal:content
oder tal:replace
tal:attributes
tal:omit-tag
TALES beschreibt die Syntax der Ausdrücke der Template Attribute Language (TAL) und der Macro Expansion Template Attribute Language (METAL).
TALES stellt mehrere Methoden für Ausdrücke zur Verfügung, die in TAL- und METAL-Attributen durch ein Präfix unterschieden werden können.
path:
Der Präfix ist optional, d.h., wird kein Präfix angegeben, so wird ein Pfad-Ausdruck erwartet.
Solche Ausdrücke referenzieren Objekte, um deren Methoden oder Attribute aufzurufen.
string:
Präfix, der beliebige Zeichenketten erlaubt und damit z.B. auch aus Variablen generierte Pfadausdrücke mit ${...}
.
not:
Präfix, der den folgenden Ausdruck auswertete und seine logische Negation zurückgibt.
python:
Präfix, der den Wert des folgenden Python-Skripts ausgibt.
Ein Zugriff dieser Python-Skripte auf sicherheitsrelevante Objekte wird jedoch unterbunden.
structure
Ein vorangestelltes structure
unterdrückt das HTML-Quoting. Damit kann z.B. ein komplettes HTML-Element erzeugt werden.
nothing
Einzelner Wert, der von TAL verwendet wird um einen Nicht-Werte anzugeben, z.B. void
, None
, Nil
, NULL
.
default
Einzelnder Wert, der in TAL spezifiziert, dass existierender Text nicht ersetzt werden options
Im Template zulässige Keyword-Argumente
repeat
Schleifenvariablen, s.a. The Zope2 Book: Repeat an element
attrs
Ein Dictionary, das die zulässigen Werte des aktuellen Tags enthält.
CONTEXTS
Liste der Standardnamen. Dies kann verwendet werden um auf eine eingebaute Variable zuzugreifen, die von einer lokalen oder globalen Variable desselben Namens verborgen wird.
Test vom Standpunkt eines Endnutzers. Üblicherweise wird ein Use Case oder eine User Story getestet. Ein Beispiel für einen funktionalen Test ist, ob eine bestimmte Nachricht angezeigt wird nachdem ein Formular ohne erforderliche Daten abgeschickt wurde.
Eine Sammlung von Testfällen, die zusammen durchlaufen werden.
Eine Sammlung von Tests.
Ein Python Traceback ist eine detaillierte Fehlermeldung, die ausgegeben wird wenn ein Fehler beim Ausführen von Python-Coder auftritt. Um sich einen solchen Traceback anzuschauen, können Sie entweder in die event log-Datei in var/log/instance
schauen oder im ZMI Ihrer Plone-Site in error_log
. Ein Traceback beginnt mit Traceback (innermost last):
oder Traceback (most recent call last):
. Meist ist die bedeutendste Information am Ende eines Tracebacks angegeben.
Ein Test für kleine Code-Einheiten, z.B. das Setzen und Erhalten von Attributen einer Klasse.
Utilities sind Komponenten mit einem Interface und die mit einem Interface und einem Namen aufgerufen werden können.
Solche Utilities können erstellt werden mit:
>>> from zope import interface
>>> class IGreeter(interface.Interface):
... def greet():
... "say hello"
>>> class Greeter:
... interface.implements(IGreeter)
...
... def __init__(self, other="world"):
... self.other = other
...
... def greet(self):
... print "Hello", self.other
queryUtility
oder getUtility
fragen das Utility nach ihrem Interface:
>>> component.queryUtility(IGreeter, 'christian').greet()
Hello chris
>>> component.getUtility(IGreeter, 'christian').greet()
Hello chris
queryUtility
und getUtility
unterscheiden sich jedoch in ihrer Fehlerbehandlung:
>>> component.queryUtility(IGreeter, 'veit')
>>> component.getUtility(IGreeter, 'veit')
...
Traceback (most recent call last):
...
ComponentLookupError: (<InterfaceClass ...IGreeter>, 'veit')
provideUtility
registriert eine Instanz einer Utility-Klasse, z.B.:
>>> from zope import component
>>> greet = Greeter('chris')
>>> component.provideUtility(greet, IGreeter, 'christian')
Ein View ist eine bestimmte Ansicht eines Objektes.
Genauer betrachtet ist ein View eine Funktion zur Berechnung der Darstellung eines Objekts.
Ansicht zusätzlicher Informationen, die nicht der Inhalt eines Objekts sind. Dabei werden Viewlets meist durch Viewlet Manager verwaltet. Viewlets und Viewlet Manager ermöglichen die Erstellung von pluggable user interfaces.
Viewlet Manager verwalten die für sie registrierten Viewlets.
virtualenv erlaubt die Erstellung einer virtuellen Python-Umgebung. Damit lassen sich andere Abhängigkeiten, Versionen und Berechtigungen verwenden als in einer systemweiten Installation.
$ easy_install-2.7 virtualenv
$ virtualenv my_virtualenv
bin
Das Verzeichnis enthält die Skripte zum Aktivieren und Deaktivieren des virtualenv, außerdem easy_install
, pip
und python
(Dabei ist python
eine Kopie desjenigen Python, mit dem das virtualenv
erstellt wurde.
include
Das Verzeichnis enthält nur einen Symlink zum include
-Verzeichnis derjenigen Python-Installation, aus dem das virtualenv
erstelt wurde.
lib
Das Verzeichnis enthält einen Symlink zum include
-Verzeichnis derjenigen Python-Installation, aus dem das virtualenv
erstellt wurde.
Ab Python 2.6 kann ein Nutzer auch einfach seine Python-Umgebung erstellen mit:
$ pip install --user foo
Siehe auch:
WebDAV steht für Web-based Distributed Authoring and Versioning und ist eine Erweiterung des Protokolls HTTP/1.1
, die in RFC 2518 spezifiziert ist. Sie erlaubt, ganze Verzeichnisse zu übertragen. Zudem können Ressourcen bei der Bearbeitung gesperrt werden um ein konkurrierendes Schreiben zu verhindern.
Wie Zope als WebDAV-Server eingerichtet werden kann, ist in WebDAV-Server beschrieben.
Eine Übersicht über diverse WebDAV-Clients finden Sie im Plone-Nutzerhandbuch.
Workflows sind eine einfache Möglichkeit, Geschäftsprozesse abzubilden. Folgende Probleme lassen sich hiermit lösen:
Artikel können unterschiedliche Zustände (Stadien) annehmen
Artikel können je nach Stadium unterschiedliche Berechtigungen haben
Benutzer können bestimmte Ereignisse beim Ändern eines Status auslösen
Es lassen sich Abnahmen und Übergaben damit realisieren
Python-Standard-Interface zwischen Webanendungen mit dem Ziel, die Portabilität von Webanwendungen zu fördern.
WSGI ist in PEP 333 definiert.
XML-RPC ist eine Definition zum Aufruf von Methoden und Funktionen durch entfernte Systeme.
Zope unterstützt XML-RPC für jedes traversierbare Objekt, z.B.:
target = 'http://localhost:8080/plone'
path = xmlrpclib.ServerProxy(target).getPhysicalPath()
Eine einfache Möglichkeit, einen Nutzer für XML-RPC zu authentifizieren, ist das Einbinden von HTTP Basic Auth in eine URL:
target = 'http://admin:secret@localhost:8080/plone'
path = xmlrpclib.ServerProxy(target).getPhysicalPath()
XML-RPC kann Objekte nicht zuverlässig an andere Aufrufe übergeben. Um an das entfernte Objekt zu gelangen, kann ZPublisher.Client.Object
verwendet werden.
Sehen Sie auch Zope2.utilities.load_site
wsapi4plone.core stellt zusätzliche Methoden für Zopes XML-PRC-Api zur Verfügung. This is an add-on product exposes more methods available through Zope’s XML-RPC api.
Im Folgenden ein Beispiel, wie aus einem pictures
-Ordner ein Bild mit der ID portrait.jpg
in einen Ordner portraits
geladen und in veit.jpg
umbenannt wird:
import os
from xmlrpclib import ServerProxy
from xmlrpclib import Binary
client = ServerProxy("http://admin:secret@localhost:8080/plone")
data = open(os.path.join('pictures', 'portrait.jpg')).read()
myimage = {'portraits/veit.jpg': [{'title': 'a Portrait of Veit', 'image':Binary(data)},'Image']}
output = client.get_object(client.post_object(myimage))
Anmerkung: transmogrify.ploneremote nutzt XML-RPC um Inhalte in eine Plone-Site zu importieren.
XPath ist eine vom W3C entwickelte Abfragesprache, um Teile eines XML-Dokumentes zu adressieren. Auf ihr basieren weitere Standards wie XSLT
, XPointer
und XQuery
.
Ein XPath-Ausdruck setzt sich zusammen aus:
einem oder mehreren Lokalisierungsschritten,
sie werden mit dem Zeichen /
getrennt.
optional gefolgt von einem oder mehreren Prädikaten.
Lokalisierungsschritte bestehen aus einer Achse und einem Knotentest, die Achse::Knotentest geschrieben werden.
Hier die gebräuchlichsten Achsen:
child
Direkt untergeordneter Knoten
./
parent
Direkt übergeordneter Elternknoten
./..
self
Der Kontextknoten selbst, der für zusätzliche Bedingungen ausgewählt wird
.
descendant
Untergeordnete Knoten
.//
attribute
Attributknoten
@
Knotentests schränken die Elementauswahl einer Achse ein:
Beispiel: .//Foo
wählt alle Elemente des untergeordneten Knotens mit dem Namen Foo.
*
Auswahl aller Elemente eines Knotens
text()
, comment()
und processing-instruction()
Auswahl von Knoten eines bestimmten Typs
ZCatalog ist die Zope Suchmaschine, die die Kategorisierung von und die Suche nach allen Zope-Pbjekten erlaubt. Dabei wird auch die Suche in externen Daten, die z.B. in einer relationalen Datenbank liegen, unterstützt. Darüber hinaus kann der ZCatalog auch zur Erstellung von Sammlungen von Objekten verwendet werden.
Volltextsuche und die gleichzeitige Suche in mehreren Indexen sowie das Gewichten der Felder in den Suchergebnissen werden unterstützt.
Weitere Informationen über den ZCatalog erhalten Sie im `Zope Book`_
XML-Dialekt, der die verschiedenen Zope-Komponenten verbindet.
Von Zope wird initial die in site-definition
angegebene Datei abgearbeitet, meist $INSTANCE/etc/site.zcml
. Diese bindet dann über <includes>-
-Tags alle weiteren ZCML-Konfigurationsdateien ein:
<!-- Load the meta -->
<include files="package-includes/*-meta.zcml" />
<five:loadProducts file="meta.zcml"/>
<!-- Load the configuration -->
<include files="package-includes/*-configure.zcml" />
<five:loadProducts />
<!-- Load the configuration overrides-->
<includeOverrides files="package-includes/*-overrides.zcml" />
<five:loadProductsOverrides />
<securityPolicy
component="Products.Five.security.FiveSecurityPolicy" />
*-meta.zcml
Diese Dateien gewährleisten, dass die ZCML-Anweisungen der eingebundenen Pakete bei der Abarbeitung der ZCML-Anweisungen vollständig zur Verfügung stehen.
*-configure.zcml
Hiermit werden ZCML-Dateien innerhalb der installierten Pakete abgearbeitet.
*-overrides.zcml
Damit können Konfigurationen von Paketen überschreiben werden.
securitypolicy.zcml
Hiermit wird die Security-Policy festgelegt
Das Starten der Instanz bricht ab, wenn in einer zcml
-Datei include package
angegeben wird, dieses Paket jedoch nicht installiert ist. Um dies zu vermeiden, kann include
an die Bedingung geknüpft werden, dass das Paket installiert ist, z.B.:
<include
zcml:condition="installed zope.app.zcmlfiles"
package="zope.app.zcmlfiles"
/>
<include
zcml:condition="not-installed zope.app.zcmlfiles"
package="zope.app"
/>
<include zcml:condition="installed some.package"
package="some.package" />
<include zcml:condition="not-installed some.package"
package=".otherpackage" />
Es können auch bestimmte Funktionen als Bedingung genannt werden. Diese Bedingungen können mit have
oder deren Abwesenheit mit not-have
angegeben werden, z.B.:
<include
package="Products.CMFCore" file="permissions.zcml"
xmlns:zcml="http://namespaces.zope.org/zcml"
zcml:condition="have plone-41" />
<configure
zcml:condition="not-have plone-4">
<!-- only when the Plone 4 feature has not been provided -->
</configure>
<configure
zcml:condition="not-have plone-5">
<!-- only when the Plone 5 feature has not been provided -->
</configure>
Mittels ZEO greift eine Zope-Instanz nicht unmittelbar auf einen Datenspeicher zu sondern per TCP/IP auf einen sog. ZEO-Server. Durch die Verwendung mehrerer Zope-Instanzen, die auf denselben ZEO-Server zugreifen, lässt sich die Last besser verteilen.
Weitere Informationen zu ZEO erhalten Sie im `Zope Book`_
Erlaubt die Verwaltung des Zope-Servers durch das Web.
Das Zope Management Interface lässt sich für die meisten vom Zope-Server ausgelieferten Objekte anzeigen, indem an die URL manage_workspace
angehängt wird.
Die ZODB bietet eine einfache Persistenz für Python-Objekte. Sie wird von Zope verwendet um Inhalte, Skripte und Konfigurationen zu speichern.
Das ZMI ist ein Web-Interface zum Verwalten der Inhalte der ZODB.
Zum Weiterlesen:
`Zope`_ ist ein objektorientierter, in der Programmiersprache Python geschriebener, freier Webanwendungsserver.
ZCA ist ein Python-Framework zur einfachen Erstellung eines komponentenbasierten Designs. Dabei sind Komponenten wiederverwendbare Objekte mit einem Interface, das beschreibt, wie dieses Objekt angesprochen werden kann. ZCA vereinfacht die Erstellung von zwei Basiskomponenten:
Komponenten, die aus anderen Komponenten erstellt werden um sie einem bestimmten Interface zur Verfügung zu stellen. Dabei sind Subscribers und Handlers zwei spezielle Typen von Adaptern.
Komponenten, die ein Interface anbieten und von einem Interface und einem Namen aufgerufen werden.
Zur ZCA gehören im wesentlichen drei Pakete:
wird verwendet um die Interfaces einer Komponente zu definieren.
bietet ein einfaches Event-System, siehe Event.
erleichtert die Erstellung, Registrierung und Retrieval der Komponenten.
Die Installation beider Paktete kann einfach erfolgen mit easy_install:
$ easy_install zope.component
ZopeSkel ist eine Sammlung von Vorlagen, mit denen sich schnell Zope-Projekte erstellen lassen.
Um solche Projekte zu erstellen verwendet ZopeSkel intern die Paste Script-Bibliothek.
Weitere Informationen zur Installation, den Vorlagen und Standardeinstellungen erhalten Sie in ZopeSkel..
Zope Page Templates ist eine Template-Sprache um XML-konforme Dokumente zu erstellen. Sie ist nahezu vollständig in TAL, TALES und METAL beschrieben.
Darüberhinaus hat ZPT jedoch einige zusätzliche Funktionen: Wird als Content-Type text/html
angegeben, müssen die Namensräume für TAL und METAL nicht angegeben werden. Auch werden HTML-Dokumente mit dem non-XML-Parser analysiert, der nachlässiger mit fehlerhaftem Markup umgeht.