Note à propos des « Content Repository » dans le cadre d'un CMS

Dimanche dernier, j'ai étudié le thème « Content Repository », voici quelques notes concernant mes recherches.

Séparation des Web CMS en deux couches

J'ai trouvé un article qui fait la distinction entre deux types de CMS, enfin il parle de WCMS (Web CMS) :

  • « Content Production Systems (CPS) »
  • « Presentation Management Systems (PMS) »
Personnellement, cet article me fait penser à une réflexion qui m'est déjà venu de nombreuses fois : là où l'auteur de l'article voit deux systèmes, deux types d'applications distinctes, je vois en fait deux couches… Bon en fait, l'auteur semble le dire aussi à la fin de son article quand il parle de CPS + PMS.
Je reprends ci-dessous à peu près la description qu'il fait d'un CPS et d'un PMS :
  • CPS - c'est le système qui gère la couche données, la logique métier du CMS, comme :
    • l'enregistrement des ressources (Pages, Images, Dossiers…)
    • un moteur de recherche plain texte (avec filtre…)
    • un workflow
    • un système de versionning
    • import / export de données
  • PMS - c'est le système qui pour résumer "affiche" les pages :
    • la navigation
    • système de template
    • layouts
    • skins / thèmes
    • back office d'édition

« Content Repository »

À mes yeux… je ne fais pas bien la distinction entre un Content Repository et un CPS.

Si j'essaie de les différencier, je dirais qu'un Content Repository est une librairie ou un serveur alors qu'un CPS est une application, avec des Interfaces Utilisateur…

Dans la suite de l'article, je vais partir du principe que Content Repository est la même chose qu'un CPS même si ce n'est pas totalement exact.

Dans le monde Java, une spécification nommée JCR (Java Content Repository) décrit ce qu'est un Content Repository et comment l'utiliser (spécification d'une API standard).

Il existe plusieurs implémentations de JCR mais la plus connue semble être Jackrabbit de la fondation Apache. Je l'avais rencontré il y a quelque années lorsque j'ai étudié de loin Nuxeo.

J'ai été agréablement surpris de découvrir les projets PHP suivants (ce qui confirme que je ne suis pas le seul à me poser ces questions) :

J'ai appris l'existence d'un projet de Content Repository pour Node.js :

J'ai aussi trouvé Lily qui est un Content Repository basé sur Hadoop, HBase et SOLR dans un écosystème Java + API Rest.

Spécificités techniques d'un CPS

J'ai trouvé un autre article qui traite des spécificités techniques d'un CPS. Voici ci-dessous sa liste de "requirements" :

  • Richly structured content types
  • Unstructured binary objects
  • Relationships / references / associations
  • The ability to evolve content models over time (what I call “schema evolution”)
  • Branch / merge (in the Source Code Management (SCM) sense of the term)
  • Snapshot based versioning
  • ACID transactions
  • Scalability to large content sets
  • Geographic distribution

Chacun de ces points sont détaillés dans son article et c'est fortement intéressant. Par exemple, je trouve le point « Branch / Merge » très juste… bien que je ne sache pas tout à fait comment l'implémenter simplement.

À partir de cette liste, voici la liste des "requirements" de mon Content Repository idéal :

  • Base de données où je peux stocker des objects JSON ainsi que des blobs (exemples : base de données NoSQL comme MongoDB, CouchDB)
  • Base de données où je peux stocker des collections (pour des dossiers), des liens (pour des redirections), des associations (pour des catégories, des tags…)
  • API Rest qui permettent d'accéder plus ou moins à toutes les features du Content Repository
  • Moteur de recherche (basé par exemple sur : Xapian, elasticsearch)
  • Un système de workflow
  • Versionning (peut-être basé sur Git ou Mercurial)
  • Système d'import / export de données (je ne sais pas si il y a un format standard pour les CMS… enfin je n'ai jamais trouvé)
  • Support d'une API CMIS (bien que je la trouve peu élégante… c'est bien une API faite par le monde Java)
  • Accès WebDAV
  • Gestion des utilisateurs

Pour le moment, je n'ai pas trouvé le Content Repository de mes rêves.

Le standard CMIS « Content Management Interoperability Services »

Le standard CMIS est un autre élément intéressant à prendre en compte dans un CPS.

