Chapter 7: Forms and validators

Formuláře a validátory

Existují 4 způsoby, jak ve Web2py vytvářet formuláře:

  • FORM je low-level implementace v souvislosti s ostatními HTML helpery (třídami pro renderování HTML elementů). FORM objekt může být serializován do HTML a spolupracuje s do něj vloženými poli. Dokáže validovat zadané (submitted) hodnoty.
  • SQLFORM je high-level rozhraní (API) k vytváření formulářů pro přidání, aktualizaci a rušení záznamů z existujících databázových tabulek.
  • SQLFORM.factory je abstrakce nad SQLFORM-em za účelem využití jeho výhod i pro případ, kdy nepracujeme s daty z databáze. Generuje tedy formulář velice podobný SQLFORM-u, a to ze zadané definice dat, která je podobná definici tabulky v modelu, ale žádnou skutečnou tabulku nevytváří.
  • CRUD metody. Jejich funkčnost je identická se SQLFORM a jsou založeny na SQLFORM. Poskytují poněkud kompaktnější zápis.

Všechny tyto formuláře se samy modifikují na základě zadávání dat. Pokud vstup neprojde validací, formulář se sám modifikuje přidáním chybových hlášení. Lze zjišťovat jednotlivé zadané údaje a výsledky a hlášení validace.

Do formuláře lze vložit nebo z něj získat jakýkoli HTML kód pomocí helperů.

FORM a SQLFORM jsou helpery a můžete s nimi tedy zacházet velmi podobně jako s DIV helperem. Např. můžete nastavit styl takto:

form = SQLFORM(..)
form['_style'] = 'border:1px solid black'

FORM

form
accepts
formname

Uvažujme pro příklad aplikaci test s následujícím kontrolérem "default.py":

def display_form():
    return dict()

a s odpovídající "default/display_form.html" šablonou (view):

{{extend 'layout.html'}}
<h2>Vstupní formulář</h2>
<form enctype="multipart/form-data"
      action="{{=URL()}}" method="post">
Vaše jméno:
<input name="name" />
<input type="submit" />
</form>
<h2>Zadané údaje</h2>
{{=BEAUTIFY(request.vars)}}

To je běžný HTML formulář, který zjišťuje jméno uživatele. Jakmile zadáte jméno a odešlete data z prohlížeče (stisknete tlačítko submit), formulář předá data a proměnná request.vars.name se zadaným jménem se zobrazí pod formulářem.

Místo přímo pomocí HTML můžete stejný formulář vytvořit za pomoci helperů. To můžete provést buď v akci kontroléru nebo až v šabloně. Web2py zpracovává formuláře v akcích kontroléru, takže je lepší i přímo tam formulář vytvořit.

Kontrolér bude vypadat takto:

def display_form():
   form = FORM('Vaše jméno:', INPUT(_name='name'), INPUT(_type='submit'))
   return dict(form=form)

a odpovídající šablona "default/display_form.html":

{{extend 'layout.html'}}
<h2>Vstupní formulář</h2>
{{=form}}
<h2>Zadané údaje</h2>
{{=BEAUTIFY(request.vars)}}

Kód je ekvivalentní předchozímu, ale formulář je vygenerován příkazem {{=form}}, který serializuje objekt FORM.

Nyní přidáme další vlastnost a sice validaci dat a jejich zpracování.

Změníme kontrolér takto:

def display_form():
    form=FORM('Vaše jméno:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request,session):
        response.flash = 'formulář úspěšně zpracován'
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    else:
        response.flash = 'prosím, vyplň formulář'
    return dict(form=form)

a šablonu "default/display_form.html":

{{extend 'layout.html'}}
<h2>Vstupní formulář</h2>
{{=form}}
<h2>Zadané údaje</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Zpracované údaje</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Chyby ve formuláři</h2>
{{=BEAUTIFY(form.errors)}}

Všimněte si, že:

  • V akci jsme přidali validátor requires=IS_NOT_EMPTY() k poli "name".
  • V akci jsme přidali volání form.accepts(..)
  • Kromě formuláře a request.vars vypisujeme šablonou také form.vars a form.errors.

Všechnu práci udělá metoda accepts objektu form. Zpracuje request.vars podle požadavků (které jsou vyjádřeny pomocí validátorů). accepts uloží proměnné po validaci do form.vars. Jestliže pole nesplňuje validační požadavek, příslušný validátor vrátí chybu a tato chyba je přidána do form.errors. Obojí form.vars i form.errors jsou objekty gluon.storage.Storage podobné objektu request.vars. První obsahuje hodnoty, zpracované ve validaci, například:

form.vars.name = "Max"

Druhý z nich obsahuje chyby:

form.errors.name = "Cannot be empty!"

Úplná signatura metody accepts je následující:

onvalidation
form.accepts(vars, session=None, formname='default',
             keepvalues=False, onvalidation=None,
             dbio=True, hideerror=False):

Význam volitelných parametrů bude vysvětlen v dalším oddíle.

První argument může být request.vars nebo request.get_vars nebo request.post_vars nebo jen request. Poslední varianta je ekvivalentní volání s request.post_vars.

Metoda accepts vrátí True, když je formulář akceptován (úspěšně prošly validace), jinak vrátí False. Formulář není akceptován, když jsou v něm chyby nebo nebyl potvrzen (submitted) (např. když je zobrazen poprvé).

Takto tedy stránka vypadá při prvním zobrazení (po navigaci na adresu (localhost:8000/)test/default/display_form):

image

Takto vypadá po zadání s chybami:

image

A nakonec po potvrzení správných dat:

image

Metody process a validate

Zkratka pro

form.accepts(request.post_vars, session, ...)

je

form.process(...).accepted

Druhá verze nevyžaduje argumenty request a session (i když jako nepovinné je můžete uvést). Od accepts se také liší tím, že návratová hodnota metody process je formulář (objekt formuláře). Vnitřně process volá accepts a předá mu své argumenty. Návratová hodnota accepts se uloží do form.accepted.

Metoda process má navíc parametry, které accepts nemá:

  • message_onsuccess
  • onsuccess: jestliže je nastaveno na 'flash' a data vyhoví validaci, zobrazí hlášení message_onsuccess
  • message_onfailure
  • onfailure: jestliže je nastaveno na 'flash' a selže validace, zobrazí hlášení message_onfailure
  • next udává adresu (url), kam bude uživatel přesměrován po úspěšném zpracování

onsuccess a onfailure mohou místo defaultního nastavení být funkce: lambda form: neco_proved(form).

form.validate(...)

je zkratka pro

form.process(..., dbio=False).accepted

Skrytá (hidden) pole

Když je formulář serializován pomocí {{=form}} a protože předem byla zavolána metoda accepts, bude nyní vypadat takto:

<form enctype="multipart/form-data" action="" method="post">
Vaše jméno:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Všimněte si dvou přidaných skrytých polí: "_formkey" a "_formname". Jejich přidání způsobilo volání metody accepts a hrají dvě různé a důležité role:

  • Skryté pole "_formkey" je jednorázový token (ověřovací řetězec), který Web2py používá, aby se zabránilo opakovanému dvojímu potvrzení formuláře. Hodnota tohoto klíče je generována, když je formulář serializován a je uložena do session. Při potvrzení formuláře se obě hodnoty musí shodovat, a pokud ne, accepts vrátí False bez vrácení chyb stejně jako v případě, že formulář ještě vůbec nebyl potvrzen/odeslán.
  • Skryté pole "_formname" je generováno jako jméno formuláře, ale toto jméno je možné přepsat. Je potřeba pro případ, kdy stránka obsahuje a zpracovává více formulářů.

Dále mohou být zařazena další skrytá pole, pokud byla přidána pomocí FORM(.., hidden=dict(...)).

Použití těchto skrytých polí ve stránkách s několika formuláři současně bude probráno podrobněji později v této kapitole.

Když odešleme pokusný formulář s prázdným polem "name", neprojde validací. Poté se serializuje poněkud odlišně:

<form enctype="multipart/form-data" action="" method="post">
Vaše jméno:
<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>

Všimněte si, že přibyl DIV s css třídou "error". Web2py vloží chybové hlášení do formuláře, aby uživatele upozornilo na nesprávně vyplněný údaj. Metoda accepts po takovém odeslání dat formuláře zjistí, že formulář byl potvrzen/odeslán, zjistí, že pole "name" navyhovělo validaci, a vloží chybové hlášení z validátoru do formuláře.

Od hlavní šablony "layout.html" se očekává, že zpracuje DIVy se třídou "error". Defaultní obsah souboru "layout.html" to řeší jQuery efekty, kdy chybové hlášení vyjede směrem dolů na červeném pozadí. Další podrobnosti můžete zjistit v kapitole 11.

keepvalues

keepvalues

Volitelný argument keepvalues se týká případu, kdy data formuláře pro přidání nového záznamu jsou akceptována a nepřesměrujete na jinou (následující) URL, takže se tentýž formulář zobrazí znovu. Defaultně jsou údaje smazány a zobrazí se prázdné. Jestliže nastavíme keepvalues na True, formulář se předvyplní naposledy zadanými hodnotami. To je užitečné tehdy, když se pomocí formuláře má zadávat více podobných záznamů.

dbio

Jestliže je dbio argument nastaven na False, Web2py neprovede po akceptování formuláře fyzické změny (insert/update) v databázi.

hideerror

Jestliže nastavíte hideerror na True, pak formulář nezobrazí chyby, které byly zjištěny ve validaci. Je pak na vás, abyste nějakým způsobem zpracovali a zobrazili form.errors.

onvalidation

Argument onvalidation může být None nebo to může být funkce, která jako argument vezme objekt formuláře a nevrátí nic. Taková funkce se zavolá (a předá se jí, jak bylo řečeno, jako argument objekt formuláře) ihned poté, kdy proběhla validace (pokud byla úspěšná). Funkci lze použít různými způsoby. Např. můžete provést další kontroly nad formulářem a případně přidat další chyby k těm, které byly zjištěny pomocí standardní validace. Dále je možné vypočíst hodnoty některých polí na základě jiných polí. Může být vyvolána nějaká akce (např. odeslání emailu) před tím, než dojde k aktualizaci záznamu.

Tady je příklad:

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 = 'součin a*b nesmí být záporný'
    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)

Kontrola změny záznamu jiným uživatelem

Když editujetě záznam ve formuláři, je zde jistá, i když velmi nízká možnost, že by jiný uživatel v tu samou chvíli změnil editovaný záznam. Může být proto vhodné zjistit případný konflikt. Lze to udělat takto:

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:
        # jiný změnil - udělej něco
    elif form.accepted:
        # akceptováno, nikdo nezměnil - udělej něco jiného
    else:
        # nic nedělat
    return dict(form=form)

Formuláře a přesměrování (redirect)

Standardní použití formulářů je se sebe-potvrzením (self-submission), takže potvrzené (odeslané) hodnoty údajů se zpracují na stejné adrese (stejnou akcí), která vystavila formulář. Jakmile je formulář akceptován, je sice možné zobrazit tutéž stránku znovu, ale není to obvyklé. (Pokud jsme to tak dělali v příkladech, tak jedině kvůli jednoduchosti ukázky.) Mnohem běžnější je přesměrovat uživatele na "další" ("next") stránku.

Tady to vidíme na příkladu:

def display_form():
    form = FORM('Vaše jméno:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.process().accepted:
        session.flash = 'formulář úspěšně zpracován'
        redirect(URL('next'))
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    else:
        response.flash = 'prosím, vyplň formulář'
    return dict(form=form)

def next():
    return dict()

Jestliže chceme zobrazit flash hlášení až na následující straně (po redirekci), musíme použít session.flash místo response.flash. Web2py tím získá hlášení ze session po redirekci, převede jej do response.flash a zobrazí. Pozor, pro použití přiřazení do session.flash nesmíte potlačovat práci se session pomocí session.forget().

Více formulářů na stránce

Obsah této sekce platí pro FORM i SQLFORM objekty. Je možné zobrazit několik formulářů na stránce, ale musíte umožnit, aby bylo Web2py schopné je rozlišovat. Jestliže jsou vygenerovány pomocí SQLFORM z různých tabulek, dá jim Web2py unikátní jména automaticky. V opačném případě musíte nastavit unikátní jména explicitně. Příklad:

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)

což vytvoří tento výstup:

image

Když uživatel potvrdí prázdný formulář form1, jen form1 zobrazí chyby; když uživatel potvrdí prázdný formulář form2, jen form2 zobrazí chyby.

Sdílení formulářů

Tento oddíl se opět týká i objektu FORM i SQLFORM objects. Co zde probíráme, se nedoporučuje, protože správný postup je vždy používat formuláře, které se samo-potvrzují ve stejné akci (self-submit). Někdy ale nemáte tu možnost, například v případě, když by akce, která vystaví formulář a akce, která jej zpracovává, patřily do dvou různých aplikací. Takové chování byste získali úpravou následujícího postupu:

Vytvořit formulář, který potvrzuje (submits) pomocí jiné akce je možné. Provedete to zadáním URL v atributech objektu FORM nebo SQLFORM. Například:

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 = 'formulář byl zpracován'
    else:
         response.flash = 've formuláři byly chyby'
    return dict()

Všimněte si, že obě stránky, "page_one" i "page_two", používají tentýž form. Definovali jsme ho jen jednou, tak, že jsme ho umístili mimo obě akce, abychom kód dvakrát neopakovali. Zde vidíme, že se úvodní část kódu v kontroléru provede pokaždé, a poté se předá řízení konkrétní akci.

Protože akce "page_one" nevolá process (ani accepts), nemá formulář žádné jméno ani klíč a musíte zadat session=None a nastavit formname=None ve volání metody process, jinak neproběhne validace poté, co jej obdrží druhá akce.

