Génerer un composant Plone avec ArgoUML et ArchgenXML

Définition

ArgoUML est un outil de modélisation d'application basé sur le formalisme UML.

Dans le cas de création d'application Plone/Zope, il est utilisé pour avoir une vue synthétique de l'architecture d'un produit.

Savoir

  • Création des diagrammes de classes correspondant à la structure du produit,
  • Création des diagrammes d'états correspondant aux workflows,
  • Création des étiquettes permettant de gérer les noms, les permissions et les worklists,
  • Dérivation des classes du système.

Installation d'ArgoUML

ArgoUML est un outil de modélisation UML open source qui gère la version 1.4 d'UML.

Bien que sommaire, il suffit à modéliser notre besoin. Son format de sauvegarde sera interprété par l'outil de génération ArchGenXML pour produire le code du module de Plone qui satisfera le besoin de notre cahier des charges.

ArgoUML est accessible à http://argouml.tigris.org nous téléchargeons la version 0.30 et l'installons.

Cette installation requière Java. Si aucune machine virtuelle java n'est présente sur votre poste, le logiciel d'installation correspondant à votre OS vous proposera d'installer celle de SUN.

Installation de ArchGenXML

Nous utiliserons ArchGenXML pour générer le code Python à partir du modèle créé avec ArgoUML.

De plus, ArchGenXML fournit un profil permettant d'ajouter à ArgoUML les types et les tags dont nous allons avoir besoin pour modéliser notre module.

L'installation peut être faite à partir de pypi ou depuis le dépôt subversion (subversion est accessible à http://subversion.apache.org).

L'intérêt du dépôt subversion est de disposer des dernières corrections :

C:\Program Files> cd C:\
C:\> svn export http://svn.plone.org/svn/archetypes/ArchGenXML/buildout ArchGenXML
C:\> cd ArchGenXML
C:\ArchGenXML> C:\python26\python.exe bootstrap.py
C:\ArchGenXML> bin\buildout.exe
C:\ArchGenXML> bin\test-archgenxml.exe
Running zope.testing.testrunner.layer.UnitTests tests:
  Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds.
  Ran 82 tests with 0 failures and 0 errors in 0.211 seconds.
Tearing down left over layers:
  Tear down zope.testing.testrunner.layer.UnitTests in 0.000 seconds.

La dernière commande permet d'exécuter les tests unitaires et donc de vérifier qu'ArchGenXML fonctionne, l'affichage montre qu'il a eu 82 tests exécutés sans erreurs.

Configuration d'ArgoUML pour utiliser le profil d'ArchGenXML

Pour ajouter le profil, allez dans le menu Édition -> Préférences... -> Profiles -> Add et saisissez le chemin vers le dossier contenant le profil ArchGenXML C:\ArchGenXML\src\archgenxml\umltools\argouml.

../_images/argouml_ajout_profile.jpg

Ajout du dossier contenant le profil ArchGenXML

Vous devez voir normalement AGXProfile dans Available Profiles. Sur certaine version d'ArgoUML il fallait le relancer pour que soit pris en compte le nouveau répertoire des profils.

On ouvre (à nouveau) la gestion des profils et l'on ajoute dans Default Profiles le profil ArchGenXML et on retire tous les autres excepté UML 1.4 qu'on ne peut pas retirer à partir d'ici.

../_images/argouml_configuration_profile.jpg

Ajout du profil ArchGenXML

Maintenant dans le panneau qui s'ouvre lorsqu'on clique sur le lien Configure project specific settings... de la fenêtre précédente, retirez le profil UML 1.4. Vous ne devez avoir que le profil ArchGenXML d'activé.

Voilà ArgoUML est prêt à être utilisé pour modéliser notre module.

Création du buildout de notre module

La première étape est de créer et ajouter la structure de notre nouveau module (Product) à Plone.

Pour cela plaçons nous dans le répertoire src de notre Plone (le créer s'il n'existe pas) et lançons la commande paster comme suit :

c:\Plone4> cd src
C:\Plone4\src> paster create -t basic_namespace Products.StandArt_AQ
Selected and implied templates:
  zopeskel#basic_namespace  A basic Python project with a namespace package

Variables:
  egg:      Products.StandArt_AQ
  package:  productsstandart_aq
  project:  Products.StandArt_AQ
Expert Mode? (What question mode would you like? (easy/expert/all)?) ['easy']:
Version (Version number for project) ['1.0']: 0.1
Description (One-line description of the project) ['']: The Stand'Art company
  Qualite Assurance
Creating template basic_namespace
Creating directory .\Products.StandArt_AQ
  Recursing into +namespace_package+
    Creating .\Products.StandArt_AQ\Products/
    Recursing into +package+

L'arborescence créée est alors :

C:\Program Files\Plone4\src> tree Products.StandArt_AQ
Structure du dossier
Le numéro de série du volume est XXXXXXXX XXXX:XXXX
C:\PROGRAM FILES\PLONE4\SRC\PRODUCTS.STANDART_AQ
|-- Products
|   `-- StandArt_AQ
|-- Products.StandArt_AQ.egg-info
`-- docs

Nous allons ajouter un répertoire model/ dans Products.StandArt_AQ pour y stocker le modèle UML que nous allons réaliser.

Création du plan documentaire

