Ecrire des templates Plone : bases

Author:Thomas Desvenain
Created:2013-02-28
Version:0.1.0

Copyright (C) 2013 Thomas Desvenain <thomas.desvenain AT gmail.com>.

Chacun est autorisé à copier, distribuer et/ou modifier ce document suivant les termes de la licence Paternité-Pas d’Utilisation Commerciale-Partage des Conditions Initiales à l’Identique 2.0 France accessible à http://creativecommons.org/licenses/by-nc-sa/2.0/fr

Le code source présent dans ce document est soumis aux conditions de la « Zope Public License », Version 2.1 (ZPL).

THE SOURCE CODE IN THIS DOCUMENT AND THE DOCUMENT ITSELF IS PROVIDED “AS IS” AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE.

Introduction

Ce chapitre fait suite au chapitre Ecrire des scripts Plone : bases, notamment dans son objectif de permettre à des webmaster Plone débutants en programmation de réaliser en ligne des outils simples pour l’animation de son site (indicateurs, etc).

Nous allons ici apprendre à réaliser nos premiers écrans, d’abord uniquement via le Web, puis sur système de fichiers (cela implique que vous ayez créé un produit pour héberger votre code ou qu’on l’ait fait pour vous). Nous apprendrons également comment ajouter également un bloc dans une page standard de Plone pour intégrer un lien vers un de vos nouveaux écrans.

Le lecteur devra de se référer également au chapitre TAL/Metal : les templates de Zope et Plone qui contient une référence assez complète.

Ma première template

Nous allons commencer en réalisant une première template qui affiche quelques informations sur l’utilisateur logué ainsi qu’un message de bienvenue.

Dans le dossier ‘custom’ (cf chapitre sur les scripts), ajoutez une “Page Template”, que vous appelez welcome. Donnez un titre à votre template: Bienvenue !

<html>
  <head>
    <title tal:content="template/title">The title</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
  </head>
  <body tal:define="plone context/@@plone">

    <h2>Bienvenue <span tal:content="python:user.getProperty('fullname')" /> !</h2>
    <p>Vous vous êtes connectés la dernière fois le <span tal:content="python:plone.toLocalizedTime(user.getProperty('login_time'))" />
    <p>Depuis votre inscription sur ce site vous avez créé <span tal:content="python:len(context.portal_catalog.searchResults(Creator=user.getId()))" /> contenus</p>
  </body>
</html>

Connectez-vous avec un autre utilisateur et allez à l’adresse http://chemin/vers/monsite/welcome.

Une fois que vous avez compris que le langage Zope Page Template consiste en un HTML étendu par des attributs dans un espace de nom TAL, qui contient les instructions, vous devriez pouvoir lire facilement cette template. Reférez vous à la référence TAL/Metal : les templates de Zope et Plone.

Nous avons deux types d’expressions TAL :

  • des expressions TAL simples, sans directive et qui se caractérisent par leur forme de chemin d’accès,
  • des expressions python, introduites par la directive “python:”

Vous noterez que, dans les expressions tal simples, la syntaxe ‘/’ permet d’exprimer de nombreuses manières d’accéder à un objet depuis un autre objet.

  • “context/@@plone” est synonyme de context.restrictedTraverse('@@plone') en python
  • “context/title_or_id” est synonyme de context.title_or_id(),
  • “template/title” est synonyme de template.title,

Appel à un script

Mettre de longues expressions python dans une expression tal est souvent une mauvaise idée car c’est assez illisible et des erreurs viennent facilement s’y glisser, et il n’est pas aiser de tester ses routines. Nous souhaiterions afficher les derniers contenus ajoutés par l’utilisateur. Nous allons faire appel à notre script qui retrouvait la liste des 50 derniers contenus créés par l’utilisateur connecté. Nous allons le modifier pour qu’il ne renvoit pas un texte, mais pour qu’il renvoit les enregistrements que nous valoriserons ensuite dans la template.

