Chapter 12: Composants et plugins
Composants et plugins
Les composants et plugins sont des fonctionnalités relativement nouvelles de web2py, et il y a quelques désaccords entre les dévelopeurs au sujet de ce qu'ils sont et ce qu'ils devraient être. La plupart des confusions proviennent des usages différents de ces termes dans d'autres projets logiciels et du fait que les développeurs sont toujours en train de travailler pour finaliser les spécifications.
Cependant, le support de plugin est une fonctionnalité importante et nous avons besoin de fournir quelques définitions. Ces définitions ne sont pas destinées à être finalies, juste consistentes avec les patterns de programmation que nous souhaitons présenter dans ce chapitre.
Nous essaierons d'adresser deux problèmes ici :
- Comment pouvons-nous construire des applications modulaires qui minimisent la charge serveur et maximiser la réutilisation de code ?
- Comment pouvons-nous distribuer des morceaux de code de façon plus ou moins plug-and-play ?
Les Components répondent au premier problème ; les plugins au deuxième.
Components, LOAD et Ajax
Un component est une fonctionnalité autonome d'une page web.
Un composant peut être composé de modules, de contrôleurs et de vues, mais il n'y a pas de besoin strict autrement que, lorsqu'embarqué dans une page, il doit être localisé dans un tag html (par exemple un DIV, un SPAN ou un IFRAME) et doit exécuter ses tâches indépendamment du reste de la page. Nous sommes spécifiquemenet intéressés dans les composants qui sont chargés dans la page et qui communiquent avec la fonction contrôleur du composant via Ajax.
Un exemple d'un composant est un "comments component" qui est contenu dans un DIV et montre les commentaires des utilisateurs et un formulaire pour poster un nouveau commentaire. Lorsque le formulaire est soumis, il est envoyé au serveur via Ajax, la liste est mise à jour, et le commentaire est stocké côté serveur dans la base de données. Le contenu du DIV est rafraichi sans recharger le reste de la page.
LOAD
La fonction web2py LOAD rend cela plus facile à faire sans connaissance explicite JavaScript/Ajax ou en programmation.
Notre but est d'être capable de développer des applications web en assemblant les composants dans des layouts de page.
Considérons une simple application web2py "test" qui étend l'application de base par défaut avec un modèle personnalisé dans le fichier "models/db_comments.py" :
db.define_table('comment_post',
Field('body','text',label='Your comment'),
auth.signature)
une action dans "controllers/comments.py"
@auth.requires_login()
def post():
return dict(form=SQLFORM(db.comment_post).process(),
comments=db(db.comment_post).select())
et le "views/comments/post.html" correspondant :
{{extend 'layout.html'}}
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <span class="post_body">{{=post.body}}</span>
</div>
{{pass}}
{{=form}}
Vous pouvez y accéder comme d'habitude à :
http://127.0.0.1:8000/test/comments/post
Tant qu'il n'y a rien de spécial dans cette action, mais que nous pouvons le mettre dans un composant en définissant une nouvelle vue avec l'extension ".load" qui n'étend pas le layout.
D'où nous créons un "views/comments/post.load" :
{{for post in comments:}}
<div class="post">
On {{=post.created_on}} {{=post.created_by.first_name}}
says <blockquote class="post_body">{{=post.body}}</blockquote>
</div>
{{pass}}
{{=form}}
Nous pouvons y accéder à l'URL
http://127.0.0.1:8000/test/comments/post.load
C'est un composant que nous pouvons embarquer dans n'importe quelle autre page en faisant simplement
{{=LOAD('comments','post.load',ajax=True)}}
Par exemple dans "controllers/default.py", nous pouvons éditer
def index():
return dict()
et dans la vue correspondante ajouter le composant :
{{extend 'layout.html'}}
{{=LOAD('comments','post.load',ajax=True)}}
Visiter la page
http://127.0.0.1:8000/test/default/index
va montrer le contenu normal et le composant de commentaires :
Le composant {{=LOAD(...)}}
est rendu comme :
<script type="text/javascript"><!--
web2py_component("/test/comment/post.load","c282718984176")
//--></script><div id="c282718984176">loading...</div>
(le code actuel généré dépend des options passées à la fonction LOAD).
La fonction web2py_component(url,id)
est définie dans "web2py_ajax.html" et effectue toute la magie : elle appelle url
via Ajax et embarque la réponse dans la DIV avec l'id
correspondant ; elle capture toutes les soumissions de formulaire dans le DIV et envoie ces formulaires via Ajax. La cible Ajax est toujours la DIV elle-même.
Signature LOAD
La signature complète de l'helper LOAD est la suivante :
LOAD(c=None, f='index', args=[], vars={},
extension=None, target=None,
ajax=False, ajax_trap=False,
url=None,user_signature=False,
timeout=None, times=1,
content='loading...',**attr):
Ici :
- Les deux premiers arguments
c
etf
sont le contrôleur et la fonction que nous voulons appeler respectivement. args
etvars
sont les arguments et les variables que nous voulons passer à la fonction. Le premier est une liste, le dernier un dictionnaire.extension
est une extension optionnelle. Notez que l'extension peut aussi être passée comme partie de la fonction comme dansf='index.load'
.target
est l'id
de la cible DIC. Si ce n'est pas spécifié, une cible aléatoireid
est générée.ajax
devrait être défini àTrue
si la DIC doit être remplie via Ajax et àFalse
si la DIV doit être remplie avant que la page courante soit retournée (évitant ainsi l'appel Ajax).ajax_trap=True
signifie que toute soumission de formulaire dans la DIC doit être capturée et envoyée via Ajax, et la réponse doit être renvoyée dans la DIV.ajax_trap=False
indique que les formulaire doivent être soumis normalement, rechargeant ainsi toute la page.ajax_trap
est ignoré et supposé àTrue
siajax=True
.url
, si spécifié, écrase les valeurs dec
,f
,args
,vars
, etextension
et charge le composant à l'url
. C'est utilisé pour charger comme composants des pages servies par d'autres applications (qui peuvent ou non être créées avec web2py).user_signature
est par défaut à False, mais si vous êtes connecté, devrait être à True. Ceci permettra de s'assurer que la callback ajax est signée numériquement. Ceci est documenté dans le chapitre 4.times
spécifie le nombre de fois où le composant est requêté. Utilisez "infinity" pour conserver le chargement du composant continuellement. Cette option est utile pour déclencher des routines régulières pour une requête donnée du document.timeout
définit le temps à attendre en millisecondes avant de démarrer la requête ou la fréquence sitimes
est plus grand que 1.content
est le contenu à afficher lorsque l'appel ajax est en cours. Ce peut être un helper commecontent=IMG(..)
.**attr
optionnel (attributs) peut être passé auDIV
contenu.
Si aucune vue .load
n'est spécifiée, il y a un generic.load
qui rend le dictionnaire retourné par l'action sans layout. Cela marche mieux si le dictionnaire contient un seul objet.
Si vous chargez (LOAD) un composant ayant l'extension .load
et la fonction contrôleur correspondant redirige vers une autre action (par exemple une formulaire de connexion), l'extension .load
se propage et la nouvelle url (celle redirigée) est aussi chargée avec une extension .load
.
Rediriger depuis un composant
Pour rediriger depuis un composant, utilisez ceci :
redirect(URL(...),client_side=True)
mais notez que l'URL redirigée sera par défaut avec l'extension du composant. Voir les notes à propos de l'argument extension
dans la fonction URL dans le Chapitre 4
Recharger la page via une redirection après la soumission d'un composant
Si vous appelez une action via Ajax et que vous voulez que l'action force une redirection de la page parent vous pouvez le faire avec un redirect depuis la fonction chargée du contrôleur (LOAD). Si vous voulez recharger la page parent, vous pouvez faire une redirection vers elle. L'URL parent est connue (voir Composant de communications Client-Serveur )
donc après avoir procédé à la soumission du formulaire, la fonction contrôleur recharge la page parent via une redirection :
if form.process().accepted:
...
redirect( request.env.http_web2py_component_location,client_side=True)
Notez que la section ci-dessous, Composant de communications Client-Serveur, décrit comment le composant peut retourner le javascript, qui pourrait être utilisé pour des actions plus sophistiquées lorsque le composant est soumis. Le cas spécifique de rechargement d'un autre composant est décrit après.
Recharger un autre composant
Si vous utilisez de multiples composants sur une page, vous pouvez vouloir que la soumission d'un composant recharge un autre composant. Vous faites cela en ayant le composant soumis qui retourne du javascript.
C'est possible de coder en dur la cible DIV, mais dans ce moyen nous utilisons une variable de requête pour informer le contrôleur soumis de quel composant on souhaite recharger. C'est identifié par l'id du DIV contenant le composant cible. Dans ce cas, le DIV a l'id 'map'. Notez qu'il est nécessaire d'utiliser target='map'
dans le LOAD de la cible ; sans cela, la cible id est aléatoire et reload() ne fonctionnera pas. Voir la signature LOAD ci-dessus.
Dans la vue, faites ceci :
{{=LOAD('default','submitting_component.load',ajax=True,vars={'reload_div':'map'})}}
Le contrôleur appartenant au composant soumis a besoin de renvoyer du javascript, donc ajoutez juste ceci au code contrôleur existant lorsque vous procédez à l'envoi :
if form.process().accepted:
...
if request.vars.reload_div:
response.js = "jQuery('#%s').get(0).reload()" % request.vars.reload_div
(Bien sûr, supprimez la redirection si vous utilisiez l'approche de la section précédente.)
C'est tout. Les librairies javascript web2py s'occupent du reload. Ceci pourrait être généralisé pour gérer de multiples composants avec javascript ressemblant à :
jQuery('#div1,#div2,#div3').get(0).reload()
Pour plus d'information à propos de response.js, voir Composant de communications Client-Serveur ci-après.
Ajax post ne supporte pas les formulaires multipart
Puisque Ajax post ne supporte pas les formulaires multipart, i.e. les uploads de fichiers, les champs upload ne fonctionneront pas avec le composant LOAD. Vous pourriez être surpris en pensant que cela fonctionne car les champs upload fonctionneront normalement si le POST est fait depuis la vue du composant individuel. Au lieu de cela, les uploads sont fait avec des widgets tiers compatibles ajax et les commandes web2py d'upload et de stockage.
Composants de communication LOAD et Client-Serveur
Lorsque l'action d'un composant est appelée via Ajax, web2py passe les en-têtes HTTP avec la requête :
web2py-component-location
web2py-component-element
qui peut être accédée par l'action via les variables :
request.env.http_web2py_component_location
request.env.http_web2py_component_element
Le dernier est aussi accessible via :
request.cid
Le premier contient l'URL de la page qui a appelé l'action du composant. Le dernier contient l'id
du DIV qui contiendra la réponse.
L'action du composant peut aussi stocker les données dans deux en-têtes de réponse HTTP qui seront interprétées par la page complète par la réponse. Ce sont :
web2py-component-flash
web2py-component-command
et ils peuvent être définis via :
response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'
ou (si l'action est appelée par un composant) automatiquement via :
response.flash='...'
response.js='...'
Le premier contient le texte que vous voulez flasher pour la réponse. Le dernier contient le code Javascript que vous voulez exécuté par la réponse. Il ne peut pas contenir de retour à la ligne.
Comme exemple, définissez un composant du formulaire de contact dans le "controllers/contact/ask.py" qui autorise l'utilisateur à poser une question. Le composant enverra la question par email à l'administrateur système, flashant un message "Merci", et en supprimant le composant de la page :
def ask():
form=SQLFORM.factory(
Field('your_email',requires=IS_EMAIL()),
Field('question',requires=IS_NOT_EMPTY()))
if form.process().accepted:
if mail.send(to='admin@example.com',
subject='from %s' % form.vars.your_email,
message = form.vars.question):
response.flash = 'Thank you'
response.js = "jQuery('#%s').hide()" % request.cid
else:
form.errors.your_email = "Unable to send the email"
return dict(form=form)
Les quatre premières lignes définissent le formulaire et l'accepte. L'objet mail utilisé pour envoyer est défini dans l'application de base par défaut. Les quatre dernières lignes implémentent toute la logique spécifique aux composants en obtenant les données depuis les en-têtes HTTP de la requête et définissant les en-têtes de la réponse HTTP.
Vous pouvez maintenant embarquer ce formulaire contact dans n'importe quelle page via
{{=LOAD('contact','ask.load',ajax=True)}}
Notez que nous ne définissons pas une vue .load
pour notre composant ask
. Nous n'avons pas à le faire puisqu'il retourne un objet simple (form) et ensuite le "generic.load" le fera très bien. Souvenez-vous que les vues génériques sont un outil de développement. En production, vous devriez copier "views/generic.load" dans "views/contact/ask.load".
user_signature
:{{=LOAD('contact','ask.load',ajax=True,user_signature=True)}}
qui ajoute une signature numérique à l'URL. La signature numérique doit ensuite être validée en utilisant un décorateur dans la fonction callback :
@auth.requires_signature()
def ask(): ...
Liens ajax capturés et le helper A
Normalement un lien n'est pas capturé, et en cliquant sur un lien dans un composant, la page liée entière est chargée. Parfois vous voulez que la pagée liée soit chargée dans le composant. Ceci peut être fait en utilisant le helper A
:
{{=A('linked page',_href='http://example.com',cid=request.cid)}}
Si cid
est spécifié, la page liée est chargée via Ajax. Le cid
est l'id
de l'élément html où placer le contenu de la page chargée. Dans ce cas, nous le définisson à request.cid
, i.e., l'id
du composant qui génère le lien. La page liée peut être et habituellement est une URL interne générée en utilisant URL helper .
Plugins
Un plugin est n'importe quel sous-ensemble des fichiers d'une application.
et nous voulons vraiment dire any :
- Un plugin n'est pas un module, ce n'est pas un modèle, ce n'est pas un contrôleur, ce n'est pas une vue, bien qu'il puisse contenir des modules, modèles, des contrôleurs et/ou des vues.
- Un plugin n'a pas besoin d'être fonctionnellement autonome et peut dépendre d'autres plugins ou de code spécifique à l'utilisateur.
- Un plugin n'est pas un système de plugins et n'a donc aucun concept d'enregistrement ou d'isolation, même si nous donnerons les règles pour essayer de réussir à faire quelque isolation.
- Nous parlons d'un plugin pour votre application, pas d'un plugin pour web2py.
Alors pourquoi est-ce appelé un plugin ? Car cela fournit un mécanisme pour packager un sous-ensemble d'une application et le dépackager dans une autre application (i.e. plug-in). Sous cette définition, n'importe quel fichier dans votre application peut être traité comme un plugin.
Lorsque l'application est distribué, ses plugins sont packagés et distribués avec.
En pratique, l'admin fournit une interface pour packager et dépackager les plugins séparément de votre application. Les fichiers et dossiers de votre application qui ont des noms avec le préfixe plugin_
name peuvent être packagés ensemble dans un fichier appelé :
web2py.plugin.
name.w2p
et distribués ensemble.
Les fichiers qui composent un plugin ne sont pas traités par web2py différemment que les autres fichiers sauf que admin comprend depuis leurs noms qu'ils sont amenés à être distribués ensemble, et les affiche dans une page séparée :
Maintenant dans les faits, par la définition ci-dessus, ces plugins sont plus généraux que ceux reconnus comme tels par admin.
En pratique, nous serons juste confrontés à deux types de plugins :
- Component Plugins. Ce sont des plugins qui contiennent les composants comme défini dans la section précédente. Un plugin de composant peut contenir un ou plusieurs composants. Nous pouvons penser par exemple à un
plugin_comments
qui contient le composant comments proposé au-dessus. Un autre exemple pourrait êtreplugin_tagging
qui contient un composant tagging et un composant tag-cloud qui partage quelques tables de base de données également définies par le plugin. - Layout Plugins. Ce sont les plugins qui contiennent une vue layout et les fichiers statiques requis par un tel layout. Lorsque le plugin est appliqué il donne un nouveau look and feel à l'application.
Par les définitions ci-dessus, les composants créés dans la section précédente, par exemple "controllers/contact.py" sont déjà des plugins. Nous pouvons les déplacer d'une application à une autre et utiliser les composants qu'ils définissent. Maintenant ils ne sont pas reconnus tels quels par admin car il n'y a rien qui les définit comme plugins. Donc il y a deux problèmes que l'on a besoin de résoudre :
- Nommer les fichiers plugin en utilisant une convention, afin que admin puisse les reconnaitre comme appartenant au même plugin.
- Si le plugin a des fichiers modèles, établir une convention afin que les objets qu'il définit ne polluent pas l'espace de nom et ne rentrent pas en conflit avec chaque autre.
Assumons maintenant qu'un plugin est appelé name. Voici les règles qui devraient être suivies :
Règle 1 : Les modèles de plugin et contrôleurs devraient être appelés, respectivement
models/plugin_
name.py
controllers/plugin_
name.py
et les fichiers de vues de plugin, modules, static, et private devraient être dans des dossiers appelés respectivement :
views/plugin_
name/
modules/plugin_
name/
static/plugin_
name/
private/plugin_
name/
Règle 2 : Les modèles de plugin peuvent simplement définir les objets avec les noms qui démarrent avec
plugin_
namePlugin
Name_
Règle 3 : Les modèles de plugin peuvent simplement définir les variables de session avec les noms qui démarrent avec
session.plugin_
namesession.Plugin
Name
Règle 4 : Les plugins devraient inclure la licence et la documentation. Ils devraient être placés dans :
static/plugin_
name/license.html
static/plugin_
name/about.html
Règle 5 : Le plugin peut simplement se baser sur l'existence d'objets globaux définis dans le "db.py" de référence, i.e.
- une connexion à la base de données appelée
db
- une instance
Auth
appeléeauth
- une instance
Crud
appeléecrud
- une instance
Service
appeléeservice
Quelques plugins peuvent être plus sophistiqués et avoir un paramètre de configuration dans le cas où plus d'une instance de db existe.
Règle 6 : Si un plugin a besoin de paramètres de configuration, ils devraient être définis via un PluginManager comme décrit ci-après.
En suivant les règles ci-dessus nous pouvons nous assurer que :
- admin reconnait tous les fichiers et dossiers
plugin_
name comme partie d'une simple entité. - Les plugins n'interfèrent pas avec chaque autre.
Les règles ci-dessus ne résolvent pas le problème des versions de plugin et les dépendances. C'est en dehors du scope.
Plugins de composant
Les plugins de composant sont des plugins qui définissent les composants. Les composants accèdent habituellement à la base de données et la définissent avec leurs propres modèles.
Ici nous transformons notre composant précédent comments
en comments_plugin
en utilisant le même code que nous avions écrit précédemment, mais en suivant toutes les règles précédentes.
D'abord, nous créons un modèle appelé "models/plugin_comments.py" :
db.define_table('plugin_comments_comment',
Field('body','text', label='Your comment'),
auth.signature)
def plugin_comments():
return LOAD('plugin_comments','post',ajax=True)
(notez que les deux dernières lignes définissent une fonction qui simplifiera l'intégration du plugin)
Ensuite, nous définissons un "controllers/plugin_comments.py"
def post():
if not auth.user:
return A('login to comment',_href=URL('default','user/login'))
comment = db.plugin_comments_comment
return dict(form=SQLFORM(comment).process(),
comments=db(comment).select())
Troisièmement, nous créons une vue appelée "views/plugin_comments/post.load" :
{{for comment in comments:}}
<div class="comment">
on {{=comment.created_on}} {{=comment.created_by.first_name}}
says <span class="comment_body">{{=comment.body}}</span>
</div>
{{pass}}
{{=form}}
Maintenant nous pouvons utiliser admin pour packager le plugin pour la distribution. Admin va enregistrer ce plugin comme :
web2py.plugin.comments.w2p
Nous pouvons utiliser ce plugin dans n'importe quelle vue en installant simplement le plugin via la page edit dans admin et en ajoutant cela à nos propres vues
{{=plugin_comments()}}
Bien entendu nous pouvons rendre le plugin plus sophistiqué en ayant des composants qui prennent des paramètres et des options de configuration. Plus les composants sont complexes, plus il devient difficile d'éviter les conflits de noms. Le Plugin Manager décrit ci-après est destiné à éviter ce problème.
Plugin manager
Le PluginManager
est une classe définie dans gluon.tools
. Avant que l'on explique comment cela fonctionne à l'intérieur, nous allons expliquer comment l'utiliser.
Nous considérons ici le plugin_comments
précédent et nous le rendons meilleur. Nous voulons être capable de personnaliser :
db.plugin_comments_comment.body.label
sans avoir à éditer le code du plugin directement.
Voici comment on peut faire cela :
D'abord, ré-écrivez le plugin "models/plugin_comments.py" de cette manière :
def _():
from gluon.tools import PluginManager
plugins = PluginManager('comments', body_label='Your comment')
db.define_table('plugin_comments_comment',
Field('body','text',label=plugins.comments.body_label),
auth.signature)
return lambda: LOAD('plugin_comments','post.load',ajax=True)
plugin_comments = _()
Notez comment tout le code, sauf la définition de table, est encapsulé dans une simple fonction appelée _
afin qu'il ne pollue pas l'espace de nom global. Notez également comment la fonction créé une instance d'un PluginManager
.
Maintenant dans tout autre modèle dans votre application, par exemple dans "models/db.py", vous pouvez configurer ce plugin comme suit :
from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comments.body_label = T('Post a comment')
L'objet
plugins
est déjà instancié dans l'application de base par défaut dans "models/db.py"
L'objet PluginManager est un objet Storage singleton niveau thread d'objets Storage. Cela signifie que vous pouvez l'instancier autant de fois que vous le voulez dans la même application mais (qu'ils aient le même nom ou pas) ils agissent comme si c'était une simple instance de PluginManager.
En particulier, chaque fichier de plugin peut faire son propre objet PluginManager et s'enregistrer lui-même et ses paramètres par défaut avec :
plugins = PluginManager('name', param1='value', param2='value')
Vous pouvez surcharger ces paramètres n'importe où (par exemple dans "models/db.py") avec le code :
plugins = PluginManager()
plugins.name.param1 = 'other value'
Vous pouvez configurer de multiples plugins dans un même endroit :
plugins = PluginManager()
plugins.name.param1 = '...'
plugins.name.param2 = '...'
plugins.name1.param3 = '...'
plugins.name2.param4 = '...'
plugins.name3.param5 = '...'
Lorsque le plugin est défini, le PluginManager doit prendre des arguments : le nom du plugin et les arguments nommés optionnels qui sont les paramètres par défaut. Cependant, lorsque les plugins sont configurés, le constructeur PluginManager ne doit prendre aucun argument. La configuration doit précéder la définition du plugin (i.e. il doit être dans une fichier modèle qui vient avant en ordre alphabétique).
Layout de plugins
Les layouts de plugins sont plus simples que les plugins de composant puisqu'ils ne contiennent habituellement pas de code, mais seulement des vues et des fichiers statiques. Une fois encore, vous devriez suivre les bonnes pratiques :
Premièrement, créez un dossier appelé "static/plugin_layout_name/" (où le nom est celui du layout) et placez tous vos fichiers statiques ici.
Ensuite, créez un fichier layout appelé "views/plugin_layout_name/layout.html" qui contient votre layout et lie les images, CSS et fichiers JavaScript dans "static/plugin_layout_name/"
Troisièmement, modifiez les "views/layout.html" afin qu'il lise simplement :
{{extend 'plugin_layout_name/layout.html'}}
{{include}}
L'avantage de ce design est que les utilisateurs de ce plugin peuvent installer de multiples layouts et choisir lequel ils souhaitent appliquer en éditant simplement "views/layout.html". De plus, "views/layout.html" ne sera pas packagé par admin avec le plugin, donc il n'y a pas de risque que le plugin surcharge le code utilisateur dans le layout précédemment installé.
Dépôts de plugin, installation de plugin via admin
Alors qu'il n'y a pas de simple depôt des plugins web2py, vous pouvez trouver beaucoup d'entre eux à l'une des URLs suivantes :
http://web2pyslices.com (c'est le dépôt principal et il est intégré à l'application admin de web2py pour des installations en un clic)
http://web2py.com/plugins
http://web2py.com/layouts
Les versions récentes de l'admin web2py autorisent la récupération automatique et l'installation de plugins depuis web2pyslices. Pour ajouter un plugin à une application, éditez le via l'application admin, et choisissez Download Plugins, couramment en bas de l'écran.
Pour publier vos propres plugins, créez un compte sur web2pyslices.
Voici une capture d'écran montrant quelques uns des plugins auto-installables :