Přidání tlačítek do formuláře

Základní formulář má jediné tlačítko pro Odeslání (Submit). Často je potřeba přidat tlačítko "Zpět", které místo potvrzení formuláře směruje uživatele na jinou stránku.

add_button

Můžete využít metodu add_button:

form.add_button('Zpět', URL('jina_akce'))

Opakovaným voláním můžete přidat více tlačítek. Argumentem metody add_button je value tlačítka (jeho text) a url, kam se přesměruje při stisknutí tlačítka.

Více o manipulaci s formuláři

Jak jsme ukázali v kapitole o šablonách (Views), FORM je HTML helper. S helpery můžeme manipulovat jako s jinými seznamy (lists) nebo slovníky (dictionaries) Pythonu, takže je můžeme sestavovat a měnit jejich obsah dynamicky za běhu.

SQLFORM

Nyní půjdeme dále a přidáme další úroveň spoluprací formuláře s modelem:

db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY(), label="Vaše jméno: "))

Změníme kontrolér takto:

def display_form():
    form = SQLFORM(db.person)
    if form.process().accepted:
        response.flash = 'formulář úspěšně zpracován'
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    else:
        response.flash = 'prosím, vyplň formulář'
    return dict(form=form)

Šablonu (view) ponecháme stejnou.

V novém kontroléru nemusíte sestavovat FORM, protože volání SQLFORM formulář sestaví na základě tabulky db.person, jak ji definoval model. Tento formulář bude po serializaci (voláním {{=form}} v šabloně) vypadat takto:

<form enctype="multipart/form-data" action="" method="post">
  <table>
    <tr id="person_name__row">
       <td><label id="person_name__label"
                  for="person_name">Vaše jméno: </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>

Automaticky sestavený formulář je trochu složitější než ten, který jsme sestavovali dříve, na nižší úrovni. Obsahuje HTML tabulku řádků, z nichž každý má 3 sloupce. V prvním sloupci je label (titulek) pole tabulky z define_table(.., Field(.., label=..)), ve druhém sloupci je INPUT prvek pro zadání pole tabulky (a po potvrzení a neúspěšné validaci případně také chybové hlášení). Třetí sloupec je prázdný a může být zadán ??((pomocí fields v konstruktoru SQLFORM))??.

Všechny HTML entity (tags) ve formuláři mají jména odvozená od jména tabulky a jejích polí. Vzhled a chování formuláře tedy můžete snadno přizpůsobit pomocí CSS nebo JavaScriptu. Podrobněji se tomu věnujeme v kapitole 11.

Podstatnější je, že metoda accepts pro vás nyní dělá mnohem více. Stejně jako v předchozím případě zajistí validaci vstupu, ale navíc, když data validaci vyhoví, provede insert nového záznamu do databáze a uloží přidělené unikátní "id" do form.vars.id.

Objekt SQLFORM také automaticky podporuje pole typu "upload" a ukládá uploadované soubory do adresáře "uploads" (poté, co je přejmenuje, a tak předejte možným konfliktům a útokům, založeným na znalosti jména souboru) a jejich (nová) jména do příslušného pole databáze. Po zpracování ve form.process() je nové jméno souboru dostupné jako form.vars.fieldname (to znamená, že nahradí standardní cgi.FieldStorage objekt, který přijde jako request.vars.fieldname), takže se můžete snadněji na soubor odkazovat ihned poté, co byl nahrán.

SQLFORM zobrazí na stránce

  • "boolean" hodnoty pomocí checkboxů,
  • hodnoty typu "text" pomocí HTML textarea,
  • hodnoty, jejichž validátor vyžaduje, aby byly ze zadané množiny nebo aby byly v tabulce databáze, pomocí drop-boxů,
  • "upload" pole pomocí odkazu, který umožňuje uživateli stáhnout uploadovaný soubor.

Skryje pole typu "blob", protože o nich se předpokládá, že budou ošetřena jinak, jak popíšeme později.

Např. uvažujme tento model:

db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('married', 'boolean'),
    Field('gender', requires=IS_IN_SET(['Muž', 'Žena', 'Nevíme'])),
    Field('profile', 'text'),
    Field('image', 'upload'))

SQLFORM(db.person) vygeneruje následující formulář:

image

Konstruktor SQLFORM umožňuje různá přizpůsobení (customizations), např. zobrazit jenom vybraná pole, změnit popisné texty (labely), přidat hodnoty do volitelného třetího sloupce, nebo změnit formulář pro vložení nového záznamu na formulář pro Update/Delete stávajícího záznamu. Objekt SQLFORM znamená nejspíš největší úsporu času při používání Web2py.

Třída SQLFORM je definovaná v "gluon/sqlhtml.py". Může být snadno rozšířena tím, že (ve vyděděné třídě) přepíšeme její xml metodu, čili metodu, která serializuje objekt do HTML, a tak změníme její HTML reprezentaci.

fields
labels
Úplná signatura SQLFORM konstruktoru je:

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)
  • Pomocí nepovinného druhého parametru změníte formulář pro vložení nového záznamu (Insert) na formulář pro aktualizaci (Update) stávajícího záznamu (více v následujícím oddíle).

showid
delete_label
id_label
submit_button

  • Jestliže nastavíte deletable na True, Update verze formuláře zobrazí checkbox "Check to delete". Text můžete přizpůsobit pomocí delete_label.
  • submit_button určuje value (text) potvrzovacího tlačítka.
  • id_label je popisný text pro "id" záznamu. "id" se vůbec nezobrazí, jestliže zadáte showid=False.
  • fields je nepovinný seznam (list) jmen polí, která chcete zobrazit. Bez jeho zadání se zobrazí všechna pole. Například:
fields = ['name']
  • labels je slovník (dictionary) popisných textů pro jednotlivá pole. Klíčem je jméno pole a hodnotou požadovaný popisný text. Není-li popisný text takto zadán, Web2py vezme popisný text z definice tabulky a když i ten chybí, použije přímo jméno pole (změní první písmeno na velké a nahradí podtržítka mezerami). Například:
labels = {'name': 'Vaše celé jméno:'}
  • col3 je slovník (dictionary) obsahu pro třetí sloupec. Například:
col3 = {'name':A('co je to?',
      _href='http://www.google.com/search?q=define:name')}
  • linkto a upload jsou nepovinné URL adresy na akce kontroléru(ů), které umožňují pracovat s referencemi (s poli, které odkazují jinam). Podrobněji si to ještě popíšeme.
  • readonly. Je-li True, formulář se zobrazí jen pro čtení (readonly).
  • comments, True/False. Řídí, zda třetí sloupec zobrazí comments z definice tabulky.
  • ignore_rw. Normálně create i update formulář nezobrazí pole, označená v definici tabulky jako writable=False, a readonly formulář nezobrazí pole, označená readable=False. Nastavením ignore_rw=True toto chování potlačíte a zobrazí se všechna pole. Používá se to především v appadmin rozhraní, když se zobrazují úplné obsahy tabulek.
  • formstyle
    formstyle určuje styl při serializování formuláře do html. Lze nastavit "table3cols" (default), "table2cols" (jeden řádek pro label a comment, jeden řádek pro input prvek), "ul" (seznam s input prvky), "divs" (div prvky s atributy pro snadné stylování). formstyle může také být funkce, která má parametry (record_id, field_label, field_widget, field_comment) a vrátí TR() objekt.
  • buttons
    je seznam (list) HTML helperů jako INPUT nebo TAG.BUTTON (případně i jiných helperů), které se přidají do DIVu, v němž se nachází tlačítko Submit.

Např. přidejme tlačítko Zpět a přejmenujme Submit na Další (vícestránkový formulář):

buttons = [TAG.button('Zpět', _type="button", _onClick = "parent.location='%s' " % URL(...),
             TAG.button('Další', _type="submit")]

nebo přidáme odkaz na jinou stránku:

buttons = [..., A("Přejít na jinou stránku", _class='btn', _href=URL("default", "anotherpage"))]
  • separator
    separator je oddělovač mezi popisnými texty údajů a input prvky.
  • Nepovinné attributes jsou argumenty, zapsané s úvodním podtržítkem, které chcete předat <form> tagu. Například:
_action = '.'
_method = 'POST'

Dalším atributem je hidden. Předáme-li jako argument hidden slovník (dictionary), z jeho prvků se vytvoří skryté (hidden) prvky INPUT. (viz příklad pro FORM helper v kapitole 5).

form = SQLFORM(...., hidden=...)

to způsobí, že skrytá (hidden) pole formuláře budou předána při potvrzení formuláře - a právě jen to. form.accepts(...) není totiž zamýšlen pro načtení skrytých polí a jejich převedení na form.vars. Je to z důvodu bezpečnosti. Se skrytými poli by mohlo být manipulováno. Proto musíte explicitně provést přiřazení z objektu request do objektu form:

form.vars.a = request.vars.a
form = SQLFORM(..., hidden=dict(a='b'))

SQLFORM a insert/update/delete

Po potvzení formuláře vytvoří SQLFORM nový záznam v databázi db. Máme-li formulář

form=SQLFORM(db.test)
, po potvrzení (a úspěšné validaci) bude id nově založeného záznamu dostupné jako form.vars.id.

delete record

Když předáte jako druhý argument pro SQLFORM konstruktor id záznamu, formulář se z formuláře pro přidání nového záznamu změní na formulář pro editaci. Poté, co je formulář potvrzen, je aktualizován záznam se zadaným id. Předáte-li také argument deletable=True, editační formulář zobrazí navíc checkbox "check to delete" (označ záznam ke zrušení). Pokud jej uživatel zaškrtne, záznam bude (po potvrzení formuláře) zrušen.

Jestliže je uvedený checkbox zaškrtnut a formulář je již potvrzen, form.deleted bude nastaveno na True.

Akci kontroléru můžete upravit tak, že když se na konec URL přidá číslo, jako v tomto příkladu:

/test/default/display_form/2

a když existuje záznam s odpovídajícím id (id=2 v našem příkladu), SQLFORM vygeneruje formulář pro editaci pro záznam, který byl vyžádán uvedeným formátem URL adresy:

def display_form():
    record = db.person(request.args(0)) or redirect(URL('index'))
    form = SQLFORM(db.person, record)
    if form.process().accepted:
        response.flash = 'formulář úspěšně zpracován'
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    return dict(form=form)

Druhý řádek najde záznam, třetí řádek vytvoří editační formulář (lze předat record nebo record.id). Řádek 4 zajistí veškeré zpracování formuláře.

Editační formulář je velmi podobný formuláři pro zadávání nových záznamů. V aktivních HTML prvcích jsou předvyplněny hodnoty z editovaného záznamu.

Editační formulář také obsahuje skrytý provek INPUT s name="id", který slouží k identifikaci záznamu. Totéž id je uloženo pro větší bezpečnost i na straně serveru a když se uživatel pokusí manipulovat s hodnotou pole s id, UPDATE se neprovede a Web2py vystaví chybu SyntaxError s informací, že uživatel nedovoleně manipuloval s formulářem ("user is tampering with form").

Jestliže je pole označeno jako writable=False, nezobrazuje se ve formulářích pro založení nového záznamu, kdežto ve formulářích pro editaci se zobrazí jen ke čtení (readonly). Ale když navíc zadáte pro toto pole (v definici tabulky nebo později) readable=False, nebude se zobrazovat vůbec ani v editačních formulářích.

Můžete ignorovat uvedené atributy readable a writable

form = SQLFORM(..., ignore_rw=True)

a tak se vždy zobrazí všechna pole. Tak se chovají formuláře v kontroléru appadmin.

Formuláře vytvořené s argumentem readonly=True

form = SQLFORM(table, record_id, readonly=True)

vždy ukazují data jen pro čtení a nemohou být potvrzeny - neboli generovat nějaký zápis do databáze a/nebo mít nastaveno form.accepted=True nebo form.deleted=True.

Označení pole writable=False zabrání poli, aby bylo (aktivním) prvkem formuláře a způsobí, že zpracování formuláře nebude brát ohled na případnou hodnotu request.vars.field. Když ale přiřadíte hodnotu do form.vars.field, pak takové pole bude součástí příkazu insert nebo update při zpracování formuláře. To vám umožňuje programově měnit i obsah polí, které z nějakého důvodu nechcete ve formuláři zobrazit.

SQLFORM a HTML

Může nastat situace, že chcete použít SQLFORM kvůli vygenerování formuláře a zpracování potvrzeného formuláře, ale chcete takové úpravy HTML, které není možné zajistit pomocí parameterů SQLFORM objektu. Formulář v tom případě musíte designovat přímo v HTML.

Editujte kontrolér a přidejte novou akci:

def display_manual_form():
    form = SQLFORM(db.person)
    if form.process(session=None, formname='test').accepted:
        response.flash = 'formulář úspěšně zpracován'
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    else:
        response.flash = 'prosím, vyplňte formulář'
    # Poznámka: do šablony nepředáváme instanci formuláře
   return dict()

a formulář do odpovídající šablony "default/display_manual_form.html" zadáme ručně:

{{extend 'layout.html'}}
<form action="#">
  <ul>
    <li>Vaše jméno je: <input name="name" /></li>
  </ul>
  <input type="submit" />
  <input type="hidden" name="_formname" value="test" />
</form>

Všimněte si, že akce nevrací formulář jako jeden z klíčů návratového slovníku, a to proto, že šablona tuto proměnnou nepotřebuje. Šablona obsahuje formulář manuálně vytvořený pomocí HTML. Formulář musí obsahovat skryté pole "_formname" s hodnotou stejnou jako formname, zadané při volání metody accepts nebo process. Web2py používá jméno formuláře (formname) v případě více formulářů na stránce k rozeznání toho, který byl potvrzen. Když je na stránce jediný formulář, může být zadáno formname=None a vynecháno skryté pole v HTML.

form.accepts / process bude v response.vars hledat data, pojmenovaná shodně s poli v databázové tabulce db.person. Takto pojmenovaná pole musíme mít (a máme) v HTML v tomto formátu:

<input name="jednotlive_jmeno_pole" />

Poznamenejme, že v uvedeném příkladu se data z formuláře přenesou v URL adrese. Když to tak nechceme, musíme vynutit použití protokolu POST. Také, chceme-li používat upload pole, musíme formulář nastavit, aby to povoloval. Obojí nastavení vidíme zde:

<form action="#" enctype="multipart/form-data" method="post">

SQLFORM a upload

Pole typu "upload" jsou zvláštní. Vykreslují se (renderují) jako INPUT pole s type="file". Není-li určeno jinak, uploadovaný soubor je streamován za použití vyrovnávacího bufferu a uložen v adresáři "uploads" pod novým bezpečným jménem, které je přiděleno automaticky. Toto nové jméno se pak zapíše do pole typu upload.

Např. máme-li tento model:

db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image', 'upload'))

můžete použít akci "display_form" kontroléru, jak jsme ji uvedli výše.

Když vkládáte nový záznam, formulář umožní hledat (browse for..) soubor. Dejme tomu, že vybereme jpg fotografii. Soubor je při uploadu uložen jako:

applications/test/uploads/person.image.XXXXX.jpg

"XXXXXX" je náhodný identifikátor, který Web2py přidělí.

content-disposition

Defaultně je původní jméno souboru převedeno funkcí b16encode() a použito pro sestavení nového jména souboru. Toto jméno pak obdrží defaultní "download" akce a použije jej pro sestavení hlavičky (content disposition header) tak, aby uživatel znovu získal původní jméno souboru.

Pro uložený soubor je zachována jen přípona. Jedná se o bezpečnostní požadavek, protože jméno souboru může obsahovat speciální znaky, zneužitelné k útoku (directory traversal attacks nebo jiné nebezpečné akce).

Nové jméno souboru je po zpracování ve form.vars.image.

Během editace záznamu v editačním formuláři je dobrá možnost zobrazit odkaz pro download na serveru uloženého souboru, a Web2py to může zajistit.

Předáte-li SQLFORM konstruktoru pomocí argumentu upload adresu URL, Web2py ji (tedy akci na této adrese) použije pro download souboru. Mějme např. v kontroléru tyto akce:

def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record, deletable=True,
                  upload=URL('download'))
   if form.process().accepted:
       response.flash = 'potvrzeno'
   elif form.errors:
       response.flash = 've formuláři jsou chyby'
   return dict(form=form)

