Espèce d'intro à Zope 3 : Partie 1

Publié le 08 janvier 2009 par Mikebrant

On va faire un générateur de questionnaire.
Donc ca va servir de petite intro à Zope 3.
Je pars du principe que Zope 3, vous savez déjà ce que ca n'est pas :
   - ça n'est pas une insulte en québécois : "calice de zope "
   - ça n'est pas un verbe : "whao, celle -là, je me la zopperais bien"
   ...
Je pars également du principe que Zope 3, vous connaissez les bases :
   - à quoi sert le fichier configure.zcml
   - les interfaces
   ...

Alors non, je ne vais pas beaucoup commenter le code :
une image vaut mieux qu'un long discours.

Il devrait y avoir plusieurs parties.
Dans ces prochaines parties il pourra y avoir :
mettre de l'ajax dans notre projet Zope,
créer un containeur(bon c'est simple/rapide à faire mais bon c'est la première partie donc bon),
créer un Skin,
faire les réponses,
compléxifier notre générateur de questionnaire
....

Intro

On aurait donc notre modèle Questionnaire implémentant IQuestionnaire.

donc j'ai opté pour ce modèle :
- une date de création
- un nom d'auteur
- questionnaireXML : une variable contenant le questionnaire en xml

Alors je ne sais pas, si c'est très correct, mais je n'ai pas vu d'autres solutions...
En avez-vous une ?

donc le xml d'un questionnaire serait de la sorte :

<questionnaire>
   <question numero="69">
   <titre>La question</titre>
   <reponse type="radio">
        <choix numero="1" valeur="cherieFM" /> #si c'est textarea ou text, il n'y en aurait pas
   ...
   </reponse>
  </question>
   ...
</questionnaire>


Dans cette première partie on aura que des réponses du type "input text" donc il n'y aura pas de balise <choix>.

Arborescence du code.

Regardez l'image  ou lisez ce pavé inintéressant.
On crée le dossier questionnaire qui va contenir tout notre petit projet ( dans $ZOPE_PATH/lib/python/ ou si vous vous servez de zopeproject : $ZOPE_PATH/src/ ).
On créer à l'intérieur de ce dernier un fichier __init__.py  : afin de faire de notre projet un package .
Le fichier __init__.py peut être vide, mais mettez  au moins un commentaire afin qu'il ne soit pas zappé si vous décidez de faire un tar du projet.
On crée aussi le fichier configure.zcml.

Maintenant on crée les dossiers Interfaces, Modèles et Vue, toujours à l'intérieur de notre dossier questionnaire.
N'oubliez pas de créer les fichiers __init__.py

Dans le dossier Interfaces, créez le fichier interfaces.py : il va contenir comme vous le savez toutes nos interfaces : c'est à dire une seule : IQuestionnaire.
Dans le dossier Modeles, on va créer pour chaque modèle, un fichier : donc créer le fichier Questionnaire.py

Voilà à quoi devrait ressembler le projet :

Interfaces/interfaces.py


Voici son contenu :

#-*-coding:Utf-8 -*-
from zope.interface import Interface, Attribute
from zope.schema import TextLine
class IQuestionnaire(Interface):
   """Notre questionnaire"""
  
   auteur = TextLine(
   title = u"Auteur",
   description = u"Auteur du questionnaire",
   required = True
   )
  
   dateTextLine(
   title = u"Date",
   description = u"Date de création",
   required = True
   )
   xml = Attribute( "Le document XML")

Donc notre interface IQuestionnaire définit le squelette de notre modèle Questionnaire qui devra donc avoir 4 attributs :
 auteur : le nom de l'auteur donc une simple chaîne : TextLine()
 date : la date de création,ca aurait pu être Date() mais on va stocker un timestamp donc je met TextLine()
 xml : ce sera la varibable contenant notre document XML, donc à défaut de pouvoir spécifier son type, ce sera un 'simple' Attribute()

Modeles/Questionnaire.py


Voici son contenu :

#-*- coding:Utf-8 -*-
from zope.interface import implements
from persistent import Persistent
from questionnaire.Interfaces.interfaces import IQuestionnaire
class Questionnaire(Persistent):
   __doc__ = IQuestionnaire.__doc__
  
   implements(IQuestionnaire)
  
   def __init__(self, auteur, date, xml):
   self.auteur = auteur
   self.date = date
   self.xml = xml  


