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
- The URL to Trackback this entry is:
- http://blog.gocept.com/zope3-objekte-mit-eindeutigen-titeln-innerhalb-eines-ordners/tbping