Archetypes

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.

Content-Basisklassen

Ü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.

Schema

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.

Felder

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 RichWidget den WYSIWYG-Editor einbindet

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 true oder false.

searchable

Der Wert true schließt das Feld in die Searchable Text-Suche ein.

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 Default. Durch den Aufruf von finalizeATCTSchema() werden verschiedene Änderungen am Schema vorgenommen, die das Plone-spezifische Erscheinungsbild ermöglichen.

read_permission,

write_permission

Die Namen der Berechtigungen, die zum Lesen bzw. Schreiben des Feldes erforderlich sind. Die Standardwerte sind View bzw. Modify portal content.

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 AttributeStorage

AttributeStorage

die Feldwerte werden in Attributen dieses Objekts gespeichert. Die Attribute haben dabei denselben Namen wie das Feld

AnnotationStorage

speichert die Werte in Zope3-Annotations, wodurch das Risiko von Namenskonflikten vermieden wird

Widgets

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

label

Eine Zeichenkette oder übersetzbare Nachricht, die als Etikett des Widgets verwendet wird

description

Eine Zeichenkette oder übersetzbare Nachricht, die als Hilfe-Text verwendet wird

condition

Ein TALES-Ausfruck, die bestimmt, ob ein Widget angezeigt wird. Die Variablen object, portal und folder sind in diesem Kontext verfügbar

size

Die Länge einer Textbox oder die Höhe einer Auswahlbox

rows

Höhe einer Textbox

default_output_type

Wird von RichWidget verwendet um den eingegebenen Text bei der Ausgabe zu verändern

text/x-html-safe

nutzt Plone’s HTML-Filter-Richtlinien zum Ausfiltern potentiell gefährlicher Tags

text/html

kann verwendet werden, wenn Sie Ihren Nutzern trauen

Vokabularien

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.

Schlagworte in einen eigenen Index schreiben

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.

Feld- und Objektvalidierung

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

Klassengenerator

atapi.registerType(Registrant, PROJECTNAME)

registriert den Artikeltyp Registrant, indem der Klassengenerator aufgerufen wird und jedem Feld der Registrant-Klasse drei Methoden hinzufügt:

Accessor

getter-Methode

Edit accessor

falls der Accessor eine Transformation vornimmt und das Editierfeld anders eingelesen werden muss.

Mutator

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",
    ...
),