Chapter 7: Formulaires et validateurs
Formulaires et validateurs
Il y a 4 moyens distincts de construire des formulaires dans web2py :
FORM
fournit une implémentation bas niveau en terme de helpers HTML. Un objetFORM
peut être sérialisé en HTML et est conscient des champs qu'il contient. Un objetFORM
sait comment valider les valeurs envoyées.SQLFORM
fournit une API haut niveau pour construire des formulaires de création, de mise à jour et de suppression de table existante.SQLFORM.factory
est une couche d'abstraction au-dessus deSQLFORM
afin de profiter des fonctionnalités de génération de formulaire même s'il n'y a pas de base de données présente. Il génère un formulaire très similaire àSQLFORM
depuis la description d'une table mais sans le besoin de créer la table de la base de données.- Les méthodes
CRUD
. Elles sont fonctionnellement équivalentes à SQLFORM et sont basées sur SQLFORM, mais fournissent une notation plus compacte.
Tous ces formulaires sont conscients d'eux-mêmes, mais si l'entrée ne passe pas la validation, ils ne peuvent pas se modifier eux-mêmes et ajoutent les messages d'erreurs. Les formulaires peuvent être requêtées pour les variables validées et pour les messages d'erreur qui ont été générés par la validation.
Le code HTML arbitraire peut être inséré dans ou extrait du formulaire en utilisant les helpers.
FORM
et SQLFORM
sont les helpers et ils peuvent être manipulés de la même manière que le DIV
. Par exemple, vous pouvez définir un style de formulaire :
form = SQLFORM(..)
form['_style']='border:1px solid black'
FORM
Considérez comme exemple une application test avec le contrôleur suivant "default.py" :
def display_form():
return dict()
et la vue associée "default/display_form.html" :
{{extend 'layout.html'}}
<h2>Input form</h2>
<form enctype="multipart/form-data"
action="{{=URL()}}" method="post">
Your name:
<input name="name" />
<input type="submit" />
</form>
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
C'est une formulaire HTML régulier qui demande le nom d'utilisateur. Lorsque vous remplissez le formulaire et que vous cliquez sur le bouton envoyer, le formulaire s'envoie seul, et la variable request.vars.name
avec sa valeur fournie est affichée en bas.
Vous pouvez générer le même formulaire en utilisant les helpers. Ceci peut être fait dans la vue ou dans l'action. Dès lors que web2py exécute le formulaire dans l'action, il est mieux de définir le formulaire dans l'action directement.
Voici le nouveau contrôleur :
def display_form():
form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
return dict(form=form)
et la vue associée "default/display_form.html" :
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
Le code est équivalent au codé précédent, mais le formulaire est généré par la déclaration {{=form}}
qui sérialise l'objet FORM
.
Maintenant nous ajoutons un niveau de ocmplexité en ajoutant la validation du formulaire et l'exécution.
Changez le contrôleur comme suit :
def display_form():
form=FORM('Your name:',
INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.accepts(request,session):
response.flash = 'form accepted'
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill the form'
return dict(form=form)
et la vue associée "default/display_form.html" :
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Accepted variables</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Errors in form</h2>
{{=BEAUTIFY(form.errors)}}
Notez que :
- Dans l'action, nous avons ajouté le validateur
requires=IS_NOT_EMPTY()
pour le champ en entrée "name". - Dans l'action, nous avons ajouté un appel à
form.accepts(..)
- Dans la vue, nous affichons
form.vars
etform.errors
en plus du formulaire etrequest.vars
.
Tout le travail est fait par la méthode accepts
de l'objet form
. Il filtre le request.vars
selon les pré-requis déclarés (exprimés par les validateurs). accepts
stocke ces variables qui passent la validation dans form.vars
. Si la valeur d'un champ ne remplit pas un pré-requis, le validateur échouant retourne une erreur et l'erreur est stockée dans form.errors
. Aussi bien form.vars
que form.errors
sont des objets gluon.storage.Storage
de même que request.vars
. Le premier contient les valeurs qui ont passé la validation, par exemple :
form.vars.name = "Max"
Le dernier contient les erreurs, par exemple :
form.errors.name = "Cannot be empty!"
La signature complète de la méthode accepts
est la suivante :
form.accepts(vars, session=None, formname='default',
keepvalues=False, onvalidation=None,
dbio=True, hideerror=False):
La signification de ces paramètres optionnels est expliquée dans les prochaines sous-sections.
Le premier argument peut être request.vars
ou request.get_vars
ou request.post_vars
ou simplement request
. Le dernier est équivalent à accepter en entrée le request.post_vars
.
La fonction accepts
retourne True
si le formulaire est accepté et False
sinon. Un formulaire n'est pas accepté si il a des erreurs ou lorsqu'il n'a pas été soumis (par exemple, la première fois qu'il est affiché).
Voici ce à quoi ressemble cette page la première fois qu'elle est affichée :
Voici ce à quoi elle ressemble lors d'une soumission invalide :
Voici ce à quoi elle ressemble lors d'une soumission valide :
Les méthodes process
et validate
Un raccourci pour
form.accepts(request.post_vars,session,...)
est
form.process(...).accepted
Le dernier n'a pas besoin des arguments request
et session
(même si vous pouvez les spécifier optionnellement). Il diffère également de accepts
car il retourne le formulaire directement. En interne, les appels process
acceptent et lui passent les arguments. La valeur retournée par accepts est stockée dans form.accepted
.
La fonction process prend quelques arguments en extra que accepts
ne prend pas :
message_onsuccess
onsuccess
: si égal à 'flash' (défaut) et que le formulaire est accepté, affichera lemessage_onsuccess
ci-dessusmessage_onfailure
onfailure
: si égal à 'flash' (défaut) et que le formulaire échoue, affichera lemessage_onfailure
ci-dessusnext
indique où rediriger l'utilisateur après que le formulaire soit accepté.
onsuccess
et onfailure
peuvent être des fonctions comme lambda form: do_something(form)
.
form.validate(...)
est un raccourci pour
form.process(...,dbio=False).accepted
Champs cachés
Lorsque l'objet form ci-dessus est sérialisé par {{=form}}
, et en raison de l'appel précédent à la méthode accepts
, cela ressemble maintenant à :
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>
Notez la présence de deux champs cachés : "_formkey" et "_formname". Leur présence est déclenchée par l'appel de accepts
et ils peuvent jouer deux rôles différents et importants :
- Le champ caché appelé "_formkey" est un token unique que web2py utilise pour éviter le double envoi de formulaires. La valeur de cette clé est générée lorsque le formulaire est sérialisé et stocké dans la
session
. Lorsque le formulaire est soumis, cette valeur doit correspondre, ou sinonaccepts
retourneFalse
sans erreur comme si le formulaire n'avait pas du tout été soumis. Ceci parce que web2py ne peut pas déterminer si le formulaire a été soumis correctement ou non. - Le champ caché appelé "_formname" est généré par web2py comme un nom pour le fomrulaire, mais le nom peut être écrasé. Ce champ est nécessaire pour autoriser les pages qui contiennent et exécutent plusieurs formulaires. Web2py distingue les différents formulaires envoyés par leurs noms.
- Des champs cachés optionnels spécifiés comme
FORM(..,hidden=dict(...))
.
Le rôle de ces champs cachés et leur usage dans des formulaires personnalisés et les pages avec de multiples formulaires est présenté plus en détail dans le chapitre.
Si le formulaire ci-dessus est soumis avec un champ "name" vide, le formulaire ne passe pas la validation. Lorsque le formulaire est sérialisé à nouveau il apparaît comme :
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>
Notez la présence d'un DIV de classe "error" dans le formulaire sérialisé. web2py insère ce message d'erreur dans le formulaire pour notifier le visiteur du champ qui n'a pas passé la validation. La méthode accepts
, lors de la soumission, détermine que le formulaire est envoyé, vérifie si le champ "name" est vide et/ou s'il est requis, et éventuellement insère le message d'erreur du validateur dans le formulaire.
La vue de base "layout.html" est supposée gérer les DIVs de classe "error". Le layout par défaut utilise les effets jQuery pour faire apparaître les erreurs et faire glisser avec un arrière plan rouge. Voir le chapitre 11 pour plus de détails.
keepvalues
L'argument optionnel keepvalues
indique à web2py ce qu'il doit faire lorsqu'un formulaire est accepté et il n'y a pas de redirection, afin que le même formulaire soit affiché à nouveau. Par défaut le formulaire est effacé. Si keepvalues
est défini à True
, le formulaire est pré-rempli avec les valeurs précédemment insérées. C'est utile lorsque vous avez un formulaire qui est supposé être utilisé de manière répétitive pour insérer de multiples enregistrements similaires. Si l'argument dbio
est défini à False
, web2py n'effecturea aucun insert/update dans la base de données après avoir accepté le formulaire. Si hideerror
est défini à True
et que le formulaire contient des erreurs, elles ne seront pas affichées lorsque le formulaire est affiché (ce sera à vous de les afficher depuis form.errors
quelque part. L'argument onvalidation
est expliqué juste après.
onvalidation
L'argument onvalidation
peut être None
ou peut être une fonction qui prend le formulaire et ne retourne rien. Une telle fonction serait appelée et passée au formulaire, immédiatement après la validation (si la validation réussit) et avant que quoique ce soit d'autre n'arrive. Cette fonction a plusieurs objectifs : par exemple, pour effectuer des vérifications additionnelles sur le formulaire et éventuellement ajouter des erreurs au formulaire, ou pour calculer les valeurs de certains champs basés sur les valeurs d'autres champs, ou pour déclencher quelques actions (comme l'envoi de mail) avant qu'un enregistrement ne soit créé/mis à jour.
Voici un exemple :
db.define_table('numbers',
Field('a', 'integer'),
Field('b', 'integer'),
Field('c', 'integer', readable=False, writable=False))
def my_form_processing(form):
c = form.vars.a * form.vars.b
if c < 0:
form.errors.b = 'a*b cannot be negative'
else:
form.vars.c = c
def insert_numbers():
form = SQLFORM(db.numbers)
if form.process(onvalidation=my_form_processing).accepted:
session.flash = 'record inserted'
redirect(URL())
return dict(form=form)
Détecter les changements d'enregistrement
Lorsque vous complétez un formulaire pour éditer un enregistrement, il y a une faible probabilité qu'un autre utilisateur puisse éditer le même enregistrement de manière concurrentielle. Donc lorsque l'on eut sauver l'enregistrement, nous voulons vérifier qu'il n'y ait pas de conflits. Ceci peut être fait avec :
db.define_table('dog',Field('name'))
def edit_dog():
dog = db.dog(request.args(0)) or redirect(URL('error'))
form=SQLFORM(db.dog,dog)
form.process(detect_record_change=True)
if form.record_changed:
# do something
elif form.accepted:
# do something else
else:
# do nothing
return dict(form=form)
Formulaires et redirection
La façon la plus commune d'utiliser les formulaires est via les auto-soumissions, adin que les variables de champs soumises soient traitées par la même action que celle qui a généré le formulaire. Une fois le formulaire accepté, il est inhabituel d'afficher la même page à nouveau (quelque chose que nous faisons ici pour rester dans un environnement simple). Il est plus habituel de rediriger le visiteur vers une page "next".
Voici le nouvel exemple de contrôleur :
def display_form():
form = FORM('Your name:',
INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.process().accepted:
session.flash = 'form accepted'
redirect(URL('next'))
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill the form'
return dict(form=form)
def next():
return dict()
Afin de définir un flash sur la page suivante au lieu de la page courante, vous devez utiliser session.flash
au lieu de reponse.flash
. web2py déplace le premier dans le deuxième après une redirection. Notez qu'utiliser session.flash
nécessite que vous n'ayez pas fait un session.forget()
.
Multiples formulaires par page
Le contenu de cette section s'applique aussi bien aux objets FORM
et SQLFORM
. Il est possible d'avoir de multiples formulaires par page, mais vous devez autoriser web2py à les distinguer. S'ils sont dérivés par SQLFORM
de tables différentes, alors web2py leur donne des noms différents automatiquement ; autrement vous aurez besoin de leur donner explicitement des noms de formulaires différents. Voici un exemple :
def two_forms():
form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form1.process(formname='form_one').accepted:
response.flash = 'form one accepted'
if form2.process(formname='form_two').accepted:
response.flash = 'form two accepted'
return dict(form1=form1, form2=form2)
et voici la sortie que cela produit :
Lorsque le visiteur soumet un form1 vide, seul form1 affiche une erreur ; si le visiteur soumet un form2 vide, seul form2 affiche un message d'erreur.
Partager des formulaires
Le contenu de cette section s'applique aussi bien aux objets FORM
et SQLFORM
. Ce que nous présentons ici est possible mais non recommandé, puisqu'il est toujours de bonne pratique d'avoir des formulaires auto-soumis. Parfois, cependant, vous n'avez pas le choix, puisque l'action qui envoie le formulaire est l'action qui reçois appartiennent à des applications différentes.
Il est possible de générer un formulaire qui envoie une action différente. Ceci est fait en spécifiant l'URL de l'action exécutée dans les attributs de l'objet FORM
ou SQLFORM
. Par exemple :
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'), _action=URL('page_two'))
def page_one():
return dict(form=form)
def page_two():
if form.process(session=None, formname=None).accepted:
response.flash = 'form accepted'
else:
response.flash = 'there was an error in the form'
return dict()
Notez que puisque "page_one" et "page_two" utilisent le même form
, nous l'avons défini seulement une fois en le plaçant en dehors de toutes les actions, afin de ne pas nous répeter. La partie commune du code au début d'un contrôleur est exécutée à chaque fois avant de donner le contrôle à l'action appelée.
Puique "page_one" n'appelle pas process
(ni accepts
), le formulaire n'a pas de nom ni de clé, vous devriez donc passer session=None
et définir formname=None
dans process
, ou le formulaire ne sera pas validé lorsque "page_two" le recevra.
Ajouter des boutons aux FORMs
Habituellement un formulaire fournit un simple bouton "submit". Il est commun de vouloir ajouter un bouton "back" qui au lieu d'envoyer le formulaire, redirige le visiteur sur une page différente.
Ceci peut être fait avec la méthode add_button
:
form.add_button('Back', URL('other_page'))
Vous pouvez ajouter plus d'un bouton à un formulaire. Les arguments de add_button
sont la valeur du bouton (son texte) et l'url vers où ils redirigent. (Voir également les arguments de boutons pour SQLFORM, qui fournissent une approche plus performante)
Plus au sujet de la manipulation des FORMs
Comme présenté dans le chapitre sur les Vues, un FORM est un helper HTML. Les helpers peuvent être manipulés comme des listes Python et comme des dictionnaires, ce qui active la création et modification d'exécution.
SQLFORM
Nous allons maintenant vers le niveau suivant en fournissant à l'application un fichier de modèle :
db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))
Modifiez le contrôleur comme suit :
def display_form():
form = SQLFORM(db.person)
if form.process().accepted:
response.flash = 'form accepted'
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill out the form'
return dict(form=form)
La vue n'a pas besoin d'être changée.
Dans le nouveau contrôleur, vous n'avez pas besoin de construire un FORM
, puisque le constructeur SQLFORM
en construit un depuis la table db.person
définie dans le modèle. Ce nouveau formulaire, lorsqu'il est sérialisé apparaît comme :
<form enctype="multipart/form-data" action="" method="post">
<table>
<tr id="person_name__row">
<td><label id="person_name__label"
for="person_name">Your name: </label></td>
<td><input type="text" class="string"
name="name" value="" id="person_name" /></td>
<td></td>
</tr>
<tr id="submit_record__row">
<td></td>
<td><input value="Submit" type="submit" /></td>
<td></td>
</tr>
</table>
<input value="9038845529" type="hidden" name="_formkey" />
<input value="person" type="hidden" name="_formname" />
</form>
Le formulaire auto-généré est plus complexe que le précédent formulaire bas-niveau. Tout d'abord, il contient une table de rows, et chaque ligne a trois colonnes. La première colonne contient le champ labels (comme défini depuis le db.person
), la seconde colonne contient les champs d'entrée (et éventuellement les messages d'erreur), et la troisième colonne est optionnelle et vide (peut être remplie avec les champs dans le constructeur SQLFORM
).
Tous les tafs dans le formulaire ont des noms dérivés du nom de la table et du champ. Ceci permet une personnalisation facile du formulaire en utilisant CSS et JavaScript. Cette possibilité est présentée plus en détails dans le chapitre 11.
Le plus important est que maintenant la méthode accepts
fait beaucoup plus de travail que vous. Comme dans le cas précédent, elle effectue la validation de l'entrée, mais en plus, si l'entrée passe la validation, effectue également l'insertion dans la base du nouvel enregistrement et stocke dans form.vars_id
l'"id" unique du nouvel enregistrement.
Un objet SQLFORM
s'arrange également automatiquement avec les champs "upload" en sauvant les fichiers sauvegardés dans le dossier "uploads" (après les avoir renommés proprement pour éviter les conflits et empêcher les attaques transverses sur répertoire) et stocke leurs noms (leurs nouveaux noms) dans le champ approprié dans la base de données. Après que le formulaire ait été exécuté, le nouveau nom de fichier est disponible dans form.vars.fieldname
(i.e., il remplace l'objet cgi.FieldStorage
dans request.vars.fieldname
), afin que vous puissiez facilement référencer le nouveau nom juste après l'upload.
Un SQLFORM
affiche les valeurs "boolean" avec des cases à cocher, les valeurs "text" avec des zones de texte, les valeurs nécessitant d'être dans un ensemble défini ou une base de données avec des listes déroulantes, et les champs "upload" avec des liens qui permettent aux utilisateurs d'uploader les fichiers. Il cache les champs "blob", puisqu'ils sont supposés être gérés différemment, comme présenté après.
Par exemple, considérez le modèle suivant :
db.define_table('person',
Field('name', requires=IS_NOT_EMPTY()),
Field('married', 'boolean'),
Field('gender', requires=IS_IN_SET(['Male', 'Female', 'Other'])),
Field('profile', 'text'),
Field('image', 'upload'))
Dans ce cas, SQLFORM(db.person)
génère le formulaire montré ci-dessous :
Le constructeur SQLFORM
autorise des personnalisations variées, telles qu'afficher seulement un sous-ensemble de champs, changer les labels, ajouter les valeurs à la troisième colonne optionnelle, ou créer des formulaires UPDATE et DELETE, en opposé aux formulaires INSERT comme le formulaire courant. SQLFORM
est l'objet qui sauve le plus temps dans web2py.
La classe SQLFORM
est définie dans "gluon/sqlhtml.py". Elle peut facilement être étendue en surchargeant sa méthode xml
, la méthode qui sérialise les objets, pour changer sa sortie.
SQLFORM
est la suivante :SQLFORM(table, record=None,
deletable=False, linkto=None,
upload=None, fields=None, labels=None,
col3={}, submit_button='Submit',
delete_label='Check to delete:',
showid=True, readonly=False,
comments=True, keepopts=[],
ignore_rw=False, record_id=None,
formstyle='table3cols',
buttons=['submit'], separator=': ',
**attributes)
- Le second argument optionnel transforme le formulaire INSERT en UPDATE pour l'enregistrement spécifié (voir la prochaine sous-section). showiddelete_labelid_labelsubmit_button
- Si
deletable
est défini à `True, le formulaire UPDATE affiche une case à cocher "Check to delete". La valeur du label pour ce champ est défini via l'argument
delete_label. -
submit_buttondéfinit la valeur du bouton d'envoi. -
id_labeldéfinit le label de l'enregistrement "id" - L'"id" de l'enregistrement n'est pas montré si
showidest défini à
False. -
fieldsest une liste de noms optionnelle que vous souhaitez afficher. Si une liste est fournie, seuls les champs dans la liste sont affichés. Par exemple :
fields = ['name'] :code
-
labels est un dictionnaire de labels de champ. La clé de dictionnaire est un nom de champ et la valeur correspondante est ce qui est affiché comme son label. Si un label n'est pas fourni, web2py dérive le label du nom de champ (il met en majuscule le nom du champ et remplace les underscores avec des espaces). Par exemple :
labels = {'name':'Your Full Name:'} :code
-
col3 est un dictionnaire de valeurs pour la troisième colonne. Par exemple :
col3 = {'name':A('what is this?', _href='http://www.google.com/search?q=define:name')} :code
-
linkto et
upload sont des URLs optionnelles pour les contrôleurs définis par l'utilisateur qui autorisent le formulaire à fonctionner avec des champs de référence. Ceci est présenté plus en détail plus tard dans la section.
-
readonly. Si défini à True, affiche le formulaire en lecture seule
-
comments. Si défini à False, n'affiche pas les commentaires col3
-
ignore_rw. Normalement, pour un formulaire create/upload, seuls les champs marqués comme writable=True sont montrés, et pour les formulaires en lecture seule, seuls les champs marqués comme readable=True sont montrés. Définir
ignore_rw=True annule ces contraintes, et tous les champs sont affichés. C'est principalement utilisé dans l'interface appadmin pour afficher tous les champs de chaque table, écrasant ainsi ce qui est indiqué par le modèle.
-
formstyle:inxx
formstyle détermine le style à utiliser lorsque l'on sérialise le formulaire en html. Ce peut être "table3cols" (défaut), "table2cols) (une ligne pour le label et le commentaire et une pour l'entrée), "ul" (fait une liste non ordonnée des champs en entrée), "divs" (représente le formulaire en utilisant les divs css habituelles, pour une personnalisation arbitraire), "bootstrap" qui utilise la classe bootstrap "form-horizontal" du formulaire.
formstyle peut aussi être une fonction qui prend (record_id, field_label, field_widget, field_comment) comme attributs et retourne un objet TR().
-
buttons:inxx
buttons est une liste de
INPUTs ou
TAG.buttons (qui pourrait d'ailleurs techniquement être n'importe quelle combinaison de helpers) qui seront ajoutés au DIV où le bouton submit ira.
Par exemple, ajouter un bouton retour basé sur URL (pour un formulaire multi-page) et un bouton submit renommé :
buttons = [TAG.button('Back',_type="button",_onClick = "parent.location='%s' " % URL(...), TAG.button('Next',_type="submit")]
:code
ou un bouton qui lie à une autre page :
buttons = [..., A("Go to another page",_class='btn',_href=URL("default","anotherpage"))] :code
-
separator:inxx
separator définit la chaîne qui sépare les labels de formulaire des champs d'entrée.
- Les
attributes optionnels sont des arguments commençant avec un underscore que vous voulez passer au tag
FORM qui affiche l'objet
SQLFORM. Les exemples sont :
_action = '.' _method = 'POST' :code
Il y a un attribut spécial
hidden. Lorsqu'un dictionnaire est passé comme
hidden, ses objets sont traduits en champs INPUT "hidden" (voir l'exemple pour le helper
FORM dans le Chapitre 5).
form = SQLFORM(...,hidden=...) :code
fait passer les champs cachés avec la soumission, ni plus, ni moins.
form.accepts(...) n'est pas prévu pour lire les champs cachés reçus et les déplacent dans form.vars. La raison pour cela est la sécurité. Les champs cachés peuvent être altérés.
Vous avez donc à explicitement déplacer les champs cachés depuis la requête vers le formulaire :
form.vars.a = request.vars.a form = SQLFORM(..., hidden=dict(a='b')) :code
#### La méthode
process
SQLFORM utilise la méthode process (comme les formulaires).
Si vous voulez utiliser keepvalues avec un SQLFORM, vous devez passer un argument à la méthode process :
if form.process(keepvalues=True).accepted::code
####
SQLFORM et
insert/
update/
delete
SQLFORM créé un nouvel enregistrement dans la base de données lorsque le formulaire est accepté. Supposant
form=SQLFORM(db.test):code, alors l'id du dernier enregistrement créé sera accessible dans
myform.vars.id.
delete record:inxx
Si vous passez un enregistrement comme second argument optionnel au constructeur
SQLFORM, le formulaire devient un formulaire UPDATE pour cet enregistrement. Ceci signifie que lorsque le formulaire est soumis, l'enregistrement existant est mis à jour et aucun enregistrement n'est inséré. Si vous définissez l'argument
deletable=True, le formulaire UPDATE affiche une case à cocher "check to delete". Si cochée, l'enregistrement est supprimé.
------
Si un formulaire est soumis et la case à cocher de suppression est cochée, l'attribut
form.deleted est défini à
True.
------
Vous pouvez modifier le contrôleur de l'exemple précédent afin que lorsque nous passons un argument entier additionnel dans le chemin URL, comme dans :
/test/default/display_form/2 :code
et s'il y a un enregistrement avec l'id correspondant,
SQLFORM génère un formulaire UPDATE/DELETE pour l'enregistrement :
def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) form = SQLFORM(db.person, record) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code
La ligne 2 trouve l'enregistrement et la ligne 3 fait le formulaire UPDATE/DELETE. La ligne 4 fait toute l'exécution correspondante du formulaire.
------
Un formulaire de mise à jour est très similaire à la création d'un formulaire sauf qu'il est pré-rempli avec l'enregistrement courant et prévisualise les images. Par défaut
deletable = True qui signifie que le formulaire de mise à jour affichera une option "delete record".
------
L'édition de formulaire contient également un champ caché INPUT avec
name="id" qui est utilisé pour identifier l'enregistrement. Cet id est également stocké côté serveur pour une sécurité complémentaire et, si le visiteur altère la valeur de ce champ, l'UPDATE n'est pas fait et web2py lève une exception SyntaxError, "user is tampering with form".
Lorsqu'un champ est marqué avec
writable=False, le champ n'est pas montré dans les formulaires de création, et est montré en lecture seule dans les formulaires de mise à jour. Si un champ est marqué comme
writable=False et
readable=False, alors le champ n'est pas montré du tout, même pas dans les formulaires de mise à jour.
Les formulaires créés avec
form = SQLFORM(...,ignore_rw=True) :code
ignore les attributs
readable et
writable et montre toujours tous les champs. Les formulaires dans
appadmin les ignorent par défaut.
Les formulaires créés avec
form = SQLFORM(table,record_id,readonly=True) :code
montrent toujours tous les champs en mode lecture seule, et ils ne peuvent pas être acceptés.
Marquer un champ avec
writable=False empêche le champs de faire partie du formulaire, et entraine l'exécution du formulaire à ne pas prendre en considération la valeur de
request.vars.field lors de l'exécution du formulaire. Cependant, si vous assignez une valeur à
form.vars.field, cette valeur ''sera'' partie de l'insertion ou de la mise à jour lorsque le formulaire est exécuté.
Ceci vous permet de changer la valeur des champs que, pour quelques raisons, vous ne souhaitez pas inclure dans un formulaire.
####
SQLFORM en HTML
Il y a des fois où vous voulez utiliser
SQLFORM pour bénéficier de sa génération de formulaire et de l'exécution, mais vous avez besoin d'un niveau de presonnalisation du formulaire en HTML que vous ne pouvez pas obtenir avec les paramètres de l'objet
SQLFORM, donc vous devez déclarer le formulaire en utilisant HTML.
Maintenant ,éditez le contrôleur précédent et ajoutez une nouvelle action :
def display_manual_form(): form = SQLFORM(db.person) if form.process(session=None, formname='test').accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' else: response.flash = 'please fill the form'Note: no form instance is passed to the view return dict()
:code
et insérez le formulaire dans la vue associée "default/display_manual_form.html" :
{{extend 'layout.html'}} <form action="#" enctype="multipart/form-data" method="post"> <ul> <li>Your name is <input name="name" /></li> </ul> <input type="submit" /> <input type="hidden" name="_formname" value="test" /> </form> :code
Notez que l'action ne retourne pas le formulaire puisqu'il n'a pas besoin de le passer à la vue. La vue contient un formulaire créé manuellement en HTML. Le formulaire contient un champ caché "_formname" qui doit être le même
formname spécifié comme un argument de
accepts dans l'action. web2py utilise le nom du formulaire dans le cas où plusieurs formulaires sur la même page, pour déterminer lequel a été soumis. Si la page contient un seul formulaire, vous pouvez définir
formname=None et oubliez le champ caché dans la vue.
form.accepts regardera dans
response.vars pour les données qui correspondent aux champs de la table de la base de données
db.person. Ces champs sont déclarés dans l'HTML au format
<input name="field_name_goes_here" /> :code
Notez que dans l'exemple donné, les variables du formulaire seront passées sur l'URL comme arguments. Si ce n'est pas désiré, le protocole
POST devra être spécifié. Notez de plus, que si les champs upload sont spécifiés, le formulaire devra être défini pour l'autoriser. Ici, deux options sont montrées :
<form enctype="multipart/form-data" method="post"> :code
####
SQLFORM et uploads
Les champs de type "upload" sont spéciaux. Ils sont rendus comme champs INPUT de
type="file". A moins que ce ne soit spécifié autrement, le fichier uploadé est envoyé en flux en utilisant un buffer, et stocké sous le dossier "uploads" de l'application en utilisant un nouveau nom propre, assigné automatiquement. Le nom de ce fichier est alors sauvé dans le champ de type uploads.
Comme exemple, considérez le modèle suivant :
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()), Field('image', 'upload'))
:code
Vous pouvez utiliser la même action contrôleur "display_form" montrée ci-dessus.
Lorsque vous insérez un nouvel enregistrement, le formulaire vous permet de parcourir un fichier.
Choisissez, par exemple, une image jpg. Le fichier est uploadé et stocké comme :
applications/test/uploads/person.image.XXXXX.jpg :code
"XXXXXX" est un identificateur aléatoire pour le fichier assigné par web2py.
content-disposition:inxx
-------
Notez que par défaut, le nom de fichier original d'un fichier uploadé est b16encoded et utilisé pour construire le nouveau nom pour le fichier. Ce nom est retrouvé par l'action par défaut "download" et utilisé pour définir l'en-tête du content disposition au nom de fichier original.
-------
Seule son extension est préservée. C'est un pré-requis de sécurité puisque le nom de fichier peut contenir des caractères spéciaux qui pourraient autoriser un visiteur à effectuer des attaques de répertoire transverses ou toute autre opération malicieuse.
Le nouveau nom de fichier est également stocké dans
form.vars.image.
Lors de l'édition de l'enregistrement en utilisant un formulaire UPDATE, il serait bien d'afficher un lien au fichier uploadé existant, et web2py fournit un moyen de le faire.
Si vous passez une URL au constructeur
SQLFORM via l'argument upload, web2py utilise l'action à cette URL pour télécharger le fichier. Considérez les actions suivantes :
def display_form(): record = db.person(request.args(0)) form = SQLFORM(db.person, record, deletable=True, upload=URL('download')) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form)
def download(): return response.download(request, db)
:code
Maintenant, insérez un nouvel enregistrement à l'URL :
http://127.0.0.1:8000/test/default/display_form
:code
Uploadez une image, soumettez le formulaire, et ensuire éditez le nouvel enregistrement créé en visitant :
http://127.0.0.1:8000/test/default/display_form/3
:code
(ici, nous supposons que le dernier enregistrement a id=3). Le formulaire affichera une prévisualisation d'image comme montré ci-après :
[[image http://www.web2py.com/books/default/image/38/en6300.png center 300px]]
Ce formulaire, lorsque sérialisé, génère le HTML suivant :
<td><label id="person_image__label" for="person_image">Image: </label></td> <td><div><input type="file" id="person_image" class="upload" name="image" />[<a href="/test/default/download/person.image.0246683463831.jpg">file</a>| <input type="checkbox" name="image__delete" />delete]</div></td><td></td></tr> <tr id="delete_record__row"><td><label id="delete_record__label" for="delete_record" >Check to delete:</label></td><td><input type="checkbox" id="delete_record" class="delete" name="delete_this_record" /></td>
:code
qui contient un lien pour autoriser le téléchargement du fichier uploadé, et une case à cocher pour supprimer le fichier de l'enregistrement en base de données, stockant ainsi NULL dans le champ "image".
Pour est-ce que ce mécanisme est exposé ? Pourquoi avez-vous besoin d'écrire la fonction download ? Parce que vous pourriez vouloir forcer quelques mécanismes d'autorisation dans la fonction download. Voir le Chapitre 9 pour un exemple.
Normalement, les fichiers uploadés sont stockés dans "app/uploads" mais vous pouvez spécifier une localisation alternative :
Field('image', 'upload', uploadfolder='...')
Dans la plupart des systèmes d'exploitation, accéder au système de fichiers peut devenir très lent lorsqu'il y a beaucoup de fichiers dans le même répertoire. Si vous planifiez d'uploader plus de 1000 fichiers vous pouvez demander à web2py d'organiser les uploads dans des sous-dossiers :
Field('image', 'upload', uploadseparate=True)
#### Stocker le nom de fichier original
web2py stocke automatiquement le nom de fichier original dans le nouvel UUID et le retrouve lorsque le fichier est téléchargé. Lors du téléchargement, le nom de fichier original est stocké dans l'en-tête content-disposition de la réponse HTTP. Tout ceci est fait de manière transparente sans le besoin de programmation.
Occasionnellement vous pouvez vouloir stocker le nom de fichier original dans un champ de la base de données. Dans ce cas, vous avez besoin de modifier le modèle et ajouter un champ pour le stocker dans :
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()), Field('image_filename'), Field('image', 'upload'))
:code
Ensuite vous avez besoin de modifier le contrôleur pour le gérer :
def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) url = URL('download') form = SQLFORM(db.person, record, deletable=True, upload=url, fields=['name', 'image']) if request.vars.image!=None: form.vars.image_filename = request.vars.image.filename if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code
Notez que le
SQLFORM n'affiche pas le champ "image_filename".
L'action "display_form" déplace le nom de fichier de
request.vars.image dans
form.vars.image_filename, afin qu'il soit exécuté par
accepts et stocké dans la base de données. La fonction download, avant de servir le fichier, vérifie dans la base de données si le nom de fichier original existe et l'utilise dans l'en-tête content-disposition.
####
autodelete
autodelete:inxx
Le
SQLFORM, lors de la suppression d'un enregistrement, ne supprime pas le(s) fichier(s) physique(s) uploadé(s) référencé par l'enregistrement. La raison est que web2py ne sait pas si le même fichier est utilisé/lié avec une autre table ou utilisé pour d'autres raisons. Si vous le savez, il est plus sûr de supprimer le fichier actuel lorsque l'enregistrement est supprimé, vous pouvez le faire de la manière suivante :
db.define_table('image', Field('name', requires=IS_NOT_EMPTY()), Field('source','upload',autodelete=True)) :code
L'attribut
autodelete est
False par défaut. Lorsque défini à
True il faut s'assurer que le fichier est supprimé lorsque l'enregistrement est supprimé.
#### Liens aux enregistrements de référence
Considérez maintenant le cas de deux tables liées par un champ de référence. Par exemple :
db.define_table('person', Field('name', requires=IS_NOT_EMPTY())) db.define_table('dog', Field('owner', 'reference person'), Field('name', requires=IS_NOT_EMPTY())) db.dog.owner.requires = IS_IN_DB(db,db.person.id,'%(name)s') :code
Une personne a des chiens, et chaque chien appartient à un maître, qui est une personne. Le propriétaire du chien est requis pour référencer un
db.person.id valide par
'%(name)s'.
Utilisons maintenant l'interface **appadmin** pour cette application pour ajouter quelques personnes et leurs chiens.
Lors de l'édition d'une personne existante, le formulaire UPDATE **appadmin** montre un lien vers une page qui liste les chiens appartenant à la personne. Ce comportement peut être répliqué en utilisant l'argument
linkto de
SQLFORM.
linkto doit pointer vers l'URL de la nouvelle action qui reçoit un requête depuis
SQLFORM et liste les enregistrements correspondants.
Voici un exemple :
def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) url = URL('download') link = URL('list_records', args='db') form = SQLFORM(db.person, record, deletable=True, upload=url, linkto=link) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code
Voici la page :
[[image http://www.web2py.com/books/default/image/38/en6400.png center 300px]]
Il y a un lien appelé "dog.owner". Le nom de ce lien peut être changé via l'argument
labels du
SQLFORM, par exemple :
labels = {'dog.owner':"This person's dogs"}:code
Si vous cliquez sur le lien vous serez redirigé vers :
/test/default/list_records/dog?query=db.dog.owner%3D%3D5 :code
"list_records" est l'action spécifiée, avec
request.args(0) défini avec le nom de la table de référencement et
request.vars.query défini vers la requête SQL.
La requête dans l'URL contient la valeur "dog.owner=5" encodée en url de manière appropriée (web2py le décode automatiquement lorsque l'URL est parsée).
Vous pouvez facilement implémenter une action très générale "list_records" comme suit :
def list_records(): import re REGEX = re.compile('^(\w+).(\w+).(\w+)\=\=(\d+)$') match = REGEX.match(request.vars.query) if not match: redirect(URL('error')) table, field, id = match.group(2), match.group(3), match.group(4) records = db(db[table][field]==id).select() return dict(records=records)
:code
avec la vue associée "default/list_records.html" :
{{extend 'layout.html'}} {{=records}}
:code
Lorsqu'un ensemble d'enregistrements est retourné par un select et sérialisé dans une vue, il est d'abord converti en objet SQLTABLE (pas le même qu'une Table) et ensuite sérialisé en table HTML, où chaque champ correspond à une colonne de table.
#### Pre-remplir le formulaire
Il est toujours possible de pré-remplir un formulaire en utilisant la syntaxe :
form.vars.name = 'fieldvalue' :code
Les déclarations comme celle ci-dessus doivent être insérées après la déclaration du formulaire et avant que le formulaire ne soit accepté, que le champ soit explicitement visualisé dans le formulaire ou non ("name" dans l'exemple).
#### Ajouter des éléments supplémentaires au formulaire
SQLFORM
Parfois vous pouvez souhaiter ajouter un élement extra à votre formulaire après qu'il ait été créé. Par exemple, vous pouvez souhaiter ajouter une case à cocher qui confirme que l'utilisateur accepte les termes et conditions de votre site web :
form = SQLFORM(db.yourtable) my_extra_element = TR(LABEL('I agree to the terms and conditions'), INPUT(_name='agree',value=True,_type='checkbox')) form[0].insert(-1,my_extra_element) :code
La variable
my_extra_element devrait être adaptée au style de forme. Dans cet exemple, le
formstyle='table3cols' par défaut a été pris.
Après soumission,
form.vars.agree contiendra le statut de la case à cocher, qui pourrait alors être utilisé dans une fonction
onvalidation, par exemple.
####
SQLFORM sans entrée/sortie à la base de données
Il y a des fois où vous voulez générer un formulaire depuis une table de la base de données en utilisant
SQLFORM et vous voulez valider une formulaire soumis depuis de la même manière, mais vous ne voulez pas de l'automatisation de INSERT/UPDATE/DELETE dans la base de données. C'est le cas, par exemple, lorsqu'un des champs a besoin d'être calculé de la valeur des autres champs en entrée. C'est aussi le cas lorsque vous avez besoin d'effectuer des validation additionnelles sur les données insérées qui ne peuvent pas être faites par des validateurs standards.
Ceci peut être fait facilement en cassant :
form = SQLFORM(db.person) if form.process().accepted: response.flash = 'record inserted':code
en :
form = SQLFORM(db.person) if form.validate():
deal with uploads explicitly form.vars.id = db.person.insert(dict(form.vars)) response.flash = 'record inserted'
:code
Le même peut être fait pour les formulaires UPDATE/DELETE en cassant :
form = SQLFORM(db.person,record) if form.process().accepted: response.flash = 'record updated'
:code
en :
form = SQLFORM(db.person,record) if form.validate(): if form.deleted: db(db.person.id==record.id).delete() else: record.update_record(dict(form.vars)) response.flash = 'record updated' :code
Dans le cas d'une table incluant un champ de type "upload" ("fieldname"), aussi bien
process(dbio=False) que
validate() fonctionnent avec le stockage du fichier uploadé comme si
process(dbio=True), le comportement par défaut.
Le nom assigné par web2py au fichier uploadé peut être trouvé dans :
form.vars.fieldname :code
### Autres types de formulaires
####
SQLFORM.factory
Il y a des cas où vous voulez générer des formulaires ''comme si'' vous aviez une table de base de données mais vous ne voulez pas cette table. Vous souhaitez simplement profiter des possibilités de
SQLFORM pour générer un joli formulaire CSS-friendly et peut être effectuer de l'upload de fichier et du renommage.
Ceci peut être fait via un
form_factory. Voici un exemple où vous générez le formulaire, effectuez la validation, uploadez un fichier et stockez tout dans la
session :
def form_from_factory(): form = SQLFORM.factory( Field('your_name', requires=IS_NOT_EMPTY()), Field('your_image', 'upload')) if form.process().accepted: response.flash = 'form accepted' session.your_name = form.vars.your_name session.your_image = form.vars.your_image elif form.errors: response.flash = 'form has errors' return dict(form=form)
:code
L'objet Field dans le constructeur SQLFORM.factory() est entièrement documenté dans [[le chapitre DAL ../06#field_constructor]].
Une technique de construction d'exécution pour SQLFORM.factory() est
fields = [] fields.append(Field(...)) form=SQLFORM.factory(*fields)
:code
Voici la vue "default/form_from_factory.html" :
{{extend 'layout.html'}} {{=form}} :code
Vous avez besoin d'utiliser un underscore au lieu d'un espace pour les labels de champ, ou de passer explicitement un dictionnaire de
labels à
form_factory, comme vous voudriez un
SQLFORM. Par défaut,
SQLFORM.factory génère le formulaire en utilisant les attributs HTML "id" générés comme si le formulaire était généré depuis une table appelée "no_table". Pour changer ce nom de table, utilisez l'attribut
table_name pour le factory :
form = SQLFORM.factory(...,table_name='other_dummy_name') :code
Changer le
table_name est nécessaire si vous avez besoin de placer deux formulaires générés par factory dans la même table et que vous voulez éviter les conflits CSS.
##### Uploader des fichiers avec SQLFORM.factory
#### Un formulaire pour de multiples tables
Il arrive souvent que vous ayez deux tables (par exemple 'client' et 'address' qui ont liés ensemble par une référence et vous voulez créer un simple formulaire qui permet d'insérer les infos sur un client et son adresse par défaut. Voici comment :
modèle :
db.define_table('client', Field('name')) db.define_table('address', Field('client','reference client', writable=False,readable=False), Field('street'),Field('city'))
:code
contrôleur :
def register(): form=SQLFORM.factory(db.client,db.address) if form.process().accepted: id = db.client.insert(db.client._filter_fields(form.vars)) form.vars.client=id id = db.address.insert(db.address._filter_fields(form.vars)) response.flash='Thanks for filling the form' return dict(form=form) :code
Notez le SQLFORM.factory (il fait UN formulaire en utilisant les champs publics depuis les deux tables et hérite leurs validateurs également).
Un formulaire acceptant ceci fait deux insertions, quelques données dans une table et quelques données dans l'autre.
-------
Ceci fonctionne uniquement lorsque les tables n'ont pas de noms de champ en commun.
-------
#### Formulaires de confirmation
confirm:inxx
Vous avez souvent besoin d'un formulaire avec un choix de confirmation. Le formulaire devrait être accepté si le choix est accepté et non pas autrement. Le formulaire peut avoir des options additionnelles qui lient d'autres pages web. web2py fournit un moyen simple de faire cela :
form = FORM.confirm('Are you sure?') if form.accepted: do_what_needs_to_be_done() :code
Notez que le formulaire de confirmation n'a pas besoin et ne doit pas appeler
.accepts ou
.process car ceci est fait en interne. Vous pouvez ajouter des boutons avec des liens vers le formulaire de confirmation sous la forme d'un dictionnaire de
{'value':'link'} :
form = FORM.confirm('Are you sure?',{'Back':URL('other_page')}) if form.accepted: do_what_needs_to_be_done()
:code
#### Formulaire pour éditer un dictionnaire
Imaginez un système qui stocke les options de configuration dans un dictionnaire,
config = dict(color='black', language='English')
:code
et vous avez besoin d'un formulaire pour permettre au visiteur de modifier ce dictionnaire.
Ceci peut être fait avec :
form = SQLFORM.dictform(config) if form.process().accepted: config.update(form.vars) :code
Le formulaire affichera un champ INPUT pour chaque objet dans le dictionnaire. Il utilisera les clés de dictionnaire comme noms d'INPUT et les labels et les valeurs courantes pour déduire les types (string, int, double, date, datetime, boolean).
Ceci marche bien mais vous laisse la logique de rendre la configuration du dictionnaire persistente. Par exemple, vous pourriez vouloir stocker la
config dans une session.
session.config or dict(color='black', language='English') form = SQLFORM.dictform(session.config) if form.process().accepted: session.config.update(form.vars) :code
### CRUD
CRUD:inxx
crud.create:inxx
crud.update:inxx
crud.select:inxx
crud.search:inxx
crud.tables:inxx
crud.delete:inxx
Un des ajouts les plus récents à web2py est l'API Create/Read/Update/Delete (CRUD) au-dessus de SQLFORM.
CRUD créé un SQLFORM, mais simplifie le codage car il incorpore la création du formulaire, l'exécution du formulaire, la notification, et la redirection, tout en une seule et simple fonction.
La première chose à noter est que CRUD diffère des autres APIs web2py que nous avons utilisé jusqu'ici car elle n'est pas déjà exposée. Elle doit être importée. Elle doit également être liée à une base de données spécifique. Par exemple :
from gluon.tools import Crud crud = Crud(db) :code
L'objet
crud défini ci-dessus fournir l'API suivante :
crud.tables:inxx
crud.create:inxx
crud.read:inxx
crud.update:inxx
crud.delete:inxx
crud.select:inxx .
-
crud.tables() retourne une liste de tables définies dans la base de données.
-
crud.create(db.tablename) retourne un formulaire de création pour la table tablename.
-
crud.read(db.tablename, id) retourne un formulaire en lecture seule pour tablename et l'id de l'enregistrement.
-
crud.update(db.tablename, id) retourne un formulaire de mise à jour pour tablename et l'id de l'enregistrement.
-
crud.delete(db.tablename, id) supprime l'enregistrement.
-
crud.select(db.tablename, query) retourne une liste d'enregistrements sélectionnés depuis la table.
-
crud.search(db.tablename) retourne un tuple (form, records) où form est un formulaire de recherche et records une liste d'enregistrements basés sur le formulaire de recherche soumis.
-
crud() retourne l'une des fonctions précédentes selon
request.args().
Par exemple, l'action suivante :
def data(): return dict(form=crud())
:code
exposerait les URLs suivantes :
http://.../[app]/[controller]/data/tables http://.../[app]/[controller]/data/create/[tablename] http://.../[app]/[controller]/data/read/[tablename]/[id] http://.../[app]/[controller]/data/update/[tablename]/[id] http://.../[app]/[controller]/data/delete/[tablename]/[id] http://.../[app]/[controller]/data/select/[tablename] http://.../[app]/[controller]/data/search/[tablename]
:code
Cependant, l'action suivante :
def create_tablename(): return dict(form=crud.create(db.tablename))
:code
exposerait seulement la méthode create
http://.../[app]/[controller]/create_tablename
:code
Alors que l'action suivante :
def update_tablename(): return dict(form=crud.update(db.tablename, request.args(0)))
:code
exposerait uniquement la méthode update
http://.../[app]/[controller]/update_tablename/[id] :code
et ainsi de suite.
Le comportement de CRUD peut être personnalisé de deux manières : en définissant quelques attributs de l'objet
crud ou en passant des paramètres complémentaires à chacunes de ses méthodes.
#### Paramètres
Voici une liste complète des attributs CRUD courants, leurs valeurs par défaut, et leur signification :
Pour forcer l'authentification sur tous les formulaires crud :
crud.settings.auth = auth :code
L'usage est expliqué dans le chapitre 9.
Pour spécifier le contrôleur qui définit la fonction
data qui retourne l'objet
crud
crud.settings.controller = 'default':code
Pour spécifier l'URL où rediriger après un enregistrement "create" réussi :
crud.settings.create_next = URL('index')
:code
Pour spécifier l'URL où rediriger après un enregistrement "update" réussi :
crud.settings.update_next = URL('index')
:code
Pour spécifier l'URL où rediriger après un enregistrement "delete" réussi :
crud.settings.delete_next = URL('index')
:code
Pour spéficier l'URL à utiliser pour lier les fichiers uploadés :
crud.settings.download_url = URL('download') :code
Pour spécifier des fonctions supplémentaires à exécuter après des procédures de validation standard pour les formulaires
crud.create :
crud.settings.create_onvalidation = StorageList() :code
StorageList est le même qu'un objet
Storage, ils sont tous les deux définis dans le fichier "gluon/storage.py", mais il est par défaut à
[] au lieu de
None. Il permet la syntaxe suivante :
crud.settings.create_onvalidation.mytablename.append(lambda form:....) :code
Pour spécifier des fonctions complémentaires à exécuter après les procédures de validation standard pour les formulaires
crud.update :
crud.settings.update_onvalidation = StorageList() :code
Pour spécifier des fonctions complémentaires à exécuter après la complétion des formulaires
crud.create :
crud.settings.create_onaccept = StorageList() :code
Pour spécifier des fonctions complémentaires à exécuter après la complétion des formulaires
crud.update :
crud.settings.update_onaccept = StorageList() :code
Pour spécifier des fonctions complémentaires à exécuter après la complétion de
crud.update si un enregistrement est supprimé :
crud.settings.update_ondelete = StorageList() :code
Pour spécifier des fonctions complémentaires à exécuter après la complétion de
crud.delete :
crud.settings.delete_onaccept = StorageList()
:code
Pour déterminer si les formulaires "update" devraient avoir un bouton "delete" :
crud.settings.update_deletable = True
:code
Pour déterminer si les formulaires "update" devraient montrer l'id de l'enregistrement édité :
crud.settings.showid = False
:code
Pour déterminer si les formulaires devraient conserver les valeurs précédemment insérées ou remettre à zéro par défaut après une soumission réussie :
crud.settings.keepvalues = False
:code
Crus détecte toujours si un enregistrement en cours d'édition a été modifié par une partie tierce entre le moment où le formulaire est affiché et le moment où il est soumis. Ce comportement est équivalent à
form.process(detect_record_change=True)
et il est défini dans :
crud.settings.detect_record_change = True :code
et il peut être changé/désactivé en définissant la variable à
False.
Vous pouvez changer le style du formulaire avec
crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul':code
Vous pouvez définir le séparateur dans tous les formulaires crud :
crud.settings.label_separator = ':'
:code
#### captcha
Vous pouvez ajouter le captcha aux formulaire, en utilisant la même convention expliquée pour auth, avec :
crud.settings.create_captcha = None crud.settings.update_captcha = None crud.settings.captcha = None
:code
#### Messages
Voici une liste de messages personnalisables :
crud.messages.submit_button = 'Submit'
:code
définit le texte du bouton "submit" pour les formulaires de création et de mise à jour.
crud.messages.delete_label = 'Check to delete:'
:code
définit le label du bouton "delete" dans les formulaires "update".
crud.messages.record_created = 'Record Created'
:code
définit le message flash sur la création réussie d'un enregistrement.
crud.messages.record_updated = 'Record Updated'
:code
définit le message flash sur la mise à jour réussie d'un enregistrement.
crud.messages.record_deleted = 'Record Deleted'
:code
définit le message flash sur la suppression réussie d'un enregistrement.
crud.messages.update_log = 'Record %(id)s updated'
:code
définit le message de log sur la mise à jour réussie d'un enregistrement.
crud.messages.create_log = 'Record %(id)s created'
:code
définit le message de log sur la création réussie d'un enregistrement.
crud.messages.read_log = 'Record %(id)s read'
:code
définit le message de log sur l'accès en lecture réussi d'un enregistrement.
crud.messages.delete_log = 'Record %(id)s deleted' :code
définit le message de log sur la suppression réussie d'un enregistrement.
------
Notez que
crud.messages appartient à la classe
gluon.storage.Message qui est similaire à
gluon.storage.Storage mais qui traduit automatiquement ses valeurs, sans le besoin de l'opérateur
T.
------
Les messages de log sont utilisés si et seulement si CRUD est connecté à Auth comme présenté dans le chapitre 9. Les événements sont logués dans la table Auth "auth_events".
#### Méthodes
Le comportement des méthodes CRUD peut aussi être personnalisé par appel. Voici leurs signatures :
crud.tables() crud.create(table, next, onvalidation, onaccept, log, message) crud.read(table, record) crud.update(table, record, next, onvalidation, onaccept, ondelete, log, message, deletable) crud.delete(table, record_id, next, message) crud.select(table, query, fields, orderby, limitby, headers, **attr) crud.search(table, query, queries, query_labels, fields, field_labels, zero, showall, chkall) :code
-
table est une table DAL ou un nom de table sur laquelle la méthode devrait agir.
-
record et
record_id sont les id de l'enregistrement sur lequel la méthode doit agir.
-
next est l'URL où rediriger après la réussite de la fonction. Si l'URL contient la sous-chaîne "[id]" celle-ci sera remplacée par l'id de l'enregistrement couramment créé/mis à jour.
-
onvalidation a la même fonction que SQLFORM(..., onvalidation)
-
onaccept est une fonction à appeler après que la soumission du formulaire soit acceptée et ait agi, mais avant redirection.
-
log est le message de log. Les messages de log dans CRUD voient les variables dans le dictionnaire
form.vars comme "%(id)s".
-
message est le message flash lors d'acceptation de formulaire.
-
ondelete est appelé à la place de
onaccept lorsqu'un enregistrement est supprimé via un formulaire "update".
-
deletable détermine si le formulaire "update" devrait avoir une option de suppression.
-
query est la requête à utiliser pour sélectionner les enregistrements.
-
fields est une liste de champs à sélectionner.
-
orderby détermine l'ordre dans lequel les enregistrements devraient être sélectionnés (voir chapitre 6).
-
limitby détermine la plage des enregistrements sélectionnés qui devraient être affichés (voir chapitre 6).
-
headers est un dictionnaire avec les noms d'en-têtes des tables.
-
queries une liste comme
['equals', 'not equal', 'contains'] contenant les méthodes autorisées dans le formulaire de recherche.
-
query_labels un dictionnaire comme
query_labels=dict(equals='Equals') donnant les noms des méthodes à rechercher.
-
fields une liste de champs à lister dans le widget de recherche.
-
field_labels un dictionnaire mappant les noms de champs en labels.
-
zero par défaut à "choose one" est utilisé comme option par défaut pour le menu déroulant dans le widget de recherche.
-
showall définissez-le à True si vous souhaitez que les lignes soient retournées selon la requête dans le premier appel (ajouté après 1.98.2).
-
chkall définissez-le à True pour activer toutes les cases à cocher dans le formulaire de recherche (ajouté après 1.98.2).
Voici un exemple d'usage dans une simple fonction contrôleur :
supposons db.define_table('person', Field('name'))
def people(): form = crud.create(db.person, next=URL('index'), message=T("record created")) persons = crud.select(db.person, fields=['name'], headers={'person.name': 'Name'}) return dict(form=form, persons=persons)
:code
Voici un autre fonction contrôleur générique qui vous laisse chercher, créer et éditer n'importe quel enregistrement depuis n'importe quelle table où le nom de table est passé dans request.args(0) :
def manage(): table=db[request.args(0)] form = crud.update(table,request.args(1)) table.id.represent = lambda id, row: A('edit:',id,_href=URL(args=(request.args(0),id))) search, rows = crud.search(table) return dict(form=form,search=search,rows=rows) :code
Notez la ligne
table.id.represent=... qui indique à web2py de changer la représentation du champ id et affiche un lien au lieu de la page elle-même et passe l'id comme request.args(1) qui transforme la page de création en une page de mise à jour.
#### Versioning d'enregistrement
Aussi bien SQLFORM que CRUD fournissent un utilitaire pour versionner les enregistrements en base :
Si vous avez une table (db.mytable) qui a besoin d'un historique complet de révision, vous pouvez juste faire :
form = SQLFORM(db.mytable, myrecord).process(onsuccess=auth.archive)
:code
form = crud.update(db.mytable, myrecord, onaccept=auth.archive) :code
auth.archive définit une nouvelle table appelée **db.mytable_archive** (le nom est dérivé du nom de la table à laquelle il se réfère) et sur la mise à jour, il stocke une copie de l'enregistrement (comme il était avant la mise à jour) dans la table d'archive créée, incluant une référence vers l'enregistrement courant.
Puisque l'enregistrement est en fait mis à jour (seul son état précédent est archivé), les références ne sont jamais cassées.
Tout ceci est fait par des mécanismes internes. Si vous souhaitez accéder à la table d'archive, vous devriez la définir dans un modèle :
db.define_table('mytable_archive', Field('current_record', 'reference mytable'), db.mytable) :code
Notez que la table est étendue de
db.mytable (incluant tous ses champs), et ajoute une référence vers le
current_record.
auth.archive ne met pas de timestamp pour l'enregistrement stocké à moins que la table originale n'ait un champ de timestamp, par exemple :
db.define_table('mytable', Field('created_on', 'datetime', default=request.now, update=request.now, writable=False), Field('created_by', 'reference auth_user', default=auth.user_id, update=auth.user_id, writable=False),
:code
Il n'y a rien de spécial sur ces champs et vous pouvez leur donner n'importe quel nom. Ils sont remplis avant que l'enregistrement ne soit archivé et sont archivés avec chaque copie de l'enregistrement. Le nom de table de l'archive et/ou le nom de champ de référence peut être changé comme cela :
db.define_table('myhistory', Field('parent_record', 'reference mytable'), db.mytable)
...
form = SQLFORM(db.mytable,myrecord) form.process(onsuccess = lambda form:auth.archive(form, archive_table=db.myhistory, current_record='parent_record'))
:code
### Formulaires personnalisés
Si un formulaire est créé avec SQLFORM, SQLFORM.factory ou CRUD, il y a de multiples façons pour l'embarquer dans une vue permettant de multiples degrés de personnalisation. Considérons par exemple le modèle suivant :
db.define_table('image', Field('name', requires=IS_NOT_EMPTY()), Field('source', 'upload'))
:code
et l'action upload
def upload_image(): return dict(form=SQLFORM(db.image).process()) :code
Le moyen le plus simple pour embarquer le formulaire dans la vue pour
upload_image est
{{=form}}
:code
Ceci résulte en un layout standard de table. Si vous souhaitez utiliser un layout différent, vous pouvez casser le formulaire en composants
{{=form.custom.begin}} Name: <div>{{=form.custom.widget.name}}</div> File: <div>{{=form.custom.widget.source}}</div> {{=form.custom.submit}} {{=form.custom.end}} :code
où
form.custom.widget[fieldname] est sérialisé dans le bon widget pour le champ. Si le formulaire est soumis et contient des erreurs, ils sont ajoutés en dessous des widgets, comme d'habitude.
L'exemple de formulaire ci-dessus est montré dans l'image ci-dessous.
[[image http://www.web2py.com/books/default/image/38/en6500.png center 300px]]
Un résultat similaire pourrait avoir été obtenu sans utiliser un formulaire personnalisé :
SQLFORM(...,formstyle='table2cols')
:code
ou dans le cas de formulaires CRUD avec le paramètre suivant :
crud.settings.formstyle='table2cols' :code
D'autres
formstyles possibles sont "table3cols" (le défaut), "divs" et "ul".
Si vous ne souhaitez pas utiliser les widgets sérialisés par web2py, vous pouvez les remplacer par de l'HTML. Il y a quelques variables qui seront utiles pour cela :
-
form.custom.label[fieldname] contient le label pour le champ.
-
form.custom.comment[fieldname] contient le commentaire pour le champ.
-
form.custom.dspval[fieldname] représentation dépendant du form-type et du field-type pour le champ donné.
-
form.custom.inpval[fieldname] valeurs form-type et field-type à utiliser dans le code de champ.
Si votre formulaire a
deletable=True vous devriez aussi insérer
{{=form.custom.delete}}
:code
pour afficher la case à cocher de suppression.
Il est important de suivre les conventions décrites ci-après.
#### Conventions CSS
Les tags dans les formulaires générés par SQLFORM, SQLFORM.factory et CRUD suivrent une convention de nommage stricte CSS qui peut être utilisée pour personnaliser plus profondément des formulaires.
Etant donné une table "mytable" et un champ "myfield" de type "string", c'est rendu par défaut par un
SQLFORM.widgets.string.widget
:code
qui ressemble à :
<input type="text" name="myfield" id="mytable_myfield" class="string" /> :code
Notez que :
- la classe du tag INPUT est le même que le type de champ. C'est très important pour que le code jQuery dans "web2py_ajax.html" fonctionne. Cela assure que vous pouvez seulement avoir des nombres dans les champs "integer" et "double", et ces champs "time", "date" et "datetime" affichent le popup calendrier/datepicker.
- l'id est le nom de la classe plus le nom du champ, joints par un underscore. Ceci vous permet d'uniquement vous référer au champ via, par exemple,
jQuery('#mytable_myfield') et manipuler le stylesheet du champ ou lier les actions associées aux événements du champ (focus, blur, keyup, etc...).
- le nom est, comme vous l'attendiez, le nom du champ.
#### Cacher les erreurs
hideerror:inxx
Occasionnellement, vous pouvez voir désactiver le placement automatique d'erreur et l'affichage des message d'erreurs ailleurs que par défaut. Ce peut être fait facilement.
- Dans le cas de FORM ou SQLFORM, passer
hideerror=True à la méthode
accepts.
- Dans le cas de CRUD, définissez
crud.settings.hideerror=True
Vous pouvez aussi vouloir modifier les vues pour afficher l'erreur (puisqu'elles ne sont plus affichées automatiquement).
Voici un exemple où les erreurs sont affichées au-dessus du formulaire et non dans le formulaire.
{{if form.errors:}} Your submitted form contains the following errors: <ul> {{for fieldname in form.errors:}} <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li> {{pass}} </ul> {{form.errors.clear()}} {{pass}} {{=form}} :code
Les erreurs seront affichées comme dans l'image ci-dessous :
[[image http://www.web2py.com/books/default/image/38/en6600.png center 300px]]
Ce mécanisme marche également pour les formulaires personnalisés.
### Validateurs
validators:inxx
Les validateurs sont des classes utilisées pour valider les champs en entrée (incluant les formulaires générés depuis les tables de base de données).
Avec les formulaires avancés dérivés de SQLFORM, les validateurs crééent les widgets tels que des menus déroulants et recherches d'autres tables.
Voici un exemple d'utilisation d'un validateur avec un
FORM :
INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10)):code
Voici un exemple de comment demander un validateur pour un champ de table :
db.define_table('person', Field('name')) db.person.name.requires = IS_NOT_EMPTY() :code
Les validateurs sont toujours assignés en utilisant l'attribut
requires d'un champ. Un champ peut avoir un simple validateur ou de multiple validateurs. De multiples validateurs sont partie intégrante d'une liste :
db.person.name.requires = [IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.name')] :code
Normalement les validateurs sont appelés automatiquement par la fonction
accepts et
process d'un
FORM ou d'un autre objet helper HTML qui contient un formulaire. Ils sont appelés dans l'ordre où ils sont listés.
On peut également appeler les validateurs explicitement pour un champ :
db.person.name.validate(value)
qui retourne un tuple
(value,error) et
error est
None si aucune valeur ne valide.
Les validateurs pré-construits ont des constructeurs qui prennent un argument optionnel :
IS_NOT_EMPTY(error_message='cannot be empty') :code
error_message vous permet de surcharger le message d'erreur par défaut pour n'importe quel validateur.
Voici un exemple d'un validateur sur une table de base de données :
db.person.name.requires = IS_NOT_EMPTY(error_message='fill this!') :code
où nous avons utilisé l'opérateur de traduction
T pour permettre l'internationalisation. Notez que les messages d'erreur par défaut ne sont pas traduits.
Gardez en tête que les seuls validateurs qui peuvent être utilisés avec les champs de type
list: sont :
-
IS_IN_DB(...,multiple=True)-
IS_IN_SET(...,multiple=True)-
IS_NOT_EMPTY()-
IS_LIST_OF(...)
Le dernier peut être utilisé pour appliquer n'importe quel validateur aux objets individuels dans la liste.
#### Validateurs de format de texte
#####
IS_ALPHANUMERIC
IS_ALPHANUMERIC:inxx
Ce validateur vérifier que la valeur d'un champ contient seulement des caractères dans la plage a-z, A-Z, or 0-9.
requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!') :code
#####
IS_LOWER
IS_LOWER:inxx
Ce validateur ne retourne jamais d'erreur. Il convertit juste la valeur en minuscule.
requires = IS_LOWER() :code
#####
IS_UPPER
IS_UPPER:inxx
Ce validateur ne retourne jamais d'erreur. Il convertit la valeur en majuscule.
requires = IS_UPPER() :code
#####
IS_EMAIL
IS_EMAIL:inxx
vérifie que la valeur de champ ressemble à une adresse mail. Il n'essaie pas d'envoyer d'email pour confirmer.
requires = IS_EMAIL(error_message='invalid email!') :code
#####
IS_MATCH
IS_MATCH:inxx
Ce validateur matche la valeur avec une expression régulière et retourne une erreur s'ils ne correspondent pas.
Voici un exemple d'usage pour valider un code postal US :
requires = IS_MATCH('^\d{5}(-\d{4})?$', error_message='not a zip code'):code
Voici un exemple d'usage pour valider une adresse IPv4 (note: le validateur IS_IPV4 est plus approprié pour cela) :
requires = IS_MATCH('^\d{1,3}(.\d{1,3}){3}$', error_message='not an IP address')
:code
Voici un exemple d'usage pour valider un numéro de téléphone US :
requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$', error_message='not a phone number') :code
Pour plus d'information sur les expressions régulières Python, référez vous à la documentation officielle Python.
IS_MATCH prend un argument optionnel
strict qui est par défaut à
False. Lorsqu'il est défini à
True il matche uniquement le début de la chaîne :
>>> IS_MATCH('a')('ba') ('ba', <lazyT 'invalid expression'>) # no pass >>> IS_MATCH('a',strict=False)('ab') ('a', None) # pass!
IS_MATCH prend un autre argument optionnel
search qui par défaut à
False. Lorsque défini à
True, il utilise la méthode d'expression régulière
search au lieu de la méthode
match pour valider la chaîne.
IS_MATCH('...', extract=True) filtre et extrait seulement la première sous-chaîne correspondante plutôt que la valeur originale.
#####
IS_LENGTH
IS_LENGTH:inxx
Vérifie si la longueur de la valeur du champ est contenue dans les limites données. Fonctionne aussi bien pour les entrées texte que fichier.
Ses arguments sont :
- maxsize : la longueur / taille maximale autorisée (a default = 255)
- minsize : la longueur / taille minimale autorisée
Exemples :
Vérifie si la chaîne texte est plus courte que 33 caractères :
INPUT(_type='text', _name='name', requires=IS_LENGTH(32)):code
Vérifie si la chaîne de mot de passe est plus longue que 5 caractères :
INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))
:code
Vérifie si le fichier uploadé a sa taille comprise entre 1KB et 1MB :
INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024)) :code
Pour tous les types de champs sauf les fichiers, il vérifie la longueur de la valeur. Dans le cas de fichiers, la valeur est un
cookie.FieldStorage, donc il valide la longueur des données dans le fichier, qui est le comportement attendu intuitivement.
#####
IS_URL
IS_URL:inxx
Rejette une chaîne URL si l'une des conditions suivantes est vraie :
- La chaîne est vide ou None
- La chaîne utilise des caractères non autorisés dans une URL
- La chaîne casse n'importe quelle règle syntaxique HTTP
- Le schéma URL spécifié (si un schéma l'est), n'est pas 'http' ou 'https'
- Le domaine de plus haut niveau (si un nom d'hôte est spécifié) n'existe pas
(Ces règles sont basées sur les RFC 2616
RFC2616:cite )
Cette fonction vérifie uniquement la syntaxe de l'URL. Elle ne vérifie pas que l'URL pointe sur un réel document, par exemple, ou cela prend autrement un sens sémantique. Cette fonction ajoute automatiquement 'http://' devant l'URL dans le cas d'URL abrégée (e.g. 'google.ca').
Si le paramètre mode='generic' est utilisé, alors le comportement de cette fonction change. Il rejette alors une chaîne d'URL si aucun des suivants n'est vrai :
- La chaîne est vide ou None
- La chaîne utilise les caractères qui ne sont pas autorisés dans une URL
- Le schéma URL spécifié (si l'un est spécifié) n'est pas valide
(Ces règles sont basées sur les RFC 2396
RFC2396:cite )
La liste des schémas autorisés est personnalisable avec le paramètre allowed_schemes. Si vous excluez None de la liste, alors les URLs abrégées (n'ayant pas de schéma tel que 'http') seront rejetées.
Le schéma ajouté par défaut est personnalisable avec le paramètre prepend_scheme. Si vous définissez prepend_scheme à None, alors l'ajout sera désactivé. Les URLs qui nécessitent un parsing seront toujours acceptées, mais la valeur de retour ne sera pas modifiée.
IS_URL est compatible avec le standard Internationalized Domain Name (IDN) spécifié dans la RFC 3490
RFC3490:cite . Comme résultat, les URLs peuvent être des chaînes régulières ou des chaînes unicode.
Si le domaine de l'URL (e.g. google.ca) contient des lettres non-US-ASCII, alors le domaine sera convertu en Punycode (défini dans la RFC 3492
RFC3492:cite ). IS_URL va un peu plus loin dans les standards, et autorise les caractères non-US-ASCII à être présents dans le chemin et les composants de la requête de l'URL également. Ces caractères non-US-ASCII seront encodés.
Par exemple, l'espace sera encode comme '%20'. Le caractère unicode avec un code hexadécimal 0x4e86 deviendra '%4e%86'.
Exemples :
requires = IS_URL()) requires = IS_URL(mode='generic') requires = IS_URL(allowed_schemes=['https']) requires = IS_URL(prepend_scheme='https') requires = IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https') :code
#####
IS_SLUG
IS_SLUG:inxx
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug') :code
Si
check est défini à
True il vérifie si la valeur validée est un slug (autorisant seulement les caractères alphanumériques et les tirets non répétés).
Si
check est défini à
False (défaut) il convertit la valeur de l'entrée en slug.
#### Validateurs de date et de temps
#####
IS_TIME
IS_TIME:inxx
Ce validateur vérifie que la valeur d'un champ contienne un temps valide dans le format spécifié.
requires = IS_TIME(error_message='must be HH:MM:SS!') :code
#####
IS_DATE
IS_DATE:inxx
Ce validateur vérifie que la valeur d'un champ contienne une date valide dans le format spécifié. Il est de bonne pratique de spécifier le format en utilisant l'opérateur de traduction, afin de supporter plusieurs formats dans différentes locales.
requires = IS_DATE(format=T('%Y-%m-%d'), error_message='must be YYYY-MM-DD!') :code
Pour la description complète sur les directives %, regardez dans le validateur IS_DATETIME.
#####
IS_DATETIME
IS_DATETIME:inxx
Ce validateur vérifier que la valeur d'un champ contienne un datetime valide dans le format spécifié. Il est de bonne pratique de spécifier le format en utilisant l'opérateur de traduction, afin de supporter différents formats dans différentes locales.
requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'), error_message='must be YYYY-MM-DD HH:MM:SS!'):code
Les symboles suivants peut être utilisés pour une chaîne de format (montre le symbole et une chaîne exemple) :
%Y '1963' %y '63' %d '28' %m '08' %b 'Aug' %b 'August' %H '14' %I '02' %p 'PM' %M '30' %S '59' :code
#####
IS_DATE_IN_RANGE
IS_DATE_IN_RANGE:inxx
Fonctionne quasiment à l'identique du validateur précédent mais permet de spécifier une plage :
requires = IS_DATE_IN_RANGE(format=T('%Y-%m-%d'), minimum=datetime.date(2008,1,1), maximum=datetime.date(2009,12,31), error_message='must be YYYY-MM-DD!') :code
#####
IS_DATETIME_IN_RANGE
IS_DATETIME_IN_RANGE:inxx
Fonctionne quasiment à l'identique du validateur précédent mais permet de spécifier une place :
requires = IS_DATETIME_IN_RANGE(format=T('%Y-%m-%d %H:%M:%S'), minimum=datetime.datetime(2008,1,1,10,30), maximum=datetime.datetime(2009,12,31,11,45), error_message='must be YYYY-MM-DD HH:MM::SS!') :code
Pour la description complète sur les directives %, regardez dans le validateur IS_DATETIME.
#### Plage, ensemble et validateurs d'égalité
#####
IS_EQUAL_TO
IS_EQUAL_TO:inxx
vérifie si la valeur validée est égale à une valeur donnée (qui peut être une variable) :
requires = IS_EQUAL_TO(request.vars.password, error_message='passwords do not match') :code
#####
IS_NOT_EMPTY
IS_NOT_EMPTY:inxx
Ce validateur vérifie que le contenu de la valeur du champ ne soit pas une chaîne vide.
requires = IS_NOT_EMPTY(error_message='cannot be empty!') :code
#####
IS_NULL_OR
IS_NULL_OR:inxx
Déprécié, un alias pour
IS_EMPTY_OR décrit ci-après.
#####
IS_EMPTY_OR
IS_EMPTY_OR:inxx
Parfois vous avez besoin d'autoriser des valeurs vides sur un champ selon d'autres pré-requis. Par exemple, un champ peut être une date mais il peut aussi être vide.
Le validateur
IS_EMPTY_OR permet ceci :
requires = IS_EMPTY_OR(IS_DATE()) :code
#####
IS_EXPR
IS_EXPR:inxx
Son premier argument est une chaîne contenant une expression logique en termes de valeur variable. Il valide une valeur de champ si l'expression évalue à
True. Par exemple :
requires = IS_EXPR('int(value)%3==0', error_message='not divisible by 3'):code
Il faudrait d'abord vérifier que la valeur soit un entier afin qu'une excteption n'arrive pas.
requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')] :code
#####
IS_DECIMAL_IN_RANGE
IS_DECIMAL_IN_RANGE:inxx
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10, dot=".")) :code
Il convertir l'entrée en décimal Python ou génère une erreur si le décimal ne tombe pas dans la plage inclusive spécifiée.
La comparaison est faite avec l'arithmétique Python Decimal.
Les limites minimum et maximum ne peuvent être None, signifiant qu'il n'y a pas de limite basse ou haute, respectivement.
L'argument
dot est optionnel et vous permet d'internationaliser le symbole utilisé pour séparer les décimales.
#####
IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE:inxx
Vérifie que la valeur de champ est un nombre flottant dans une plage définie,
0 <= value <= 100 dans l'exemple suivant :
requires = IS_FLOAT_IN_RANGE(0, 100, dot=".", error_message='too small or too large!') :code
L'argument
dot est optionnel et vous permet d'internationaliser le symbole utilisé pour séparer les décimales.
#####
IS_INT_IN_RANGE
IS_INT_IN_RANGE:inxx
Vérifie que la valeur de champ soit un nombre entier dans la plage définie,
0 <= value < 100 dans l'exemple suivant :
requires = IS_INT_IN_RANGE(0, 100, error_message='too small or too large!') :code
#####
IS_IN_SET
IS_IN_SET:inxx
multiple:inxx
Dans SQLFORM (et les grids) ce validateur définira automatiquement le champ formulaire vers un champ option (i.e., avec un menu déroulant).
IS_IN_SET vérifie que les valeurs de champ sont dans l'ensemble :
requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'), error_message='must be a or b or c') :code
L'argument zéro est optionnel et il détermine le texte de l'option sélectionnée par défaut, une option qui n'est pas acceptée par le validateur
IS_IN_SET lui-même. Si vous ne voulez pas une option "choose one", définissez
zero=None.
Les éléments de l'ensemble peuvent être combinés avec un validateur numérique, tant que IS_IN_SET est le premier dans la liste. Faire cela forcera la conversion par le validateur vers le type numérique. Alors IS_IN_SET peut être suivi par
IS_INT_IN_RANGE (qui convertit la valeur en int) ou
IS_FLOAT_IN_RANGE (qui convertit la valeur en flottant). Par exemple :
requires = [ IS_IN_SET([2, 3, 5, 7],IS_INT_IN_RANGE(0, 8), error_message='must be prime and less than 10')]:code
[[checkbox_validation]]
###### Validation de case à cocher
Pour forcer une case à cocher de formulaire à être remplie (telle que l'acceptation de termes et conditions), utilisez ceci :
requires=IS_IN_SET(['on'])
:code
###### Dictionnaires et tuples avec IS_IN_SET
Vous pouvez également utiliser un dictionnaire ou une liste de tuples pour rendre le menu déroulant plus descriptif :
Exemple de dictionnaire :
requires = IS_IN_SET({'A':'Apple','B':'Banana','C':'Cherry'},zero=None)
:code
Exemple de liste de tuples :
requires = IS_IN_SET([('A','Apple'),('B','Banana'),('C','Cherry')]) :code
#####
IS_IN_SET et Tagging
Le validateur
IS_IN_SET a un attribut optionnel
multiple=False. Si défini à True, de multiples valeurs peuvent être stockées dans un champ. Le champ devrait être de type
list:integerou
list:string. Les références
multiple sont gérées automatiquement dans les formulaires de création et de mise à jour, mais ils sont transparents pour la DAL. Nous recommandons fortement d'utiliser les plugins de multi-sélection jQuery pour rendre de multiples champs.
------
Notez que lorsque
multiple=True,
IS_IN_SET acceptera
zero ou plus de valeurs, i.e. il acceptera le champ lorsque rien n'a été sélectionné.
multiple peut également être un tuple de la forme
(a,b) où
a et
b sont le nombre minimum et maximum (exclusif) d'objets qui peuvent être sélectionnés respectivement.
------
#### Complexité et validateurs de sécurité
#####
IS_STRONG
IS_STRONG:inxx
Force les pré-requis complexes sur un champ (habituellement un champ de mot de passe)
Exemple :
requires = IS_STRONG(min=10, special=2, upper=2) :code
où
- min est la longueur minimum de la valeur
- special est le nombre minimum de caractères spéciaux requis. Les caractères spéciaux sont n'importe lesquels dans
!@#$%^&*(){}[]-+- upper est le nombre minimum de caractères en majuscules
#####
CRYPT
CRYPT:inxx
C'est également un filtre. Il effectue un hash sécurisé sur l'entrée et est utilisé pour empêcher les mots de passe d'être passés en clair à la base de données.
requires = CRYPT():code
Par défaut, CRYPT utilise 1000 itérations sur l'algorithme pbkdf2 combiné avec SHA512 pour produire un hash de 20 octets. Les plus anciennes version de web2py utilisaient "md5" ou HMAC+SHA512 selong si une clé était spécifiée ou non.
Si une clé est spécifiée, CRYPT utilise l'algorithme HMAC. La clé peut contenir un préfixe qui détermine l'algorithme à utiliser avec HMAC, par exemple SHA512 :
requires = CRYPT(key='sha512:thisisthekey')
:code
C'est la syntaxe recommandée. La clé doit être une chaîne unique associée avec la base de données utilisée. La clé ne peut jamais être changée. Si vous perdez la clé, les valeurs précédemment enregistrées deviendront inutiles.
Par défaut, CRYPT utilise un sel aléatoire, de telle sorte que chaque résultat est différent. Pour utiliser une constante comme valeur de sel, spécifiez sa valeur :
requires = CRYPT(salt='mysaltvalue')
:code
Ou, pour ne pas utiliser de sel :
requires = CRYPT(salt=False)
:code
Le calidateur CRYPT hashe son entrée, et ceci le rend quelque peu spécial. Si vous avez besoin de valider un champ de mot de passe avant qu'il soit hashé, vous pouvez utiliser CRYPT dans une liste de validateurs, mais devez vous assurer que c'est le dernier élément de la liste, afin qu'il soit appelé à la fin. Par exemple :
requires = [IS_STRONG(),CRYPT(key='sha512:thisisthekey')] :code
CRYPT prend également un argument
min_length, qui est par défaut à zéro.
Le hash qui en résulte prend la forme de
alg$salt$hash, où
alg est l'algorithme de hash utilisé,
salt est la chaîne de sel (qui peut être vide), et
hash est l'algorithme de sortie. Par conséquent, le hash est auto-identifié, autorisant, par exemple, l'algorithme à être changé sans invalider les hash précédents. La clé, cependant, doit rester la même.
#### Les validateurs de type spécial
#####
IS_LIST_OF
IS_LIST_OF:inxx
Ce n'est réellement un validateur. Son usage initial est de permettre les validations de champs qui retournent des valeurs multiples. C'est utilisé dans ces rares cas où un formulaire contient de multiples champs avec le même nom ou une boite de sélection multiple. On seul argument est un autre validateur, et tout ce qu'il fait est d'appliquer l'autre validateur à chaque élément de la liste. Par exemple, l'expression suivante vérifie que tout objet de la liste soit un entier dans la plage 0-10 :
requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10)) :code
Elle ne retourne jamais d'erreur et ne contient pas de message d'erreur. Le validateur interne contrôle la génération d'erreur.
#####
IS_IMAGE
IS_IMAGE:inxx
Ce validateur vérifie si un fichier uploadé via l'entrée fichier a été sauvée dans l'un des formats d'image sélectionnés et a les dimensions (largeur et hauteur) dans les limites données.
Cela ne vérifie pas la taille maximum pour le fichier (utilisez IS_LENGTH pour cela). Cela retourne un échec de validation si aucune donnée n'est uploadée. Cela supporte les formats de fichier BMP, GIF, JPEG, PNG, et ne nécessite pas de librairie Python d'Imaging.
Les parties de code prises depuis la réf.
source1:cite
Il accepte les arguments suivants :
- extensions : un itérable contenant les extensions d'image autorisées en minuscule
- maxsize : un itérable contenant la largeur et hauteur maximales de l'image
- minsize : un itérable contenant la largeur et hauteur minimales de l'image
Utilisez (-1, -1) comme minsize pour bypasser la vérification de la taille de l'image.
Voici quelques exemples :
- Vérifier si le fichier uploadé est dans l'un des formats d'image supportés :
requires = IS_IMAGE():code
- Vérifie si le fichier uploadé est soit en JPEG ou en PNG :
requires = IS_IMAGE(extensions=('jpeg', 'png'))
:code
- Vérifie si le fichier uploadé est PNG avec une taille maximum de 200x200 pixels :
requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200)) :code
- Note : sur l'affichage d'un formulaire d'édition pour une table incluant
requires = IS_IMAGE(), une case à cocher
delete n'apparaîtra pas car supprimer le fichier causerait un échec de la validation. Pour afficher la case à cocher
delete utilisez la validation :
requires = IS_EMPTY_OR(IS_IMAGE()) :code
#####
IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME:inxx
Ce validateur vérifie si le nom de l'extension d'un fichier uploadé à travers l'entrée de fichier correspondant aux critères donnés.
Ceci n'assure pas le type de fichier en aucun cas. Retourne un échec de validation si aucune donnée n'a été uploadée.
Ses arguments sont :
- filename : regex sur le nom de fichier (avant le point).
- extension : regex sur l'extension (après le point).
- lastdot : quel point devrait être utilisé comme séparateur de nom de fichier / extension :
True indique le dernier point (e.g. "file.tar.gz" sera cassé en "file.tar" + "gz") alors que
False signifie le premier point (e.g., "file.tar.gz" sera cassé en "file" + "tar.gz").
- case : 0 indique de conserver la casse ; 1 indique de transformer la chaîne en minuscule (par défaut) ; 2 indique de transformer la chaîne en majuscule.
S'il n'y a pas de point présent, la vérification de l'extension sera faite sur une chaîne vide et la vérification du nom de fichier sera faite sur la valeur entière.
Exemples :
Vérifie si le fichier a une extension PDF (non sensible à la casse) :
requires = IS_UPLOAD_FILENAME(extension='pdf'):code
Vérifie si le fichier a une extension tar.gz et un nom commençant par backup :
requires = IS_UPLOAD_FILENAME(filename='backup.*', extension='tar.gz', lastdot=False)
:code
Vérifie si le fichier n'a pas d'extension et un nom correspondant à README (sensible à la casse) :
requires = IS_UPLOAD_FILENAME(filename='^README$', extension='^$', case=0) :code
#####
IS_IPV4
IS_IPV4:inxx
Ce validateur vérifie si la valeur d'un champ est une adresse IP version 4 sous forme décimale. Peut être défini pour forcer les adresses dans une certaine plage.
L'expression régulière IPv4 prise depuis ref.
regexlib:cite
Ses arguments sont :
-
minip adresse la plus basse autorisée ; accepte : **str**, e.g., 192.168.0.1; **iterable of numbers**, e.g., [192, 168, 0, 1]; **int**, e.g., 3232235521
-
maxip adresse la plus haute autorisée ; de même que ci-dessus
Ces trois valeurs exemples sont égales, puisque les adresses sont converties en entiers pour la vérification d'inclusion avec la fonction suivante :
number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]:code
Exemples :
Vérifie qu'une adresse IPv4 soit valide :
requires = IS_IPV4()
:code
Vérifie une adresse IPv4 valide dans un réseau privé :
requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255') :code
#### Autres validateurs
#####
CLEANUP
CLEANUP:inxx
Ceci est un filtre. Il n'échoue jamais. Il supprime juste tous les caractères dont les codes décimaux ASCII ne sont pas dans la liste [10, 13, 32-127].
requires = CLEANUP() :code
#### Validateurs de base de données
#####
IS_NOT_IN_DB
IS_NOT_IN_DB:inxx
Synopsis:
IS_NOT_IN_DB(db|set, 'table.field')
Considérons l'exemple suivant :
db.define_table('person', Field('name')) db.person.name.requires = IS_NOT_IN_DB(db, 'person.name') :code
Ceci nécessite que vous insériez une nouvelle personne, que son nom ne soit pas déjà dans la base de données,
db, dans le champ
person.name.
Un ensemble peut être utilisé au lieu de
db.
Comme avec tous les autres validateurs, ce pré-requis est forcé au niveau de l'exécution du formulaire, et non au niveau de la base de données. Ceci signifie qu'il y a une faible probabilité que, si deux visiteurs essaient d'insérer de manière concurrentielle des enregistrements avec le même person.name, ceci résulte en une condition de vitesse et que les deux enregistrements soient acceptés. Il est cependant plus sûr d'informer également la base de données que ce champ devrait avoir une valeur unique :
db.define_table('person', Field('name', unique=True)) db.person.name.requires = IS_NOT_IN_DB(db, 'person.name') :code
Maintenant, si une condition de vitesse arrive, la base de données lève une OperationalError et d'une des deux insertions est rejetée.
Le premier argument de
IS_NOT_IN_DB peut être une connexion à la base de données ou un Set. Dans le dernier cas, vous vérifiriez juste l'ensemble défini par le Set.
Une liste complète d'arguments pour
IS_NOT_IN_DB() est ainsi :
IS_NOT_IN_DB(dbset, field, error_message='value already in database or empty', allowed_override=[], ignore_common_filters=True):code
Le code suivant, par exemple, ne permet pas l'enregistrement de deux personnes avec le même nom dans les 10 jours :
import datetime now = datetime.datetime.today() db.define_table('person', Field('name'), Field('registration_stamp', 'datetime', default=now)) recent = db(db.person.registration_stamp>now-datetime.timedelta(10)) db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name') :code
#####
IS_IN_DB
IS_IN_DB:inxx
Synopsis:
IS_IN_DB(db|set,'table.value_field','%(representing_field)s',zero='choose one')où le troisième et le quatrième arguments sont optionnels.
Considérons les tables suivantes et le pré-requis :
db.define_table('person', Field('name', unique=True)) db.define_table('dog', Field('name'), Field('owner', db.person) db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s', zero=T('choose one')) *ou* db.person.name.requires = IS_IN_DB(db(db.person.id>10), 'person.id', '%(name)s') :code
Il est forcé au niveau du chien les formulaires INSERT/UPDATE/DELETE. Ceci nécessite qu'un
dog.owner soit un id valide dans le champ
person.id dans la base de données
db. Etant donné le validateur, le champ
dog.owner est représenté comme une liste déroulante. Le troisième argument du validateur est une chaîne qui décrit les éléments dans la liste déroulante. Dans l'exemple vous voulez voir la personne
%(name)s au lieu de la personne
%(id)s.
%(...)s est remplacé par la valeur du champ entre parenthèses pour chaque enregistrement.
L'option
zero fonctionne exactement comme pour le validateur
IS_IN_SET.
Le premier argument du validateur peut être une connexion à la base ou un Set de DAL, comme dans
IS_NOT_IN_DB. Ceci peut être utile par exemple lorsque vous souhaitez limiter les enregistrements dans la liste déroulante. Dans cet exemple, nous utilisons
IS_IN_DB dans un contrôleur pour limiter les enregistrements dynamique à chaque appel du contrôleur :
def index(): (...) query = (db.table.field == 'xyz') #in practice 'xyz' would be a variable db.table.field.requires=IS_IN_DB(db(query),....) form=SQLFORM(...) if form.process().accepted: ... (...):code
Si vous voulez que le champ soit validé, mais que vous ne voulez pas une liste déroulante, vous pouvez pousser le validateur dans une liste.
db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')] :code
_and:inxx
Occasionnellement, vous voulez la liste déroulante (donc vous ne voulez pas utiliser la syntaxe de liste ci-dessus) vous voulez utiliser les validateurs additionnels. Pour cet usage, le validateur
IS_IN_DB prend un argument complémentaire
_and qui peut pointer vers une liste d'autres validateurs appliqués si la valeur passe la validation
IS_IN_DB. Par exemple, pour valider tous les propriétaires de chien dans la base qui ne sont pas dans un sous-ensemble :
subset=db(db.person.id>100) db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s', _and=IS_NOT_IN_DB(subset,'person.id')) :code
IS_IN_DB a un argument booléen
distinct qui est par défaut à
False. Lorsque définie à
True il empêche les valeurs répétées dans la liste déroulante.
IS_IN_DB prend également un argument
cache qui fonctionne comme l'argument
cache du select.
#####
IS_IN_DB et Tagging
tags:inxx
multiple:inxx
Le validateur
IS_IN_DB a un attribut optionnel
multiple=False. Si défini à
True des valeurs multiples peuvent être stockées dans un champ. Ce champ devrait être de type
list:reference comme présenté dans le Chapitre 6. Un exemple explicite de tagging est présenté ici. Des références
multiple sont gérées automatiquement dans les formulaires de création et de mise à jour, mais sont transparents pour la DAL. Il est fortement recommandé d'utiliser le plugin multiselect de jQuery pour afficher des champs multiples.
#### Custom validators
custom validator:inxx
Tous les validateurs suivent le prototype ci-dessous :
class sample_validator: def __init__(self, *a, error_message='error'): self.a = a self.e = error_message def __call__(self, value): if validate(value): return (parsed(value), None) return (value, self.e) def formatter(self, value): return format(value) :code
i.e., lorsqu'appelé pour valider une valeur, un validateur retourne un tuple
(x, y). Si
y est
None, alors la valeur a passé la validation et
x contient une valeur parsée. Par exemple, si le validateur a besoin que la valeur soit un entier,
x est converti en
int(value). Si la valeur n'a pas passé la validation, alors
x contient la valeur d'entrée et
y contient un message d'erreur qui explique la validation échouée. Ce message d'erreur est utilisé pour reporter l'erreur dans les formulaires qui ne valident pas.
Le validateur peut aussi contenit une méthode
formatter. Il peut effectuer la conversion opposée à celle que fait
__call__. Par exemple, considérons le code source pour
IS_DATE :
class IS_DATE(object): def __init__(self, format='%Y-%m-%d', error_message='must be YYYY-MM-DD!'): self.format = format self.error_message = error_message def __call__(self, value): try: y, m, d, hh, mm, ss, t0, t1, t2 = time.strptime(value, str(self.format)) value = datetime.date(y, m, d) return (value, None) except: return (value, self.error_message) def formatter(self, value): return value.strftime(str(self.format)) :code
En cas de succès, la méthode
__call__ lit une chaîne date depuis le formulaire et la convertir en un objet datetime.date en utilisant la chaîne de format spécifiée dans le constructeur. L'objet
formatter prend un objet datetime.date et le convertit en une représentation chaîne en utilisant le même format. Le
formatter est appelé automatiquement dans les formulaires, mais vous pouvez aussi l'appeler explicitement pour convertir les objets dans leur propre représentation. Par exemple :
>>> db = DAL() >>> db.define_table('atable', Field('birth', 'date', requires=IS_DATE('%m/%d/%Y'))) >>> id = db.atable.insert(birth=datetime.date(2008, 1, 1)) >>> row = db.atable[id] >>> print db.atable.formatter(row.birth) 01/01/2008 :code
Lorsque de multiples validateurs sont nécessaires (et stockés dans une liste), ils sont exécutés dans l'odre et la sortie de l'un est passée comme entrée du suivant. La chaîne se casse lorsque l'un des validateurs échoue.
Inversement, lorsque l'on appelle la méthode
formatter d'un champ, les formatters des validateurs associés sont aussi chaînés, mais en ordre inverse.
------
Notez que comme alternative pour personnaliser les validateurs, vous pouvez aussi utiliser l'argument
onvalidate de
form.accepts(...),
form.process(...) et
form.validate(...).
------
#### Validateurs avec dépendances
Habituellement les validateurs sont définis une fois pour toute dans les modèles.
Occasionnellement, vous avez besoin de valider un champ et le validateur dépend de la valeur d'un autre champ. Ceci peut être fait de différentes manières. Ce peut être fait dans le modèle ou dans le contrôleur.
Par exemple, voici une page uqi génère un formulaire d'enregistrement qui demande un nom d'utilisateur et un mot de passe deux fois. Aucun de ces champs ne peut être vide, et les deux mots de passe doivent correspondre.
def index(): form = SQLFORM.factory( Field('username', requires=IS_NOT_EMPTY()), Field('password', requires=IS_NOT_EMPTY()), Field('password_again', requires=IS_EQUAL_TO(request.vars.password))) if form.process().accepted: pass # or take some action return dict(form=form)
:code
Le même mécanisme peut être appliqué aux objets FORM et SQLFORM.
### Widgets
Voici une liste des widgets web2py disponibles :
SQLFORM.widgets.string.widget SQLFORM.widgets.text.widget SQLFORM.widgets.password.widget SQLFORM.widgets.integer.widget SQLFORM.widgets.double.widget SQLFORM.widgets.time.widget SQLFORM.widgets.date.widget SQLFORM.widgets.datetime.widget SQLFORM.widgets.upload.widget SQLFORM.widgets.boolean.widget SQLFORM.widgets.options.widget SQLFORM.widgets.multiple.widget SQLFORM.widgets.radio.widget SQLFORM.widgets.checkboxes.widget SQLFORM.widgets.autocomplete :code
Les dix premiers sont par défaut pour les types de champs correspondants. Le widget "options" est utilisé lorsqu'un champ nécessite
IS_IN_SET ou
IS_IN_DB avec
multiple=False (comportement par défaut). Le widget "multiple" est utilisé lorsqu'un champ nécessite d'être dans
IS_IN_SET ou
IS_IN_DB avec
multiple=True. Les widgets "radio" et "checkboxes" ne sont jamais utilisés par défaut, mais peuvent être définis manuellement. Le widget autocomplete est spécial et présenté dans sa propre section après.
Par exemple, pour avoir un champ "string" représenté par un textarea :
Field('comment', 'string', widget=SQLFORM.widgets.text.widget)
:code
Les widgets peuvent aussi être assignés aux champs ''a posteriori'' :
db.mytable.myfield.widget = SQLFORM.widgets.string.widget
Parfois, les widgets prennent des arguments additionnels et il est nécessaire de spécifier leurs valeurs. Dans ce cas, on peut utiliser
lambda.
db.mytable.myfield.widget = lambda field,value: SQLFORM.widgets.string.widget(field,value,_style='color:blue')
Les widgets sont des constructeurs de helper et leurs deux premiers arguments sont toujours
field et
value. Les autres arguments peuvent inclure les attributs d'un helper normal tels que
_style,
_class, etc. Quelques widgets prennent également des arguments spéciaux. En particulier
SQLFORM.widgets.radio et
SQLFORM.widgets.checkboxes prennent un argument
style (à ne pas confondre avec
_style) qui peut être défini à "table", "ul", ou "divs" afin de matcher le
formstyle du formulaire contenu.
Vous pouvez créer de nouveaux widgets ou étendre ceux existants.
SQLFORM.widgets[type] est une classe et
SQLFORM.widgets[type].widget est une fonction de membre statique de la classe correspondante. Chaque fonction de widget prend deux arguments : l'objet champ et la valeur courante de ce champ. Il retourne une représentation du widget. Comme exemple, le widget string pourrait être recoder comme suit :
def my_string_widget(field, value): return INPUT(_name=field.name, _id="%s_%s" % (field._tablename, field.name), _class=field.type, _value=value, requires=field.requires)
Field('comment', 'string', widget=my_string_widget) :code
Les valeurs d'id et de classe doivent suivre la convention décrite plus loin dans ce chapitre. Un widget peut contenir ses propres validateurs, mais il est de bonne pratique d'associer les validateurs à l'attribut "requires" du champ et de laisser le widget les récupérer d'ici.
#### Autocomplete widget
autocomplete:inxx
Il y a deux usages possibles pour le widget autocomplete : pour auto-compléter un champ qui prend une valeur depuis une liste, ou pour auto-compléter un champ de référence (où la chaîne à auto-compléter est une représentation de la référence qui est implémentée comme un id).
Le premier cas est simple :
db.define_table('category',Field('name')) db.define_table('product',Field('name'),Field('category')) db.product.category.widget = SQLFORM.widgets.autocomplete( request, db.category.name, limitby=(0,10), min_length=2) :code
Où
limitby indique au widget d'afficher pas plus de 10 suggestions à la fois, et
min_length indique au widget d'effectuer un callback Ajax pour rassembler les suggestions seulement après que l'utilisateur ait tapé au moins 2 caractères dans la boite de recherche.
Le second cas est plus complexe :
db.define_table('category',Field('name')) db.define_table('product',Field('name'),Field('category')) db.product.category.widget = SQLFORM.widgets.autocomplete( request, db.category.name, id_field=db.category.id) :code
Dans ce cas, la valeur de
id_field indique au widget que même si la valeur à auto-compléter est un
db.category.name, la valeur à stocker est le
db.category.id correspondant. Un paramètre optionnel est
orderby qui indique au widget comment trier les suggestions (alphabétiquement par défaut).
Ce widget fonctionne via Ajax. Où est le callback Ajax ? Un peu de magie vient avec ce widget. Le callback est une méthode de l'objet widget lui-même. Comment est-il exposé ? Dans web2py, n'importe quelle partie de code peut générer une réponse en levant une exception HTTP. Ce widget exploite la possibilité de la manière suivante : le widget envoie l'appel Ajax à la même URL qui a généré le widget dans un premier temps et pousse un token spécial dans request.vars. Le widget devrait-il être encore instancié, il trouve le token et lève une exception HTTP qui répond à la requête. Tout cela est fait en arrière-plan et caché au développeur.
##
SQLFORM.grid et
SQLFORM.smartgrid
-------
Attention : grid et smartgrid étaient en état expérimental avant web2py version 2.0 et étaient vulnérables à des fuites d'information. Le grid et smartgrid ne sont plus en état expérimental, mais nous ne garantissons toujours pas la retro-compatibilité de la couche de présentation de la grille, seulement de ses APIs.
-------
Ce sont deux objets haut-niveau qui créent ces contrôles complexes CRUD. Ils fournissent une pagination, la possibilité de parcourir, rechercher, trier, créer, mettre à jour et supprimer les enregistrements depuis un simple objet.
Puisque les objets HTML web2py construisent de manière sous-jacente, des objets plus simples, les grids créent des SQLFORMs pour voir, éditer et créer ses lignes. Beaucoup de ces arguments sont passés aux grids par ce SQLFORM. Cela signifie que la documentation pour SQLFORM (et FORM) est pertinente. Par exemple, une grid prend un callback
onvalidation. La logique d'exécution de grid passe cela via la méthode sous-jacente process() du FORM, ce qui signifie que vous devriez consulter la documentation de
onvalidation pour les FORMs.
Comme la grid passe à travers différents états, tel que l'édition d'une ligne, une nouvelle requête est générée. request.args a l'information de la grid dans laquelle il est.
###
SQLFORM.grid
Le plus simple des deux est
SQLFORM.grid. Voici un exemple d'usage :
@auth.requires_login() def manage_users(): grid = SQLFORM.grid(db.auth_user) return locals() :code
qui produit la page suivante :
[[image http://www.web2py.com/books/default/image/38/en6700.png center 480px]]
Le premier argument de
SQLFORM.grid peut être une table ou une requête. L'objet grid fournira l'accès aux enregistrement correspondants à la requête.
Avant que nous rentrions dans la longue liste des arguments de l'objet grid nous avons besoin de comprendre comment il fonctionne. L'objet regarde dans
request.args afin de décider ce qu'il doit faire (parcourir, rechercher, créer, mettre à jour, supprimer, etc...). Chaque bouton créé par l'objet lie la même fonction (
manage_users dans le cas ci-dessus) mais passe des
request.args différents.
#### login requis par défaut pour les mises à jour de données
Par défaut, toutes les URLs générées par la grid sont signées numériquement et vérifiées. Cela signifie que l'on ne peut pas effectuer certaines actions (créer, mettre à jour, supprimer) sans être connecté. Ces restrictions peuvent être relâchées.
def manage_users(): grid = SQLFORM.grid(db.auth_user,user_signature=False) return locals() :code
mais nous ne le recommandons pas.
#### Multiples grids par fonction contrôleur
-----
Etant donné la manière dont grid fonctionne, on peut seulement avoir un grid par fonction contrôleur, à moins qu'ils ne soient embarqués comme composants via
LOAD.
Pour faire fonctionner la grille de recherche par défaut dans plus d'une grid LOADed, il faut utiliser un
formname différent pour chacun d'entre eux.
-----
#### Utiliser requests.args de façon sécurisée
Puisque la fonction contrôleur qui contient la grid peut manipuler elle-même les arguments de l'URL (connus dans web2py comme response.args et response.vars), la grid a besoin de connaître quels sont les arguments qui devraient gérés par la grid et ceux qui ne devraient pas. Voici un exemple de code qui en autorise un à gérer n'importe quelle table :
@auth.requires_login() def manage(): table = request.args(0) if not table in db.tables(): redirect(URL('error')) grid = SQLFORM.grid(db[table],args=request.args[:1]) return locals() :code
L'argument
args de
grid spécifie quels
request.args devraient être passés ou ignorés par le
grid. Dans notre cas,
request.args[:1] est le nom de la table que nous voulons gérer et elle est gérée par la fonction
manage elle-même, et non par
grid. Donc,
args=request.args[:1] indique à la grid de préserver le premier argument de l'URL dans tous les liens qu'il génère, en ajoutant n'importe quel argument spécifique à grid après ce premier argument.
#### Signature SQLFORM.grid
La signature complète pour la grid est la suivante :
SQLFORM.grid( query, fields=None, field_id=None, left=None, headers={}, orderby=None, groupby=None, searchable=True, sortable=True, paginate=20, deletable=True, editable=True, details=True, selectable=None, create=True, csv=True, links=None, links_in_grid=True, upload='<default>', args=[], user_signature=True, maxtextlengths={}, maxtextlength=20, onvalidation=None, oncreate=None, onupdate=None, ondelete=None, sorter_icons=(XML('↑'), XML('↓')), ui = 'web2py', showbuttontext=True, _class="web2py_grid", formname='web2py_grid', search_widget='default', ignore_rw = False, formstyle = 'table3cols', exportclasses = None, formargs={}, createargs={}, editargs={}, viewargs={}, buttons_placement = 'right', links_placement = 'right' ) :code
-
fields est une liste qui doit être récupérée depuis la base de données. Il est également utilisé pour détermine quels champs doivent être montrés dans la vue. Cependant, il ne contrôle pas ce qui est affiché dans le formulaire séparé utilisé pour éditer les lignes. Pour cela, utiliser les attributs readable et writable des champs de la base. Par exemple, dans une grid éditable, supprimez les mises à jour d'un champ comme ceci : avant de créer le SQLFORM.grid, définissez :
db.my_table.a_field.writable = False db.my_table.a_field.readable = False
:code
-
field_id doit être le champ de la table à utiliser comme ID, par exemple
db.mytable.id.
-
left est une expression de jointure gauche optionnelle utilisé pour construire
...select(left=...).
-
headers est un dictionnaire qui mappe 'tablename.fieldname' avec le label d'en-tête correspondant, e.g.
{'auth_user.email' : 'Email Address'}-
orderby est utilisé comme tri par défaut pour les lignes.
-
groupby est utilisé pour grouper l'ensemble. Utilisez la même syntaxe que vous passeriez dans un simple
select(groupby=...).
-
searchable,
sortable,
deletable,
editable,
details,
create déterminent ce qui peut chercher, trier, supprimer, éditer, voir les détails, et créer des nouveaux enregistrements respectivement.
-
selectable peut être utilisé pour appeler une fonction personnalisée sur de multiples enregistrements (une case à cocher sera insérée pour toutes les lignes) e.g.
selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids))):code
ou pour des boutons à action multiple, utilisez une liste de tuples :
selectable = [('button label1',lambda...),('button label2',lambda ...)] :code
-
paginate définit le nombre maximum de lignes par page.
-
csv si défini à true permet de télécharger la grid dans différents formats (plus d'informations après).
-
links est utilisé pour afficher les nouvelles colonnes qui peuvent être des liens vers d'autres pages. L'argument
links doit être une liste de
dict(header='name',body=lambda row: A(...)) où
header est l'en-tête de la nouvelle colonne et
body est une fonction qui prend une ligne et retourne une valeur. Dans l'exemple, la valeur est un helper
A(...).
-
links_in_grid si défini à False, les liens seront seulement affichés dans les "details" et la page "edit" (donc pas sur la grid principale)
-
upload de même que celui de SQLFORM. web2py utilise l'action à cette URL pour télécharger le fichier
-
maxtextlength définit la longueur maximale de texte à afficher pour chaque valeur de champ, dans la vue grid. Cette valeur peut être surchargée pour chaque champ en utilisant
maxtextlengths, un dictionnaire de 'tablename.fieldname':length e.g.
{'auth_user.email' : 50}-
onvalidation,
oncreate,
onupdate et
ondelete sont des fonctions callback. Toutes sauf
ondelete prennent un objet form en entrée, ondelete prend la table et l'id de l'enregistrement
Puisque le formulaire d'édition/création est un SQLFORM qui étend FORM, ces callbacks sont essentiellement utilisés de la même manière que documentés dans les sections pour FORM et SQLFORM.
Voici le squelette de code :
def myonvalidation(form): print "In onvalidation callback" print form.vars form.errors= True #this prevents the submission from completing
...or to add messages to specific elements on the form form.errors.first_name = "Do not name your child after prominent deities" form.errors.last_name = "Last names must start with a letter" response.flash = "I don't like your submission"
def myoncreate(form): print 'create!' print form.vars
def myonupdate(form): print 'update!' print form.vars
def myondelete(table, id): print 'delete!' print table, id :code
onupdate et oncreate sont les mêmes callbacks que celles disponibles dans SQLFORM.process()
-
sorter_icons est une liste de deux chaînes (ou helpers) qui seront utilisés pour représentés les options de tri croissant et décroissant pour chaque champ.
-
ui peut être défini comme égal à 'web2py' et générera des noms de classe sympathiques, qui peut être défini à
jquery-ui et qui générera des noms de jQuery UI sympathique, mais peut aussi être son propre ensemble de noms de classe pour les divers composants de la grid :
ui = dict( widget=, header=, content=, default=, cornerall=, cornertop=, cornerbottom='', button='button', buttontext='buttontext button', buttonadd='icon plus', buttonback='icon leftarrow', buttonexport='icon downarrow', buttondelete='icon trash', buttonedit='icon pen', buttontable='icon rightarrow', buttonview='icon magnifier') :code
-
search_widget permet de surcharger le widget de recherche par défaut et nous nous référons au code source dans "gluon/sqlhtml.py" pour les détails.
-
showbuttontext permet des boutons sans texte (ce ne seront que des icônes)
-
_class est la classe pour le conteneur du grid.
-
exportclasses prend un dictionaire de tuples : par défaut, défini comme
csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'), csv=(ExporterCSV, 'CSV'), xml=(ExporterXML, 'XML'), html=(ExporterHTML, 'HTML'), tsv_with_hidden_cols=(ExporterTSV, 'TSV (Excel compatible, hidden cols)'), tsv=(ExporterTSV, 'TSV (Excel compatible)')) :code
ExporterCSV, ExporterXML, ExporterHTML et ExporterTSV sont tous définis dans gluon/sqlhtml.py. Regardez les pour créer votre propre exporteur. Si vous passer un dict comme
dict(xml=False, html=False) vous désactiverez les formats d'export xml et html.
-
formargs est passé à tous les objets SQLFORM utilisés par le grid, alors que
createargs,
editargs et
viewargs sont passés uniquement aux SQLFORMs spécifiques de création, édition et détails.
-
formname,
ignore_rw et
formstyle sont passés aux objets SQLFORM, utilisé par le grid pour les formulaires de création/mise à jour.
-
buttons_placement et
links_placement prennent tous deux un paramètre ('right', 'left', 'both') qui affecteront le placement des boutons (ou des liens) sur la ligne
------
deletable,
editable et
details sont habituellement des valeurs booléennes mais peuvent être des fonctions qui prennent un objet row et décident d'afficher le bouton correspondant ou non.
-----
#### Champs virtuels dans SQLFORM.grid et smartgrid
Dans les versions de web2py après 2.6, les champs virtuels sont montrés dans les grids comme des champs normaux : soit montré aux côtés de tous les autres champs par défaut, ou en les incluant dans l'argument
fields. Cependant, les champs virtuels ne sont pas triables.
Dans les versions plus anciennes de web2py, montrer les champs virtuels dans une grid nécessite l'usage de l'argument
links. Le support est conservé pour les versions plus récentes. Si la db.t1 a un champ appelé t1.vfield qui est basée sur les valeurs de t1.field1 et t1.field2, faites ceci :
grid = SQLFORM.grid(db.t1, ..., fields = [t1.field1, t1.field2,...], links = [dict(header='Virtual Field 1',body=lamba row:row.vfield),...] )
:code
Dans tous les cas, puisque t1.vfield dépent de t1.field1 et t1.field2, ces champs doivent être présents dans la ligne. Dans l'exemple ci-dessus, c'est garanti en incluant t1.field1 et t1.field2 en argument des champs. De façon alternative, montrer tous les champs fonctionnera également. Vous pouvez supprimer un champ de l'affichage en définissant l'attribut readable à False.
Notez que lors de la définition d'un champ virtuel, la fonction lambda doit qualifier les champs avec le nom de base de données, mais dans l'argument links, ce n'est pas nécessaire.
Donc, pour l'exemple ci-dessus, le champ virtuel peut être défini comme :
db.define_table('t1',Field('field1','string'), Field('field2','string'), Field.Virtual('virtual1', lambda row: row.t1.field1 + row.t1.field2), ...) :code
### SQLFORM.smartgrid
Un
SQLFORM.smartgrid ressemble beaucoup à un
grid, en fait il contient un grid mais est destiné à prendre en entrée non pas une requête mais seulement une table et pour parcourir la dite table les tables de référencement sélectionnées.
Par exemple, considérons la structure de table suivante :
db.define_table('parent',Field('name')) db.define_table('child',Field('name'),Field('parent','reference parent'))
:code
Avec SQLFORM.grid vous pouvez liste tous les parents :
SQLFORM.grid(db.parent)
:code
tous les fils :
SQLFORM.grid(db.child)
:code
et tous les parents et fils d'une table :
SQLFORM.grid(db.parent,left=db.child.on(db.child.parent==db.parent.id))
:code
Avec SQLFORM.smartgrid vous pouvez pousser toutes les données dans un objet qui fait apparaître les deux tables :
@auth.requires_login() def manage(): grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) return locals() :code
ce qui ressemble à :
[[image http://www.web2py.com/books/default/image/38/en6800.png center 480px]]
Notez les liens complémentaires "children". On peut pourrait créer les
links complémentaires en utilisant un
grid régulier mais ils pointeraient vers une action différente. Avec un
smartgrid ils sont créés automatiquement et gérés par le même objet.
Notez également que lorsque vous cliquez sur le lien "children" pour un parent donné on obtient uniquement la liste de fils pour ce parent (et c'est évident) mais notez également si une ligne essaie maintenant d'ajouter un nouveau fils, la valeur parent pour le nouveau fils est automatiquement défini au parent sélectionné (affiché dans les aides à la navigation associées à l'objet). La valeur de ce champ peut être surchargée. Nous pouvons empêcher ceci en le rendant readonly :
@auth.requires_login(): def manage(): db.child.parent.writable = False grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) return locals() :code
Si l'argument
linked_tables n'est pas spécifié, toutes les tables de référencement sont automatiquement liées. Quoi qu'il en soit, pour éviter d'exposer accidentellement les données, il est fortement recommandé de lister explicitement les tables qui devraient être liées.
Le code suivant créé une interface de gestion très puissante pour toutes les tables du système :
@auth.requires_membership('managers'): def manage(): table = request.args(0) or 'auth_user' if not table in db.tables(): redirect(URL('error')) grid = SQLFORM.smartgrid(db[table],args=request.args[:1]) return locals() :code
#### La signature smartgrid
Le
smartgrid prend les mêmes arguments qu'un
grid et quelques uns supplémentaires avec quelques spécifications :
- Le premier argument est une table, non pas une requête
- Il y a un argument complémentaire
constraints qui est un dictionnaire de 'tablename':query qui peut être utilisé pour restreindre plus l'accès aux enregistrements affichés dans le grid 'tablename'.
- Il y a un argument complémentaire
linked_tables qui est une liste de tablenames de tables qui devrait être accessible via le smartgrid.
-
divider permet de spécifier un caractère à utiliser dans les aides à la navigation,
breadcrumbs_class appliquera la classe à l'élément breadcrumb
- Tous les arguments sauf la table,
args,
linked_tables et
user_signatures peuvent être des dictionnaires comme expliqué après.
Considérons la grid précédente :
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) :code
Elle autorise un accès aussi bien à
db.parent et
db.child. Sauf pour les contrôles de navigation, pour chacune des tables, un smarttable n'est rien d'autre qu'une grid. Cela signifie que, dans ce cas, un smartgrid peut créer un grid pour parent et un grid pour le fils. Nous pourrions vouloir passer différents ensemble de paramètres pour chaque grid. Par exemple, différents ensembles de paramètres
searchable.
Alors que pour un grid, nous passerions un booléen :
grid = SQLFORM.grid(db.parent,searchable=True)
:code
Pour un smartgrid nous passerions un dictionnaire de booléens :
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'], searchable= dict(parent=True, child=False)) :code
De cette manière nous avons rendu les parents searchable mais les fils pour chaque parent non searchable (il ne devrait pas y avoir autant de besoin de widget de recherche).
### Contrôle d'accès à grid et smartgrid
grid et
smartgrid ne forcent pas automatiquement le contrôle d'accès comme crud le fait mais vous pouvez l'intégrer avec
auth en utilisant la vérification de permission explicite :
grid = SQLFORM.grid(db.auth_user, editable = auth.has_membership('managers'), deletable = auth.has_membership('managers'))
:code
ou
grid = SQLFORM.grid(db.auth_user, editable = auth.has_permission('edit','auth_user'), deletable = auth.has_permission('delete','auth_user')) :code
### Les pluriels smartgrid
Le
smartgrid est le seul objet dans web2py qui affiche le nom de la table et il a besoin du singulier et du pluriel. Par exemple, un parent peut avoir un "Child" ou plusieurs "Children". Ainsi un objet de table a besoin de connaître son propre nom singulier et pluriel. web2py les devine normalement mais vous pouvez les définir explicitement :
db.define_table('child', ..., singular="Child", plural="Children") :code
ou avec :
singular:inxx
plural:inxx
db.define_table('child', ...) db.child._singular = "Child" db.child._plural = "Children" :code
Ils devraient aussi être internationalisés en utilisant l'opérateur
T.
Les valeurs pluriel et singulier sont alors utilisées par
smartgrid`` pour fournit les noms corrects pour les en-têtes et les liens.