Adottare ed estendere prodotti esistenti con Zope 3
Note: Return to tutorial view.
Introduzione
Grazie alle caratteristiche di Zope 3 (presenti anche in Zope 2) si può estendere un prodotto esistente in maniera pulita, senza cambiare parti del codice esistente in maniera diretta. Questo tutorial aggiunge alcune note sulla creazione di un tipo di contenuto basato su categorie.
Prenderemo come sempio un caso d'uso: estendere Quills. Il codice si trova nel prodotto keywordadapter nel Collective.
Partiamo da un weblog (creato dall'autore di questo tutorial in lingua inglese, Maurits van Rees). Al momento della sua creazione si trattava di un blog scritto "a mano" con Zope, costituito interamente da page templates e scripts, che sono immagazzinate nel database Zope. Funziona, è abbastanza veloce, però non è molto elegante, è un po' confuso e quasi "raffazzonato". È necessario eseguire troppe operazioni prima di poter pubblicare una nuova voce sul weblog. Quindi c'è molto margine di miglioramento. Inoltre, ci sono molti altri weblogs disponibili in Plone. Quills sembra essere una valida soluzione, Maurits scopre di conoscere un paio di persone che già lo usano, per esempio suo fratello; decide dunque anche lui di usarlo.
In realtà, Maurits ha due weblogs sul suo sito. Uno è un blog generale in cui pubblica contenuti di carattere personale o voci legate alla programmazione. L'altro è un blog in olandese per la sua chiesa. Oggi è principalmente costituito da podcast, contiene brevi voci con un link che porta ad un file mp3 esterno, contenente le messe della sua chiesa, e voci più consistenti con un link ad un file mp3 locale, contenente solo i sermoni. Quindi è possibile che una singola messa abbia due voci. Di nuovo, funziona, è abbastanza veloce, però non è molto elegante, è un po' confuso...
Per questo secondo blog, Quills sembra essere una valida scelta. Ma per rendere il cambiamento davvero utile, Maurits vorrebbe due campi extra per il suo blog audio, in modo che possano contenere i link a questi file mp3. Per quanto riguarda i sermoni, sarebbe ancora più utile avere la possibilità di caricare i file via Quills, in modo che si generi un link sempre funzionante. Ma questo può anche essere fatto in seguito. Per ora, Maurits si accontenta di due link extra nei WeblogEntry di Quills.
Estendere con Zope 2
Un nuovo tipo di contenuto
Partiamo con la creazione di un tipo di contenuto totalmente nuovo. Con ArchGenXML se ne può creare facilmente uno di base, che possiamo chiamare AudioEntry, con tutte le caratteristiche che servono. Ma, di nuovo, altre persone hanno già pensato molto su cosa una voce di un weblog dovrebbe contenere e cosa dovrebbe fare. Si introdurrebbe un'altra applicazione per il weblog, che Plone in realtà non necessita e che è in realtà mantenuta interamente da Maurits e dal suo team. In alcune occasioni anche questa opzione torna utile, però...
I tempi passati di Zope 2
La seconda opzione disponibile è estendere Quills con Zope 2: si può creare una classe AudioEntry che erediti dalla WeblogEntry di Quills. Se davvero questa classe offre qualcosa di nuovo, questa potrebbe essere una scelta profittevole. Un buon esempio è RichDocument, che estende lo schema di ATDocument al fine di aggiungere alcuni attributi. La classe eredita da ATDocument e usa quindi le funzioni già definite per quel documento base, aggiungendone alcune che possono essere utili. Per ulteriori informazioni si può consultare il tutorial in inglese di Martin Aspeli Extending ATContentTypes.
Ma come ciò può essere utile per questo caso d'uso, "Estendere Quills"? AudioEntry erediterebbe da WeblogEntry e la estenderebbe con due attributi per i link. Ma non è tutto qui. Per ora un Weblog Quills può solo contenere cartelle e WeblogEntries. Aggiungendo AudioEntry non si ha ancora in mano niente. Certo ci si può lavorare per cercare di raggiungere le funzionalità che si desiderano, ma potrebbe anche essere necessario riscrivere la classe Weblog con la classe AudioWeblog, tipo di operazione che può facilmente sfuggire di mano e risultare in una lunghissima serie di altre operazioni di riscrittura. Inoltre, se qualcuno avesse un'idea simile e introducesse una VideoWeblogEntry, questi due prodotti sarebbero incompatibili.
Certamente ci sono dei casi d'uso, come RichDocument, in cui utilizzare Zope 2 porta a ottimi risultati. Ma in questo caso si vogliono solo aggiungere due link. Ci sarà un via più facile e meno intrusiva per raggiungere questo obiettivo?
Zope 3: una nuova speranza
Quando si estende un prodotto con Zope 3, si adatta un tipo di contenuto esistente senza crearne uno nuovo. In questo caso d'uso ciò significa che bisogna solo adattare WeblogEntry, senza dover cambiare la classe Weblog. Inoltre, si può ottenere che gli adattamenti fatti funzionino anche con la classe VideoWeblogEntry, a patto che quella classe implementi l'interfaccia IWeblogEntry definita da Quills.
Già ci si accorge che in questo caso Zope 3 porta dei vantaggi, rendendo i cambiamenti che si devono effettuare molto meno intrusivi e molto più compatibili con altri aggiustamenti.
Verranno usate e introdotte le seguenti tecnologie di Zope 3:
- events (eventi)
- utilities (utility)
- adapters (adattatori)
- annotations (annotazioni)
Strategia generale
Verranno creati due prodotti: uno generale per estendere un oggetto quando questo soddisfa alcuni criteri, e uno specifico per estendere le WeblogEntry di Quills quando contengono dei contenuti audio. La strategia per il prodotto generale è:
- Un event è gestito da un gestore di events, che usa una...
- utility per decidere se un...
- adapter è necessario, nel qual caso...
- annotations vengono aggiunte all'oggetto originale.
Quindi: quando un certo evento ha luogo, viene richiesto ad una utility se questo evento giustifica un adattamento di un oggetto. L'adattamento è implementato aggiungendo annotazioni a quell'oggetto.
Tutto ciò è implementato dal prodotto keywordannotator. Questa strategia può probabilmente sembrare un po' misteriosa all'inizio, ma è normale e risulterà inoltre più chiara leggendo la sezione successiva, in cui questa idea generale viene tradotta in una strategia specifica per il caso d'uso che stiamo seguendo.
La strategia in Quills
Ecco come vengono usate le quattro tecnologie di Zope 3 che abbiamo appena visto nella strategia specifica relativa al caso d'uso:
- event
- Qualcuno aggiunge una parola chiave audio ad una voce WeblogEntry. A questo punto Zope lancia quello che viene chiamato un evento IObjectModified. Si registra un gestore di eventi che agisce ogni qual volta una WeblogEntry (o in realtà qualsiasi oggetto che richiede di implementare l'interfaccia IWeblogEntry) viene modificata o aggiunta.
- utility
Il codice che gestisce questo evento richiama una utility. Questa si rivolge alla WeblogEntry e decide che l'aggiunta di questa parola chiave significa che questa voce ora implementa anch'essa l'interfaccia marker IMaudio che definiremo. Se un oggetto implementa o fornisce un'interfaccia "normale", allora questo oggetto possiede tutte le funzioni e gli attributi che sono definiti da quell'interfaccia. Un'interfaccia marker non ha nessuna funzione o attributo, quindi si tratta fondamentalmente solo di un'etichetta: l'oggetto viene marchiato come IMaudio.
Bisogna anche assicurarsi che sia possibile aggiungere in seguito annotazioni a questa voce, facendo in modo che fornisca l'interfaccia standard IAttributeAnnotatable definita in Zope 3.
- adapter
- Ora si può adattare (estendere) quest'oggetto che implementa IMaudio e aggiungere annotazioni. L'adattamento fondamentalmente è molto simile all'ereditare (ciò che avviene in Zope 2), ma con una tecnica differente. Viene messo temporaneamente un involucro ad un oggetto, viene eseguita un'azione su di esso (in questo caso aggiungere annotazioni) e poi viene rimosso l'involucro. Ogni altro codice (per esempio lo stesso codice in Quills) non sa o non presta attenzione al fatto che l'oggetto WeblogEntry è stato adattato o messo in un involucro e ora ha qualcosa in più. Per quel codice, la WeblogEntry è ancora una normale WeblogEntry, anche se ora possiede delle annotazioni.
- annotations
- Le annotazioni possono essere aggiunte in diversi modi, ma il più usato è il metodo associato con l'interfaccia IAttributeAnnotatable che è stata menzionata sopra. Nel nostro caso, due link vengono aggiunti in un nuovo attributo nascosto di quella WeblogEntry.
Tutto ciò è implementato dal prodotto quadapter (Quills Adapter), che usa il prodotto keywordannotator. Infatti, il prodotto quadapter viene aggiunto nella examples directory di keywordannotator. In ogni modo, entrambi i prodotti hanno la possibilità di essere testati, si può quindi eseguire i test e controllare che tutto funzioni.
Questa sezione è piuttosto importante, perché definisce gli obiettivi e la strategia generali, elementi essenziali per capire a fondo la sezione seguente che presenta il codice effettivo. Dall'altra parte, leggere la sezione che segue potrebbe essere un aiuto per comprendere bene questa strategia.
Ognuna delle sezioni seguenti presentano come prima cosa il codice dal prodotto generale keywordannotator e poi il codice dal prodotto specifico quadapter. Può essere difficile capire le azioni di keywordannotator, ma sarà in genere di aiuto leggere il codice corrispondente di quadapter.
Gestione eventi
Registrazione zcml
Per il prodotto generale keywordannotator serve una gestione degli eventi che si attivi ogni volta che un oggetto che implementa l'interfaccia IAttributeAnnotatable viene modificato o aggiunto. Ciò viene registrato nel file configure.zcml in questo modo:
<subscriber
for="zope.app.annotation.interfaces.IAttributeAnnotatable
zope.app.event.interfaces.IObjectModifiedEvent"
handler=".events.annotationEventHandler" />
Con questo ci prendiamo curadegli oggetti modificati. Non vengono però trattati gli oggetti aggiunti. Per questi ultimi basta sostituire 'IObjectModifiedEvent' con 'IObjectCreatedEvent'.
Nel caso di quadapter serve una gestione degli eventi che si attivi solo per oggetti che implementano l'interfaccia IWeblogEntry:
<subscriber
for="Products.Quills.interfaces.IWeblogEntry
zope.app.event.interfaces.IObjectModifiedEvent"
handler="Products.keywordannotator.events.annotationEventHandler" />
Codice del gestore eventi
In keywordannotator il codice è questo:
def annotationEventHandler(ob, event):
from zope.component import getUtility
decider = getUtility(IAnnotationDecider, context=ob)
if decider.matchesKeywords(ob):
decider.provideInterfaces(ob)
Quindi questo gestore eventi cerca una utility. Quella utility specifica se l'oggetto per cui l'evento è stato scatenato è stato marcato come speciale con una keyword nel codice o meno. Se queste condizioni si verificano, l'utility viene istruita in modo che ora l'oggetto fornisca alcune interfacce in più.
Attenzione: quadapter usa semplicemente questo gestore eventi da keywordannotator senza sovrascriverlo.
Nella prossima sezione si vedrà come appare un'utility.
Utility
Registrazione zcml
In keywordannotator appare in questo modo:
<utility
provides=".interfaces.IAnnotationDecider"
factory=".events.DefaultAnnotationDecider" />
Ciò significa che quando del codice ha bisogno di un'utility che fornisca l'interfaccia IAnnotationDecider (il codice del gestore eventi lo richiede, vedere sopra), questa utility può essere creata richiamando la classe DefaultAnnotationDecider nel file events.py nel prodotto keywordannotator.
Quadapter sovrascrive questa utility in overrides.zcml:
<utility
provides="Products.keywordannotator.interfaces.IAnnotationDecider"
factory="Products.quadapter.events.AudioDecider" />
Dato che si tratta di una riscrittura, significa che l'unica maniera per creare un'utility che fornisca l'interfaccia IAnnotationDecider, è ora richiamare la classe AudioDecider nel file events.py nel prodotto quadapter.
Codice di una utility
Quindi come appare il codice? In keywordannotator appare in questo modo:
class DefaultAnnotationDecider(object):
implements(IAnnotationDecider)
keywords = KEYWORDS
ifaces = (IKeywordMatch,)
def matchesKeywords(self, object):
...
def provideInterfaces(self, object):
for iface in self.ifaces:
if not iface.providedBy(object):
alsoProvides(object, iface)
L'implementazione della funzione 'matchesKeywords' non interessa per il momento. Controlla semplicemente se le keyword dell'oggetto corrispondono a qualcuna delle keyword speciali. Di default, solo la parola letterale 'special' è considerata speciale.
La funzione provideInterfaces è invece più interessante. Si assicura che un certo oggetto fornisca tutte le interfacce desiderate. Di default si tratta dell'interfaccia marker IKeywordMatch. Nella sezione seguente si mostrerà come si registra un adapter per oggetti che implementano quell'interfaccia.
In quadapter il codice di un'utility è costituita solo da 4 righe:
class AudioDecider(DefaultAnnotationDecider):
implements(IAnnotationDecider)
keywords = KEYWORDS
ifaces = PROVIDE_INTERFACES
Usa una lista differente di keyword che sono considerate speciali, e interfacce differenti che devono essere fornite agli oggetti che corrispondono a una delle keyword. Questi valori sono specificati nel file config.py:
KEYWORDS = ['audio', 'preken']
PROVIDE_INTERFACES = (IMaudio, IAttributeAnnotatable,)
Nota: 'preken' è il termine olandese per 'sermoni'.
Quindi, con poche linee, il prodotto quadapter cambia l'adapter di default in modo che reagisca a differenti keyword e fornisca interfacce diverse.
Si noti il fatto che una WeblogEntry di Quills non fornisce di default l'interfaccia IAttributeAnnotatable, bisogna quindi istruire l'utility per far si che le WeblogEntry con una delle keyword speciali implementino ora anche quell'interfaccia. Se lo si desidera, si può anche aggiungere del codice a quadapter in modo che tutte le WeblogEntry forniscano anche l'interfaccia IAttributeAnnotatable; in questo caso bisogna riferirsi al file utils.py di keywordannotator.
A questo punto sarebbe opportuno riguardare il codice del gestore eventi per vedere come usa questa utility.
Stato corrente
Molto lavoro è già stato fatto. Questa è una panoramica su ciò che è stato raggiunto finora, prima di continuare con adapters e annotations.
Se si sta usando solo il prodotto keywordannotator, la situazione è questa:
- Un oggetto che implementa l'interfaccia IAttributeAnnotatable,
- data la keyword speciale ('special'),
- ora implementa anche l'interfaccia IKeywordMatch.
Se assieme a keywordannotator si sta usando anche Quills e il prodotto quadapter, allora la situazione è questa:
- Un oggetto che implementa l'interfaccia IWeblogEntry,
- date una delle keyword speciali ('audio' o 'preken'),
- implementa ora anche l'interfaccia IMaudio
- e l'interfaccia IAttributeAnnotatable.
Se tutto ciò ancora non è chiaro, è forse opportuno rileggere le sezioni precedenti. In caso contrario è ora di passare ad adapters e annotations.
Adapter
Registrazione zcml
Ciò che si vuole ottenere è adattare oggetti che implementano una certa interfaccia dando loro delle annotazioni. In keywordannotator si registra un adapter per oggetti che implementano l'interfaccia IKeywordMatch. Questo adapter dovrebbe fornire per questi oggetti una nuova interfaccia IKeywordBasedAnnotations, che è solo un'interfaccia marker (o etichetta). Questo adapter può essere creato richiamando la classe KeywordBasedAnnotations in events.py. Si registra quell'adapter nel file configure.zcml:
<adapter
for=".interfaces.IKeywordMatch"
provides=".interfaces.IKeywordBasedAnnotations"
factory=".events.KeywordBasedAnnotations"
/>
In quadapter si agisce in modo simile:
<adapter
for=".interfaces.IMaudio"
provides=".interfaces.IAudioAnnotations"
factory=".events.AudioAnnotations"
/>
Ciò significa che l'adapter può essere creato richiamando la classe AudioAnnotations nel file events.py di quadapter. Questo adapter fornisce l'interfaccia IAudioAnnotations per oggetti che implementano già l'interfaccia IMaudio.
IAudioAnnotations non è un'interfaccia marker ma un'interfaccia "normale". Nel file interfaces.py di quadapter si è stabilito che ogno oggetto che richiede di implementare l'interfaccia IAudioAnnotations dovrebbe avere gli attributi completeURL e partURL:
class IAudioAnnotations(Interface):
"""Provide access to the audio annotations of an IMaudio object.
"""
completeURL = schema.URI(title=u'URL to complete audio content')
partURL = schema.URI(title=u'URL to a part of the audio content')
A questo punto si potrebbe essere giunti alla conclusione che Zope 3 utilizza molte interfacce. È così!
Codice di un adapter
Sono stati registrati due adapter. Come appare ora il codice? In keywordannotator:
class KeywordBasedAnnotations(object):
...
def __init__(self, context):
self.context = context
annotations = IAnnotations(self.context)
self._metadata = annotations.get(self._anno_key, None)
if self._metadata is None:
self._metadata = PersistentDict()
annotations[self._anno_key] = self._metadata
La funzione __init__ è la factory; in altre parole, è la funzione che crea un oggetto KeywordBasedAnnotations basato su un altro oggetto che viene passato attraverso il parametro 'context'. A questo punto per la prima volta compare realmente una annotazione. In questa linea:
annotations = IAnnotations(self.context)
Qui l'oggetto context è adattato all'interfaccia IAnnotations e l'oggetto racchiuso o adattato viene immagazzinato nella variabile delle annotazioni. Quella variabile è fondamentalmente un dizionario python che per ora non contiene probabilmente nessun valore. Le altre linee si assicurano che sia disponibile ora in quell'oggetto una struttura di base per immagazzinare annotazioni.
Ora nel codice di adapter in quadapter:
class AudioAnnotations(KeywordBasedAnnotations):
...
def __get_completeURL(self):
return self._metadata.get(COMP_ANNO)
def __set_completeURL(self, url):
self._metadata[COMP_ANNO] = url
completeURL = property(__get_completeURL, __set_completeURL)
Questa classe subclassa la classe KeywordBasedAnnotations da keywordannotator, in questo modo eredita la funzione vista prima. Ma le righe in questo codice eseguono proprio ciò che costituisce il motivo primario per cui si è iniziato questo progetto: aggiungono la proprietà completeURL, in cui immagazzinare il link alla funzione completa della mia chiesa. Questo link è memorizzato nella proprietà self._metadata, che è un'annotazione a quest'oggetto.
La stessa cosa avviene (con una porzione di codice che qui non viene mostrata perché praticamente uguale a quella sopra) per la proprietà partURL, in cui si può ora immagazzinare un link ad una parte della predica o in termini ragionevoli: il sermone.
Annotation
Nella sezione precedente si è in realtà già visto come le annotazioni vengono aggiunte ad un oggetto e che sono fondamentalmente un dizionario python. Ma prima che ciò sia possibile, si deve aggiungere una linea al configure.zcml di keywordannotator:
<include package="zope.app.annotation" />
Questa direttiva carica un'altra diretiva zcml che si trova nella directory del software zope, nel file lib/python/zope/app/annotation/configure.zcml:
<adapter
for=".interfaces.IAttributeAnnotatable"
provides=".interfaces.IAnnotations"
factory=".attribute.AttributeAnnotations"
/>
Ciò in realtà conclude la strategia che si voleva attuare. Tutto il codice e la configurazione sono ora pronti per aggiungere quelle annotazioni ad una WeblogEntry adattandola se implementa o fornisce l'interfaccia IMaudio, che viene aggiunta ad essa da una utility se essa decide che si verificano le condizioni appropriate dopo che ha avuto luogo un evento gestito da un gestore eventi. (scritto a parole sembra un processo quasi incomprenibile).
Aggiungere annotazioni
A questo punto, se si verifica lo stato delle cose al livello Plone, si può aggiungere una normale WeblogEntry e contrassegnarla con una keyword 'audio'. Così il codice si assicura che venga fornita l'interfaccia IMaudio. E questo è tutto, ma non c'è ancora un modo per mettere qualcosa nelle annotazioni. Ciò può essere risolto. Si installa per prima cosa un nuovo prodotto: CMFonFive.
Forse un giorno questo prodotto verrà assimilato nel nucleo del CMF (Plone è basato su un CMF, Content Management Framework). Ma per ora questo prodotto extra servirà per le seguenti linee che vanno aggiunte nel configure.zcml di quadapter:
<browser:menu
id="object_tabs"
title="Object tabs" />
<browser:menuItem
for="Products.quadapter.interfaces.IMaudio"
menu="object_tabs"
title="Audio urls"
action="maudio_edit"
description="Edit form for audio urls"
permission="zope2.ManageProperties"
/>
Ciò aggiunge nel sito Plone un elemento nel menù ai tab di ogni oggetto che implementa l'interfaccia IMaudio. Quando si aggiunge questo codice, riavviare l'istanza Zope, e osservare una WeblogEntry che abbia una delle parole speciali e quindi che fornisca l'interfaccia IMaudio, si vedrà un tab che collega all'azione maudio_edit.
Il lavoro è quasi finito. Serve creare un pannello che corrisponda a quel tab, che richiami il codice che imposta Annotations per quest'oggetto. Se si conosce come creare un pannello di modifica (edit form) in html, si è probabilmente in grado di superare senza problemi questa fase, possibilmente usando il prodotto Archetypes.
Visualizzare annotazioni
Una volta aggiunti i link nell'annotazione di una WeblogEntry, si vuole anche poterli visualizzare quando si accede a una WeblogEntry nel proprio browser. Si può usare un'altra tecnica Zope 3 per far ciò: la BrowserView. Essenzialmente anche questo è un adapter. Si deve definire una BrowserView in quadapter:
<browser:page
name="audio_entry_view"
for="Products.quadapter.interfaces.IMaudio"
permission="zope2.View"
allowed_interface="Products.quadapter.interfaces.IAudioWeblogView"
class=".browser.AudioWeblogView"
/>
Ciò significa essenzialmente che per un oggetto che fornisce l'interfaccia IMaudio si ha una classe python che fornisce alcune funzioni da richiamare in una page template html. Infatti, si può ora copiare il file entry_macros.pt da Quills e aggiungere queste linee al punto giusto:
<tal:imaudio tal:define="view entry/@@audio_entry_view|nothing;"
tal:condition="view">
<metal:block use-macro="here/maudio_macros/macros/extratext" />
</tal:imaudio>
Ciò usa un file maudio_macros.pt che prende i link dagli oggetti. Ora quando si visualizza una normale WeblogEntry appare ciò che ci si aspetta appaia. Ma quando si visualizza una voce con l'interfaccia IMaudio, per cui sono stati aggiunti link con il pannello di cui si è parlato sopra, si noterà subito anche del testo extra, ovvero i link. I dettagli e le rifiniture del lavoro sono esercizi abbastanza semplici e sono costituiti da tecniche standard riguardanti i page template, che dovrebbero essere familiari e che non sono molto interessanti in questo contesto. Se non si riesce a completare il progetto, si può contattare l'autore.
Conclusioni
Usare Zope 3 in questo modo è:
- Pulito:
- Solo un page template da Quills viene modificato e non viene sovrascritto codice.
- Se si aggiungesse una metal:macro con un fill-slot a Quills, anche quella singola riscrittura non sarebbe necessaria.
- Compatibile:
- Vengono solo aggiunte funzioni, non si modificano i comportamenti di WeblogEntries.
- Le WeblogEntries audio enhanced sono ancora WeblogEntries di prima classe. Codice esistente che suppone una normale WeblogEntry può tranquillamente gestire le voci audio.
- I cambiamenti effettuati funzionano per qualsiasi tipo di contenuto che implementi l'interfaccia IWeblogEntry. Quindi se qualcuno crea una VideoWeblogEntry che implementa IWeblogEntry, il codice funzionerà anche per quell'oggetto.
In altre parole, Zope 3 fornisce strategie molto efficaci!
Credits
Questo documento è stato realizzato da:
- Alice Narduzzo
- Maurizio Delmonte
Fonti e contributi:
Questo how-to è una libera traduzione del testo originale Embrace and Extend Existing Products: The Zope 3 Way
.Nota: Questo tutorial si riferisce alla versione Plone 3.0.x e 2.5.x.