Créez le script get_user_recent_added :

from Products.CMFCore.utils import getToolByName
portal_catalog = getToolByName(context, 'portal_catalog')
portal_membership = getToolByName(context, 'portal_membership')
user = portal_membership.getAuthenticatedMember()
brains = portal_catalog.searchResults(Creator=user.getId(),
                                      portal_type=('News Item', 'File', 'Document', 'Event'), # on ne s'intéresse pas aux dossiers
                                      sort_on='created', # le tri se fait sur l'index 'created' (date de création)
                                      sort_order='reverse', # le tri est inversé (antéchronologique)
                                      sort_limit=50) # on limite à 50 résultats

return brains

Complétez ainsi votre template :

<html>
  <head>
    <title tal:content="template/title">The title</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
  </head>
  <body tal:define="plone context/@@plone;
                    portal_state context/@@plone_portal_state;">

    <h2>Bienvenue <span tal:content="python:user.getProperty('fullname')" /> !</h2>
    <p>Vous vous êtes connectés la dernière fois le <span tal:content="python:plone.toLocalizedTime(user.getProperty('login_time'))" />
    <p>Depuis votre inscription sur ce site vous avez créé <span tal:content="python:len(context.portal_catalog.searchResults(Creator=user.getId()))" /> contenus</p>

    <h3>Derniers contenus ajoutés</h3>
    <ul>
      <li tal:repeat="brain context/get_user_recent_added">
        <img tal:condition="brain/getIcon"
             tal:attributes="src string:${portal_state/portal_url}/${brain/getIcon}" />
        <a tal:attributes="href brain/getURL" tal:content="brain/Title"></a>
      </li>
    </ul>
  </body>
</html>

Quelques remarques :

  • Nous avons rajouté une boucle : tal:repeat=”brain context/get_user_recent_added” qui s’écrirait, dans un script python: for brain in context.get_user_recent_added():.
  • Comme vous pouvez le voir, brain/Title et brain/getURL équivalent respectivement à brain.Title et brain.getURL() (les expressions TAL ont cela d’agréable qu’elles sont sobres en caractères spéciaux).
  • Une expression TAL introduite par la directive string: permet de renvoyer une chaine de caractères composée de contenu statique et du rendu de sous-expressions TALES.
  • Nous utilisons ici une autre “vue standard” : plone_portal_state, qui permet notamment de récupérer l’URL absolue du site.

Utiliser la master template de Plone

Le but est d’intégrer votre template dans le gabarit de Plone. L’usage des macros se restreint principalement aujourd’hui à utiliser la “main template”. (Vous ne devriez pas avoir à définir des macros, c’est pourquoi on ne présentera pas dans le cadre de ce tutorial.)

Indiquez simplement dans l’élément html que vous utilisez la macro master en introduisant l’attribut suivant : metal:use-macro="here/main_template/macros/master".

Et dans l’élément body, que le contenu de cet élément remplit la slot “main” du master metal:fill-slot="main"

Affichez la vue welcome. Elle s’intègre dans le corps d’une page Plone.

Allez consulter maintenant la main_template. Faites une recherche sur main_template dans l’onglet “Find” du portal_skins, et choisissez celle qui est mise en valeur par une étoile : c’est celle qui est prioritaire dans l’ordre des skins.

La macro définit un ensemble de slots avec un contenu par défaut. Vous pouvez surcharger ce contenu par défaut à l’aide de la directive metal:fill-slot.

Exercice : modifiez votre template welcome de sorte que les colonnes de gauche et de droite n’affichent rien.

Utiliser une structure plus complexe dans une template

Nous avons, dans la template précédente, valorisé une liste de brains. Nous allons maintenant valoriser un dictionnaire.

Vous voyez que la récupération d’une propriété utilisateur dans la template est assez verbeuse. Nous allons écrire un script qui renvoit un dictionnaire avec les informations qui nous intéressent.