CMIS est une couche d'abstraction dont le but est de permettre de faire communiquer plusieurs instances de CMS identiques ou différentes ensemble. Elle doit permettre par exemple d'importer les données d'une instance à une autre.

Il existe de nombreux clients CMIS pour différent langage…

La suite…

J'ai prévu d'écrire d'autres billets à propos des Content Repository :

  • Un retour d'expérience et mes réflexions concernant un premier Content Repository que j'ai écrit en Python, basé sur la base de données NoSQL ZODB et le moteur de recherche Whoosh. Il comporte aussi un connecteur WebDAV.

  • Un autre billet sur une proposition d'API RESTful pour un Content Repository… quelque chose de naturel et vraiment dans l'esprit RESTful.

    Exemple :

    GET http://example.com/blog/my-post         => est dans l'esprit RESTful
    GET http://example.com/post?name="my-post"  => est moins dans l'esprit RESTful
    

    Dans l'API REST de CMIS j'ai trouvé beaucoup de requête suivant la seconde forme d'URL et très peu ou pas sous la première forme d'URL.

Read and Post Comments

Idée d'une librairie basée sur Selenium pour tester des formulaires web

Introduction

Je viens de commencer à utiliser Selenium pour tester des formulaires, divers pages de listes de données… dans une application métier.