C'est assez simple :
notre classe Questionnaire hérite de Persistent afin d'être pouvoir stocké dans la ZODB.
Elle implémente son interface IQuestionnaire,
et donc à son initialisation elle prend  3 paramètres.

configure.zcml


On en a un peu rien à faire dans cette première partie mais bon :

<configure xmlns="http://namespaces.zope.org/zope">
   <include package = ".Vue" />
   <interface
   interface = ".Interfaces.interfaces.IQuestionnaire"
   type = "zope.app.content.interfaces.IContentType"
   />
   <class class=".Modeles.Questionnaire.Questionnaire">
   <require
   permission="zope.View"
   interface=".Interfaces.interfaces.IQuestionnaire"
   />
   <require
   permission="zope.ManageContent"
   set_schema=".Interfaces.interfaces.IQuestionnaire"
   />
   </class>
 
</configure>


On inclut la package Vue vu qu'il contient un configure.zcml
Après on déclare notre interface et notre classe.
Et puis j'ai la flemme d'expliquer.

La vue


Bon maintenant ne rous reste plus qu'à nous occuper de la Vue.
Le fichier configure.zcml va gérer tout ce qui est en rapport avec la Vue ( logique).

On aura pour chaque vue(classe) un template associé.

Donc on commence .
La page d'acceuil : on nous demandera seulement le nombre de questions.
voici Vue/accueil.pt (le template):

<html>
   <body>
   <form action="questionnaire" method = "post" >
   <fieldset>
   <legend>Le commencement</legend>
   <p>Nombre de questions : <input type="text" name="nombre" /></p>
   <input type="submit" value = "ok" />
   </fieldset>
   </form>
   </body>
</html>


Il n'y a rien à expliquer je pense.

Voici Vue/Accueil.py (la vue):

#-*- coding:Utf-8 -*-
class Accueil :
   """la page d'accueil"""
   def __init__(self, context, request):
   self.context = context
   self.request = request


Là, aussi, une classe qui sert à rien, mais bon c'est comme ça.

Et voici le fichier Vue/configure.zcml :

<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser">
   <browser:page
   for = "zope.app.container.interfaces.IContainer"
   name = "accueil"
   template = "accueil.pt"
   class = ".Accueil.Accueil"
   permission = "zope.View"
   />
</configure>


Alors, la page accueil sera accessible à partir de n'importe quel containeur , c'est à dire que l'on peut dès à présent accéder à notre page via : http://localhost:8080/accueil.
Le template qui va se charger d'afficher la classe Accueil sera accueil.pt et ca donnera la page accueil .
Enfin, tout le monde pourra voir la page.

Maintenant place à la page questionnaire. qui va contenir les questions et l'auteur.

On rajoute ça dans Vue/configure.zcml :

<browser:page
   for = "zope.app.container.interfaces.IContainer"
   name = "questionnaire"
   template = "questionnaire.pt"
   class = ".Questionnaire.Questionnaire"
   permission = "zope.View"
   />


Je ne ré-explique pas.

Voici le fichier Vue/Questionnaire.py :

#-*- coding:Utf-8 -*-
class Questionnaire :
   """On va pouvoir créer nos questions """
   def __init__(self, context, request):
   self.context = context
   self.request = request
   self.questions = 10
  
   if self.request.form.has_key('nombre') :
   try:
   self.questions = int( self.request.form['nombre'] )
   except:
   self.questions = 10
   if self.questions < 1:
   self.questions = 1


Les paramètres passés( en GET ou POST, peu importe) à notre requête se trouvent dans le dictionnaire form de l'attribut request.
Si on accède directement à la page questionnaire alors on n'a pas validé le formulaire de la page accueil donc form ne contient pas le paramètre nombre (le nombre de questions) : on ne rentre pas dans le if.

Voici Vue/questionnaire.pt :

<html>
   <body>
   <form action="ajout" method="post">
   <fieldset>
   <p>Nom : <input type="text" name="auteur" /></p>
   <div id="liste">
   <tal:block repeat="compteur python:range(1,view.questions+1)">
   <div class="question">
   <p class="numero" tal:content="string:question n°$compteur"></p>
   <input type="text" class="texte" tal:attributes="name string:question$compteur" />
   </div>
   </tal:block>
   </div>
   <br /><br />
   <input type="submit" value="Ok" />  
   </fieldset>
   </form>
   </body>
</html>


