Création de modules Plone
Voici quelques généralités sur l'architecture et la création des modules sur Plone.
Pour la création de produits:
ZopeSkel ( Zope Skeleton )
installation
ajouter dans le buildout:
parts = ... zopeskel [zopeskel] # installs paster and Zopeskel recipe = zc.recipe.egg eggs = PasteScript ZopeSkel
ligne de commande pour ajouter un produit (dans le /src):
../bin/paster create -t plone Mon.Produit
Pour un produit installable depuis les modules de Plone, "Register Profile" > True.
Il faut ensuite ajouter le nouveau module dans le buildout
eggs = ... Mon.Produit zcml = ... Mon.Produit develop = ... src/Mon.Produit
Architecture d'un module
Éléments constituants
- dossiers structure du module
- .py fichiers python
- .pyc fichiers python compilés
- .zcml (Zope Configuration Markup Language) fichiers de configuration Zope
- .xml généralement dans /profiles, fichiers de configuration pour d'autres modules, importés lors de l'installation du module.
- .pt (Page Template) une page utilisant le langage "html" et "tal"
- .txt souvent des fichiers de documentation, ne sont en général pas indispensables au module
- .cfg configuration du module pour le buildout
- autres (images, fichiers javascript, textes, etc), sont en général des resources
Fichiers particuliers
- __init__.py : demande à python de compiler les scripts qui se trouvent dans le dossier où est placé le __init__.py.
- configure.zcml : fichier principal de configuration pour Zope
Le ZCML
C'est un fichier au formattage xml avec son propre espace de noms. Cet espace de noms doit être déclaré dans la balise <configure> au début du fichier.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:security="http://namespaces.zope.org/security" xmlns:zmi="http://namespaces.zope.org/zmi">
Toutes les balises utilisées dans le fichier .zcml doivent être importées depuis la balise <configure>.
Les profiles
Les profiles sont une collection de fichiers xml qui définissent ou configure certains modules du site plone.
Ils sont déclarés dans les .zcml par l'instruction <genericsetup:registerProfile> et sont importés au moment de l'installation du module depuis l'interface de configuration de Plone.
On y trouve par exemple:
- rolemap.xml définition des rôles (onglet "security")
- types.xml définition des types d'objets
- jsregistry.xml et cssregistry.xml définition des javascripts et des css
- catalog.xml définition de l'index et des metadata du catalogue (portal_catalog)
- propertiestool.xml définition des propriétés du site dans site_properties
- browserlayer.xml enregistrement du layer pour le produit
Création d'une page pour Plone 4.2 (Zope3 style)
Une page se compose essentiellement d'une classe python et en général d'un template html (page template .pt). Cette page doit ensuite être déclarée dans un fichier .zcml par l'instruction suivante:
La déclaration dans un .zcml
<browser:page for="Products.Archetypes.interfaces.IBaseObject" name="edit" factory=".edit.Edit" template="edit.pt" permission="zope.View" />
les attributs:
- for : pour quel interface notre page sera disponible ( for="*" pour tous les interfaces )
- name : le nom de la page
- factory : la classe python ( class Edit(): ) qui se trouve dans le fichier "edit.py"
- permission : la permission que doit avoir l'utilisateur pour voir cette page
- template : la page template ( .pt ) contenant le code html de notre page.
- allowed_attributes : les attributs de notre page qui vont pouvoir être appelés depuis une autre page ( à condition d'avoir la permission définie dans "permission".
Au démarrage de Zope, toutes les pages et adapteurs ( et autres ) déclarés dans les .zcml sont inscrits dans un registre.
La déclaration browser:page est en fait un adapteur avec des particularités déjà prédéfinies:
<adapter for="Products.Archetypes.interfaces.IBaseObject zope.publisher.browser.interfaces.IDefaultBrowserLayer" provides="zope.publisher.browser.interfaces.IBrowserPage" name="edit" factory=".edit.Edit" permission="zope.View" />
Une page Zope déclarée par browser:page est en réalité un adapteur dont les particularités sont de
- ( dans "for" ) adapter une classe qui implémente un interface particulier ( IBaseObject ) et une deuxième classe qui implémente IDefaultBrowserLayer ( le "request" )
- ( dans "provides" ) nous fournir l'interface IBrowserPage
La classe et le template
La classe utilisée pour une page doit être basée sur la classe BrowserView.
from Products.Five.browser import BrowserView from zope.component import getMultiAdapter class Edit(BrowserView): """la classe de la page edit""" def isAnonymous(self): portal_state = getMultiAdapter((self.context, self.request), name=u'plone_portal_state') return portal_state.anonymous()
Dans cette classe Edit, le context et le request sont accessibles par self.context et self.request, initialisés dans la fonction __init__ de la classe BrowserView.
Le template lié à cette page par la déclaration dans le .zcml et un fichier .pt contenant du html et du tal.
Depuis le template, la classe "Edit" définie pour notre page est accessible depuis la variable "view" et on peut appeler toutes les méthodes et les attributs de la classe "Edit" depuis "view":
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" lang="en" metal:use-macro="context/main_template/macros/master" i18n:domain="plone"> <body> <metal:main fill-slot="main"> Suis-je anonyme? <span tal:content="view/isAnonymous"/> </metal:main> </body> </html>
Appel de la page
On peut appeler cette page (=adapteur) depuis le site en ajoutant le nom de la page dans l'url ( /@@edit ).
On peut également appeler cette page (=adapteur) depuis une classe python en utilisant getMultiAdapter. Cette fonction parcoure le registre de Zope en recherchant un adapteur multiple ( un adapteur qui adapte plus de une classe ) suivant les interfaces implémentés par les classes à adapter, suivant le nom, suivant l'interface fourni.
from zope.component import getMultiAdapter from zope.publisher.browser.interfaces import IBrowserPage, IDefaultBrowserLayer from Products.Archetypes.interfaces import IBaseObject myPage = getMultiAdapter((context, request), IBrowserPage, name='edit') # On peut ensuite tester: print IBaseObject.providedBy(context) True # Veut dire que la classe du context implémente l'interface IBaseObject print IDefaultBrowserLayer.providedBy(request) True # Veut dire que la classe du request implémente l'interface IDefaultBrowserLayer print IBrowserPage.providedBy(myPage) True # Veut dire que la classe "Edit" définie dans edit.py implémente l'interface IBrowserPage
Pour faire en sorte que la page ne soit accesible que lorsque notre produit est installé, il faut créer un "layer" propre à notre produit.
Il faut en premier lieu créer un interface qui sera utilisé comme marqeur pour le request:
from zope.interface import Interface class IMonProduitLayer(Interface): """Cet interface est declare dans /profiles/default/browserlayer.xml"""
Il faut ensuite créer le fichier browserlayer.xml dans le profile de défaut qui sera importé lors de l'installation de notre produit:
<?xml version="1.0"?> <layers> <layer name="mon.produit.layer" interface="Mon.Produit.Mon.Produit.interfaces.IMonProduitLayer" /> </layers>
Nous pouvons ensuite déclarer nos pages spécifiquement pour ce nouveau layer en utilisant l'attribut "layer":
<browser:page for="Products.Archetypes.interfaces.IBaseObject" name="edit" factory=".edit.Edit" template="edit.pt" permission="zope.View" layer=".interfaces.IMonProduitLayer" />
De la même manière on pourra surclasser des pages existantes en les déclarant dans notre produit et en y ajoutant l'attribut "layer".
Création d'un formulaire de base
Pour créer un formulaire dans l'architecture zope3, nous utilisons les classes définies dans le module z3c.form.
Il faut en premier lieu créer un interface qui sera la déclaration des champs (et méthodes) de notre formulaire dans un fichier python:
import zope.interface import zope.schema from z3c.form import form, field, button class IArticle(zope.interface.Interface): prix = zope.schema.Float(title=u'Prix HT') tva = zope.schema.Float(title=u'TVA', description=u"TVA en pourcent")
On crée ensuite une classe qui sera une page montrant notre formulaire. Cette classe est basée sur form.Form qui est un la classe de base pour un formulaire standard, sans sauvegarde de données.
class ArticleForm(form.Form): fields = field.Fields(IArticle) ignoreContext = True # n'utilise pas le context pour obtenir les donnees des champs. label = u"Pour ajouter un prix" prixTTC = '' @button.buttonAndHandler(u'Calcule') def handleSoumettre(self, action): data, errors = self.extractData() #recupere les donnees depuis le request if errors: self.status = self.formErrorsMessage return if data.get('prix') and data.get('tva'): self.prixTTC = data.get('prix')*(1+data.get('tva')/100)
Enfin il s'agit "d'emballer" ce formulaire pour qu'il réagisse comme une page Plone standard en utilisant wrap_form. Ceci nous permettra notamment d'utiliser l'attribut "template" dans la déclaration zcml.
from plone.z3cform.layout import wrap_form ArticleFormView = wrap_form(ArticleForm)
Il faut maintenant déclarer le formulaire dans le .zcml ( l'attribut template est optionnel ):
<browser:page for="*" name="edit_article" class=".article.ArticleFormView" template="articleform.pt" permission="cmf.ModifyPortalContent" layer=".interfaces.IMonProduitLayer" />
Depuis la page template "articleform.pt" on accède au formulaire (ArticleForm) en utilisant view/form_instance et on obtient l'affichage du formulaire avec tal:replace="structure view/contents"
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" lang="en" metal:use-macro="here/main_template/macros/master" i18n:domain="plone"> <body> <metal:main fill-slot="main"> <metal:main-macro define-macro="main"> <h1 class="documentFirstHeading" tal:content="view/label">Title</h1> <div id="content-core" tal:content="structure view/contents" /> <p tal:condition="view/form_instance/prixTTC">Le prix TTC est : <span tal:replace="view/form_instance/prixTTC"/></p> </metal:main-macro> </metal:main> </body> </html>
Pour sauvegarder les données du formulaire dans les objets du site
Nous allons d'abord utiliser le formulaire form.EditForm de z3c.form dans lequel sont déjà définies les méthodes pour le traitement des données reçues dans le request ainsi que l'action et le bouton de sauvegarde des données. Nous allons également remplacer l'attribut prixTTC par une méthode qui nous retournera le prix TTC du contexte.
class ArticleForm(form.EditForm): fields = field.Fields(IArticle) # ignoreContext = True # n'utilise pas le context pour obtenir les donnees des champs. label = u"Pour ajouter un prix" def prixTTC(self): article = IArticle(self.context, None) if article: return article.getTTC() return None
Il faut également créer une classe utilisée comme adapteur et qui va sauvegarder nos données prix et tva dans l'objet:
from persistent import Persistent from zope.component import adapts from zope.annotation import factory class ArticleAdapterBase(Persistent): implements(IArticle) adapts(IBaseObject) prix = 0.0 #initialisation tva = 0.0 #initialisation def getTTC(self): return self.prix*(1+self.tva/100) ArticleAdapter = factory(ArticleAdapterBase)
Il faut maitnenant enregistrer l'adapteur dans le fichier .zcml:
<adapter for="Products.Archetypes.interfaces.IBaseObject" provides=".page.IArticle" factory=".page.ArticleAdapter" />
Désormais les données soumises par ce formulaire seront enregsitrées dans l'objet. Pour accéder à ces données, il faut récupérer notre adapteur : IArticle(objet) qui nous retournera : ArticleAdapterBase content les données (prix, tva) et les méthodes (getTTC).