Modifiez le script user_infos que vous avez créé au chapitre précédent pour qu’il n’affiche pas un texte mais renvoie un dictionnaire.

from Products.CMFCore.utils import getToolByName
member = getToolByName(context, 'portal_membership').getAuthenticatedMember()
plone_view = context.restrictedTraverse('@@plone')

infos = {'login': member.getId(),
         'email': member.getProperty('email'),
         'fullname': member.getProperty('fullname') or member.getId(),
         'login_time': plone_view.toLocalizedTime(member.getProperty('login_time'), long_format=1),
         'last_login_time': plone_view.toLocalizedTime(member.getProperty('last_login_time'), long_format=1),
         'groups': ", ".join(member.getGroupIds()) or "Aucun"}

return infos

Puis modifiez votre template pour qu’elle récupère cette structure et qu’elle affiche les infos.

<html metal:use-macro="here/main_template/macros/master">
  <head>
    <title tal:content="template/title">The title</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
  </head>
  <body metal:fill-slot="main"
        tal:define="plone context/@@plone;
                    portal_state context/@@plone_portal_state;
                    user_infos context/user_infos">

    <h2>Bienvenue <span tal:replace="user_infos/fullname" /> !</h2>

    <p>Vous êtes connectés depuis <span tal:content="user_infos/login_time" />
    <p>Votre email est :
        <a tal:attributes="href string:mailto:${user_infos/email}"
           tal:content="user_infos/email" /></p>
    <p>Vous vous êtes connectés la dernière fois le <span tal:content="user_infos/last_login_time" />
    <p>Depuis votre inscription sur ce site vous avez créé <span tal:content="python:len(context.portal_catalog.searchResults(Creator=user.getId()))" /> contenus</p>
    <p>Vous êtes membres des groupes <span tal:content="user_infos/groups" /></p>
    <h3>Derniers contenus ajoutés</h3>
    <ul>
      <li tal:repeat="brain context/get_user_recent_added">
        <img tal:condition="brain/getIcon"
             tal:attributes="src string:${portal_state/portal_url}/${brain/getIcon}" />
        <a tal:attributes="href brain/getURL" tal:content="brain/Title"></a>
      </li>
    </ul>
  </body>
</html>

Vous observez que la template est beaucoup plus lisible. Ce que nous avons fait, c’est déporter la préparation des informations à afficher dans du code python, et réserver la template pour la disposition des informations.

Exercices

  • Vous réaliserez un tableau qui présentera, pour les dossiers à la racine du site, le nombre de documents (hors dossiers) qu’il contient (récursivement), et le nombre de documents qui y ont été créés depuis moins d’un mois.
  • Vous réaliserez une page destinée à être affichée au niveau d’un dossier, qui affichera la liste des utilisateurs qui ont un rôle sur le dossier, et pour chacun d’entre eux le nombre de documents qu’ils ont créé.
  • Vous réaliserez une template qui affiche, pour chaque mot clé disponible sur le site, le nombre d’occurences et un lien vers une page de résultat (dont vous devrez également écrire la template). (Astuces : 1/ le script context.collectKeywords('subject', 'Subject') vous permet de récupérer la liste des mots clés, 2/ l’index concerné est l’index Subject.). Bonus : la taille de l’affichage du mot clé sera proportionnelle au nombre d’occurences !

Ecrire la template sous la forme d’une vue

Si vous estimez que votre template et ses scripts seront utilisés sur le long terme, il est temps de penser à les transformer en vue. Vous aurez beaucoup plus de facilités à les déployer en production.

Une vue est composée d’une template et d’une classe. La template va reprendre la template que vous avez créé.

Ce chapitre n’a pas pour objectif d’expliquer la création d’un package. Référez-vous, pour cela, à la documentation développeur, ou demandez à votre prestataire favori.

Nous allons créer une vue “welcome”. Pour cela, il faut d’abord la déclarer dans une zcml.