Notre modélisation reposera en partie sur le mécanisme des Archetypes proposé par Plone et sur celui des vues Zope 3. Il est donc possible de consulter la documentation dédiée à ces deux technologies pour compléter ce cours.

Dans le cahier des charges nous avons vu que l'ensemble des documents sous assurance qualité allait être stocké dans un répertoire dédié et que les documents seront stockés dans un conteneur spécial possédant plusieurs champs.

Modifier le nom du paquetage "untitledModel" en "Products.StandArt_AQ". Pour cela mettez vous en vue "Orienté Paquetage", cliquer sur "untitledModel" dans la navigation puis sur l'onglet "Propriété" et dans le champ "nom" saisissez "StandArt_AQ".

../_images/argouml_nommer_paquetage.jpg

Nommer le paquetage en Products.StandArt_AQ

Puis enregistrez le modèle sous le nom StandArt_AQ dans le répertoire model

Cliquez sur "Diagramme de classe" dans la barre de navigation, vous pouvez alors ajouter les éléments symbolisés par les icônes de la zone de dessin.

Ajoutez y un paquetage nommé "content", par convention nous mettons tous les types de contenu dans ce répertoire, les classes utilitaires seront mises dans un paquetage nommé "tools".

De même, puisque généralement nos paquets ont vocation à être partagés avec la communauté Plone nous réalisons la modélisation en Anglais.

Dans le paquetage "content", créez un diagramme de classes "Content diagram"

Éditez le diagramme pour ajouter les classes de bases ATFolder et ATDocument qui sont les types des répertoires et des documents de Plone.

Nous les utiliserons comme classes de bases pour nos types de façon à réutiliser leur comportement.

Ajoutez y le stéréotype stub qui permettra d'indiquer à ArchGenXML qu'il ne doit pas générer ces classes car elles existent déjà.

Pour ATFolder allez dans l'onglet Étiquettes et ajoutez le marqueur (tag) import_from ayant pour valeur Products.ATContentTypes.content.folder.

Pour ATDocument ajoutez le marqueur (tag) import_from avec pour valeur Products.ATContentTypes.content.document.

On peut aussi utiliser le stéréotype <<ATFolder>> et <<ATDocument>> dans les classes dérivées au lieu de modéliser la généralisation, mais il arrive qu'ArchGenXML ne réussisse pas à générer le code.

Nous créons le répertoire qui contiendra tous les documents sous assurance qualité en dérivant ATFolder et en nommant cette nouvelle classe StandArt_FolderAQ.

Il est conseillé de préfixer ses types de contenu afin d'éviter toute collision avec d'autres modules Plone.

De même pour les attributs, préfixez les d'un m pour "membre" par exemple pour éviter de surcharger par accident tout attribut ou méthode existant dans la classe de base (et avec le mécanisme de wrapper c'est près d'une centaine de membres).

Toutefois, si vous souhaitez redéfinir un attribut déjà existant gardez le nom d'origine (exemple : title, description).

../_images/argouml_plan_documentaire_01.jpg

Création du plan documentaire

Pour les classes, la signification des marqueurs est la suivante :

label Le nom du type tel qu'il apparaitra dans le menu d'ajout de contenu.
creation_permission Permission qu'il faut avoir pour créer une instance.
creation_roles Rôles qui ont la permission par défaut (à la racine).
use_portal_factory 1 signifie que l'on travaille dans une instance temporaire tant que l'enregistrement n'est pas fait.
content_icon Permet d'associer un fichier d'icône aux instances.
base_class 0 signifie qu'ArchGenXML ne doit pas associer par lui même de classe de base car elle est précisée dans le modèle par l'héritage.
description Donne la description du type d'objet.

Nous avons dérivé nos types de ceux contenus dans Plone, mais nous aurions très bien pu sur cet exemple partir d'un type défini dans un module. De cette façon il est possible d'étendre et particulariser des produits que l'on n'a pas fait soit même.

Si l'on avait voulu qu'il y ait plusieurs roles pouvant créer des instances nous aurions alors donné au marqueur creation_roles la valeur python:("Manager", "Contributor").

Nous allons remplir les informations concernant le champ documentation :

../_images/argouml_champ_document.jpg

Information concernant le champ mDocument

La signification des marqueurs du champs mDocument est :

widget:label Le nom du champ.
widget:label_msgid L'identifiant de traduction associé au nom.
widget:description La description du champ.
widget:description_msgid L'identifiant de traduction associé à la description.
read_permission Le nom de la permission à avoir pour voir le champ. On peut ainsi en créer de nouvelle.
write_permission Le nom de la permission à avoir pour modifier le champ.
storage Comment le contenu du champ doit être stocké.

Les formulaires d'édition ou de consultation sont générés automatiquement par Plone à partir du type des champs.

Toutefois, un type de champ peut être affiché ou édité de façons différentes selon le widget qui lui est associé.

Ces widgets sont configurés en étiquetant le champ avec des marqueurs préfixés par widget:.

Il est également possible de créer ses propres widgets.

Voici une liste des types des champs possibles.

Nom Type Widget par défaut Description
BackReferenceField backreference BackReferenceWidget Référence navigable.
BooleanField boolean ComputedWidget Champ stockant vrai ou faux.
ColorField color ColorWiget Sélection d'une couleur.
ComputedField computed ComputedWidget Champ calculé.
  copy   Permet de surcharger un champ d'une des classes de base.