def download():
    return response.download(request, db)

Vložme nový záznam pomocí URL adresy:

http://127.0.0.1:8000/test/default/display_form

Uploadujte fotografii, potvrďte formulář, a pak editujte nově vytvořený záznam pomocí adresy:

http://127.0.0.1:8000/test/default/display_form/3

(předpokládáme tady, že jsme právě přidali záznam id=3). Formulář zobrazí preview fotografie:

image

Serializovaný formulář generuje toto HTML:

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

které obsahuje odkaz na stažení souboru dříve uploadovaného na server a checkbox pro možnost zrušení souboru (fotografie) neboli pro jeho odstranění z editovaného záznamu tabulky, neboli pro uložení hodnoty NULL do pole "image".

Proč je tento mechanismus zpřístupněn pomocí separátní akce pro download? Protože možná budete chtít vynutit nějakou autorizaci ve funkci pro download. V kapitole 9 najdete příklad.

Standardně jsou uploadované soubory ukládány do "<aplikace>/uploads", ale můžete zadat jiné umístění:

Field('image', 'upload', uploadfolder='...')

Řada operačních systémů bude zpomalovat s narůstajícím počtem souborů v adresáři. Víte-li, že bude uploadováno více než 1000 souborů, nastavte Web2py, aby je rozdělilo do více podadresářů:

Field('image', 'upload', uploadseparate=True)

Uložení původního jména souboru

Web2py automaticky ukládá původní jméno uploadovaného souboru do nového unikátního jména souboru. Při downloadu je pak původní jméno součástí hlavičky (content-disposition header) HTTP odpovědi. To se provede automaticky bez jakéhokoli programování.

Někdy můžete chtít uložit původní jméno souboru do databázového pole. V tom případě je potřeba upravit model a přidat pole na uložení původního jména:

db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image_filename'),
    Field('image', 'upload'))

a dále je potřeba upravit kontrolér:

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 = 'potvrzeno'
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    return dict(form=form)

Všimněte si, že v SQLFORM nezobrazujeme pole "image_filename". Akce "display_form" přesune původní jméno souboru z request.vars.image do form.vars.image_filename, čímž se nové pole přidá ke zpracování v accepts (nebo process) a bude uloženo do databáze.

autodelete

autodelete

Standardně SQLFORM při rušení záznamu neruší fyzicky uploadované soubory, na které záznam odkazuje. Důvod je, že Web2py neví, zda soubor není případně odkazován odjinud nebo používán k jiným účelům. Jestliže víte, že je bezpečné a vhodné spolu s rušeným záznamem zrušit i soubor, udělejte následující:

db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source', 'upload', autodelete=True))

Atribut autodelete je defaultně False a jestliže ho změníme na True, způsobí to právě automatické zrušení odkazovaného souboru během rušení záznamu.

Odkazy (links) na související záznamy

Nyní předpokládejme případ dvou tabulek propojených pomocí odkazu (cizího klíče). Například:

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')

Osoba má psy a každý ze psů patří některé osobě, jeho pánovi. Pole owner v tabulce psů odkazuje na správného majitele pomocí platného db.person.id a pro zobrazení majitelů na výběr se použije formátování '%(name)s'.

Použijme appadmin rozhraní této aplikace <aplikace>/appadmin a přidejme několik osob a jejich psů.

Když v appadmin rozhraní editujeme existující osobu, editační formulář ukáže odkaz na psy, které tato osoba vlastní. Toto chování si ve vlastním kódu zprovozníme pomocí argumentu linkto formuláře SQLFORM. linkto by mělo vést na další URL adresu (na novou akci), která z SQLFORM převezme dotaz (query string) a vypíše odpovídající záznamy. Tady je příklad:

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)

Výsledná stránka je zde:

image

Je na ní odkaz "dog.owner". Text odkazu lze změnit pomocí argumentu labels formuláře SQLFORM, například:

labels = {'dog.owner': "Psi tohoto majitele"}

Klikem na odkaz jste přesměrováni na:

/test/default/list_records/dog?query=db.dog.owner%3D%3D5

"list_records" je požadovaná akce, která jako request.args(0) obdrží jméno odkazující tabulky a jako request.vars.query dotaz (query string). Tento dotaz na konci URL obsahuje podmínku "dog.owner==5" správně escapovanou (url-encoded, tj. po nahrazení nebezpečných znaků). Escapování (konverze znaků) provádí Web2py automaticky.

Snadno můžete implementovat hodně obecnou akci "list_records" třeba takto (ošetřeno včetně kontroly správného (povoleného) formátu dotazu, ale s tím, že proměnnou db předpokládáme jako pevně danou):

def list_records():
    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)

a k akci přidružená šablona (view) "default/list_records.html":

{{extend 'layout.html'}}
{{=records}}

Když select vrátí Rows objekt (sadu záznamů) a ten je serializována v šabloně, je nejprve konvertován na objekt SQLTABLE (neplést s Table) a pak je serializován do HTML tabulky, v níž každé pole vytvoří sloupec.

Předvyplnění formuláře

Vždy je možné formulář (obvykle částečně) předvyplnit pomocí příkazů:

form.vars.jmenopole = 'hodnota'

Tyto příkazy musíme umístit za deklaraci formuláře (za instanciování objektu z SQLFORM) a před volání metody process nebo accepts, a to bez ohledu na to, zda se údaj ("jmenopole" v našem příkladu) ve formuláři zobrazuje nebo ne.

Přidání dalších formulářových prvků do SQLFORM

Někdy můžete chtít přidat do vytvořeného formuláře další prvky navíc. Třeba budete chtít zobrazit checkbox pro potvrzení, že uživatel souhlasí s podmínkami služby:

form = SQLFORM(db.yourtable)
my_extra_element = TR(LABEL('Souhlasím se závaznými podmínkami'),                       INPUT(_name='agree', value=True, _type='checkbox'))
form[0].insert(-1, my_extra_element)

Proměnnou my_extra_element je potřeba přizpůsobit aktuálně použitému argumentu formstyle. V příkladu jsme předpokládali defaultní formstyle='table3cols'.

Po potvrzení bude form.vars.agree obsahovat status přidaného checkboxu, který můžeme například použít ve funkci onvalidation nebo můžeme rozložit zpracování na validaci a fyzický zápis, jak si ukážeme dále.

SQLFORM bez zápisu do databáze

Někdy může být potřebné generovat formulář pomocí SQLFORM a řádně validovat potvrzená data, ale neprovést automatický Insert/Update/Delete do databáze. Dejme tomu, že po validaci chcete ještě něco udělat, např. určit hodnoty dalších odvozených polí. Také můžete chtít ještě aplikovat dodatečné validace, které pomocí standardních validátorů nejsou možné.

Lze to udělat velmi jednoduše rozdělením:

form = SQLFORM(db.person)
if form.process().accepted:
    response.flash = 'záznam byl přidán'

na:

form = SQLFORM(db.person)
if form.validate():
    ### případná další práce se záznamem ve form.vars
    form.vars.id = db.person.insert(**dict(form.vars))
    response.flash = 'záznam byl přidán'

Podobně pro Update/Delete:

form = SQLFORM(db.person, record)
if form.process().accepted:
    response.flash = 'záznam byl aktualizován'

rozložením na:

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 = 'záznam byl aktualizován'

Pro pole typu "upload" se i při volání process(dbio=False) resp. zkráceného validate() pracuje s upload polem stejně jako při process(dbio=True), takže nově přidělené jméno máte po validaci ve:

form.vars.jmeno_upload_pole

Další typy formulářů

SQLFORM.factory

Je často výhodné sestavit formulář jako kdyby pracoval nad databázovou tabulkou (jak jsme právě popsali), i když skutečnou databázovou tabulku nemáte. Chcete totiž využít přednosti SQLFORM objektu k sestavení standardní a dobře CSS stylovatelné podoby formuláře včetně např. docela sofistikované podpory pro upload.

K tomu slouží SQLFORM.factory. Tady je příklad, kde se sestaví formulář, provedou validace, uploaduje soubor a vše se uloží do 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 = 'potvrzeno'
        session.your_name = form.vars.your_name
        session.your_image = form.vars.your_image
    elif form.errors:
        response.flash = 've formuláři jsou chyby'
    return dict(form=form)

Objekt Field v konstruktoru SQLFORM.factory() je identický s definicí tabulky v modelu a je tedy dokumentován v kapitole o DAL (6.kap.).

Také můžeme použít tuto techniku:

fields = []
fields.append(Field(...))
form=SQLFORM.factory(*fields)

Šablona "default/form_from_factory.html" pak bude v nejjednodušším případě vypadat takto:

{{extend 'layout.html'}}
{{=form}}

SQLFORM.factory vygeneruje formulář s html "id" atributy takovými, jako kdyby pracoval s tabulkou, nazvanou "no_table". Chcete-li změnit toto defaultní jméno tabulky, můžete uvést atribut table_name:

form = SQLFORM.factory(..., table_name='jine_pomocne_jmeno_tabulky')

table_name je potřeba explicitně uvést/změnit, když chcete umístit více než jeden '.factory' formulář na stránce.

Společný formulář pro více tabulek

Často se stane, že data jsou rozdělena do dvou relačně spojených tabulek (např. 'klient' a 'adresa') a vy je chcete zadávat v jediném formuláři. Tady si ukážeme, jak na to:

model:

db.define_table('client',
    Field('name'))
db.define_table('address',
    Field('client', 'reference client',
          writable=False, readable=False),
    Field('street'),
    Field('city'))

kontrolér:

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='Díky za vyplnění..'
    return dict(form=form)

SQLFORM.factory vytvoří jediný formulář z polí obou tabulek a převezme i validátory polí). Po validaci rozdělíme data zpět do dvou insertů, podle tabulek, kam patří.

Bude to ale fungovat jen tehdy, když v tabulkách nebudou duplicitní jména polí. V opačném případě je potřeba např. vytvořit kopii seznamu polí db.xxx.fields a změnit v něm duplicitní jméno před přidáním do SQLFORM.factory(). Také při zápisu do databáze bude potřeba pole jmenovat explicitně.

Potvrzovací formuláře

confirm

Často se mohou hodit formuláře s tlačítkem pro potvrzení. Pokud tlačítko stiskneme, formulář bude potvrzen. Ve formuláři mohou být další volby s odkazy na další akce nebo stránky. Ve Web2py to můžete jednoduše dělat takhle:

form = FORM.confirm('Ano, pokračovat.')
if form.accepted:
    redirect(URL('pokracujeme'))

Všimněme si, že potvrzovací formulář nepotřebuje a nesmí volat .accepts nebo .process, protože to už se dělá interně. Do formuláře můžete přidat další tlačítka s odkazy pomocí slovníku (dictionary) {'value':'link'}:

form = FORM.confirm('Pokračovat', {'Zpět': URL('jina_akce')})
if form.accepted:
    udelej_co_je_potreba()

Formuláře pro editaci slovníku (dictionary)

Představme si např. konfigurační volby, uložené ve slovníku:

config = dict(color='black', language='English')

a chcete jednoduchý formulář, který uživateli umoží volby nastavit. Můžete to udělat takto:

form = SQLFORM.dictform(config)
if form.process().accepted:
    config.update(form.vars)

Formulář zobrazí INPUT pro každý prvek slovníku. Klíče slovníku budou použity jako name i jako popisný text pro INPUT a vstupní hodnoty určí typ INPUT prvku (string, int, double, date, datetime, boolean).

Je to užitečné, ale musíte samozřejmě ošetřit vše potřebné, v tomto případě uchovat konfiguraci trvale. Například můžete uložit config do sessiony.

session.config or dict(color='black', language='English')
form = SQLFORM.dictform(session.config)
if form.process().accepted:
    session.config.update(form.vars)

CRUD

CRUD
crud.create
crud.update
crud.select
crud.search
crud.tables
crud.delete

Create/Read/Update/Delete (CRUD) API (vytvoř/čti/aktualizuj/ruš) je nadstavba SQLFORM. CRUD vytvoří SQLFORM a poněkud zjednoduší kód, protože zahrnuje standardní varianty zpracování do jediné funkce.

Na rozdíl od dosud probíraných prvků se použití CRUD rozhraní tak běžně neuvažuje a není tedy importován. Pokud se pro něj rozhodnete, musíte jej implicitně importovat. Také mu musíte předat odkaz na databázi, se kterou spolupracuje:

from gluon.tools import Crud
crud = Crud(db)

Objekt crud pak poskytuje následující rozhraní (API):

crud.tables
crud.create
crud.read
crud.update
crud.delete
crud.select
.

  • crud.tables() vrátí seznam tabulek, definovaných v databázi.
  • crud.create(db.tablename) vrátí formulář pro vkládání nových záznamů do zadané tabulky.
  • crud.read(db.tablename, id) vrátí readonly (pro čtení) formulář pro zobrazení záznamu dané tabulky.
  • crud.update(db.tablename, id) vrátí editační formulář pro změnu údajů záznamu z dané tabulky s uvedeným id.
  • crud.delete(db.tablename, id) zruší zadaný záznam.
  • crud.select(db.tablename, query) vrátí SQLTABLE objekt z dotazu (query) vyhovujících záznamů (pomocí str() serializovatelný na HTML).
  • crud.search(db.tablename) vrátí dvojici (tuple) (form, records), kde formulář je jednoduchý formulář pro hledání a records jsou záznamy, nalezené podle zadání, jakmile je tento formulář potvrzen.
  • crud() vrátí jedno z předchozího, a to podle aktuálního request.args().

Např. tato akce:

def data():
    return dict(form=crud())

zveřejní všechny tyto URL adresy:

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]

kdežto tato akce:

def create_tablename():
    return dict(form=crud.create(db.tablename))

zveřejní jedinou adresu pro vytváření záznamů v tabulce tablename:

http://.../[app]/[controller]/create_tablename

Podobně tato akce:

def update_tablename():
    return dict(form=crud.update(db.tablename, request.args(0)))

zveřejní jen URL pro aktualizaci zadaného záznamu:

http://.../[app]/[controller]/update_tablename/[id]

... a tak dále.

Chování CRUDu lze přizpůsobit dvěma způsoby: nastavením některých atributů objektu crud nebo předáním parametrů navíc při volání jeho metod.

Nastavení

Následuje přehled atributů CRUDu, jejich defaultních hodnot a významu:

K vynucení autentikace pro všechny crud formuláře:

crud.settings.auth = auth

To si podrobně ještě vysvětlíme v kapitole 9.

Můžete určit kontrolér, který zveřejní akci data, která bude vracet objekt crud:

crud.settings.controller = 'default'

Můžete určit adresy (URL) přesměrování po úspěšném zpracování záznamu. Po přidání ("create"):

crud.settings.create_next = URL('index')

Po aktualizaci ("update"):

crud.settings.update_next = URL('index')

Po zrušení ("delete"):

crud.settings.delete_next = URL('index')

Pro určení adresy (URL), z níž je možné stahovat uploadované soubory:

crud.settings.download_url = URL('download')

Lze určit seznam funkcí, které se provedou po standardní validaci pro crud.create:

crud.settings.create_onvalidation = StorageList()

StorageList je podobný Storage objektu, oba jsou definovány v "gluon/storage.py". Na rozdíl od prvního (s defaultem: None) je jeho default []. Umožňuje následující syntaxi:

crud.settings.create_onvalidation.jmenotabulky.append(lambda form:....)

Podobně dodatečné funkce, které se provedou po validaci při aktualizaci záznamu (pro crud.update):

crud.settings.update_onvalidation = StorageList()

Resp. pro crud.create formulář po zpracování:

crud.settings.create_onaccept = StorageList()

A podobně pro crud.update:

crud.settings.update_onaccept = StorageList()

Po potvrzení editačního (crud.update) formuláře, jestliže uživatel požaduje zrušení záznamu:

crud.settings.update_ondelete = StorageList()

Po dokončení rušení pomocí crud.delete:

crud.settings.delete_onaccept = StorageList()

Lze určitm, zda editační formulář bude mít tlačítko pro rušení záznamu:

crud.settings.update_deletable = True

Lze určit, zda editační formulář bude zobrazovat id:

crud.settings.showid = False

Lze určit, zda formulář pro vkládání záznamu zachová data z předchozího záznamu, zůstane-li nadále zobrazen pro zadání dalšího záznamu:

crud.settings.keepvalues = False

Crud vždy detekuje, zda editovaný záznam byl nebo nebyl v době editace změněn někým jiným. Toto chování je ekvivalentní k tomuto chování normálního formuláře:

form.process(detect_record_change=True)

a je nastaveno pomocí:

crud.settings.detect_record_change = True

takže nastavením na False můžete toto chování potlačit.

Můžete změnit generovaný HTML styl formuláře

crud.settings.formstyle = 'table3cols' # nebo 'table2cols' nebo 'divs' nebo 'ul'

Lze nastavit oddělovač mezi popisnými texty a aktivními prvky:

crud.settings.label_separator = ':'

captcha

Můžete přidat kontrolní kód (captcha) se stejným chováním, jak je vysvětleno pro auth (kapit.9):

crud.settings.create_captcha = None
crud.settings.update_captcha = None
crud.settings.captcha = None

Texty (messages)

Lze přizpůsobit různé texty CRUDu:

crud.messages.submit_button = 'Submit'  # 'Odeslat'

pro text potvrzovacího ("submit") tlačítka.

crud.messages.delete_label = 'Check to delete:'  # 'Zrušit tento záznam'

pro text předvolby rušení ("delete") v editačním formuláři.

crud.messages.record_created = 'Record Created'  # 'Záznam byl přidán'

pro flash (krátkodobě zobrazené) hlášení po úspěšném založení záznamu.

crud.messages.record_updated = 'Record Updated'  # 'Záznam byl aktualizován'

pro flash hlášení po aktualizaci záznamu.

crud.messages.record_deleted = 'Record Deleted'  # 'Záznam byl zrušen'

pro flash hlášení po zrušení záznamu.

crud.messages.update_log = 'Record %(id)s updated'  # 'Záznam %(id)s byl aktualizován'

pro text logování při aktualizaci záznamu.

crud.messages.create_log = 'Record %(id)s created'  # 'Záznam %(id)s byl vytvořen'

pro text logování při založení záznamu.

crud.messages.read_log = 'Record %(id)s read'  # 'Záznam %(id)s byl zobrazen'

pro text logování při čtení záznamu.

crud.messages.delete_log = 'Record %(id)s deleted'  # 'Záznam %(id)s byl zrušen'

pro text logování přizrušení záznamu.

Poznamenejme, že crud.messages je instancí třídy gluon.storage.Message, která je podobná gluon.storage.Storage, ale automaticky překládá své hodnoty, takže není potřeba používat T operátor pro překlad.

Texty logování se použijí jen tehdy, když je CRUD propojen s Auth, jak popisujeme v kapitole 9. Události (events) jsou pak logovány do Auth tabulky "auth_events".

Metody

Chování metod CRUDu lze také přizpůsobit až při jejich volání. Signatury volání jsou:

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)   # vrací dvojici (tuple): form, results = crud.search(..)
  • table je jméno DAL tabulky, se kterou má metoda pracovat.
  • record a record_id určují záznam (id), se kterým se má pracovat.
  • next je URL pro přesměrování po úspěšné akci. Může obsahovat podřetězec "[id]", který bude v tom případě nahrazen skutečným id právě vytvořeného nebo aktualizovaného záznamu.
  • onvalidation odpovídá funkci SQLFORM(..., onvalidation)
  • onaccept je funkce, volaná po zpracování formuláře, před přesměrováním na novou URL adresu.
  • log text logování. Texty logování mají přístupný slovník (dictionary) form.vars, takže lze použít dosazení např. "%(id)s".
  • message je krátkodobě zobrazený (flash) text po úspěšném zpracování.
  • ondelete je voláno místo onaccept, když je záznam zrušen pomocí "update" formuláře.
  • deletable určuje, zda "update" formulář bude umožňovat rušení.
  • query je dotaz (query) pro výběr záznamů.
  • fields je seznam polí pro výsledek výběru záznamů.
  • orderby pořadí pro vyhovující záznamy výběru (viz kapitola 6).
  • limitby omezení počtu vrácených vyhovujících záznamů z výběru (viz kapitola 6).
  • headers je slovník (dictionary) s názvy sloupců.
  • queries je seznam (např. ['equals', 'not equal', 'contains']) s metodami, které budou k dispozici ve volbě search formuláře.
  • query_labels slovník (dictionary), např. query_labels=dict(equals='se shoduje s ..'), který určí jména (popisy) search metod.
  • fields seznam polí, která se nabídnou ve vyhledávacím widgetu.
  • field_labels slovník (dictionary), který mapuje jména polí na popisné texty.
  • zero s defaultem "choose one" ("vyberte.."), je použito jako defaultní volba pro drop-down prvek ve vyhledávacím widgetu.
  • showall nastavte na True, jestliže chcete, aby již první zavolání (navigace na URL akce) vrátila výsledky dotazu.
  • chkall nastavte na True, pokud ve vyhledávacím widgetu chcete mít zaškrtnuty všechny checkboxy (checkboxy všech polí).

Příklady použití:

## předpokládáme db.define_table('person', Field('name'))
def people():
    form = crud.create(db.person, next=URL('index'),
           message=T("záznam byl vytvořen"))
    persons = crud.select(db.person, fields=['name'],
           headers={'person.name': 'Jméno'})
    return dict(form=form, persons=persons)

Jiná akce, která umožní vyhledávat, vytvářet a editovat záznamy ve kterékoli tabulce, když jméno tabulky předáte jako request.args(0) (app/kontroler/akce/jmenotabulky):

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)

Všimněte si řádky table.id.represent=..., která Web2py řekne, aby místo id zobrazilo odkaz na tutéž stránku, v němž předá id jako request.args(1) a tím změní formulář pro založení záznamu na editační formulář.

Sledování verze záznamu

Jak SQLFORM, tak i CRUD poskytují možnost sledovat verzi záznamu:

Když máte tabulku (db.mytable), u níž chcete znát úplnou historii změn, stačí udělat toto:

form = SQLFORM(db.mytable, myrecord).process(onsuccess=auth.archive)
form = crud.update(db.mytable, myrecord, onaccept=auth.archive)

auth.archive vytvoří novou tabulku db.mytable_archive (ke jménu tabulky připojí "_archive") a při každé aktualizaci uloží stav záznamu před aktualizací do této archivní tabulky, spolus s přidaným odkazem archivního záznamu na platný záznam.

"Živý" záznam se vždy aktualizuje (archivuje se jen předešlý stav), takže odkazy (vazby, reference) budou stále v pořádku.

K tomu není potřeba definovat archivní tabulku v modelu. Pokud ale budete chtít do archivní tabulky přistupovat, je potřeba v modelu definici přidat:

db.define_table('mytable_archive',
   Field('current_record', 'reference mytable'),
   db.mytable)

Všimněte si, že archivní tabulka rozšiřuje db.mytable (zachová všechna její pole) a přidá jedno pole, odkaz na platný záznam: current_record.

auth.archive nezajišťuje zápis časové značky (timestamp), ale zajistí ho, když do "živé" tabulky přidáte timestamp údaje. Například:

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),

Není zde vlastně nic specifického na těchto přidaných polích, takže jim můžete dát jakákoli jména. V záznamu jsou tyto údaje vždy vyplněny, a se záznamem jsou tedy zaarchivovány - to se týká každé postupné změny. Jméno archivní tabulky a/nebo jméno pole, které odkazuje na aktuální záznam, je možné změnit takto:

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'))

Uživatelská úprava formulářů

Pro formulář, vytvořený pomocí SQLFORM, SQLFORM.factory nebo CRUDu, je více možností, jak může být renderován v šabloně, a každá z nich nabízí různou úroveň možností přizpůsobení. Např. uvažujme model:

db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source', 'upload'))

a upload akci:

def upload_image():
    return dict(form=SQLFORM(db.image).process())

Nejjednodušší způsob, jak formulář začlenit do šablony, je:

{{=form}}

To vykreslí formulář ve standardním vzhledu tabulky (table layout). Chcete-li jiný vzhled, můžete rozčlenit formulář na komponenty:

{{=form.custom.begin}}
Name: <div>{{=form.custom.widget.name}}</div>
File: <div>{{=form.custom.widget.source}}</div>
{{=form.custom.submit}}
{{=form.custom.end}}

form.custom.widget[jmenopole] bude serializováno pomocí widgetu, který odpovídá typu pole. Když validace zjistí problémy, chyby se zobrazí jako obvykle pod widgety.

Formulář vidíme na tomto obrázku.

image

Argument formstyle umožní určitou změnu vzhledu i bez použití form.custom. Např. podobný vzhled dostaneme takto:

SQLFORM(..., formstyle='table2cols')

nebo u CRUD formuláře nastavením:

crud.settings.formstyle = 'table2cols'

Další možná nastavení formstyle jsou "table3cols" (default), "divs" nebo "ul".

Když nechcete, aby widgety serializovalo Web2py, můžete je nahradit pomocí HTML. Pomohou některé proměnné:

  • form.custom.label[fieldname] obsahuje popisný text (label) zadaného pole.
  • form.custom.comment[fieldname] obsahuje komentář (comment) pole.
  • form.custom.dspval[fieldname] zobrazená hodnota pole, která je závislá na typu ??((stylu))?? formuláře a na typu pole.
  • form.custom.inpval[fieldname] prvek pro editaci pole, opět závislý na typu/stylu formuláře a typu pole.

Jestliže jste pro formulář nastavili deletable=True, přidejte ještě

{{=form.custom.delete}}

tam, kde chcete zobrazit checkbox pro zrušení záznamu.

Je důležité se přizpůsobit konvencím, které nyní popíšeme.

CSS konvence

Značky (tags) ve formulářích, které generuje SQLFORM, SQLFORM.factory nebo CRUD se drží CSS konvence pojmenování, takže je lze využít pro přizpůsobení vzhledu formuláře snadno pochopitelným způsobem.

Mějme tabulku "mojetabulka" a pole "mojepole" typu "string", které se defaultně vykreslí jako

SQLFORM.widgets.string.widget

v HTML to bude vypadat takto:

<input type="text" name="mojepole" id="mojetabulka_mojepole" class="string" />

Všimněte si, že:

  • třída tagu INPUT je stejná, jako typ pole. To je zásadní pro práci jQuery kódu v "web2py_ajax.html". Tento javascript zajistí, že do polí "integer" nebo "double" lze zadávat pouze čísla a že "time", "date" a "datetime" pole zobrazí javascriptové prvky kalendáře nebo volby času.
  • id je složeno ze jména tabulky, podtržítka a jména pole. Umožňuje to (kromě stylování) unikátně identifikovat prvek v javascriptu, např. jQuery('#mojetabulka_mojepole'), a např. přiřazovat javascriptové události (events): focus, blur, keyup, apod.
  • name je totožné se jménem pole.

Skrytí chybových hlášení

hideerror

Někdy můžete chtít, aby bylo potlačeno automatické umístění chybového hlášení a aby chyby byly zobrazeny jinde nebo jinak. Lze to zajistit jednoduše:

  • Pro FORM nebo SQLFORM, zavolejte metodu accepts s argumentem hideerror=True.
  • Pro CRUD nastavte crud.settings.hideerror = True.

Asi také upravíte šablonu, aby vhodným způsobem zobrazila chyby (protože defaultní nastavení je nyní potlačeno).

Tady je příklad, kdy se chyby zobrazí nad formulářem místo v něm:

{{if form.errors:}}
  Vámi potvrzené údaje obsahují tyto chyby:
  <ul>
  {{for fieldname in form.errors:}}
    <li>{{=fieldname}} chyba: {{=form.errors[fieldname]}}</li>
  {{pass}}
  </ul>
  {{form.errors.clear()}}
{{pass}}
{{=form}}

Chyby se pak zobrazí takto:

image

Tento mechanismus bude fungovat i při použití form.custom.

Validátory

validators

Validátory jsou třídy pro validování (ověření správnosti) hodnoty z HTML Input polí.

Příklad použití validátoru u prvku INPUT ve FORM:

INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))

Podobně při definici datového modelu můžeme přiřadit validátor k poli tabulky:

db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_EMPTY()

Validátory vždy připojujeme atributem requires. Pole může mít nejen jeden validátor ale i více. V tom případě je uvedeme v seznamu (list):

db.person.name.requires = [IS_NOT_EMPTY(),
                           IS_NOT_IN_DB(db, 'person.name')]

Normálně jsou validátory volány automaticky funkcemi formuláře accepts/process. Zpracovávají se v pořadí, v jakém jsou uvedeny.

Je také možné volat validátory určitého pole explicitně:

db.person.name.validate(value)

což vrátí dvojici (tuple) (value, error), error je None, jestliže validace projde bez problému.

Vestavěné validátory mají signaturu s volitelným parametrem:

IS_NOT_EMPTY(error_message='musí být uvedeno')

error_message umožňuje přepsat defaultní chybové hlášení validátoru.

Příklad:

db.person.name.requires = IS_NOT_EMPTY(error_message=T('vyplň to!'))

Zde jsme použili operátor T pro překlad (internacionalizaci). Poznamenejme, že defaultní chybová hlášení nejsou do cílového jazyka překládána.

Pro pole typu list: lze použít jedině tyto validátory:

  • IS_IN_DB(...,multiple=True)
  • IS_IN_SET(...,multiple=True)
  • IS_NOT_EMPTY()
  • IS_LIST_OF(...)

Poslední z nich lze použít pro aplikování jakéhokoli validátoru na jednotlivé prvky seznamu.

Validátory textu

IS_ALPHANUMERIC
IS_ALPHANUMERIC

Validátor ověří, že hodnota obsahuje jen znaky a-z, A-Z nebo 0-9.

requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!')   # zadej alfanumerické znaky
IS_LOWER
IS_LOWER

Také tento validátor neohlašuje žádnou chybu. Převede zadanou hodnotu na malá písmena.

requires = IS_LOWER()
IS_UPPER
IS_UPPER

Také tento validátor neohlašuje žádnou chybu. Převede zadanou hodnotu na velká písmena.

requires = IS_LOWER()
IS_EMAIL
IS_EMAIL

Validátor ověří, že hodnota formálně vypadá jako správná emailová adresa.

requires = IS_EMAIL(error_message='invalid email!')   # chybná e-mailová adresa
IS_MATCH
IS_MATCH

Validátor ověří, že zadaná hodnota souhlasí s regulárním výrazem. V příkladu kontrolujeme, že zadaná hodnota je platný americký (US) poštovní kód (zip code):

requires = IS_MATCH('^\d{5}(-\d{4})?$',
         error_message='not a zip code')   # není zip kód

V tomto příkladu validujeme IPv4 adresu (a sice jen jako příklad, protože je vhodnější použít přímo validátor IS_IPV4):

requires = IS_MATCH('^\d{1,3}(.\d{1,3}){3}$',
         error_message='zadejte správnou IP adresu')

Takto můžeme ověřit správnost US telefonního čísla:

requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$',
         error_message='not a phone number')   # není US telefonní číslo

S používáním regulárních výrazů v Pythonu odkazujeme na oficiální dokumentaci.

IS_MATCH má volitelný argument strict, defaultně False. Není-li nastaven na True, porovnává se jen začátek řetězce:

>>> IS_MATCH('a')('ab')
('a', None)                          # projde
>>> IS_MATCH('a', strict=False)('ab')
('a', None)                          # projde
>>> IS_MATCH('a', strict=True)('ab')
('a', 'invalid expression')          # neprojde

IS_MATCH má také volitelný argument search s defaultní hodnotou False. Nastavíme-li jej na True, bude použita místo regex metody match metoda search.

IS_MATCH('...', extract=True) - v tomto případě se nevrátí původní hodnota, ale vrátí se první vyhovující podřetězec.

IS_LENGTH
IS_LENGTH

Ověří, že zadaný počet znaků je v daném rozsahu. Lze použít pro vstup textu i upload souboru.

Argumenty jsou:

  • maxsize: maximální přípustná délka / velikost (default = 255)
  • minsize: minimální vyžadovaná délka / velikost

Příklady: Je zadaný text kratší než 33 znaků?

INPUT(_type='text', _name='name', requires=IS_LENGTH(32))

Je heslo delší než 5 znaků?

INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))

Je uploadovaný soubor velký od 1KB do 1MB?

INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))

Pro jiná pole, než jsou soubory, testuje délku zadané hodnoty. V případě souborů je zadanou hodnotou cookie.FieldStorage, a tím je testována délka (velikost) dat, což je chování, které intuitivně čekáme.

IS_URL
IS_URL

Kontrola URL adresy (zadané pomocí doménového jména nebo IP). Validace neprojde, pokud je pravda kterýkoli z následujících bodů:

  • Řetězec je prázdný nebo není nic zadáno (is None)
  • Řetězec obsahuje v URL nepovolené znaky
  • Řetězec porušuje kterékoli HTTP syntaktické pravidlo
  • Zadané URL schéma (pokud je uvedeno) je jiné než 'http' nebo 'https'
  • Top-level doména (je-li použito doménové jméno) neexistuje

(Tato pravidla jsou založena na RFC 2616[RFC2616] )

Validátor zkonroluje URL syntaxi. Nezjišťuje, zda URL odkazuje na skutečný dokument nebo je tu nějaký sémantický problém. Není-li zadáno schéma, bude automaticky přidáno 'http://' před zadaný text.

Použijeme-li argument mode='generic', chování validátoru se změní. Validace pak neprojde, pokud je pravda kterýkoli z následujících bodů:

  • Řetězec je prázdný nebo není nic zadáno (is None)
  • Řetězec obsahuje v URL nepovolené znaky
  • Zadané URL schéma (pokud je uvedeno) není dovolené

(Tato pravidla jsou založena na RFC 2396[RFC2396] )

Seznam (list) povolených schémat lze přizpůsobit pomocí argumentu allowed_schemes. Pokud v tomto seznamu neuvedete také None, validací neprojdou ani zkrácené adresy, které nezačínají schématem (např. 'http').

Pomocí argumentu prepend_scheme lze zvolit schéma, které se přidá v případě, že žádné není uvedeno. Zadáte-li prepend_scheme=None, schéma se nikdy nepřipojí. URL, kterému chybí schéma, nicméně validací projde. Vrácená hodnota se nezmění proti zadané hodnotě.

IS_URL je kompatibilní s Internationalized Domain Name (IDN) standardem, jak jej specifikuje RFC 3490[RFC3490] ). URL tedy může být ASCII řetězec nebo řetězec unicode. Pokud doménová část URL (např. google.cz) bude obsahovat znaky mimo ASCII, bude doménové jméno zkonvertováno do Punycode (jak jej definuje RFC 3492[RFC3492] ). IS_URL jde poněkud za hranici standardu a dovoluje, aby znaky mimo US-ASCII byly obsaženy i v path složce a ve query složce adresy. Tyto znaky zakóduje, např. místo mezery zapíše %20, nebo místo unicode znaku s hex kódem 0x4e86 zapíše %4e%86.

Příklady:

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')
IS_SLUG
IS_SLUG
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug')    # povolena jsou jen normalizovaná jména s pomlčkami

S argumentem check=True zjistí, zda se jedná o normalizované jméno s pomlčkami (slug), kde jsou dovoleny jen alfanumerické znaky a neopakující se pomlčky.

S argumentem check=False (což je default) konvertuje vstup na slug: nahradí podtržítko a mezeru pomlčkou, odstraní ostatní nevhodné znaky a pak vynechá opakování pomlček.

Validátory Data a Času

IS_TIME
IS_TIME

Validátor ověří, že je zadán platný časový údaj v uvedeném formátu.

requires = IS_TIME(error_message='must be HH:MM:SS!')   # použij formát ...
IS_DATE
IS_DATE

Validátor ověří, že hodnota je platné datum v zadaném formátu. Je vhodné v argumentu format použít operátor pro překlad, čímž můžeme podporovat různé formáty data podle národních zvyklostí.

requires = IS_DATE(format=T('%Y-%m-%d'),
                   error_message='must be YYYY-MM-DD!')   # použij formát YYYY-MM-DD

"%" direktivy popisujeme u IS_DATETIME validátoru.

IS_DATETIME
IS_DATETIME

Validátor ověří, že hodnota je platné datum spolu s časem, v zadaném formátu. Je vhodné v argumentu format použít operátor pro překlad, čímž můžeme podporovat různé formáty data podle národních zvyklostí.

requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'),
                       error_message='must be YYYY-MM-DD HH:MM:SS!')   # použij formát ...

Možné hodnoty operátoru/symbolu "%" si ukážeme na příkladu:

%Y  '1963'
%y  '63'
%d  '28'
%m  '08'
%b  'Aug'
%b  'August'
%H  '14'
%I  '02'
%p  'PM'
%M  '30'
%S  '59'
IS_DATE_IN_RANGE
IS_DATE_IN_RANGE

Pracuje jako předchozí validátor a navíc umožňuje omezit rozsah doby od-do:

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!')   # použij formát ...

"%" direktivy popisujeme u IS_DATETIME validátoru.

IS_DATETIME_IN_RANGE
IS_DATETIME_IN_RANGE

Pracuje jako předchozí validátor a navíc umožňuje omezit rozsah doby od-do:

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!')   # použij formát ...

"%" direktivy popisujeme u IS_DATETIME validátoru.

Validátory pro rozsahu, výběr a porovnání

IS_EQUAL_TO
IS_EQUEL_TO

Validátor ověří, že hodnota je shodná s hodnotou argumentu, což může být proměnná:

requires = IS_EQUAL_TO(request.vars.password,
                       error_message='passwords do not match')   # hesla nesouhlasí
IS_NOT_EMPTY
IS_NOT_EMPTY

Validátor ověří, že hodnota je zadána (je zadán jiný než prázdný řetězec).

requires = IS_NOT_EMPTY(error_message='cannot be empty!')   # musí být zadáno
IS_NULL_OR
IS_NULL_OR

Zastaralý. Alias pro modernější IS_EMPTY_OR popsaný níže.

IS_EMPTY_OR
IS_EMPTY_OR

Někdy můžete potřebovat povolit nevyplněný údaj a zároveň jej chtít validovat, pokud vyplněn bude. Např. můžete chtít, aby pole bylo buď prázdné nebo aby bylo zadáno datum. To umožňuje validátor IS_EMPTY_OR:

requires = IS_EMPTY_OR(IS_DATE())
IS_EXPR
IS_EXPR

Prvním argumentem je řetězec s logickým výrazem nad hodnotou "value". Validace projde, jestliže vyčíslená hodnota výrazu je True. Příklad:

requires = IS_EXPR('int(value)%3==0',
                   error_message='not divisible by 3')   # není dělitelné třemi

Defaultní chybové hlášení je "Invalid expression" ("chybný výraz"). V uvedeném příkladu je vhodné v seznamu validátorů předřadit validátor, který nejprve otestuje, že předaná hodnota je typu Integer. Jinak dojde ve validátoru IS_EXPR() k výjimce.

requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')]
IS_DECIMAL_IN_RANGE
IS_DECIMAL_IN_RANGE
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10, dot="."))

Konvertuje vstup na Python typ Decimal a generuje chybu, není-li výsledek v daném rozsahu. Porovnání se provádí pomocí Python aritmetiky pro Decimal.

Minimum a maximum omezení mohou být None, což znamená, že daný limit není nastaven.

Argument dot je volitelný a umožňuje internacionalizovat znak pro oddělení desetinných míst.

IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE

Validátor ověří, že hodnota je float číslo v zadaném rozsahu od-do, v našem příkladu 0 <= hodnota <= 100:

requires = IS_FLOAT_IN_RANGE(0, 100, dot=".",
         error_message='too small or too large!')   # mimo rozsah 0..100

Volitený argument dot umožňuje internacionalizaci symbolu pro oddělení desetinných míst.

IS_INT_IN_RANGE
IS_INT_IN_RANGE

Validátor ověří, že hodnota je integer číslo v zadaném rozsahu od-do, v našem příkladu 0 <= hodnota <= 100:

requires = IS_INT_IN_RANGE(0, 100,
         error_message='too small or too large!')   # mimo rozsah 0..100
IS_IN_SET
IS_IN_SET
multiple

Validátor ověří, že hodnota je rovna některé ze zadaných hodnot:

requires = IS_IN_SET(['a', 'b', 'c'], zero=T('choose one'),
         error_message='must be a or b or c')        # vyber jednu možnost / musí být vybráno a|b|c

Argument zero je volitelný a uplatní se jako text volby (OPTION) SELECTu, která bude přednastavena. Tuto hodnotu IS_IN_SET validátor neakceptuje. Pokud toto chování chcete potlačit, nastavte zero=None.

Povolené hodnoty musí vždy být řetězce, pokud ovšem nepředřadíte validátor IS_INT_IN_RANGE nebo IS_FLOAT_IN_RANGE, které konvertují vstup na číslo daného typu. Příklad:

requires = [IS_INT_IN_RANGE(0, 8), IS_IN_SET([2, 3, 5, 7],
          error_message='Zadej prvočíslo menší než 10.')]

Validace checkboxu

Pro validaci zaškrtnutí checkboxu lze použít toto:

requires=IS_IN_SET(['on'])

Můžete také použít slovník (dictionary) nebo seznam dvojic (tuples), takže nabízené volby budou více popisné:

# Příklad se slovníkem:
requires = IS_IN_SET({'J':'Jablka', 'T':'Třešně', 'B':'Banány'}, zero=None)
# Příklad se seznamem dvojic:
requires = IS_IN_SET([('J','Jablka'), ('T','Třešně'), ('B','Banány')])
IS_IN_SET a označování (tagging)

Validátor IS_IN_SET má volitelný atribut multiple=False. Pokud jej nastavíme na True, do pole může být uloženo více hodnot. K tomu je potřeba použít pole typu list:integer nebo list:string. Důrazně doporučujeme používat pro výběr možností jQuery multiselect plugin.

Jestliže je nastaveno multiple=True, IS_IN_SET akceptuje žádnou, jednu nebo několik hodnot, neboli validace projde, i když není nic vybráno. multiple můžete také nastavit jako dvojici (tuple) (a, b), kde a a b jsou minimální a maximální (maximum je ovšem zadáno exkluzivně, tj. testuje se po zmenšení o 1) počet voleb, které uživatel musí označit.

Validátory pro hesla

IS_STRONG
IS_STRONG

Ověří, že hodnota pole (typicky hesla) je dostatečně složitá.

Example:

requires = IS_STRONG(min=10, special=2, upper=2)

kde

  • min je minimální počet znaků (délka)
  • special je minimální počet speciálních znaků !@#$%^&*(){}[]-+
  • upper je minimální počet velkých písmen
CRYPT
CRYPT

Provede bezpečný hash vstupu. Používá se, aby hesla nebyla ukládána nezakódovaná do databáze.

requires = CRYPT()

Defaultně CRYPT použije 1000 iterací algoritmu pbkdf2 v kombinaci s SHA512 a vytvoří 20 bytů dlouhý hash. (Starší verze Web2py používaly "md5" nebo HMAC+SHA512 podle toho, zda byl nebo nebyl zadán klíč.)

Je-li použit argument key, CRYPT použije HMAC algoritmus. Argument key může obsahovat prefix, kterým se určí algoritmus, použitý spolu s HMAC, např. SHA512:

requires = CRYPT(key='sha512:tohlejeklic')

To je doporučená syntaxe. Klíč by měl být unikátní pro danou databázi. Nesmíte ho nikdy změnit. Pokud jej ztratíte, dříve hashované hodnoty budou nadále nepoužitelné.

CRYPT defaultně používá náhodnou hodnotu salt, takže každý výsledek je jiný. Chcete-li použít konstantní hodnotu pro salt, zadejte ji:

requires = CRYPT(salt='mojesalthodnota')

Nebo nechcete-li salt použít:

requires = CRYPT(salt=False)

CRYPT validátor hashuje vstup, takže jeho výsledek nelze dále validovat. Chcete-li heslo validovat dříve, než je zahashujete, použijte CRYPT v seznamu validátorů - musíte jej ale volat jako poslední z nich. Například:

requires = [IS_STRONG(),CRYPT(key='sha512:tohlejeklic')]

CRYPT má také argument min_length, default je =0 (nula).

Výsledek má tvar alg$salt$hash, kde alg je použitý hashovací algoritmus, salt je salt řetězec (jak jsme již uvedli, může být také prázdný) a hash je výsledek algoritmu. Z toho plyne, že hash (jako celek) umožňuje identifikaci sebe sama, a to například dovoluje změnit během doby kryptovací algoritmus, aniž bychom tím zneplatnili dříve provedené hashe. Ale klíč, jak jsme již řekli, musí zůstat trvale stejný.

Zvláštní validátory

IS_LIST_OF
IS_LIST_OF

Toto není v pravém slova smyslu validátor. Slouží k tomu, aby bylo možné validovat jednotlivé hodnoty u polí, která vrací několik hodnot. Může být použit v atypických případech, kdy formulář obsahuje více polí stejného jména a nebo pro listbox s možností výběru více položek. Argumentem volání je jiný validátor, a IS_LIST_OF zajistí, že zadaný validátor se postupně aplikuje na všechny prvky seznamu. Např. zde budeme testovat, že v seznamu jsou jen čísla 0-10:

requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))

Tento pseudovalidátor tedy sám nic nekontroluje, nevrací chybu a nemá žádné chybové validační hlášení. Ohlašování chyb řídí vnitřní validátor.

IS_IMAGE
IS_IMAGE

Ověří, že uploadovaný soubor byl uložen v některém formátu pro obrázky a že má rozměry (šířku a výšku) v požadovaném rozsahu.

Validátor nekontroluje velikost souboru (k tomu účelu použijte IS_LENGTH). Vrátí validační chybu, pokud nebyla uploadována žádná data. Podporuje BMP, GIF, JPEG, PNG a nezávisí na knihovně PIL (Python Imaging Library).

[source1]

Validátor má tyto argumenty:

  • extensions: iterable (iterovatelná struktura: seznam (list) nebo vektor (tuple)), se seznamem dovolených přípon souboru (uváděných malými písmeny)
  • maxsize: iterable, resp. dvojice, s maximální šířkou a výškou obrázku
  • minsize: iterable, resp. dvojice, s minimální šířkou a výškou obrázku

Použijte minsize (-1, -1) pro vynechání testu velikosti rozlišení.

Příklady:

  • Ověřit, že uploadovaný soubor je ve kterémkoli z povolených formátů pro obrázky:
requires = IS_IMAGE()
  • Ověřit, že soubor je JPEG nebo PNG:
requires = IS_IMAGE(extensions=('jpeg', 'png'))
  • Ověřit, že byl uploadován PNG o rozměrech maximálně 200x200 pixelů:
requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200))
  • Poznámka: při zobrazení editačního formuláře nad tabulkou, kde je použit validátor requires = IS_IMAGE() se NEZOBRAZÍ checkbox pro zrušení (delete), protože by selhala validace. Aby se checkbox rušení (delete) zobrazil, použijte takovouto validaci:
requires = IS_EMPTY_OR(IS_IMAGE())
IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME

Validátor ověří, že jméno a přípona uploadovaného souboru vyhovují zadaným kritériím.

Nijak nezjišťuje skutečný typ souboru. Validace neprojde, pokud nebyl uploadován žádný soubor.

Argumenty jsou:

  • filename: jméno souboru (před tečkou), regulární výraz.
  • extension: přípona (za tečkou), regulární výraz.
  • lastdot: kterou tečku použít jako oddělovač přípony: True znamená poslední tečku ("file.tar.gz" se rozdělí jako "file.tar" + "gz"), False první tečku ("file" + "tar.gz").
  • case: 0 zachovat velikost písmen; 1 transformovat na malá písmena (default); 2 transformovat na velká písmena.

Chybí-li v názvu tečka, test přípony se provede proti prázdnému řetězci a test jména proti celému textu.

Příklady:

Má soubor příponu pdf, bez ohledu na velikost písmen (case insensitive)?

requires = IS_UPLOAD_FILENAME(extension='pdf')

Má soubor příponu tar.gz a začíná jeho jméno backup....?

requires = IS_UPLOAD_FILENAME(filename='backup.*', extension='tar.gz', lastdot=False)

Jmenuje se soubor README, přičemž velikost písmen je důležitá (case sensitive) a současně nemá žádnou příponu?

requires = IS_UPLOAD_FILENAME(filename='^README$', extension='^$', case=0)
IS_IPV4
IS_IPV4

Validátor ověří, zda hodnota pole je 4-bytová adresa IP, zapsaná pomocí desítkové soustavy. Lze nastavit i ke kontrole, zda se jedná o IP adresu ze zadaného rozsahu adres.

IPv4 regulární výraz byl převzat z ref.[regexlib]

Argumenty jsou:

  • minip nejnižší dovolená adresa; akceptuje: str, např. 192.168.0.1; iterable (seznam nebo vektor) čísel, např. [192, 168, 0, 1]; int, např. 3232235521
  • maxip nejvyšší dovolená adresa (ve stejném formátu)

Všechny tři příklady jsou ekvivalentní vzhledem k převodu na jediné číslo pomocí vah jednotlivých bytů:

number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]

Příklady:

Platná IPv4 adresa?

requires = IS_IPV4()

Platná IPv4 adresa v privátní síti?

requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255')

Ostatní validátory

CLEANUP
CLEANUP

Jedná se o filtr. Nikdy nevrací validační chybu. Z hodnoty odstraní všechny znaky, jejichž ASCII kód není ze seznamu [10, 13, 32-127].

requires = CLEANUP()

Databázové validátory

IS_NOT_IN_DB
IS_NOT_IN_DB
Volání:
IS_NOT_IN_DB(db|set, 'table.field')

Předpokládejme tento příklad:

db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Validátor ověří, že zadané jméno zatím ještě není v databázi db v poli person.name.

Místo db lze použít objekt Set (sada, množina záznamů). Připomeňme, že objekt Set vznikne voláním db s parametrem dotaz (query), tj. db(query); tím ještě nedošlo k fyzickému dotazu do databáze a načtení vyhovujících záznamů.

Stejně jako ostatní validátory pracuje i tento na úrovni formuláře a nikoli na úrovni databáze. Znamená to, že ve spíše hypotetickém případě, kdy dva uživatelé přidají současně osobu stejného jména by se mohlo stát, že validátor projde úspěšně v obou případech, a protože nemáme kontrolu na úrovni databáze, uloží se oba záznamy i přes duplicitu jména. Proto je jistější požadovat i po databázi, že testované pole má mít unikátní hodnoty:

db.define_table('person', Field('name', unique=True))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Nyní pokud by došlo ke zmíněnému hypotetickému případu, druhý Insert způsobí OperationalError a bude revertován.

První argument validátoru IS_NOT_IN_DB může být databázové připojení nebo objekt Set. Použijete-li objekt Set, bude duplicita testována jen vzhledem k zadané množině.

Úplný seznam parametrů validátoru IS_NOT_IN_DB() je následující:

IS_NOT_IN_DB(dbset, field, error_message='value already in database or empty', allowed_override=[],
    ignore_common_filters=True)

Např. tento kód by nedovolil registraci dvou osob se stejným jménem 10 dnů po sobě:

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')
IS_IN_DB
IS_IN_DB

Syntaxe:
IS_IN_DB(db|set, 'table.value_field', '%(zobrazene_pole)s' ,zero='choose one')

třetí a čtvrtý parametr jsou volitelné.

Podobně jako v případě IS_IN_SET validátoru je také možný argument multiple=, pokud typ validovaného pole je list (seznam). Default je False. Nastavení můžeme změnit na True nebo na dvojici (tuple) (min,max), ve druhém případě oemzíme počet hodnot (záznamů), které má uživatel vybrat. Např. multiple=(1,10) vynutí, že uživatel bude muset vybrat alespoň jeden záznam a nejvýše 9 záznamů.