<browser:page
    name="welcome"
    template="welcome.pt"
    class=".views.Welcome"
    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
    permission="zope2.View"
    />

Vous pouvez ensuite simplement récupérer la template (utilisez le lien click context du formulaire d’édition de la template), et l’enregistrer dans un fichier welcome.pt.

Vous reprenez ensuite dans la classe Welcome (qui hérite de BrowserView) d’un module views.py le code des scripts que vous incluez dans des méthodes du même nom. La classe Welcome contient un attribut context (l’équivalent du context dans le script ou la template) et un attribut request (l’équivalent de context.REQUEST dans le script ou request dans la template).

# -*- encoding: utf-8 -*-

from Products.CMFCore.utils import getToolByName
from Products.Five.browser import BrowserView

class Welcome(BrowserView):

    def user_infos(self):
        member = getToolByName(self.context, 'portal_membership').getAuthenticatedMember()
        plone_view = self.context.restrictedTraverse('@@plone')
        infos = {'login': member.getId(),
                 'email': member.getProperty('email'),
                 'fullname': member.getProperty('fullname') or member.getId(),
                 'login_time': plone_view.toLocalizedTime(member.getProperty('login_time'), long_format=1),
                 'last_login_time': plone_view.toLocalizedTime(member.getProperty('last_login_time'), long_format=1),
                 'groups': ", ".join(member.getGroupIds()) or "Aucun"}

        return infos

    def get_user_recent_added(self):
        portal_catalog = getToolByName(self.context, 'portal_catalog')
        portal_membership = getToolByName(self.context, 'portal_membership')
        user = portal_membership.getAuthenticatedMember()
        brains = portal_catalog.searchResults(Creator=user.getId(),
                                              portal_type=('News Item', 'File', 'Document', 'Event'), # on ne s'intéresse pas aux dossiers
                                              sort_on='created', # le tri se fait sur l'index 'created' (date de création)
                                              sort_order='reverse', # le tri est inversé (antéchronologique)
                                              sort_limit=50) # on limite à 50 résultats

        return brains

Modifiez votre template de sorte qu’elle aille chercher les méthodes non pas sur le contexte (context) mais sur la vue (view). Retirez toute référence à l’objet template.

<html metal:use-macro="here/main_template/macros/master">
  <body metal:fill-slot="main"
        tal:define="plone context/@@plone;
                    portal_state context/@@plone_portal_state;
                    user_infos view/user_infos">

    <h2>Bienvenue <span tal:content="user_infos/fullname" /> !</h2>

    <p>Vous êtes connectés depuis <span tal:content="user_infos/login_time" />
    <p>Votre email est : <a tal:attributes="href string:mailto:${user_infos/email}" tal:content="user_infos/email" /></p>
    <p>Vous vous êtes connectés la dernière fois le <span tal:content="user_infos/last_login_time" />
    <p>Depuis votre inscription sur ce site vous avez créé <span tal:content="python:len(context.portal_catalog.searchResults(Creator=user.getId()))" /> contenus</p>
    <p>Vous êtes membres des groupes <span tal:content="user_infos/groups" /></p>
    <h3>Derniers contenus ajoutés</h3>
    <ul>
      <li tal:repeat="brain view/get_user_recent_added">
        <img tal:condition="brain/getIcon"
             tal:attributes="src string:${portal_state/portal_url}/${brain/getIcon}" />
        <a tal:attributes="href brain/getURL" tal:content="brain/Title"></a>
      </li>
    </ul>
  </body>
</html>

Pensez à renommer vos anciens scipts et templates qui restent dans le custom, pour éviter les conflits.

Une vue pour un export csv

Le script d’export csv peut également être transformé en vue. Nous allons faire une vue pour l’export all-contents.csv pour lequel nous avons fait un script dans le chapitre précédent.

<browser:view
    name="all-contents.csv"
    class=".views.AllContentsCSV"
    for="Products.CMFCore.interfaces.IFolderish"
    permission="cmf.AddPortalContent"
    />