DataGridField datagrid DataGridWidget Des lignes de tableau.
DateTimeField date CalendarWidget Stocke des dates et heures.
FileField file FileWidget Stocke un fichier sans réaliser de traitement.
FixedPointField fixedpoint FixedPointWidget Champ numérique à virgule fixe
FloatField float FloatWidget Champ numérique à virgule fixe
ImageField image ImageWidget Stocke une image et permet de la retailler dynamiquement.
IntegerField int IntegerWidget Stocke une donnée numérique entière.
LinesField keywords KeywordWidget Une liste de données, par exemple des mots clés.
LinesField lines LinesWidget Une liste de ligne.
LinesField multiselection MultiSelectionWidget Une liste de données à sélections multiples.
ReferenceField reference ReferenceBrowserWidget Permet de référencer un autre objet.
TextField richtext RichWidget Texte avec mise en forme.
StringField selection SelectionWidget Permet de sélectionner une ligne de texte parmi plusieurs.
StringField string StringWidget Champ texte pour les chaines de moins de 100 caractères.
TextField string TextAreaWidget Champ texte pour les grandes chaînes.

Certains de ces champs reposent sur des produits. Ainsi DataGridField repose sur le produit Products.DataGridField qu'il faut alors ajouter au buildout.

Le type copy est une astuce qui permet de surcharger les marqueurs d'un champ défini dans l'une des classes mères.

Par exemple pour changer la description, le titre et la permission de lecture et d'écriture du champ title, il suffit de créer un champ portant comme Nom title et de lui associer les marqueurs suivants avec les valeurs voulues :

Marqueur Valeur
widget:label Le nom du champ
widget:label_msgid IdentifiantDuNomPourLesTraductions
widget:description La description du champ
widget:description_msgid IdentifiantDeLaDescriptionPourLesTraductions
read_permission View
write_permission MonProduit: le nom de la permission

Les identifiants doivent être en ASCII sans espaces.

Le nom des permissions est soit celui d'une permission déjà existante soit une chaîne de caractères ASCII qui, par convention, doit être préfixée par le nom du produit, ce qui permettra de les regrouper dans l'onglet security de Zope.

Parfois, ArchGenXML ne réalise pas automatiquement l'importation de la définition de certains types de champ ou de widget. Il faut alors l'ajouter soit comme marqueur de la classe en utilisant l'étiquette import soit dans la zone protégée du module python généré, c'est-à-dire entre les lignes :

##code-section module-header #fill in your manual code here
from archetypes.referencebrowserwidget import ReferenceBrowserWidget
##/code-section module-header

@TODO mettre à jour la liste suivante et expliquer l'usage

Chacun de ces types de champs possède un ou plusieurs attributs qui permettent de paramétrer leur comportement :

  • accessor
  • default
  • default_method
  • edit_accessor
  • enforceVocabulary
  • index
  • name
  • mode
  • multiValued
  • mutator
  • primary
  • required
  • schemata
  • searchable
  • validators
  • vocabulary
  • storage

Nous allons générer une première version de notre module :