Další volitelné argumenty diskutujeme níže.

Příklad
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('vyber majitele'))
*nebo s objektem Set* (povolíme jen některé majitele)
db.dog.owner.requires = IS_IN_DB(db(db.person.id>10), 'person.id', '%(name)s')

Validátor se uplatní při zpracování formuláře pro zadání údajů o psovi (dog). Kód v tomto příkladu vyžaduje, aby majitel, dog.owner, byl platnou osobou v tabulce osob (aby zvolený cizí klíč dog.owner existoval jako primární klíč person.id v tabulce osob v db. Tento validátor způsobí, že pole dog.owner bude vykresleno jako drop-down list. Třetí argument validátoru je řetězec, který říká, jaký text se má v položkách drop-down listu zobrazit. V příkladu chceme, aby se zobrazovalo snadno pochopitelné jméno %(name)s místo ne právě přívětivého %(id)s. %(...)s se nahradí hodnotou pole, uvedeného v závorce, a to pro každý jednotlivý záznam.

Argument zero pracuje stejně jako u IS_IN_SET validátoru - je to text, který se zobrazí, pokud ještě není vybrána žádná možnost.

První argument může být stejně jako u IS_NOT_IN_DB buď databázové připojení nebo DAL objekt Set. To se může hodit třeba když chcete omezit počet nabídnutých záznamů v drop-down listu. V tomto příkladu nastavíme IS_IN_DB v akci kontroléru, abychom vybrali záznamy dynamicky vždy, když je akce volána:

def index():
    (...)
    query = (db.table.field == 'xyz')  # v praxi bude 'xyz' proměnná, třeba odvozená z request.args
    db.table.field.requires=IS_IN_DB(db(query),....)
    form=SQLFORM(...)
    if form.process().accepted: ...
    (...)

Chcete-li, aby hodnota byla validována, ale zároveň chcete potlačit defaultní přiřazení widgetu (nepoužít drop-down list), uveďte validátor jako prvek seznamu:

db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')]
_and

Jindy naopak můžete chtít drop-down list (takže výše uvedená syntaxe se vám nehodí), ale současně potřebujete aplikovat další validátory. Abyste to nemuseli řešit explicitním přiřazením widgetu, má validátor IS_IN_DB další argument _and, který může přiřazovat jednotlivý (další) validátor nebo seznam (list) validátorů, které se aplikují poté, co úspěšně projde validace podle IS_IN_DB. Např. nechceme aby bylo možné volit majitele psů z určité množiny:

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'))

IS_IN_DB má dále boolean argument distinct s defaultem False. Nastavení True potlačí případné duplicity v drop-down listu.

IS_IN_DB má také argument cache, který pracuje stejně jako cache argument příkazu select.

IS_IN_DB a označování (tagging)
tags
multiple

IS_IN_DB validátor má volitelný argument multiple=False. Nastavíme-li jej na True, vícenásobné hodnoty budou ukládány do jediného pole. Toto pole by mělo být typu list:reference, jak rozebíráme v kapitole 6. Tam najdete příklad pro vícenásobné označování. Pro renderování polí k označení více možností (multiple=True) striktně doporučujeme jQuery multiselect plugin.

Jak definovat další (custom) validátory

custom validator

Všechny validátory jsou vytvořeny podle vzoru:

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)

Vždy, když má validovat vstupní hodnotu, validátor vrátí vektor (tuple) (x, y). Jestliže y je None, znamená to, že hodnota prošla validací úspěšně a x obsahuje parsovanou (korigovanou) hodnotu, např. validátor, který vyžaduje, aby validovaná hodnota byla integer, zkonvertuje x na int(value). V opačném případě, když validace selže, v x zůstane původní vstupní hodnota a y bude obsahovat chybové hlášení, které popisuje problém. Toto hlášení se používá k ohlášení chyby ve formulářích, jejichž validace neprošla.

Validátor může dále obsahovat metodu formatter. Ta musí provádět opačný převod než zajišťuje metoda __call__. Např. se podívejme na zdrojový kód validátoru 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))

V případě úspěšné validace metoda __call__ načte řetězec z formuláře a konvertuje ho na objekt datetime.date, za použití formátu, který zadáme v konstruktoru. Naopak metoda formatter vezme objekt datetime.date a převede ho na řetězec pomocí stejného formátování. formatter se volá uatomaticky ve formulářích, ale můžete ho také zavolat explicitně, když místo základní hodnoty chcete její reprezentaci k zobrazení. Například:

>>> db = DAL()
>>> db.define_table('tabulka',
       Field('birth', 'date', requires=IS_DATE('%d.%m.%Y')))
>>> id = db.tabulka.insert(birth=datetime.date(2008, 1, 1))
>>> row = db.tabulka[id]
>>> print db.tabulka.formatter(row.birth)
01.01.2008

Když je požadováno více validací a validátory jsou tedy uloženy v seznamu (list), vykonávají se v zadaném pořadí a parsovaný výstup z jednoho je předáván jako vstup validátoru následujícímu. Řetězec se přeruší, jestliže některá z validací neprojde.

Když zavoláme formatter metodu pole, formattery validátorů v seznamu opět proběhnou řetězově, jenže v obráceném pořadí.

Připomeňme si, že jako alternativu vlastních (custom) validátorů můžete použít argument onvalidate metod form.accepts(...), form.process(...) a form.validate(...).

Validátory se závislostmi

Nejčastěji validátory závisí jen na jednotlivých polích modelu.

Někdy ale potřebujete validovat pole a validace závisí na hodnotě jiného pole. To lze realizovat několika způsoby. Můžeme to zajistit v modelu nebo v kontroléru.

Například tady máme stránku, která generuje registrační formulář, který se ptá na jméno (username) a dvakrát na heslo (password). Žádné z polí nesmí zůstat prázdné a obě hesla musí souhlasit:

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 # nebo něco uděláme ..
    return dict(form=form)

Widgety

Widgety jsou bloky HTML prvků. Pro prezentaci (renderování) polí jsou ve Web2py k dispozici tyto widgety:

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

Prvních 10 (po "boolean") jsou defaultní widgety pro jim odpovídající typy polí. Widget "options" se použije, když je .requires pole nastaveno na validátor IS_IN_SET nebo IS_IN_DB s defaultním nastavením multiple=False. Widget "multiple" se použije při těch samých validátorech, ale při nastavení multiple=True. "radio" a "checkboxes" widgety nejsou nikde použity defaultně, můžete je ale připojit explicitně. "autocomplete" widget je speciální a věnujeme mu samostatný oddíl.

Např. kdybychom chtěli pro reprezentaci obyčejného pole "string" použít HTML textarea, uděláme toto:

Field('comment', 'string', widget=SQLFORM.widgets.text.widget)

Také widgety můžete polím přiřazovat až dodatečně, dynamicky:

db.mytable.myfield.widget = SQLFORM.widgets.string.widget

Některé widgety vyžadují předat argumenty, typicky hodnotu. V těchto případech je vhodná lambda funkce:

db.mytable.myfield.widget = lambda field,value:     SQLFORM.widgets.string.widget(field, value, _style='color:blue')

Widgety jsou nadstavbou helperů a jejich první dva argumenty jsou vždy field a value. Další argumenty mohou být obvyklé atributy helperů jako _style, _class, apod. Některé widgety mají navíc speciální argumenty. Konkrétně SQLFORM.widgets.radio a SQLFORM.widgets.checkboxes mají argument style (nezaměňovat se _style), který lze zadat "table", "ul" nebo "divs", a který zajistí, aby widget odpovídal stylu formstyle aktuálního formuláře.

Můžete vytvářet nové widgety nebo dědit a rozšířit vlastnosti stávajících widgetů.

SQLFORM.widgets[type] je třída a SQLFORM.widgets[type].widget statická metoda této třídy. Každá widget metoda má dva parametry: objekt pole (field) a aktuální hodnotu pole. Vrátí HTML reprezentaci pole. Například string widget můžeme pozměnit takto:

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)

Hodnoty id a class by měly dodržovat konvence, popsané v této kapitole. Widgety mohou obsahovat vlastní validátory, ale doporučený postup je asociovat validátory s atributem "requires" některého pole a vytvořit widget tak, aby je od nastavení pole převzal.

Autocomplete widget

autocomplete

Autocomplete widget má dvě použití: k dokončení zápisu pole, které vybírá hodnotu ze seznamu, a k dokončení výběru odkazu (reference), kde dokončovaný text je reprezentace odkazu (tedy text, zobrazený místo id).

První případ je snadný:

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)

Kde limitby nastavuje widget, aby nezobrazoval více než 10 návrhů současně, a min_length nastavuje, aby se Ajax callbacky prováděly teprve poté, kdy uživatel zadá alespoň 2 znaky.

Druhý případ je trochu složitější:

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)

V tomto případě argument id_field říká widgetu, aby převzal hodnotu db.category.id, ačkoli dokončovaná hodnota je db.category.name. Volitelný parametr je orderby, který udává, jak budou návrhy setříděny (defaultně abecedně).

Widget pracuje pomocí Ajaxu. Kde je Ajax callback? Něco se tu děje skrytě (some magic is going on..). Callback je metoda widgetu. Jakým způsobem je zveřejněna (exposed)? Ve Web2py může kterýkoli úsek kódu generovat odezvu (response) jako vyvolání HTTP výjimky. Autocomplete widget tuto možnost využívá, a sice takto: widget vyšle Ajaxové volání na stejnou URL adresu, která vygenerovala widget napoprvé, a přidá speciální token (označení) do request.vars. Místo toho, aby se widget instancioval podruhé, najde token a vyvolá HTTP výjimku, která zajistí odpověď na požadavek. To se děje skrytě a vývojář se tím nemusí zabývat.

SQLFORM.grid a SQLFORM.smartgrid

Pozor: grid a smartgrid byly experimentální ve starších verzích Web2py (před 2.0). To už sice neplatí, ale zatím zaručujeme zpětnou kompatibilitu jen API gridu (způsobu jeho použití/volání), kdežto v prezentační vrstvě gridu může ještě případně dojít k nějakým změnám v budoucnu.

Obojí jsou objekty vysoké úrovně, které generují složité CRUD prvky (prvky pro vytváření, zobrazení, editaci nebo rušení záznamů). Zajišťují stránkování, možnost prohlížení seznamu (browse), vyhledávání, třídění, vytváření, aktualizaci a rušení záznamů pomocí jediného objektu.

Pro vytváření, prohlížení a editaci záznamů Grid vytváří jako objekty nižší úrovně SQLFORM formuláře. Mnoho argumentů gridu je předáváno těmto SQLFORM formulářům. Znamená to, že obvykle platí i informace z dokumentace pro SQLFORM (a FORM). Např. má Grid onvalidation callback. Logika zpracování Gridu je zajištěna na nižší úrovni metodou formuláře process(), takže více informací o onvalidation můžete nalézt v dokumentaci pro formuláře.

Důležité: Vždy, když Grid prochází různými stavy (myslíme tím: zobrazení záznamů, editace záznamu, přidání záznamu, apod.), je generován nový požadavek (request) na server, na původní URL adresu. Při tom je request.args rozšířen o informaci, do jakého stavu má Grid přejít.

SQLFORM.grid

Jednodušší z obou prvků je SQLFORM.grid. Tady je příklad použití:

@auth.requires_login()
def manage_users():
    grid = SQLFORM.grid(db.auth_user)
    return locals()

což vytvoří tuto stránku:

image

První argument pro SQLFORM.grid může být tabulka nebo dotaz (query). Grid objekt promítne (a zpřístupní k úpravám) záznamy, které vyhovují uvedenému dotazu.

Jak objekt pracuje? Objekt zpracuje své request.args a tím zjistí, co má právě udělat (promítat seznam, vyhledávat, vytvářet záznamy, editovat, rušit záznam). Každé tlačítko (button), které Grid objekt vytvoří, odkazuje na stejnou akci (manage_users v našem příkladu), ale předává jí různé request.args.

přihlášení uživatele (login) je defaultně vyžadováno pro změny dat

Defaultně jsou opakované, tlačítky gridu generované, přístupy na adresu akce gridu digitálně podespány a verifikovány. Znamená to, že není možné některé akce provést (vytvářet, aktualizovat, rušit záznamy), když uživatel není přihlášen. Toto omezení můžete potlačit:

def manage_users():
    grid = SQLFORM.grid(db.auth_user, user_signature=False)
    return locals()

ale nedoporučujeme to.

Více gridů v jedné akci kontroléru

S ohledem na to, jak grid pracuje, můžete mít jen jediný grid na stránce - v akci kontroléru. Toto omezení lze překonat natažením gridů jako komponent pomocí LOAD. Aby nedošlo k záměnám funkcionality mezi gridy, vytvořenými pomocí LOAD, nastavte každému unikátní formname.

Specifické používání requests.args

Řekli jsme si, že Grid používá request.args pro určení, který stav je požadován (má být právě renderován). Neznamená to, že request.args nemůžete použít pro předání dat do akce k jakémukoli jinému účelu. Grid ale musí vědět, které request.args jsou určeny pro něj a které ne. Zde je příklad kódu, který dovoluje editovat kteroukoli tabulku databáze, přičemž první request.args udává jméno tabulky a případné další request.args jsou přidány a využívány Gridem samotným:

@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()

Argument gridu args tedy udává, které request.args má grid ignorovat. V našem případě request.args[:1] je jméno tabulky, které zpracuje přímo akce manage a nikoli grid. args=request.args[:1] řekne gridu, že má zachovat první argument URL v každém odkazu, který bude (pro změny svého stavu) generovat. Argumenty specifické pro grid budou přidány až za tento první argument.