Rien de bien compliqué.
Pour accéder aux attributs et méthodes de notre classe Questionnaire on se sert de view.
On va répéter nombreDeQuestions fois l'intérieur du block .
On affiche juste le numéro de la question et un input text.
string nous permet d'écrire une chaine contenant la variable compteur, accessible via $compteur.

Il ne reste plus qu'à faire la page ajout.

On rajoute ça dans Vue/configure.zcml :

<browser:page
   for = "zope.app.container.interfaces.IContainer"
   name = "ajout"
   template = "ajout.pt"
   class = ".Ajout.Ajout"
   permission = "zope.View"
   />


Rien ne change.
Le fichier Vue/ajout.pt :

<html>
   <body>
   <p>Questionnaire ajouté.</p>
   </body>
</html>


Je vous laisse comprendre ce bout de code très glagocyclant .
Et maintenant le code le plus important :  Vue/Ajout.py :

#-*- coding:Utf-8 -*-
from xml.dom.minidom import Document
from time import time
from questionnaire.Modeles.Questionnaire import Questionnaire
def pagePrecedente(maFonction):
   """ retourne juste verif"""
  
   def verif(self):
   """ vérifie que l'on vient bien de "/questionnaire" sinon redirige sur "/accueil" """
  
   if self.request.getHeader('HTTP_REFERER') and self.request.getHeader('HTTP_REFERER').split('/')[-1] == 'questionnaire':
   return maFonction(self)
   return self.request.response.redirect('/accueil')
  
   return verif
class Ajout:
   """on crée enfin notre objet Questionnaire(stocké dans la ZODB)"""
  
   def __init__(self, context, request):
   self.context = context
   self.request = request
  
   self.sauvegarde()
   @pagePrecedente
   def sauvegarde(self):
   """ sauve le questionnaire"""  
  
   questions = self.request.form.keys()
   questions.remove('auteur')
   questions.sort()
  
   auteur = self.request.form['auteur']
  
   doc = Document()
   baliseQuestionnaire = doc.createElement("questionnaire")
   doc.appendChild(baliseQuestionnaire)#on crée l'élément racine <questionnaire>
   i = 1 # un compteur
  
   for question in questions :
   #pour chaque question on crée la balise <question numero="x"> etc...
  
   baliseQuestion = doc.createElement("question")
   baliseQuestion.setAttribute('numero',str(i))
   baliseQuestionnaire.appendChild(baliseQuestion)
  
   baliseTitre = doc.createElement("titre")
   baliseTitre.appendChild(doc.createTextNode(self.request.form[question]))
   baliseQuestion.appendChild(baliseTitre)
  
   baliseReponse = doc.createElement("reponse")
   baliseReponse.setAttribute("type","text")
   baliseQuestion.appendChild(baliseReponse)
  
   i = i+1
   doc = doc.toxml()
  
   #on sauvegarde notre objet
   self.context[str(time())] =  Questionnaire( auteur, float(time()) ,doc)


__init__() appelle la méthode sauvegarde().
sauvegarde() comme le dit son nom va sauver notre questionnaire, mais on va d'abord vérifier que l'on vient bien de la page questionnaire : d'où le décorateur pagePrecedente.
Il prend en paramètre sauvegarde(), et ne fait que retourner la fonction verif().
verif(), quant à elle, prend en paramètre les paramètres de sauvegarde() : c'est à dire self.

Si l'on a tappé directement ajout dans notre navigateur ou si la page d'où l'on vient n'est pas questionnaire alors on est redirigé sur la page d'accueil grâce à la méthode redirect().
Sinon, on peut entrer dans notre méthode sauvegarde().
nb : c'est juste une petite sécurité que l'on peut largement outrepasser mais ça nous montre quelques méthodes intéressantes.

La méthode sauvegarde() n'est pas vraiment compliquée :
on prend tous les paramètres du formulaire excepté auteur, et l'on trie par ordre alphabétique.
Ensuite on crée notre xml : j'explique pas, c'est assez clair ainsi.
On va sauver nos questionnaires dans notre containeur, représenté ici par self.context (context représente l'objet pour lequel on fournir une vue)
Il marche comme un dictionnaire, c'est pourquoi, on lui passe comme clé le timestamp de la date actuelle.

Voilà cette première partie est enfin finie.
Il va y avoir d'autres parties comme dit plus haut :
ajout containeur, créer un skin, pouvoir répondre,etc..., etc... (comme dit tout en haut).
Le code est ici.