C:\Program Files\Plone4\src\Products.StandArt_AQ\Products> c:\ArchGenXML\bin\ar
chgenxml.exe ..\model\Products.StandArt_AQ.zargo StandArt_AQ
INFO  ArchGenXML Version 2.5.dev
(c) 2003-2009 BlueDynamics Alliance, Austria, GPL 2.0 or later
INFO  Directories to search for profiles: ['C:\\ArchGenXML\\src\\archgenxml\\u
mltools\\argouml']
INFO  Parsing...
INFO  Profile files: '{u'archgenxml_profile.xmi': u'C:\\ArchGenXML\\src\\archg
enxml\\umltools\\argouml\\archgenxml_profile.xmi'}'
INFO  Directory in which we're generating the files: 'StandArt_AQ'.
INFO  Generating...
INFO  Starting new Product: 'StandArt_AQ'.
c:\archgenxml\eggs\i18ndude-3.1.2-py2.6.egg\i18ndude\odict.py:7: DeprecationWa
rning: object.__init__() takes no parameters
  dict.__init__(self, dict)
INFO      Generating package 'content'.
INFO          Generating class 'StandArt_DocumentAQ'.
INFO          Generating class 'StandArt_FolderAQ'.
INFO  generator run took 0.96 sec.

archgenxml.exe ..\model\Products.StandArt_AQ.zargo StandArt_AQ a lancé l'exécution de ArchGenXML en lui demandant de prendre comme fichier d'entrée notre modèle et de générer un répertoire StandArt_AQ contenant le code du module. Pour faire cela nous nous sommes d'abord placé dans le répertoire src\Products.StandArt_AQ\Products.

Puis, nous pouvons ajouter ce nouveau module au buildout.cfg de notre Plone pour le faire apparaitre dans la liste des modules disponibles.

Pour cela, il suffit d'y ajouter :

eggs =
  Plone
  Products.StandArt_AQ

develop =
  src/Products.StandArt_AQ

D'enregistrer et de lancer la commande bin\buildout.

Puis de démarrer l'instance et d'ajouter le produit.

On peut alors, lorsqu'on est administrateur, ajouter au site Plone une instance de StandArt_FolderQA.

Vous constaterez également en regardant l'onglet Security de la ZMI que nos nouvelles permissions ont été ajoutées.

Si l'on regarde le code du module StandArt_DocumentAQ.py, on constate qu'il contient la définition d'un schéma Archetypes :

schema = Schema((
  FileField(
       name='mDocument',
      widget=FileField._properties['widget'](
          label="Document",
          label_msgid="StandArt_DocumentAQ_Document_label",
          description="A document under Quality Assurance",
          description_msgid="StandArt_DocumentAQ_Document_description",
          i18n_domain='StandArt_AQ',
      ),
      storage=AttributeStorage(),
      read_permission="View",
      write_permission="StandArt: write AQ document",
  ),
),
)

StandArt_DocumentAQ_schema = BaseSchema.copy() + \
  getattr(ATDocument, 'schema', Schema(())).copy() + \
  schema.copy()

La variable schema est une liste de champs, qui ici contient la déclaration du champ mDocument. On peut voir comment sont implémentés les différents marqueurs du champ et comment lui est associé son widget.

Suit la définition de la classe :

class StandArt_DocumentAQ(ATDocument, BrowserDefaultMixin):
  """
  """
  security = ClassSecurityInfo()

  implements(interfaces.IStandArt_DocumentAQ)

  meta_type = 'StandArt_DocumentAQ'
  _at_rename_after_creation = True

  schema = StandArt_DocumentAQ_schema

  ##code-section class-header #fill in your manual code here
  ##/code-section class-header

  # Methods

Qui utilise le schéma créé.

Lors des futures générations du code, ArchGenXML conservera les modifications qui auront été apportées aux modules du produit, et complètera le schéma Archetypes avec les champs ajoutés à la classe dans le modèle UML.

Le modèle et le code généré sont multi-plateforme. Ainsi vous pouvez éditer le modèle sous Windows ou GNU/Linux sans être lié à l'OS de développement ni à celui de déploiement.

Dans la suite de ce cours nous continuerons notre exposé sous Ubuntu.

En conséquence les chemins devront être adaptés, c'est-à-dire que les séparateurs de fichiers sont des slash au lieu d'être des anti-slash.

Création des workflows

Nous allons associer un workflow documentaire à chaque classe créée.

Pour cela sélectionnez StandArt_FolderAQ dans la barre de navigation, puis cliquez sur le bouton droit pour faire apparaître le menu contextuel. Sélectionnez le menu "Create Diagram" et le sous menu "Diagramme d'état".

../_images/argouml_creation_diagramme_etats.jpg

Ajout diagramme d'états-transition

Une machine d'états-transitions avec le nom "(Unnamed StateMachine)" est créée dans "StandArt_FolderAQ".

Depuis la barre de navigation, descendez dans "StandArt_FolderAQ", puis sélectionnez "(Unnamed StateMachine)". Renommez le en "standart_folder_workflow".

Descendez d'un niveau dans "standart_folder_workflow", s'y trouve le diagramme d'états-transitions "StandArt_FolderAQ 1", renommez le "standart_folderaq_state_machine".

Lorsque vous sélectionnez le diagramme "standart_folderaq_state_machine" vous pouvez y créer des états et transitions. Ainsi vous définissez le workflow de votre objet.

Pour ajouter des états il suffit de sélectionner le symbole correspondant dans la barre d'icônes, de positionner le curseur dans la zone de dessin et de cliquer.

Pour ajouter des transitions, soit vous sélectionnez l'icône correspondante dans la barre et pouvez alors relier deux états déjà existants, soit vous sélectionnez un état et cliquez sur l'une des flèches apparaissant à la droite et à la gauche de l'état sélectionné.

Nous allons créer le workflow de FolderAQ, qui contient un état private et un état published, reliés par une transition publish.

Les noms des états et transitions doivent être saisi dans le champ Nom accessible par l'onglet Propriétés sur chaque état et transition.

Les marqueurs des états vont nous permettre de définir qu'elles seront les permissions de l'objet lorsqu'il sera dans cet état.

ArchGenXML permet de manipuler quatre meta permissions qui sont access, view, modify et list. Ainsi, à la génération du code, ArchGenXML remplace ces meta par le vrai nom des permissions.

Si l'on veut directement travailler avec les permissions de Plone, il faut créer pour chaque permission une Tag Definition en cliquant sur l'icône TD de l'onglet Étiquettes et en lui donnant le nom de la permission voulue.

On peut alors les ajouter comme marqueurs dans l'onglet Étiquettes de l'état. En valeur on précise alors les rôles qui auront cette permission.

C'est de cette façon que l'on peut préciser qui a les permissions de lecture ou d'écriture que l'on a affecté spécifiquement aux champs de nos objets.

Dans notre cas on crée un TD StandArt: write AQ document pour la permission d'écriture de notre champ mDocument et l'on donne cette permission au rôle Reviewer dans l'état submited pour que seuls les membres ayant le rôle Reviewer puissent changer le fichier associé au champ mDocument.

../_images/argouml_folderaq_workflow.jpg

workflow de FolderAQ

Il reste à créer de la même façon le workflow de DocumentAQ, qui contiendra trois états nommés private, submited, published.

../_images/argouml_documentaq_workflow.jpg

workflow de DocumentAQ

Puis éditez les transitions, nommez les submit, publish.

Il est possible d'ajouter aux transitions un garde qui vérifiera que l'on peut ou non réaliser cette transition. Plone n'affichera la transition que si le garde est vérifié.

Pour créer un garde il suffit de faire un clic droit dans la la zone d'édition du champ Garde dans l'onglet Propriété de la transition, puis de sélectionner l'item de menu Nouveau.

On entre alors dans le panneau d'édition du garde.

../_images/argouml_garde_publish.jpg

Garde de la transition publish

On peut alors expliciter comment on veut filtrer la transition en remplissant le champ Expression. Pour cela on dispose de trois possibilités :

Prefixe du garde Valeur du garde Exemple
guard_roles Une liste de rôles guard_roles: Manager; Owner
guard_permissions Liste de permissions guard_permissions: View
guard_expr Expression Tales guard_expr:python:object.isOk()

Les expressions peuvent être combinées, par exemple la transition avec le garde guard_roles:Reviewer|guard_permission:Modify Portal Content ne sera déclenchable que par les modérateurs ayant les droits de modification.

En plus des gardes, il est possible d'exécuter des scripts avant et après transition. Pour cela il faut ajouter une conséquence à la transition comme on l'a fait pour le garde. Le nom de la conséquence sera le nom de la fonction python appelée lors de la transition.

../_images/argouml_garde_et_consequence.jpg

Garde et conséquence

ArchGenXML va créer une arborescence Products.StandArt_AQ/Products/StandArt_AQ/profiles/default/workflows contenant un répertoire contenant la définition XML du workflow pour FolderAQ et un autre pour DocumentAQ.

Il génère aussi un fichier wfsubscribers.py contenant la définition des scripts appelés lors des transitions :

# -*- coding: utf-8 -*-
#
# File: wfsubscribers.py
#
# Copyright (c) 2010 by Michael Launay <michaellaunay@ecreall.com>
# Generator: ArchGenXML Version 2.5.dev
#            http://plone.org/products/archgenxml
#
# GNU General Public License (GPL)
#

__author__ = """Michael Launay <michaellaunay@ecreall.com>"""
__docformat__ = 'plaintext'


##code-section module-header #fill in your manual code here
##/code-section module-header


def onSubmitDocumentAQ(obj, event):
    """generated workflow subscriber."""
    # do only change the code section inside this function.
    if not event.transition \
       or event.transition.id not in [u'submit'] \
       or obj != event.object:
        return
    ##code-section onSubmitDocumentAQ #fill in your manual code here
    ##/code-section onSubmitDocumentAQ



##code-section module-footer #fill in your manual code here
##/code-section module-footer

Pour ajouter du code spécifique tel que la notification par mail des principaux modérateurs, il suffit de le mettre entre la balise ##code-section onSubmitDocumentAQ et ##/code-section onSubmitDocumentAQ, mais si le code est volumineux ou doit être partagé nous pouvons le mettre dans un module qui sera importé dans la section ##code-section module-header et l'appel se fera dans la section ##code-section onSubmitDocumentAQ.

L'événement event contient l'objet qui subit la transition, ainsi que la requête.

Si vous voulez faire de l'introspection et voir ce qui se passe vous pouvez mettre un pdb.set_trace () dans la section, relancer Zope en mode foreground bin/instance fg, et déclencher la transition. Python s'arrêtera sur le point d'arrêt et en tapant la commande h vous aurez la liste des commandes possibles :

##code-section onSubmitDocumentAQ #fill in your manual code here
import pdb
pdb.set_trace ()
##/code-section onSubmitDocumentAQ

Il est possible d'associer un workflow créé pour un type de contenu donné à un autre type de contenu avec l'étiquette "use_workflow", il suffit alors de donner le nom du workflow comme valeur à l'étiquette.

Ajout d'une worklist

Il est possible pour un état donné d'un objet de le déclarer comme devant apparaître dans une liste de modération.

Pour cela il suffit d'ajouter le marqueur worklist à la liste des étiquettes de l'état dans lequel l'objet doit être modéré.

La valeur est alors le nom de la woklist dans laquelle devrait apparaître l'objet, mais la liste de modération de Plone regroupe tous les objets associés à une worklist.

Le marqueur worklist:guard_roles permet de restreindre la notification de modération aux rôles donnés en valeur au marqueur.

Ajout d'un validateur sur un champ

Les #validators# permettent de vérifier la cohérence des valeurs des champs saisis. Ils sont appelés à la saisie des champs en mode d'édition online ou à l'enregistrement des modifications réalisées sur le contenu en mode édition.

Les champs de type Archetypes utilisent des objets implémentant l'interface IValidator.

Lors de la saisie du champ, les validateurs associés à un champ sont appelés successivement. Dés que l'un deux retourne une valeur différente de True, le champ est en erreur et un message correspondant est positionné dans la variable error.

Par défaut Plone propose plusieurs instances de validators en voici les plus utiles :

Nom Usage Détails
isDecimal L'entrée est elle un un nombre décimal ? Autorise la notation scientifique.
isInt Est-ce un entier ?  
isPrintable Ne doit pas contenir de caractères non imprimables r'[a-zA-Z0-9s]+$'
isInternationalPhone Number Est un numéro de téléphone international ? Vérifie le format et ignore les espaces, parenthèse et barres obliques.
isURL Est-ce une url ? Reconnait la plus part des protocoles.
isEmail Est-ce une adresse mél ? Vérifiée par une expression régulière.
isMailTo Est-ce une adresse mél précédée par "mailto:"  
isUnixLikeName Est-ce un nom respectant le style Unix ? r"^[A-Za-z][wd-_] {0,7}$"
isMaxSize Vérifie si un fichier uploadé ou un objet supportant l'opération len() est plus petit qu'une valeur donnée. Vérifie à partir de la valeur maxsize définit sur le champ.
isValidDate Est-ce que la chaîne de caractères peut être convertie en date ?  
isEmpty La valeur doit être vide.  
isEmptyNoError La valeur doit être vide. L'erreur ne sera pas montrée.
isValidId La valeur doit correspondre à l'identifiant d'un objet.  
isTidyHtml Utilise mx.Tidy pour valider l'entrée HTML. Échoue sur les erreurs et les alertes.
isTidyHtmlWithCleanup Utilise mx.Tidy pour valider l'entrée HTML. Échoue sur les erreurs mais nettoie le code des alertes.
isNonEmptyFile Vérifie que le fichier uploader n'est pas vide.  
isTAL Est-ce une expression Template Attribute Language ?  

Pour les utiliser il suffit d'ajouter "validators" comme étiquette au champ avec pour valeur le nom du ou des validators à utiliser.

Plone offre également la possibilité de paramétrer des validators en les instanciant avec des paramètres puis en utilisant comme validator ces instances.

Par exemple ExpressionValidator qui permet de définir un validator utilisant une expression TALES pour dire si la valeur saisie est correcte ou non.

L'instanciation de ce validator est supportée par ArchGenXML via les étiquettes validation_expression pour l'expression et validation_expression_errormsg pour le message d'erreur associé.

RegexValidator du paquet Products.validation.validators.RegexValidator permet d'utiliser une expression régulière pour la validation, il faut l'instancier et l'enregistrer dans le code généré dans les parties réservées au code manuel.

RangeValidator permet quant à lui de vérifier qu'une valeur est comprise entre deux bornes. Il est dans Products.validation.validators.RangeValidator.

Nous allons voir comment créer un validator qui rappelle une méthode de notre objet.

Pour cela nous commençons par créer un fichier objectcallbackvalidation.py qui contiendra un validator appelant une méthode dont le nom sera dynamiquement calculée à partir du nom du champ validé. Par convention nous imposons que la méthode ait un nom de la forme is_a_valid_nomduchamp sur la classe du contenu :

from Products.validation.interfaces.IValidator import IValidator
from zope.interface import implements


class ObjectCallbackValidation:
    implements(IValidator)
    def __init__(self,name):
        self.name = name
    def __call__(self, value, *args, **kwargs):
        fieldname = kwargs['field'].getName()
        instance = kwargs['instance']
        method = "is_a_valid_"+fieldname
        # callback the object vaildation methode for the field
        return getattr(instance, method)(value)

object_callback_validation = ObjectCallbackValidation("object_callback_validation")

# validator registration
from Products.validation import validation
validation.register(object_callback_validation)

Pour qu'au chargement de notre module ce code soit appelé il nous suffit d'en faire un import dans la fonction "initialize" du fichier __init__.py du produit.

Ensuite il suffit de créer une méthode "is_a_valid_nomDeMonChamp" dans notre modèle sur notre classe pour chacun des champs à qui on a déclaré object_callback_validation pour validator.

Ajout d'une méthode

L'ajout d'une méthode est simple puisqu'il suffit de l'ajouter à l'une des classes de nos contenus.

Par exemple si nous voulons écrire une méthode que l'on utilisera comme validator à l'aide du code écrit précédemment pour le champ mDocument du type de contenu StandArtDocumentAQ pour vérifier que le fichier uplaoder est un pdf.

../_images/argouml_ajout_methode.jpg

Ajout d'une méthode

La génération du code produit la méthode suivante dans le code StandArt_DocumentAQ.py :

security.declarePublic('is_a_valid_mDocument')
def is_a_valid_mDocument(self):
    """
    """
    pass

Important

En l'abscence de précision, la méthode générée est accessible par toute page template ou pythonscript quelque soit le rôle de l'utilisateur.

Pour la protéger, il faut ajouter l'étiquette "permission" avec pour valeur le nom de la permission à avoir par exemple "Modify portal content".

Cette exposition est due à la présence de la documentation de la méthode qui est interprétée par Plone comme la volonté à exposer la méthode.

Ajoutons au modèle le pramètre "value" à la méthode de type None (les informations de types ne sont pas utilisés par ArchGenXML) et générons le code à nouveau.

Nous pouvons maintenant remplacer l'instruction pass par notre code :

##code-section module-header #fill in your manual code here
import pyPdf
from Products.StandArt_AQ  import StandArt_AQFactory as _
##/code-section module-header

#...

class StandArt_DocumentAQ(ATDocument, BrowserDefaultMixin):
  #...

  security.declareProtected("Modify portal content", 'is_a_valid_mDocument')
  def is_a_valid_mDocument(self,value):
      """ Test if the file is a pdf ?
      """
      try:
          tell = value.tell()
          value.seek(0)
          f = pyPdf.PdfFileReader(value) #imported at the top of the file
          f.documentInfo #nothing is done yet
          value.seek(tell)
          return 1
      except:
          return _(u'file must be a pdf')

Pour la traduction il faut ajouter StandArt_AQFactory dans le __init__.py

from zope.i18nmessageid import MessageFactory
StandArt_AQFactory = MessageFactory('StandArt')

Maintenant nous pouvons ajouter l'étiquette "validators" à mDocument avec pour valeur "object_callback_validation".

Ajout d'une validation sur l'ensemble de l'édition

Lorsque plusieurs champs sont liés entres eux il n'est possible de tester leur validité qu'à l'enregistrement de l'instance du contenu.

Il suffit d'ajouter la méthode post_validate(self, REQUEST, errrors) au type de contenu. Si l'on constate des erreurs, on les remonte en remplissant le dictionnaire "errors" :

security.declarePublic('post_validate')
def post_validate(self,REQUEST,errors):
    """ Check if the pdf info version match the DocumentNumber
    """
    if not REQUEST : return
    pdf_file = REQUEST.has_key('mDocument_file') and \
                    REQUEST.form['mDocument_file'] or None
    version_number = REQUEST.has_key('mDocumentNumber') and \
                    REQUEST.form['mDocumentNumber'] or None
    m_document = self.getMDocument()
    m_document_number = self.getMDocumentNumber()
    if pdf_file is None and version_number is None or \
        pdf_file is None and not m_document or \
        version_number is None and m_document_number is None :
            return
    if pdf_file is None :
        pdf_file = StringIO.StringIO(m_document)
    if version_number is None :
        version_number = m_document_number
    tell = pdf_file.tell()
    pdf_file.seek(0)
    infos = pyPdf.PdfFileReader(pdf_file).documentInfo
    keywords = infos['/Keywords']
    pdf_file.seek(tell)
    str_version = u"Version : "
    posv = keywords.find(str_version)
    if posv != -1 :
        version = keywords[posv + len(str_version):].split()[0]
        if version != version_number :
            errors['mDocumentNumber'] = _("Document numbers doesn't match")

Il est possible aussi de surcharger la méthode pre_validate qui est appellée avant l'appel individuel des "validators" des champs.

Important

Dans le cas d'une édition en ligne c'est à dire sans passer par l'onglet Modifier la méthode post_validate n'est pas appelée.

Ajout d'une action

En stéréotypant une méthode par "Action" nous allons ajouter un onglet à la barre d'édition de l'instance d'action. Cet onglet renvoie allors sur une page template que l'on peut définir.

Par exemple nous allons ajouter un onglet "Pdf Informations" à notre DocumentAQ :

../_images/argouml_action.jpg

Ajout d'une action

Le nom de l'onglet, son identifiant, la permission d'accès, le nom de la template associée, et la condition d'affichage sont données comme valeurs des étiquettes correspondantes.

L'action est alors ajoutée dans la déclaration xml du type StandArt_AQ/profiles/default/types/StandArt_DocumentAQ.xml :

<action title="Pdf Informations"
    i18n:attributes="title"
    action_id="pdf_informations"
    category="object"
    condition_expr="python:object.getMDocument()"
    url_expr="string:${object_url}/document_aq_pdf_informations"
    visible="True">
<permission value="View"/>

Nous pouvons alors créer la template StandArt_AQ/skins/standart_aq_templates/document_aq_pdf_informations.pt qui contient :

<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>
    <div metal:fill-slot="main">
      <table>
       <tbody tal:define="pdf_informations python:context.getPdfInformations()">
        <tr tal:repeat="keyword pdf_informations">
          <td tal:content="keyword">Title</td>
          <td tal:content="python:pdf_informations[keyword]">Value</td>
        </tr>
       </tbody>
      </table>
    </div>
  </body>
</html>

Cette template appèle une méthode du type de contenu appelée getPdfInformations qui retourne le dictionnaire des informations. Pour la définir il nous suffit d'ajouter cette méthode comme nous avons fait pour les méthodes précédentes.

security.declareProtected("View", 'getPdfInformations')
def getPdfInformations(self):
    """ Return the pdf document informations
    """
    document = self.getMDocument()
    if not document :
        return {}
    strfile = StringIO.StringIO(document)
    documentInfo = pyPdf.PdfFileReader(strfile).documentInfo
    return documentInfo.copy() #The copy is needed by security check

Il faut ajouter l'import

Ajout d'un portlet d'affichage des Documents soumis

La portlet de modération est générique, il est possible de créer ses propres portlets et par exemple d'en créer des dédiées à la modération de nos types de données à l'exclusion des autres.

Pour ajouter une portlet il suffit de créer une classe et de lui mettre le stéréotype "portlet".

../_images/argouml_ajout_portlet.jpg

Ajout d'une portlet

La génération du module avec ArchGenXML produit deux fichiers.

Le fichier StandArt_AQ/profiles/default/portlets.xml contient la déclaration de la portlet :

<portlets>
 <portlet
   addview="Products.StandArt_AQ.content.StandArt_WorkList.StandArt_WorkList"
   title="StandArt_WorkList"
   description=""
  />

  <!-- ##code-section PORTLETS -->
  <!-- ##/code-section PORTLETS -->

</portlets>

On y voit le nom de la portlet et la vue associée.

Si l'on ouvre le fichier de la vue générée StandArt_AQ/content/StandArt_WorkList.py on y voit principalement :

class IStandArt_WorkList(IPortletDataProvider):
    """A portlet which renders the cart.
    """

class Assignment(base.Assignment):
    implements(IStandArt_WorkList)
    title = _(u'StandArt_WorkList')
    ##code-section assignment-body #fill in your manual code here
    ##/code-section assignment-body

class Renderer(base.Renderer):
    render = ViewPageTemplateFile('templates/StandArt_WorkList.pt')
    ##code-section renderer-body #fill in your manual code here
    ##/code-section renderer-body

class AddForm(base.NullAddForm):
    def create(self):
        return Assignment()
    ##code-section addform-body #fill in your manual code here
    ##/code-section addform-body

Pour remplir et rendre fonctionnel ce code lisez le chapitre Les portlets.

Dans notre cas pour réaliser la worklist il faut faire :

#...
from Products.CMFCore.utils import getToolByName
#...

class Renderer(base.Renderer):
    render = ViewPageTemplateFile('templates/StandArt_WorkList.pt')
    ##code-section renderer-body #fill in your manual code here
    @memoize
    def _data(self, target):
        if self.anonymous:
            return []
        context = aq_inner(self.context)
        ctool = getToolByName(context, 'portal_catalog')
        wtool = getToolByName(context, 'portal_workflow')
        ttool = getToolByName(context, 'portal_types')
        ENM_AMC = ttool['StandArt_DocumentAQ']
        wfs = wtool.getWorkflowsFor('StandArt_DocumentAQ')
        review_states = []
        for wf in wfs:
           wl = wf.worklists._objects
           wld = wf.worklists._mapping[wl[0]['id']]
           review_states.extend(wld.var_matches['review_state'])

        return ctool.searchResults(meta_type = 'StandArt_DocumentAQ',
                 review_state = review_states)
    ##/code-section renderer-body

Et la page template StandArt_AQ/content/templates/StandArt_WorkList.pt

<dl class="portlet portletStandArt_WorkList"
      i18n:domain="StandArt_AQ">
  <dt class="portletHeader">
      <span class="portletTopLeft"></span>
      <h3>
          <span i18n:domain="plone" i18n:translate="box_review_list">
             Review List
          </span>
          <span tal:content="view/data/search_filter"/>
      </h3>
      <span class="portletTopRight"></span>
  </dt>

  <tal:items tal:repeat="b view/review_items">
      <dd class="portletItem"
          tal:define="oddrow repeat/b/odd;
                      review_state b/review_state;"
          tal:attributes="class python:oddrow and 'portletItem even' or
                           'portletItem odd'">

          <a href="#"
             tal:attributes="href string:${b/getPath}/view;
                             title b/Description;
                             ">
              <span tal:replace="b/Title">
                  Title
              </span>
              <span class="portletItemDetails">
                  <span tal:replace="b/Creator">Jim Smith</span> &mdash;
                  <span tal:replace="b/modified"> May 5</span>
              </span>
          </a>

      </dd>
  </tal:items>

  <dd class="portletFooter">
      <span class="portletBottomLeft"></span>
      Footer
      <span class="portletBottomRight"></span>
  </dd>
</dl>

Modification de la vue d'édition

Plone propose un mécanisme trés simple de modification de la vue d'édition d'un type donné.

Il suffit de créer un fichier template dans le répertoire skins basé sur le nom du type mis en minucule et suivit du postfixe _edit par exemple pour modifier la vue d'édition de StandArt_DocumentAQ il suffit de créer le fichier StandArt_AQ/skins/standart_aq_templates/standart_documentaq_edit.pt qui contient alors les slots de la template de base que l'on veut surcharger.

Par exemple si l'on souhaite "Faire un truc super cool" juste avant l'affichage du champ mDocumentNumero on peut faire des choses compliquées comme :

<metal:macro define-macro="body">
  <metal:use-macro use-macro="here/edit_macros/macros/body">
    <metal:fill-slot fill-slot="widgets">
      <tal:tabbed tal:condition="allow_tabbing | nothing">
  <fieldset tal:define="sole_fieldset python:len(fieldsets) == 1"
      tal:repeat="fieldset fieldsets"
      tal:attributes="id string:fieldset-${fieldset}"
      tal:omit-tag="sole_fieldset">
    <legend id=""
      tal:content="python: view.getTranslatedSchemaLabel(fieldset)"
      tal:attributes="id string:fieldsetlegend-${fieldset}"
      tal:condition="not:sole_fieldset" />
    <tal:fields repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)">
      <div tal:condition="python:field.getName() == 'mDocumentNumero'">
    Faire un truc super cool !
      </div>
      <metal:fieldMacro use-macro="python:here.widget(field.getName(), mode='edit')" />
    </tal:fields>
  </fieldset>
      </tal:tabbed>
      <tal:nottabbed tal:condition="not: allow_tabbing | nothing">
  <tal:fields repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)">
    <metal:fieldMacro use-macro="python:here.widget(field.getName(), mode='edit')" />
  </tal:fields>
      </tal:nottabbed>
    </metal:fill-slot>
  </metal:use-macro>
</metal:macro>

Pour savoir quoi surcharger, il faut aller voir les templates archetypes/edit_macro et archetypes/base_edit .

Modification de la vue de consultation

La vue de consultation repose sur le même principe sauf que la template de base doit porter le nom du type en minuscule postfixé de _view et que la template de base est archetypes/base_view

exercice

Création des fondements du modèle de l'application "StandArt_AQ".


blog comments powered by Disqus