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 nadSQLFORM-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
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
aform.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í:
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):
Takto vypadá po zadání s chybami:
A nakonec po potvrzení správných dat:
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
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:
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.
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ář:
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.
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).
- Jestliže nastavíte
deletable
naTrue
, 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áteshowid=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
aupload
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ímignore_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. - buttonsje seznam (list) HTML helperů jako
INPUT
neboTAG.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)
form.vars.id
.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 naTrue
.
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í.
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:
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
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:
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
Č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
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()
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íhorequest.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řídygluon.storage.Message
, která je podobnágluon.storage.Storage
, ale automaticky překládá své hodnoty, takže není potřeba používatT
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
arecord_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ístoonaccept
, 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.
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í
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 argumentemhideerror=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:
Tento mechanismus bude fungovat i při použití form.custom.
Validátory
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
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
Také tento validátor neohlašuje žádnou chybu. Převede zadanou hodnotu na malá písmena.
requires = IS_LOWER()
IS_UPPER
Také tento validátor neohlašuje žádnou chybu. Převede zadanou hodnotu na velká písmena.
requires = IS_LOWER()
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
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
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
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
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
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
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
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
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
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
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
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
Zastaralý. Alias pro modernější IS_EMPTY_OR
popsaný níže.
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
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
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
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
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
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)
, kdea
ab
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
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
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
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
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
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
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ř. 3232235521maxip
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
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
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
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')]
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)
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
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
metodform.accepts(...)
,form.process(...)
aform.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 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:
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('↑'), XML('↓')),
ui = 'web2py',
showbuttontext=True,
_class="web2py_grid",
formname='web2py_grid',
search_widget='default',
ignore_rw = False,
formstyle = 'table3cols',
exportclasses = None,
formargs={},
createargs={},
editargs={},
viewargs={},
buttons_placement = 'right',
links_placement = 'right'
)
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 vselect(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 prolinks
musí být seznam (list) slovníkůdict(header='name', body=lambda row: A(...))
, kdeheader
je název sloupce abody
je funkce, která jako parametr dostane záznam (Row objekt) a vrátí hodnotu. V příkladu používáme helperA(...)
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
aondelete
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 najquery-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
aformstyle
se předají pod stejným jménem SQLFORM objektům pro přidání a editaci záznamu.buttons_placement
alinks_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
adetails
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:
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
auser_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:
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ů.