Zope2: ZODB-Inhalte mit GenericSetup exportieren und importieren
Wenn Konfigurationsobjekte in der ZODB liegen kann man sie mit Products.GenericSetup exportieren und dann in eine Versionsverwaltung stecken.
Ich habe meine ersten Schritte mit Products.GenericSetup (Version 1.2) in Zope 2.8 gemacht. Diese Versionen sind zwar nicht mehr ganz aktuell, aber das Prinzip sollte auch mit aktuellen Versionen ähnlich sein.
Voraussetzungen
Für einen Export braucht man folgende Dinge:
- Export/Import-Handler für das zu exportierende Objekt
- Registrierung des Handlers in ZCML
- Exporter (um das Objekt in der ZODB zu finden und dann zu exportieren)
- Profil
- Registrierung des Exporters
- in ZCML registrierten Export/Import-Handler (wie beim Export)
- Importer (um das Elternobjekt in der ZODB zu finden und da zu importieren)
- Registrierung des Importers
Export/Import-Handler
Dieser Handler ist eine Klasse, Basis dafür ist Products.GenericSetup.utils.XMLAdapterBase. Beispiel für eine Klasse, die alle Properies eines Objektes exportieren und importieren kann: (In Anlehnung an Products.GenericSetup.OFSP.exportimport.FolderXMLAdapter)
class ELWTicket(Products.GenericSetup.utils.XMLAdapterBase,
Products.GenericSetup.utils.PropertyManagerHelpers):
# __uses_for__ ist nur ein Hinweis für den Entwickler, es wird sonst nicht
# benutzt.
__used_for__ = mypackage.IELWTicket
# damit wird der Name angegeben der im Log erscheint.
_LOGGER_ID = 'elwticket'
def _exportNode(self):
"""Export the object as a DOM node."""
# Anlegen des Objekts im XML
node = self._getObjectNode('object')
# speichern der Properties
node.appendChild(self._extractProperties())
# Nachricht an den Logger über erfolgreichen Export.
self._logger.info('ELWTicket exported.')
return node
def _importNode(self, node):
"""Import the object from the DOM node."""
# Säubern der Properties vor Import, wenn erforderlich.
if self.environ.shouldPurge():
self._purgeProperties()
# Properties schreiben.
self._initProperties(node)
self._logger.info('ELWTicket imported.')
def _importSimpleNode(self, node):
"""Import node attributes set in WebTickets.xml file."""
# Beim Import wird erwartet, dass das erstellte Objekt eine ID hat, aber
# die wird nirgends gesetzt. GenericSetup ist sonst wohl eher for Tools
# gedacht.
obj = self.context
obj.id = str(node.getAttribute('name'))
# auf _importSimpleNode wird über eine Property zugegriffen, die noch zu bauen
# ist
node = property(
Products.GenericSetup.utils.XMLAdapterBase._exportSimpleNode,
_importSimpleNode)
ZCML-Registrierung des Handlers
Der Handler muss als Multiadapter für das Interface Products.GenericSetup.interfaces.IBody registriert werden:
<zope:adapter
factory=".exportimport.ELWTicket"
provides="Products.GenericSetup.interfaces.IBody"
for="mypackage.interfaces.IELWTicket
Products.GenericSetup.interfaces.ISetupEnviron"
/>
Es muss (zumindest in Zope 2.8) das Interface des Objektes angegeben werden, das exportiert werden soll, damit der Adapter auch gefunden wird.
Exporter
Der Exporter ist eine Funktion, die das zu exportierende Objekt findet und den Export auslöst.
def exportELWTickets(context):
"""Exporter."""
# Die Site auf dem context liefert den Ordner in dem das setup_tool
# (GenericSetup-Objekt) liegt.
parent = context.getSite()
folder = getattr(parent, 'WebTickets')
Products.GenericSetup.utils.exportObjects(
folder, '/Auftritte/', context)
Meinem Exporter liegen folgende Annahmen zu grunde:
- Das GenericSetup-Werkzeug (Tool) liegt in /Auftritte
- Die zu exportierenden ELWTickets liegen in /Auftritte/WebTickets (einem gewöhnlichen Folder)
Der Export führt dann dazu, dass der Ordner WebTickets und die darin liegenden ELWTickets exportiert werden.
Profil
Im Profil wird der Exporter registriert und dort können die Ergebnisse des Exports abgelegt werden, damit beim Import nicht ein Dokument hochgeladen werden muss. Das Profil ist ein Ordner und liegt in einem Ordner mit dem Namen profiles. Es ist per ZCML zu registrien:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="genericsetup">
<genericsetup:registerProfile
name="default"
title="Webtickets"
description="Im- and export the webticket objects."
/>
</configure>
Das hier registriere Profil heißt default, es muss also einen Ordner profiles/default neben configure.zcml geben. Titel und Beschreibung (description) werden in der Benutzungsoberfläche von GenericSetup angezeigt.
Registrierung des Exporters
Der Exporter wird leider nicht per ZCML sondern über ein eigenes XML-Format registriert. Das Dokument dazu muss export_steps.xml heißen und in profiles/default liegen.
<?xml version="1.0"?>
<export-steps>
<export-step
id="elw_tickets"
handler="mypackage.exportimport.exportELWTickets"
title="Export ELWTickets.">
</export-step>
</export-steps>
Export
Exportiert wird über setup_tool --> Export. Dort wird „Export ELWTickets.“ als Schritt (Step) angezeigt. Wenn man exportieren auswählt erhält man ein Dokument (mit tar und gzip verpackt), welches eine XML-Repräsentation der exportierten Objekte enthält.
- Category(s)
- Zope 2
Zope3: Objekte mit Attributen, die innerhalb eines Ordners eindeutig sind
Wenn sicherzustellen ist, dass alle Objekte in einem Container für ein bestimmtes Attribut nur eindeutige Werte haben, ist eine ganze Menge Arbeit nötig.
Beispiel: Ein Ordner enthält Objekte, die als Schlüsselwort anderen Objekten zugewiesen werden können. Es ist sicherzustellen, dass keiner der Titel der Schlüsselworte doppelt vorkommt.
So geht's (leider) nicht ...
Diese Aufgabe lässt sich nicht über eine Bedingung (constraint) auf dem Schema-Feld lösen, da der Test Zugriff auf den Container benötigt, in dem die Schlüsselworte liegen. Dieser Zugriff ist aber beim Anlegen des Schlüsselwort-Objektes nicht möglich, da in der Regel zuerst die Attributwerte geschrieben werden und dann das Objekt zum Container hinzugefügt wird. Feldübergreifende Bedingungen scheiden für den Test auch aus, da sich die zu modellierende Bedingung über mehrere Objekte erstreckt.
Es handelt sich hier eigentlich um eine Bedingung auf dem Container: kein Wert eines Titel-Attributes aller Objekte darf mehrfach vorkommen. Aber die Bedingungen aus zope.app.container.constraints können dafür nicht benutzt werden, da sie nur prüfen, ob ein Objekt einem Container hinzugefügt werden kann. Wohingegen das Ereignis „Container verändert“ nur dann ausgelöst wird, wenn der Container geändert wurde, was aber nicht der Fall ist, wenn der Titel eines Schlüsselworts nachträglich geändert wird.
... aber so
Es bleibt nur noch die Möglichkeit, beim Ereignis „Objekt verändert“ ein Veto einzulegen, wenn durch die Veränderung Dubletten erzeugt wurden:
import zope.interface
def uniqueTitles(obj, event):
# Wenn das Objekt noch nicht in einem Container liegt, kann nichts getestet
# werden.
if getattr(obj, '__parent__', None) is None:
return
container = obj.__parent__
# Die Titel der Objekte im Container stehen z. B. auf dem title-Attribut
titles = [x.title for x in container.values()]
# Wenn die Menge der Titel (ohne Duplikate) kleiner ist als die Länge der
# Liste der Titel (mit eventuellen Duplikaten), dann wird ein Fehler
# erzeugt.
if len(titles) != len(set(titles)):
raise zope.interface.Invalid(u'Dieses Schlüsselwort existiert schon.')
Anmerkung: Diese Implementierung ist dann problematisch, wenn an dieser Methode vorbei uneindeutige Titel in den Container gekommen sind. Dann kann nämlich kein weiteres Objekte hinzugefügt werden und es wird eine irreführende Fehlermeldung präsentiert.
Damit die Test-Methode ausgeführt wird, muss sie via ZCML noch als Abonnent registriert werden: Da das Schlüsselwort beim ersten Ereignis „Objekt verändert“ eventuell noch nicht im Container liegt, wird die Methode zusätzlich noch für das Ereignis „Objekt hinzugefügt“ registriert.
<subscriber
for="meinpaket.interfaces.IMeinObjekt
zope.lifecycleevent.IObjectModifiedEvent"
handler=".mycontainer.uniqueTitles"
/>
<subscriber
for="meinpaket.interfaces.IMeinObjekt
zope.app.container.interfaces.IObjectAddedEvent"
handler=".mycontainer.uniqueTitles"
/>
Leider ist in den gängigen Formular-Bibliotheken (z3c.form und zope.formlib) nicht per se vorgesehen, dass die Ereignisse „Objekt hinzugefügt“ bzw. „Objekt geändert“ zu einer Ausnahme (exception) führen. Also sind die Formulare zum Hinzufügen bzw. Bearbeiten des Schlüsselworts entsprechend anzupassen. Hierbei unterscheiden sich die Formular-Bibliotheken etwas.
z3c.form
Das Hinzufügen-Formular muss folgende add-Methode haben:
import transaction
import z3c.form.form
import zope.app.container.interfaces
import z3c.form.interfaces
class AddForm(z3c.form.form.AddForm)
def add(self, obj):
nc = zope.app.container.interfaces.INameChooser(self.context)
name = nc.chooseName('', obj)
try:
self.context[name] = obj
except zope.interface.Invalid, e:
transaction.doom()
raise z3c.form.interfaces.ActionExecutionError(e)
Das Hinzufügen des Objekts zum Container (self.context) löst das Ereignis „Objekt hinzugefügt“ aus. Das führt dazu, dass die Konsistenz der Titel überprüft wird. Kommt es dabei zu einer Ausnahme wird die Transaktion für gescheitert erklärt (transaction.doom()). Die Transaktion wird zwar ganz normal zu Ende geführt, aber am Ende garantiert abgebrochen. Die aufgefangene Ausnahme wird, in eine neue Ausnahme eingewickelt, weitergegeben und später von z3c.form dem Nutzer als Fehler im Formular präsentiert.
Das Bearbeiten-Formular muss folgende applyChanges-Methode haben:
import transaction
import z3c.form.form
import z3c.form.interfaces
class EditForm(z3c.form.form.EditForm):
def applyChanges(self, data):
try:
super(EditForm, self).applyChanges(data)
except zope.interface.Invalid, e:
transaction.doom()
raise z3c.form.interfaces.ActionExecutionError(e)
Die Methode applyChanges wird beim Speichern des Formulars aufgerufen, es wird dabei das Ereignis „Objekt geändert“ ausgelöst, Wird dann in der Test-Methode eine Inkonsistenz festgestellt (Ausnahme wird ausgelöst), wird wiederum dafür gesorgt, dass die Transaktion nicht in die ZODB geschrieben wird, sondern dem Nutzer eine Fehlermeldung angezeigt wird.
zope.formlib
Für zope.formlib ist das Vorgehen ähnlich, aber etwas komplizierter, weil mehr händisch zu machen ist. Das Hinzufügen-Formular muss folgende add-Methode haben.
import transaction
import zope.formlib
import zope.interface
from zope.i18nmessageid import ZopeMessageFactory as _
class AddForm(zope.formlib.form.AddForm):
def add(self, object):
try:
super(AddForm, self).add(object)
except zope.interface.Invalid, e:
self.errors = (e,)
self.form_reset = False
self.status = _('There were errors')
transaction.doom()
Das Hinzufügen des Objekts zum Container (super-Aufruf) löst
das Ereignis „Objekt hinzugefügt“ aus. Das führt dazu, dass die
Konsistenz der Titel überprüft wird. Kommt es dabei zu einer Ausnahme,
wird die Transaktion für gescheitert erklärt (transaction.doom()).
Das heißt, die Transaktion wird zwar ganz normal zu Ende geführt,
aber am Ende garantiert abgebrochen. Die aufgefangene Ausnahme
wird gespeichert und später von
zope.formlib dem Nutzer als Fehler im Formular präsentiert. Es muss dafür gesorgt werden, dass das Formular nicht zurückgesetzt, sondern mit den Nutzereingaben wiederbefüllt wird.
Im Bearbeiten-Formular muss die Methode handle_edit_action geeignet überschrieben werden:
import transaction
import zope.formlib
import zope.interface
from zope.i18nmessageid import ZopeMessageFactory as _
class EditForm(zope.formlib.form.EditForm):
@zope.formlib.form.action(
_("Apply"), condition=zope.formlib.form.haveInputWidgets)
def handle_edit_action(self, action, data):
try:
super(EditForm, self).handle_edit_action.success(data)
except zope.interface.Invalid, e:
self.errors = (e,)
self.form_reset = False
self.status = _('There were errors')
transaction.doom()
Der Aufruf der Bearbeiten-Aktion der Elternklasse ist etwas kompliziert, weil der Dekorator die Methode in eine Aktions-Instanz einwickelt. Der Rest ist analog zum Hinzufügen-Fomular.
- Category(s)
- Zope 3
Programmatisch einen Zope3-Ordner in eine Site (lokale Komponentenregistrierung) umwandeln
Eine Site enthält einen "SiteManager" und kann damit lokale Utilities beinhalten. Aus einem Ordner im ZMI eine Site zu machen, sind nur ein paar Klicks, programmatisch ist es aber auch nicht viel schwerer.
Der Begriff Site ist nicht mehr gebräuchlich, treffender spricht man von einer lokalen Komponentenregistrierung.
Angenommen, immer wenn eine Instanz der Klasse MySite angelegt wird, soll daraus eine Site gemacht werden. Es wird weiterhin angenommen, dass MySite das Interface IMySite implementiert.
import zope.interface
from zope.app.component.site import SiteManagerContainer
from zope.app.container.btree import BTreeContainer
class IMySite(zope.interface.Interface):
pass
class MySite(BTreeContainer, SiteManagerContainer):
pass
MySite muss von SiteManagerContainer zu erben, damit die Instanz in eine Site umgewandelt werden kann.
Die Umwandlung geschieht, nachdem die Instanz von MySite dem Container hinzugefügt wurde. Es ist also folgendes in mysite.py zu ergänzen:
import zope.app.appsetup.bootstrap
import zope.app.component.interfaces
import zope.app.component.site
import zope.app.component.hooks
import zope.app.intids
import zope.app.intids.interfaces
import zope.component
import zope.app.container.interfaces
import mypackage.interfaces
@zope.component.adapter(mypackage.interfaces.IMySite,
zope.app.container.interfaces.IObjectAddedEvent)
def onMySiteAdded(my_site, event):
# Macht aus my_site eine Site, wenn es noch keine ist
if not zope.app.component.interfaces.ISite.providedBy(my_site):
site_manager = zope.app.component.site.LocalSiteManager(my_site)
my_site.setSiteManager(site_manager)
# Wenn in der Site lokale Utilities installiert werden sollen, die
# sich auf den thread-globalen Context verlassen, dann ist dieser
# temporär zu setzen, damit in der richtigen, nämlich der neuen
# Site gesucht wird.
try:
old_site = zope.app.component.hooks.getSite()
zope.app.component.hooks.setSite(my_site)
# Hier kann man jetzt die Utilities installieren.
# z.B. ein IntId-Util
zope.app.appsetup.bootstrap.ensureUtility(my_site,
zope.app.intid.interfaces.IIntIds, '', zope.app.intid.IntIds)
finally:
# Zum Schluss wird der alte Zustand wieder hergestellt.
zope.app.component.hooks.setSite(old_site)
Der Dekorator vor der Methode stellt sicher, dass die Methode nur für Objekte aufgerufen wird, die das angegebenen Interface bereitstellen und ach dann nur, wenn sie zu einem Container hinzugefügt werden. Da ein Verschieben eines Objektes als Löschen am ursprünglichen Platz und Hinzufügen am neuen Platz realisiert ist, muss geprüft werden, ob das Objekt schon eine Site ist.
Nun ist die Methode noch als Abonnent zu registrieren. (Die Details des Abonnements regelt der Dekorator der Methode.)
<subscriber handler=".mysite.onMySiteAdded" />
- Category(s)
- Zope 3
Python 2.3 unter Mac OS X 10.5 (Leopard) bauen
Python 2.3 lässt sich unter Leopard nicht ohne Eingriffe bauen. Ich erläutere hier, was zu tun ist, um ein Python ohne Framework-Unterstützung zu bauen, so wie man es zum Beispiel für ältere Zope-Versionen benötigt.
Nach dem Herunterladen von Python 2.3.6 und dem Entpacken muss man erst mal das configure-Script bearbeiten, da ein Workaround benutzt wird, der in Leopard nicht mehr nötig ist und nicht mehr funktioniert.
sed "s/-u __dummy //" configure > configure.leo
chmod 755 configure.leo
./configure.leo --prefix=<Installationsverzeichnis>
Das Configure-Script setzt einige Präprozessordirektiven falsch, die sind folgendermaßen zu korrigieren:
echo '#undef _POSIX_C_SOURCE' >> pyconfig.h
echo '#undef _XOPEN_SOURCE' >> pyconfig.h
echo '#define HAVE_BROKEN_POSIX_SEMAPHORES' >> pyconfig.h
Jetzt ist es möglich, Python zu bauen und zu installieren:
make
make install
Nun fehlt noch readline-Unterstützung. Dazu gibt es verschiedene Varianten. Die folgende von bbum@mac.com war die einzige, die bei mir funktioniert hat:
svn co http://svn.red-bean.com/bbum/trunk/pyreadline/
cd pyreadline
tar -xzf readline-5.1.tar.gz
cd readline-5.1
./configure --disable-shared --enable-static
sudo make install
cd ../readline-0.0.0
<Pfad zu installiertem python> setup.py install
Achung: readline muss global (per sudo) installiert werden, weil readline-0.0.0 leider mit absoluten Pfaden arbeitet.
Diese Anleitung basiert auf der Anleitung für Python2.3 unter Tiger und Readline für Python.
- Category(s)
- Python
Zope3: Testen von felderübergreifenden Bedingungen in Interfaces mit zope.formlib -- aktualisiert
Um felderübergreifende Bedinungen (interface invariants) mit zope.formlib testen zu können sind in Zope 3.4 keine Klimmzüge mehr nötig.
Dieser Eintrag ist eine Aktualisierung des Eintrags Testen von felderübergreifenden Bedingungen in Interfaces mit zope.formlib, der sich auf Zope 3.2 bezieht. Mit Zope 3.4 ist alles viel einfacher. (Zope 3.3 habe ich nicht getestet, da die Ideen aber aus Philipp von Weitershausens Buch stammen, sollte es auch da funktionieren.)
Definition
Feldübergreiffende Bedingungen werden in Interfaces als invariants
definiert. Als Beispiel soll ein Passwortfeld und eins für die
Wiederholung des Passworts dienen. Beide Werte müssen also gleich sein.
Wenn die Bedingung nicht erfüllt ist, soll eine Exception geworfen
werden. Da es sich um einen Validierungsfehler handelt, nutze ich zope.interface.Invalid.
Das Interface hat dann im Minimum folgende Gestalt:
import zope.interfaceEs werden die beiden Felder definiert und zum Schluss wird die Funktion arePasswordsEqual als über den Dekorator (@zope.interface.invariant) als Invariante des Interfaces definiert.
class IUser(zope.interface.Interface):
password = zope.schema.Password(title=u"Passwort")
password2 = zope.schema.Password(title=u"Wiederholung des Passworts")
@zope.interface.invariant
def arePasswordsEqual(user):
if user.password != user.password2:
raise zope.interface.Invalid(
u"""Das Passwort und die Wiederholung sind nicht gleich.""")
Benutzung mit zope.formlib
zope.formlib testet die Invarianten automatisch und zeigt Fehler oberhalb des Fomulars an. Es wird der Text der Exception angezeigt, der sich problemlos internationalisieren lassen sollte.
- Category(s)
- Zope 3
Grundlagen von Webdesign
Überblick
Das Medium Web unterscheidet sich vom Medium Druck. Dies erfordert entsprechend angepasste Arbeitsweisen bei der Gestaltung von Webseiten. Die folgenden Artikel geben einen Überblick über die Besonderheiten und weisen insbesondere auf Unterschiede zwischen Web und Druck hin:- Artikel über Webdesign (Wikipedia)
- Artikel über Webtypographie (Wikipedia)
- Planung und Ablauf von Webprojekten (selfhtml.org)
- Designtheorie (webdesign-referenz.org)
Medium Internet
Das Medium Internet fordert den Designer, sich mit weitergehenden Themen zu beschäftigen, die im Druck nicht oder in anderer Form relevant sind:- Barrierefreiheit
- Benutzerfreundlichkeit
Technische Detailinformationen
Umfassende Publikationen und Archive
Da es bereits umfassende Webseiten gibt, die Informationen für gute Gestaltung im Web zusammengetragen haben und pflegen, haben wir hier einige verlinkt:- webdesign-referenz.de
- A List Apart (englisch)
- Eric Meyer on CSS
- Current Style in Webdesign
- Web Design from Scratch
Gallerien
Diese Seiten sammeln Beispiele guter Gestaltung, die sich mit guter technischer Implementierung verträgt:- Category(s)
- Webdesign