Deux remarques sur le zcml : Comme on n’a pas de template on utilise non pas une browser:page mais une browser:view. Pour faire simple, par rapport à ce que vous connaissez, une browser:page équivaut à une template et une browser view à un script.

Le critère for indique une restriction sur l’interface du contenu - ici, on restreint aux contextes IFolderish, qui est une interface qu’ont tous les objets qui peuvent en contenir d’autres. Les autres contextes n’auront pas cette vue (pour connaitre les interfaces d’un contexte, dans la ZMI, allez dans l’onglet Interfaces de l’objet). Le critère permission permet protéger la vue. Un utilisateur essayant d’y accéder sans avoir ladit permission aura une erreur de privilèges insuffisants.

Pour une browser view, il faut écrire une méthode __call__ qui renvoit le rendu attendu.

class AllContentsCSV(BrowserView):

    def __call__(self):

        response = self.request.response
        response.setHeader('Cache-Control', 'no-cache')
        response.setHeader('Pragma', 'no-cache')
        response.setHeader(
          'Content-type', 'application/vnd.ms-excel;charset=windows-1252')
        response.setHeader(
          'Content-disposition',
          'attachment; filename="all-contents.csv"')

        portal_catalog = getToolByName(self.context, 'portal_catalog')
        brains = portal_catalog.searchResults(path="/".join(self.context.getPhysicalPath()), # critère chemin d'accès
                                              portal_type=('File', 'Document', 'Event', 'News'), # critère sur le type (on exclut les fichiers)
                                              )
        lines = ['"Path";"Titre";"Auteur";"MAJ"']
        for brain in brains:
            lines.append('"%s";"%s";"%s";"%s"' % (brain.getPath(),
                                           brain.Title.decode('utf-8').encode('windows-1252'),
                                           brain.Creator, brain.modified.strftime('%d/%m/%Y')))

        return "\n".join(lines)

Nous sommes dans une vue, qui est du code python non restreint. À ce titre, on a accès à toute l’API python disponible. Dans ce cas, nous gagnerons à utiliser la librairie csv pour produire notre fichier.

from csv import writer
from StringIO import StringIO


class AllContentsCSV(BrowserView):

    def __call__(self):

        response = self.request.response
        response.setHeader('Cache-Control', 'no-cache')
        response.setHeader('Pragma', 'no-cache')
        response.setHeader(
          'Content-type', 'application/vnd.ms-excel;charset=windows-1252')
        response.setHeader(
          'Content-disposition',
          'attachment; filename="all-contents.csv"')

        portal_catalog = getToolByName(self.context, 'portal_catalog')
        brains = portal_catalog.searchResults(path="/".join(self.context.getPhysicalPath()), # critère chemin d'accès
                                              portal_type=('File', 'Document', 'Event', 'News'), # critère sur le type (on exclut les fichiers)
                                              )

        csvdata = StringIO()
        csvhandler = writer(csvdata, dialect='excel', delimiter=';')

        csvhandler.writerow(["Path","Titre","Auteur","MAJ"])
        for brain in brains:
            csvhandler.writerow((brain.getPath(),
                                 brain.Title,
                                 brain.Creator,
                                 brain.modified.strftime('%d/%m/%Y')))

        csvdata.seek(0)
        return csvdata.read().decode('utf-8').encode('windows-1252')

Ce qui est tout de même bien plus propre.

Une viewlet pour ajouter un lien vers une vue

Ma première viewlet

Tous les développements que nous avons réalisés jusqu’à présent souffrent, vous avez du le remarquer, d’un gros inconvénient : nous n’avons nulle part, dans notre site, de lien vers les différentes pages et exports que nous avons réalisés ! Autrement dit : nos développements ne sont pas intégrés.

Comment ajouter un lien vers nos nouvelles pages ?