Signatura SQLFORM.grid-u

Úplná signatura gridu je následující:

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'
    )
  • fields je seznam polí, která se mají načíst z databáze. Také určuje, která pole se mají v gridu zobrazit. Neovlivňuje ale pole, která se zobrazí při editaci záznamu. K nastavení polí při editaci použijte standardní atributy polí readable a writable. Např. editaci pole zaakážeme (v kódu před SQLFORM.grid) takto:
db.tabulka.pole.writable = False
db.tabulka.pole.readable = False

Pozn.: Nechcete-li pole explicitně vyjmenovat a chcete jedno z polí v gridu zakázat (často id), použijte rovněž .id.readable=False.

  • field_id je pole, které se používá jako primární klíč, např. db.mytable.id (což je default).
  • left volitelný výraz pro left join, přidá se do konstrukce ...select(left=...).
  • headers je slovník (dictionary), který mapuje 'tablename.fieldname' na odpovídající texty v hlavičce, např. {'auth_user.email' : 'Emailová adresa', ...}
  • orderby je počáteční třídění záznamů. Viz DAL kapitola (může být zadáno více polí).
  • groupby pokud se mají záznamy seskupit. Používá se stejná syntaxe jako v select(groupby=...).
  • searchable, sortable, deletable, editable, details, create určuje, zda lze hledat, třídit, rušit záznamy, editovat záznamy, prohlížet záznam a vytvářet nové záznamy.
  • selectable bude přidán checkbox pro každý záznam; nad označenými záznamy pak bude zavolána zadaná akce.
       selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids)))
    

Pomocí zápisu jako seznam vektrorů (list of tuples) lze vytvořit více tlačítek, které volají různé akce:

 selectable = [('text tlačítka 1', lambda...), ('text tlačítka 2', lambda ...)]
  • paginate zadává počet řádků na stránku.
  • csv je-li True, je zpřístupněna možnost downloadu obsahu gridu v různých formátech (více o exportu bude uvedeno níže).
  • links lze použít ke zobrazení dalších sloupců - odkazů na jiné stránky. Argument pro links musí být seznam (list) slovníků dict(header='name', body=lambda row: A(...)), kde header je název sloupce a body je funkce, která jako parametr dostane záznam (Row objekt) a vrátí hodnotu. V příkladu používáme helper A(...) pro vytvoření odkazu.
  • links_in_grid if set to False, links will only be displayed in the "details" and "edit" page (so, not on the main grid)
  • upload, stejně jako u SQLFORM formuláře. Web2py umožní pomocí akce na této adrese download uloženého souboru.
  • maxtextlength nastavuje maximální počet znaků, na který budou zkráceny zobrazené hodnoty (v gridu). Tato hodnota může být přepsána a nastavena individuálně pro jednotlivá pole pomocí maxtextlengths, což je slovník (dictionary) 'tabulka.pole':počet_znaků např. {'auth_user.email' : 50}
  • onvalidation, oncreate, onupdate a ondelete jsou callback funkce. Všechny kromě ondelete dostanou jako vstup (parametr) objekt formuláře, ondelete dostane jako parametry tabulku a id záznamu.

Vzhledem k tomu, že formulář pro editaci/přidávání záznamů není nic jiného než SQLFORM (který rozšiřuje vlastnosti formuláře FORM), tyto callbacky se používají způsobem, který najdete v dokumentaci u FORM a SQLFORM.

Příklad:

def myonvalidation(form):
    print "onvalidation callback"
    print form.vars
    form.errors= True  # zabrání potvrzení formuláře
    
    #... nebo přidáme chybový stav jednotlivým polím
    form.errors.first_name = "Dítě nelze pojmenovat takhle hloupě."	
    form.errors.last_name = "Příjmení musí začínat písmenem."
    response.flash = "Tohle zadání se mi nelíbí"

def myoncreate(form):
    print 'přidej záznam!'
    print form.vars

def myonupdate(form):
    print 'aktualizuj!'
    print form.vars

def myondelete(table, id):
    print 'zruš!'
    print table, id
  • sorter_icons je seznam (list) dvou řetězců (nebo helperů), které budou použity jako symboly pro vzestupné a sestupné setřídění sloupce.
  • ui může být nastaveno na 'web2py', čímž budou generována jména css tříd ve Web2py stylu, může být nastaveno na jquery-ui, což vygeneruje jména css tříd ve stylu jquery UI. Může to ale být i slovník se jmény css tříd pro jednotlivé komponenty gridu:
    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')
    
  • search_widget umožňuje zařadit jiný než default search widget (widget pro vyhledávání); pro podrobnosti odkazujeme čtenáře do zdrojového kódu "gluon/sqlhtml.py".
  • showbuttontext umožňuje vypnout popisný text tlačítek (takže se zobrazí jen ikony)
  • _class je třída grid kontejneru.
  • exportclasses může být slovník dvojic (dictionary of tuples), defaultně je nastaven takto:
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)'))

ExporterCSV, ExporterXML, ExporterHTML a ExporterTSV jsou vesměs definovány v gluon/sqlhtml.py. Můžete se případně podívat do zdrojového kódu a dopsat si podle potřeby vlastní exportér. Pomocí hodnot False ve slovníku potlačíte příslušnou možnost exportu, např. dict(xml=False, html=False) zakáže xml a html export.

  • formargs se předá všem SQLFORM objektům, které Grid generuje, kdežto případné createargs, editargs resp. viewargs se předají jen SQLFORMu pro vytvořebí záznamu, editaci a readonly zobrazení
  • formname, ignore_rw a formstyle se předají pod stejným jménem SQLFORM objektům pro přidání a editaci záznamu.
  • buttons_placement a links_placement (umístění tlačítek a odkazů) mají oba možné hodnoty ('right', 'left', 'both'), čímž zadáme, kde se na řádku gridu zobrazí tlačítka (resp. odkazy)

deletable, editable a details jsou typicky boolean hodnoty, ale mohou to být také funkce (např. lambda funkce), které vezmou Row objekt a určí, zda pro tento záznam bude nebo nebude zobrazeno příslušné tlačítko.

Virtuální pole v SQLFORM.gridu a smartgridu

Ve nových verzích Web2py (po 2.6) se virtuální pole zobrazují v gridu jako normální pole: buď spolu se všemi ostatními, když se zobrazují všechna pole, nebo když je explicitně uvedete při použití výčtu polí pomocí fields argumentu. V gridu ale podle nich nelze nastavovat pořadí.

Ve starších verzích Web2py bylo možné zobrazovat virtuální pole v gridu leda pomocí argumentu links. To lze i nadále v novějších verzích použít. Dejme tomu, že tabulka db.t1 má pole, nazvané t1.vfield, odvozené z hodnot t1.field1 a t1.field2, pak můžete udělat toto:

grid = SQLFORM.grid(db.t1, ..., fields = [t1.field1, t1.field2,...], 
   links = [dict(header='Virtuální pole 1', body=lambda row: row.vfield), ...] )

Ať už budete chtít zobrazit virtuální pole kterýmkoli způsobem, protože je např. t1.vfield odvozeno z t1.field1 a t1.field2, i tato pole musí být načtena a obsažena v záznamu. V příkladu výše je to zajištěno explicitním uvedením t1.field1 a t1.field2 v argumentu fields. Při zobrazení všech polí to bude rovněž zajištěno a virtuální pole se zobrazí správně. Pokud některé pole nechcete zobrazovat (a musíte jej z principu věci načíst), potlačte jeho zobrazení pomocí atributu readable, např. db.t1.field2.readable = False před instanciováním Gridu (stejně jako např. pro skrytí nechtěného id pole).

Poznamenejme, že když definujeme virtuální pole, lambda funkce musí uvádět databázové jméno, kdežto v argumentu links to není nutné. Takže např. definice virtuálních polí v modelu bude pro předchozí příklad vypadat takto:

db.define_table('t1',
    Field('field1', 'string'),
    Field('field2','string'),
    Field.Virtual('vfield', lambda row: row.t1.field1 + row.t1.field2),
  ...)

SQLFORM.smartgrid

SQLFORM.smartgrid se podobá Gridu, který také ve skutečnosti obsahuje. Je ale konstruován tak, že jeho vstupem nemůže být dotaz (query), ale pouze tabulka. Tato tabulka se ve Smartgridu zobrazí a proti Gridu navíc je umožněno zobrazit také obsah odkazovaných (relačně spřažených) tabulek.

Předpokládejme např. následující strukturu tabulek:

db.define_table('parent', Field('name'))    # rodiče
db.define_table('child', Field('name'), Field('parent', 'reference parent'))  # děti

Pomocí SQLFORM.grid můžete zobrazit všechny rodiče:

SQLFORM.grid(db.parent)

všechny děti:

SQLFORM.grid(db.child)

a všechny rodiče a děti v jedné (prezentační) tabulce:

SQLFORM.grid(db.parent, left=db.child.on(db.child.parent==db.parent.id))

Pomocí SQLFORM.smartgrid můžete všechna data zahrnout do jednoho objektu, který obsluhuje obě uvedené tabulky:

@auth.requires_login()
def manage():
    grid = SQLFORM.smartgrid(db.parent, linked_tables=['child'])
    return locals()

což bude vypadat takto:

image

Všimněte si extra odkazu na "děti". Při použití Gridu můžete také udělat extra odkazy na "děti", ale ty povedou na jinou akci. Kdežto v případě Smartgridu budou vytvořeny automaticky a zpracovány stejnou akcí a stejným objektem.

Také si všimneme, že při použití odkazu na "děti" dostaneme seznam dětí daného rodiče (což jsme čekali), ale jestliže nyní přidáme nové dítě, odkaz na rodiče bude automaticky vyplněn podle dříve vybraného rodiče (který se zobrazuje v breadcrumbs prvku objektu Smartgrid, což je prvek, který ukazuje pozici aktuálně vygenerované stránky v logice celého Smartgridu). Tuto hodnotu lze přepsat, pokud tomu ovšem nezabráníme tak, že uděláme odkaz readonly (jen pro čtení):

@auth.requires_login():
def manage():
    db.child.parent.writable = False
    grid = SQLFORM.smartgrid(db.parent, linked_tables=['child'])
    return locals()

Jestliže argument linked_tables neuvedete, propojí se a vytvoří se odkazy na všechny relačně spřažené tabulky. Ale s ohledem na to, abychom nezveřejnili něco, co nechceme, raději doporučujeme explicitně uvádět odkazované tabulky, které chceme zpřístupnit.

Následující kód vytvoří docela mocné rozhraní pro správu všech tabulek v databázi:

@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()

Signatura Smartgridu

smartgrid má stejné argumenty jako grid, s několika odlišnostmi, a několik navíc:

  • První argument je vždy tabulka, nemůže to být dotaz (query)
  • Navíc je k dispozici argument constraints, což je slovník (dictionary) prvků 'jmenotabulky':dotaz, kterým lze dodatečně omezit přístup k záznamům, které se zobrazí v gridu pro zadanou tabulku 'jmenotabulky'.
  • Navíc je argument linked_tables, což je seznam jmen tabulek, které budou (kromě hlavní tabulky) ve Smartgridu přístupné.
  • divider umožňuje zadat oddělovací znak v breadcrumb navigaci, breadcrumbs_class přidá css třídu breadcrumb prvku.
  • Všechny argumenty, s výjimkou tabulky (hlavní tabulky), args, linked_tables a user_signature mohou být slovníky (dictionaries), jak vysvětlujeme dále.

Uvažujme předchozí grid:

grid = SQLFORM.smartgrid(db.parent, linked_tables=['child'])

Umožňuje zobrazit záznamy jak db.parent tak i db.child tabulky. S výjimkou prvku pro navigaci, pro jednotlivou tabulku to, co se zobrazí, je totéž, co dříve popisovaný Grid. To znamená, že v našem příkladu Smartgrid vytváří dva Gridy, jeden pro tabulku rodičů, druhý pro tabulku dětí. A můžeme chtít předat každému z těchot dílčích Gridů odlišné parametry. Např. budeme chtít, aby searchable bylo v jednotlivých dílčích Gridech nastaveno jinak.

Tam, kde pro obyčejný Grid předáváme boolean:

grid = SQLFORM.grid(db.parent, searchable=True)

tam pro Smartgrid můžeme předat slovník (dictionary) s boolean prvky:

grid = SQLFORM.smartgrid(db.parent, linked_tables=['child'],
     searchable= dict(parent=True, child=False))

Tím umožníme prohledávání v seznamu rodičů, kdežto v seznamu dětí se widget pro vyhledávání nezobrazí (jsou totiž zobrazeny pro jednotlivého rodiče a tak jich nebude až tak mnoho).

řízení oprávnění přístupu v Gridu a Smartgridu

grid a smartgrid nezajišťují automatické řízení přístupu jako Crud, ale můžete je propojit s auth takto:

grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_membership('managers'),
     deletable = auth.has_membership('managers'))

nebo

grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_permission('edit', 'auth_user'),
     deletable = auth.has_permission('delete', 'auth_user'))

Smartgrid a množné číslo

smartgrid je objekt, který zobrazuje jméno tabulky a při tom je vhodné, aby ho zobrazoval (v různém kontextu) v jednotném (singular) i množném čísle (plural). Web2py množné číslo odvodí (i když pro češtinu asi příliš neuspějete), můžete ale oba tvary zadat v modelu explicitně:

db.define_table('child', ..., singular="Dítě", plural="Děti")

nebo takto:

singular
plural

db.define_table('child', ...)
db.child._singular = "Dítě"
db.child._plural = "Děti"

Vhodné je internacionalizovat pomocí T operátoru, např. singular=T("Child").

smartgrid pak obě hodnoty využije ke zobrazení vhodných textů hlaviček a odkazů.

 top