Dynamisches Validierungs-Constraint erstellen

Es ist möglich, im Quellcode eines Plone add-ons nicht nur hart-codiert mit regulären Ausdrücken Validierungsfunktionen zu erstellen, die dann als Constraint bei einem Inhaltsfeld aufgerufen werden, beispielsweise um zu prüfen, ob in einem Dateifeld nur Dateien mit der zulässigen Dateierweiterung ausgewählt werden. Solche Funktionen lassen sich auch dynamisch erstellen.

Das will ich hier an einem Beispiel-Add-on mit drei Inhaltstypen (Dexterity Content Types) zeigen, die hierarchisch strukturiert sind. Der erste Typ ist das Center, das dann die Typen “Projekt” beinhaltet, welches wieder den Typ “Release” beinhaltet. Die letzteren beiden Typen dürfen die Mitglieder (Member) anlegen, während das Center nur vom Seiten-Admin angelegt und bearbeitet werden kann.

Im Center-Typ wird ein spezielles Feld angelegt, das vom Seiten-Admin mit den aktuell zugelassenen Dateiendungen befüllt wird. Diese einzelnen zulässigen Endungen werden durch einen senkrechten Strich (Pipe) getrennt. Dieses Feld sieht nun folgendermaßen aus:

allowed_apimageextension = schema.TextLine(
    title=_(u'Allowed Image File Extension'),
    description=_(u'Fill in the allowed image file extensions'),
    )

Es ist nun erforderlich, den Inhalt dieses Feldes in den “portal_catalog” von Plone zu indexieren. Dazu wird der Datei “catalog.xml” ein neuer Index hinzugefügt:

<index name="allowedapimageextensions" meta_type="FieldIndex">
<indexed_attr value="allowed_apimageextension" />
</index>

In dem Programm-Modul “Projekt” wird nun eine neue Funktion angelegt:

from plone import api
from zope.interface import Invalid

def validateimagefileextension(value):
    catalog = api.portal.get_tool(name='portal_catalog')
    result = catalog.uniqueValuesFor('allowedapimageextensions')
    pattern = r'^.*\.({0})'.format(result[0])
    matches = re.compile(pattern, re.IGNORECASE).match
    if not matches(value.filename):
        raise Invalid(u'Your message to the user.')
    return True

Die Funktion ruft zunächst den portal_catalog über das Plone-Api auf.
Danach werden nur die im Index “allowedapimageextensions” enthaltenen einzelnen Werte aufgerufen. Dies gibt ein Tupel zurück, das nur einen Wert an der ersten Stelle [0] enthält, die erlaubten Dateiendungen, getrennt durch einen senkrechten Strich (Pipe). Dieser Wert wird zum Erstellen des Musters (“pattern”) als veränderbarer Wert für die erlaubten Dateiendungen verwendet.
Damit wird dann weiter zum Erstellen des regulären Musterobjektes gearbeitet und geprüft, ob es mit dem Dateinamen bzw. dessen Namenserweiterung passt. Falls nicht, wird mit “Invalid” dem Benutzer eine Fehlermeldung angezeigt.

Der Name dieser Funktion wird dann bei dem zu validerenden Feld als Constraint eingetragen.

from plone.namedfile.field import NamedBlobImage

project_logo = NamedBlobImage(
    title=_(u"Logo"),
    description=_(u"Add a logo for the project."),
    constraint=validateimagefileextension
)

Damit der Benutzer auch weiß, welche Dateiendungen aktuell zulässig sind, ist es erforderlich, als Ergänzung zu dem Feld “project_logo” ein Inhaltsfeld zu erstellen, das nur angezeigt, aber nicht bearbeitet werden kann. Dies geschieht mit dem Modus “Display”.

from plone.autoform import directives

directives.mode(eupimageextension='display')
eupimageextension = schema.TextLine(
    title=_(u'The following file extensions are allowed for project logo files (upper case and lower case and mix of both):'),
    defaultFactory=allowedapimagefileextensions,
)

Der Inhalt dieses Feldes wird mit einer Funktion mit einem @provider-Dekorator aus dem Inhaltsfeld des Center geholt:

from zope.schema.interfaces import IContextAwareDefaultFactory

@provider(IContextAwareDefaultFactory)
def allowedapimagefileextensions(context):
    return context.allowed_apimageextension.replace("|", ", ")

In der Return-Funktion wird für eine bessere Integration in den Anzeigetext wird der senkrechte Strich zwischen den Dateiendungen durch ein Komma und ein Leerzeichen ersetzt.

Die letztere Funktion ist in dem Inhaltstyp “Release” ein wenig anders zu schreiben, da sich die Release-Objekte in Relation zum Center-Inhaltstyp noch eine Stufe tiefer in der Hierarchie befinden (hier mit einem Beispiel für eine andere Dateierweiterung):

from zope.schema.interfaces import IContextAwareDefaultFactory
from Acquisition import aq_inner, aq_parent

@provider(IContextAwareDefaultFactory)
def allowedaddonfileextensions(context):
    context = context.aq_inner.aq_parent
    return context.allowed_addonfileextension.replace("|", ", ")

Hier wird in der vorletzten Zeile der Context anders gesetzt.