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 objet FORM peut être sérialisé en HTML et est conscient des champs qu'il contient. Un objet FORM 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 de SQLFORM 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

form
accepts
formname

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 et form.errors en plus du formulaire et request.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 :

onvalidation
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 :

image

Voici ce à quoi elle ressemble lors d'une soumission invalide :

image

Voici ce à quoi elle ressemble lors d'une soumission valide :

image

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 le message_onsuccess ci-dessus
  • message_onfailure
  • onfailure: si égal à 'flash' (défaut) et que le formulaire échoue, affichera le message_onfailure ci-dessus
  • next 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 sinon accepts retourne False 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

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 :

image

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.

add_button

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 :

image

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.

fields
labels
La signature pour le constructeur 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).
    showid
    delete_label
    id_label
    submit_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_button définit la valeur du bouton d'envoi. - id_label définit le label de l'enregistrement "id" - L'"id" de l'enregistrement n'est pas montré si showid est défini à False. - fields est 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://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://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 ### CRUDCRUD: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://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 erreurshideerror: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://web2py.com/books/default/image/38/en6600.png center 300px]] Ce mécanisme marche également pour les formulaires personnalisés. ### Validateursvalidators: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 2616RFC2616: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 2396RFC2396: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 3490RFC3490: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 3492RFC3492: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:inxxmultiple: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)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 Taggingtags:inxxmultiple: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 validatorscustom 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 widgetautocomplete: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://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('&#x2191;'), XML('&#x2193;')), 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(...))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://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 smartgridgrid 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.

 top