Mes objectifs :

  • tester l'ajout de nouvelles entités (1)
    • les données sont injectées dans un formulaire d'une page html
    • une page de résultat est utilisées pour vérifier que les données ont bien été enregistré (ça peut être une page d'édition)
  • tester la modification d'une entités (2)
    • les données sont injectées dans un formulaire d'une page html
    • une page de résultat est utilisées pour vérifier que les données ont bien été enregistré (ça peut être une page d'édition)
  • tester la validité d'une page de liste (3)
  • tester la validité des résultats d'un moteur de recherche (4)

Pour le moment je me suis concentré uniquement sur les points 1 et 2.

Première méthode (à l'arrache)

J'ai commencé par réaliser des fonctions du type :

  • login()
  • add_customer(data)
  • edit_customer(data)
  • check_customer(data)
  • add_customer(data)
  • edit_customer(data)
  • check_customer(data)

Les fonctions de type "add" et "edit" prennent en paramètre des données à injecter dans des pages. Les fonctions de type "add" correspondent aux pages d'ajout d'entités, les fonctions de type "edit" correspondent aux pages de modification d'entités.

Ensuite j'ai des fonctions "check", là aussi je passe en paramètre des données qui seront utilisées comme valeur de vérification face à des pages de résultats ou pages d'éditions (une page d'édition contient déjà des données, le but ici est de vérifier leurs validitées).

Ma variable "data" est du type :

data = [
    ("reference", u"C1345"),
    ("firstname", u"Stéphane),
    ("lastname", u"Klein"),
    ...
]

Dans mes fonctions ("add", "check"…) j'ai une boucle qui parcourt la structure de données et utilise les fonctions suivantes soit pour injecter des données, soit pour tester la validité des données.

def inject_value(driver, name, value):
    element = driver.find_element_by_id(name)
    if element.tag_name == 'input':
        if element.get_attribute("type") == "checkbox":
            if element.is_enabled() != value:
                element.click()
        else:
            element.clear()
            element.send_keys(value)

    elif element.tag_name == 'select':
        option_element = element.find_element_by_xpath(".//option[@value='%s']" % value)
        option_element.click()

    elif element.tag_name == 'textarea':
        element.clear()
        element.send_keys(value)

def check_value(driver, name, value):
    element = driver.find_element_by_id(name)
    if element.tag_name == 'input':
        if element.get_attribute("type") == "checkbox":
            return element.is_enabled() == value
        else:
            return element.get_attribute("value") == value

    elif element.tag_name == 'select':
        return element.get_attribute("value") == value

    elif element.tag_name == 'textarea':
        return element.text == value

Pour le moment, cela fonctionne correctement mais je trouve mon code fastidieux pour plusieurs raisons :

  • j'aimerais pouvoir définir des valeurs par défaut pour les formulaires
  • j'aimerais pouvoir choisir d'autres types de "selecteur", pour le moment je fais des recherches uniquement par ID
  • j'aimerais pouvoir facilement indiquer le type de champ, car pour le moment je fais de l'auto détection… mais cela ne sera pas toujours faisable

Ce que j'aimerais avoir

À noter que ce code n'est pas complet… c'est un brouillon.

from sealchemy import Form, TextField, SelectField, BooleanField

...

class AddCustomer(Form):
    __submit__ = Submit(name="_same")

    reference = TextField(default=u"C1345")
    type_user = SelectField(default=u"external")
    firstname = TextField(required=True)
    lastname = TextField(required=True)
    activated = BooleanField(default=True)
    comment = TextAreaField(default=u"")

    def go_to_page(self):
        self.driver.get("/customers/add/")

class EditCustomer(Form):
    __submit__ = Submit(name="_same")

    reference = TextField(default=u"C1345")
    type_user = SelectField(default=u"external")
    firstname = TextField(required=True)
    lastname = TextField(required=True)
    activated = BooleanField(default=True)
    comment = TextAreaField(default=u"")

    def go_to_page(self, id):
        self.driver.get("/customers/%s/" % id)

    def go_to_last_inserted(self):
        """Va sur la page du dernier client qui a été ajouté"""
        ...

Cela ressemble beaucoup à l'API de wtforms que j'utilise dans mon projet. Cela ressemble aussi à FormAlchemy que j'aime aussi.

__submit__ permet d'indiquer le champ à utiliser par la commande submit.

Note : je n'utilise pas une seule classe pour faire mes traitements "add" et "edit" car les formulaires d'ajouts et d'édition sont en pratique souvent différents.

L'interface de la classe de type Form :

class IForm(zope.interface.Interface):
    def inject():
        """Cette méthode injecte les données vers le formulaire HTML"""

    def submit():
        """Cette méthode lance le submit du formulaire"""

    def inject_and_submit():
        """Exécute inject et ensuite submit"""

    def check():
        """Cette méthode retourne True si les données correspondent aux
           données présentes dans le formulaire HTML"""

    def clear():
        """Réinitialise la valeur de tous les champs de l'instance avec
           les valeurs par défauts"""

    def populate(values):
        """Affecte des valeurs aux champs de l'objet."""

La classe Session de mon projet :

class MyProject(Session):
    def __init__(self, login, password):
        ...

    def login(self):
        ...

    add_customer = AddCustomer()
    edit_customer = EditCustomer()

Dans MyProject, j'ai ajouté les propriétés add_customer et edit_customer afin que ces objets aient accès à l'objet driver de Selenium.

Exemple d'utilisation :

session = MyProject(login="username", password="password", url="http://localhost:5000/")
session.login()

values = {
    "reference": u"C1871",
    "firstname": u"Stéphane",
    "lastname": u"Klein",
}
session.add_customer.populate(values)
session.add_customer.go_to_page()
session.add_customer.inject_and_submit()
session.edit_customer.go_to_last_inserted()
session.edit_customer.populate(values)
assert session.edit_customer.check()

Plus d'informations à propos des classes Field

Les classes de type Field comme TextField, BooleanField … ont un constructeur avec plusieurs paramètres :

  • name (optionnel) : définit la méthode de recherche du champ par l'attribut "name"
  • id (optionnel) : définit la méthode de recherche du champ par l'attirbut "id"
  • xpath (optionnel) : définit la méthode de recherche via xpath
  • required (optionnel) : définit si le champ est requis ou non
  • default (optionnel) : définit la valeur par défaut du champ

Par défaut, si j'ai ceci :

class ...(Form):
    firstname = TextField()

C'est équivalent à cela :

class ...(Form):
    firstname = TextField(name="firstname")

Conclusion, questions

Je vous ai donc présenté l'API que j'imagine créer sous le nom de "sealchemy".

J'ai plusieurs questions :

  • est-ce que vous pensez que cette librairie serait utile ?
  • est-ce que l'API, le mode de fonctionnement est judicieux ?
  • est-ce que cela vous intéresse ?
  • est-ce que vous avez déjà créé quelque chose du même genre ?
  • quelle est votre méthode pour faire ce genre de test ?

Merci d'avance pour vos commentaires.

Read and Post Comments

Mes flux :