Vous pouvez tout simplement mettre un lien à la main dans un document Plone, mais cela peut être fastidieux si vous souhaitez que ce lien apparaisse dans de nombreux contextes. Un réflexe de développement “à l’ancienne” est, à l’extrême opposé, de modifier le code de la template de base et d’y mettre le lien (éventuellement conditionnel) vers notre page. Mais “si tout le monde fait cela”, à la fin cette template ne ressemblera plus à rien. Surtout, si vous voulez que votre développement soit utilisé par d’autres, ou sur d’autres de vos sites, il ne faut pas qu’il intervienne sur le comportement par défaut du site.

Pour cela, Plone fonctionne avec ce qu’on appelle des viewlets. Ce sont des blocs que l’on peut enregistrer pour qu’ils s’affichent dans des slots appelés viewletmanagers. Une documentation complète des viewlets est disponible dans la documentation développeur.

Nous allons, nous, simplement ajouter un lien (sous forme d’image) vers notre export xml sous le titre des dossiers de notre site, pour les utilisateurs ayant le droit d’ajouter des contenus.

Nous allons ajouter ceci dans une zcml de notre package :

<browser:viewlet
    name="link-allcontents-csv"
    manager="plone.app.layout.viewlets.interfaces.IAboveContentTitle"
    for="Products.CMFCore.interfaces.IFolderish"
    class=".viewlets.LinkAllContentsCSV"
    permission="cmf.AddPortalContent"
    />

Remarques : - class indique la classe qui fournira le rendu html de notre lien (voir ci–après). - manager indique le bloc dans lequel l’élément sera ajouté. Pour choisir le manager, allez sur la vue /@@manage-viewlets sur une des pages de votre site. Les viewlet managers ont un nom et une interface. C’est l’interface que vous indiquez ici. - for permet de mettre une contrainte sur le contexte (ici, on n’affichera le lien que sur les dossiers). - permission permet de n’afficher la viewlet que pour certains utilisateurs (on la fera correspondre avec la permission de la vue). - cela ne fait pas l’objet du présent cours, mais pour information un attribut layer est souvent utilisé afin de mettre une contrainte sur les produits installés / l’interface de la request. - name est utile si vous souhaitez remplacer une viewlet existante. Si deux viewlets ont le même nom et sont associées au même manager, alors une seule est affichée : celle qui déclare une interface for la plus proche du contexte en cours, et celle qui a un critère layer le plus proche du layer en cours.

Dans un module viewlets.py nous allons créer cette classe LinkAllContentsCSV :

from plone.app.layout.viewlets.common import ViewletBase
from Products.CMFCore.utils import getToolByName

class LinkAllContentsCSV(ViewletBase):

    def render(self):
        portal_url = getToolByName(self.context, 'portal_url')()
        return u"""<a href="%(url)s" id="link-allcontents-csv"
                      style="float: right;"
                      title="Exporter la liste des contenus en csv">
                     <img src="%(portal_url)s/xls.png" />
                   </a>
                """ % ({'url': self.context.absolute_url() + '/all-contents.csv',
                        'portal_url': portal_url})

Les viewlets hériteront de la classe ViewletBase, qui fournit quelques services. Pour les besoins les plus simples, vous surchargez ensuite la méthode render, qui génèrera le HTML en python pur. (en principe vous ne mettrez jamais de css en inline : vous indiquerez dans une css : #link-allcontents-csv{float: right;}).

Pour des besoins plus complexes, vous utiliserez une template en surchargeant l’attribut index :

from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile

class LinkAllContentsCSV(ViewletBase):

    index = ViewPageTemplateFile('link-allcontents-csv.pt')

Exercices

  • Ajoutez un lien sur les dossiers pour conduire vers un écran que vous avez développé lors d’un exercice sur les templates ou les vues.
  • Affichez sous le titre d’un dossier un bloc d’informations indiquant notamment la liste des personnes qui ont ajouté des documents dans ce dossier.

blog comments powered by Disqus