Chapter 7: Formularze i walidacja

Formularze i walidatory

W web2py istnieją cztery różne sposoby budowania formularzy:

  • FORM zapewnia implementację niskiego poziomu w zakresie helperów HTML. Obiekt FORM może być serializowany do kodu HTML i jest mu znana zawartość pól. Wie, jak sprawdzać zgłoszone formularze.
  • SQLFORM dostarcza API wysokiego poziomu do budowania funkcjonalności tworzenia, aktualizowania i usuwania formularzy powiązanych z istniejącymi tabelami bazy danych.
  • SQLFORM.factory jest abstrakcyjną warstwą na szczycie SQLFORM w celu wykorzystania funkcjonalności generowania formularzy, nawet jeśli nie jest dostępna baza danych. Generuje to formularze bardzo podobnie do SQLFORM, z opisem tabeli, ale bez potrzeby tworzenia tabeli bazy danych.
  • Metody obiektu CRUD. Są one funkcjonalnym równoważnikiem SQLFORM i są oparte na SQLFORM, ale dostarczają bardziej zwartą notację.

Wszystkie te formularze są samoinformujące się i jeśli dane wejściowe nie przechodzą walidacji, mogą zostać dodane komunikaty błędów. Formularze można wypytywać o zmienne walidacyjne i o komunikaty błędów generowanych podczas walidacji.

Dowolny kod HTML może być wstawiany do lub ekstrahowany z formularza przy użyciu helperów.

FORM i SQLFORM są helperami, którymi można manipulować w podobny sposób jak z DIV. Na przykład można ustawić styl formularza:

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

FORM

FORM
accepts
formname

Rozważmy dla przykładu aplikację test z następującym kontrolerem "default.py":

1
2
def display_form():
    return dict()

i związananym widokiem "default/display_form.html":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{extend 'layout.html'}}
<h2>Formularz wejściowy</h2>
<form enctype="multipart/form-data"
      action="{{=URL()}}" method="post">
Twoja nazwa:
<input name="name" />
<input type="submit" />
</form>
<h2>Zgłoszone zmienne</h2>
{{=BEAUTIFY(request.vars)}}

Jest to zwykły formularz HTML, który pyta o nazwę użytkownika. Gdy wypełni się ten formularz i kliknie przycisk zatwierdzający, nastąpi samozgłoszenie formularza i zmienna request.vars.name wraz z wartością zostanie wyświetlona na przycisku.

Można wygenerować taki sam formularz używając helperów. Można to zrobić w widoku lub w akcji. Ponieważ web2py przetwarza formularz w akcji, lepiej jest też zdefiniować taki formularz w akcji.

Oto nowy kontroler:

1
2
3
def display_form():
   form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
   return dict(form=form)

i związany z nim widok "default/display_form.html":

1
2
3
4
5
{{extend 'layout.html'}}
<h2>Formularz wejściowy</h2>
{{=form}}
<h2>Zgłoszone zmienne</h2>
{{=BEAUTIFY(request.vars)}}

Kod ten, jak do tej pory, jest równoważny poprzedniemu kodowi, ale formularz jest generowany przez wyrażenie {{=form}}, które serializuje obiekt FORM.

Teraz dodamy jeden poziom złożoności, dodając walidację formularza i przetwarzanie.

Zmieńmy kontroler następujaco:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
    form=FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request,session):
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

oraz związany widok "default/display_form.html":

1
2
3
4
5
6
7
8
9
{{extend 'layout.html'}}
<h2>Formularz wejściowy</h2>
{{=form}}
<h2>Zgłoszone zmienne</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Zaakceptowane zmienne</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Błędy w formularzu</h2>
{{=BEAUTIFY(form.errors)}}

Proszę zauważyć, że:

  • W akcji dodaliśmy walidator requires=IS_NOT_EMPTY() dla pola input "name".
  • W akcji dodaliśmy wywołanie form.accepts(..)
  • W widoku drukujemy form.vars i form.errors jak również formularz i request.vars.

Cała praca wykonywana jest przez metodę accepts obiektu form. Filtruje ona request.vars zgodnie z deklarowanymi wymaganiami (wyrażonymi przez walidatory). Metoda accepts przechowuje zmienne, które przechodzą walidację, w form.vars. Jeśli wartość pola nie spełnia wymagań, wykorzystywany walidator zwraca błąd, który zostaje przechowany w form.errors. Zarówno form.vars jak i form.errors są obiektami gluon.storage.Storage podobnymi do request.vars. Pierwszy z nich zawiera wartości, które przeszły walidację, na przykład:

1
form.vars.name = "Max"

Ten drugi obiekt zawiera błędy, na przykład:

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

Pełna sygnatura metody accepts jest następująca:

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

Znaczenie parametrów opcjonalnych jest wyjaśnione w następnych podrozdziałach.

Pierwszym argumentem może być request.vars lub request.get_vars lub request.post_vars lub po prostu request. Ten ostatni parametr jest równoznaczny z zaakceptowaniem jako wejścia zmiennej request.post_vars.

Funkcja accepts zwraca True jeśli formularz został zaakceptowany a w przeciwnym razie False. Formularz nie zostanie zaakceptowany, jeśli ma błędy lub gdy nie został zgłoszony (na przykład, za pierwszym razem jest pokazywany).

Oto jak wygląda ta strona, gdy formularz jest wyświetlany za pierwszym razem:

image

To jest wygląd formularza, po nieprawidłowym zgłoszeniu:

image

Po prawidłowym zgłoszeniu nasz formularz będzie wyglądać tak:

image

Metody process i validate

Skrótem dla

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

jest

1
form.process(...).accepted

Skrót nie potrzebuje argumentów request i session (choć ewentualnie można je określić). Różni się on również tym od accepts, ponieważ sam zwraca formularz. Wewnętrznie process akceptuje i przekazuje do niego swoje argumenty. Wartości zwracane przez accepts są zapisywane w zmiennej form.accepted.

Funkcja process pobiera kilka dodatkowych argumentów, który nie pobiera accepts:

  • message_onsuccess;
  • onsuccess: jeśli równe 'flash' (domyślnie) i formularz został zaakceptowany, to pojawi się o tym komunikat message_onsuccess ;
  • message_onfailure ;
  • onfailure: jeśli równe 'flash' (domyślnie) i walidacja formularza nie powiodła się, to pojawi się o tym komunikat message_onfailure ;
  • next wskazuje gdzie przekierować użytkownika po zaakceptowaniu formularza.

Argumenty onsuccess i onfailure mogą być funkcjami takimi jak lambda form: do_something(form).

Skrót

1
form.validate(...)

równoważny jest wyrażeniu

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

Pola warunkowe

Czasem zachodzi potrzeba pokazania pola, jeśli spełniony jest jakiś warunek. Rozważmy następujący model:

db.define_table('purchase', Field('have_coupon','boolean'),Field('coupon_code'))

Przyjmijmy, że chcemy wyświetlić pole coupon_code, wtedy i tylko wtedy, gdy zostało zaznaczone pole have_coupon. Można to wykonać w JavaScript. W web2py, odpowiedni kod JavaScript może zostać wygenerowany automatycznie. Wystarczy zadeklarować, że to pole jest zależne od wyrażenia. Wykorzystując atrybut show_if pola:

def index():
    db.purchase.coupon_code.show_if = (db.purchase.have_coupon==True)
    form = SQLFORM(db.purchase).process()
    return dict(form = form)

Wartość show_if jest zapytaniem i wykorzystuje tą samą składnię DAL, którą używa się w zapytaniach do bazy danych. Różnica polega na tym, że to zapytanie nie jest wysyłane do bazy danych, ale jest konwertowane do kodu JavaScript i wysyłane do przeglądarki, gdzie kod ten jest wykonywany, gdy użytkownik edytuje formularz.

Pola ukryte

Gdy serializowany jest powyższy obiekt formularza przez {{=form}} i ponieważ wywołana została metodę accepts, to kod HTML formularza będzie wyglądać tak:

1
2
3
4
5
6
7
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Proszę zwrócić uwagę na obecność dwóch ukrytych pól: "_formkey" i "_formname". Ich obecność jest wyzwalana przez wywołanie accepts. Odgrywają one dwie ważne role:

  • Ukryte pole o nazwie "_formkey" jest jednorazowym tokenem, który jest używany do zabezpieczenia przed podwójnym złożeniem formularza. Wartość tego klucza jest generowana podczas serializowania formularza i przechowywana w session. Gdy formularz zostaje złożony, wartość ta musi pasować, w przeciwnym przypadku metoda accepts zwraca False bez błędów, tak jakby formularz w ogóle nie został złożony. Dzieje się tak dlatego, że web2py nie może ustalić, czy formularz został złożony poprawnie.
  • Ukryte pole o nazwie "_formname" jest generowane przez web2py jako nazwa dla formularza, ale nazwa ta może zostać nadpisana. Pole to jest niezbędne, aby umożliwić strony, które zawierają i przetwarzają wiele formularzy. web2py rozróżnia różne złożone formularze po ich nazwach.
  • Opcjonalnie, ukryte pola można zdefiniować przez FORM(..,hidden=dict(...)).

Rola tych ukrytych pól i ich wykorzystanie w niestandardowych formularzach i stronach z wieloma formularzami jest omówione szczegółowo w dalszej części rozdziału.

Jeśli rozpatrywany formularz został złożony z pustym polem "name", nie przejdzie walidacji. Gdy formularz jest ponownie serializowany, to pojawi się jako:

1
2
3
4
5
6
7
8
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Proszę zwrócić uwagę na występowanie DIV klasy "error" w serializowanym formularzu. web2py wstawia ten komunikat błędu w formularzu, aby powiadomić odwiedzającego o polu, które nie przeszło walidacji. Po złożeniu formularza, metoda accepts określa, ze formularz został złożony, sprawdza czy pole "name" jest puste i czy wartość tego pola jest obowiązkowa i ewentualnie umieszcza w formularzu komunikat błędu z walidatora.

Bazowy widok "layout.html" przewiduje obsługę bloków DIV z klasa "error". Domyślny szablon układu wykorzystuje efekty jQuery do pokazywania komunikatów błędów z efektem zsuwania na czerwonym tle. Proszę zapoznać się ze szczegółami w rozdziale 11.

Argument keepvalues

keepvalues

Opcjonalny argument keepvalues powiadamia web2py, co zrobić gdy formularz został zaakceptowany i nie ma przekierowania, tak więc ten sam formularz będzie wyświetlony ponownie. Domyślnie w takim przypadku formularz jest czyszczony. Jeśli keepvalues jest ustawione na True, formularz jest wstępnie wypełniany poprzednio wprowadzonymi danymi. Jest to pomocne, gdy ten sam formularz ma być używany wielokrotnie do wstawiania podobnych rekordów. Jeśli argument dbio jest ustawiony na False, web2py nie będzie wykonywał jakichkolwiek operacji wstawiania lub aktualizowania rekordów w bazie danych po zaakceptowaniu formularza. Jeśli hideerror zostanie ustawiony na True a formularz zawiera błędy, nie będą one wyświetlane podczas renderowania formularza (będzie rolą programisty wyświetlenie jakoś tych błędów ze zmiennej form.errors). Argument onvalidation jest omówiony poniżej.

Argument onvalidation

onvalidation

Wartością argumentu onvalidation może być None lub funkcja, która pobiera formularz i zwraca nothing. Taka funkcja może być wywołana i przekazana do formularza, niezwłocznie po pozytywnej walidacji i przed czymkolwiek co się stanie po tej walidacji. Funkcja ta ma wiele zastosowań. Na przykład, wykonanie dodatkowego sprawdzenia formularza i ewentualnie dodanie do formularza błędów lub obliczenie wartości jakichś pól na podstawie wartości innych pól lub wyzwolenie jakiejś akcji (jak na przykład wysłanie wiadomości email) przed utworzeniem lub zaktualizowaniem rekordu.

Oto przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
db.define_table('numbers',
    Field('a', 'integer'),
    Field('b', 'integer'),
    Field('c', 'integer', readable=False, writable=False))

def my_form_processing(form):
    c = form.vars.a * form.vars.b
    if c < 0:
       form.errors.b = 'a*b cannot be negative'
    else:
       form.vars.c = c

def insert_numbers():
   form = SQLFORM(db.numbers)
   if form.process(onvalidation=my_form_processing).accepted:
       session.flash = 'record inserted'
       redirect(URL())
   return dict(form=form)

Wykrywanie zmian rekordu

Podczas wypełniania formularza podczas edycji istnieje pewne prawdopodobieństwo, że inny użytkownik może jednocześnie edytować ten sam rekord. Więc w czasie zapisywania rekordu dobrze jest sprawdzić, czy nie uległ on w międzyczasie zmianie, w celu uniknięcia konfliktów. Można to zrobić tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
db.define_table('dog',Field('name'))

def edit_dog():
    dog = db.dog(request.args(0)) or redirect(URL('error'))
    form=SQLFORM(db.dog,dog)
    form.process(detect_record_change=True)
    if form.record_changed:
        # do something
    elif form.accepted:
        # do something else
    else:
        # do nothing
    return dict(form=form)

Formularze a przekierowanie

Najczęstszym sposobem wykorzystania formularzy jest tzw. samozgłoszenie, co oznacza, że zgłoszone zmienne pól są przetwarzane w tej samej akcji, w której wygenerowano formularz. Po zaakceptowaniu formularza, niezwykła rzeczą jest jego ponowne wyświetlenie na bieżącej stronie. Bardziej rozpowszechnione jest przekierowanie odwiedzającego do "następnej" strony.

Oto nowy przykład kontrolera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def display_form():
    form = FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.process().accepted:
        session.flash = 'form accepted'
        redirect(URL('next'))
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

def next():
    return dict()

W celu ustawienia komunikatu fleszowego na następnej stronie a nie na bieżącej, trzeba zastosować session.flash zamiast response.flash. web2py przeniesie to pierwsze wyrażenie na następną stronę po przekierowaniu. Trzeba mieć na uwadze, że używanie session.flash wymaga nie stosowania session.forget().

Wiele formularzy na jednej stronie

Treść tego rozdziału dotyczy zarówno klasy FORM jak i SQLFORM. Jest możliwe posiadanie wielu formularzy na stronie, ale trzeba umożliwić ich rozróżnianie przez web2py. Jeśli formularze są uzyskiwane z SQLFORM z różnych tabel, to web2py automatycznie przydziela im różne nazwy. W przeciwnym razie zachodzi potrzeba jawnego określenia nazw dla poszczególnych formularzy. Oto przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)

a to jest wyprodukowane wyjście:

image

Gdy użytkownik zgłasza pusty form1, to tylko w form1 wyświetlany jest błąd, jeśli odwiedzający zgłosi pusty form2, to błąd będzie wyświetlony tylko w form2.

Udostępnianie formularzy

Treść tego rozdziału dotyczy klasy FORM oraz SQLFORM. To co tu omówimy jest możliwe, ale nie zalecane, ponieważ zawsze jest dobrą praktyką, aby tworzyć formularz samozgłaszający się. Czasami jednak nie ma się wyboru, gdy akcja wysyłająca formularz i akcja odbierająca należą do różnych aplikacji.

Jest możliwe wygenerowanie formularza, który zgłaszany jest do różnych akcji. Realizuje się to przez określenie adresu URL akcji przetwarzającej w atrybucie obiektów FORM lub SQLFORM. Na przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
        INPUT(_type='submit'), _action=URL('page_two'))

def page_one():
    return dict(form=form)

def page_two():
    if form.process(session=None, formname=None).accepted:
         response.flash = 'form accepted'
    else:
         response.flash = 'there was an error in the form'
    return dict()

Proszę zauważyć, że ponieważ "page_one" oraz "page_two" używają ten sam obiekt form, zdefiniowaliśmy go tylko raz, umieszczając na zewnątrz wszystkich akcji, aby się nie powtarzać. Wspólna część kodu na początku kontrolera zostaje wykonana za każdym razem przed przekazaniem sterowania do wywoływanej akcji.

Ponieważ "page_one" nie wywołuje process (ani accepts), formularz nie ma nazwy ani klucza, więc trzeba przekazać session=None i ustawić formname=None w process lub formularz nie będzie walidowany, gdy otrzyma go "page_two".

Dodawanie przycisków do obiektów FORM

Zazwyczaj formularz zawiera jeden przycisk zgłaszający „Prześlij”. Często chce się dodać przycisk "Wstecz" zamiast przycisku „Prześlij”, przekierowujący odwiedzającego do innej strony.

add_button

Można to zrobić przy wykorzystując metodę add_button:

1
form.add_button('Wstecz', URL('other_page'))

Do formularza można dodawać więcej niż jeden przycisk. Argumentami metody add_button są wartość przycisku (jego tekst) i adres url przekierowania. Proszę zapoznać się też z argumentem buttons dla SQLFORM, który dostarcza bardziej zaawansowane rozwiązanie.

Więcej o manipulowaniu obiektami FORM

Tak jak opisano to w rozdziale poświęconemu widokom, klasa FORM jest helperem HTML. Helpery mogą być manipulowane jak listy Pythona i jak słowniki, które można tworzyć i manipulować nimi w czasie wykonania.

SQLFORM

Przejdźmy teraz do następnego poziomu i stwórzmy aplikację z plikiem takiego oto modelu:

1
2
db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))

Zmodyfikujmy kontroler w ten sposób:

1
2
3
4
5
6
7
8
9
def display_form():
   form = SQLFORM(db.person)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   else:
       response.flash = 'please fill out the form'
   return dict(form=form)

Widoku nie potrzebujemy zmieniać.

W nowym kontrolerze nie potrzebujemy budowania FORM, ponieważ konstruktor SQLFORM zbudował jeden formularz z danymi z tabeli db.person, zdefiniowany w modelu. Jest to nowy formularz, który po zserializowaniu do kodu HTML będzie wyglądał następująco:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<form enctype="multipart/form-data" action="" method="post">
  <table>
    <tr id="person_name__row">
       <td><label id="person_name__label"
                  for="person_name">Your name: </label></td>
       <td><input type="text" class="string"
                  name="name" value="" id="person_name" /></td>
       <td></td>
    </tr>
    <tr id="submit_record__row">
       <td></td>
       <td><input value="Submit" type="submit" /></td>
       <td></td>
    </tr>
  </table>
  <input value="9038845529" type="hidden" name="_formkey" />
  <input value="person" type="hidden" name="_formname" />
</form>

Ten automatycznie wygenerowany formularz jest bardziej złożony niż poprzedni formularz niskiego poziomu. Przede wszystkim zawiera wiersze tabeli a każdy wiersz posiada trzy kolumny. Pierwsza kolumna zawiera etykiety pól (tak jak określono to w db.person), druga kolumna zawiera pola wejściowe (input) i ewentualnie komunikaty błedów, a trzecia kolumna jest opcjonalna i dlatego pusta (może zostać wypełniona z pól w konstruktorze SQLFORM).

Wszystkie znaczniki w formularzu mają nazwy pochodzące z tabeli i nazwy pól. Pozwala to na łatwe dostosowanie formularza przy użyciu CSS i JavaScript. Możliwości te są szczegółowiej opisane w rozdziale 11.

Bardziej istotne jest to, że teraz nowa metoda accepts ma dużo więcej pracy. Podobnie jak w poprzednim przypadku wykonuje walidację danych wejściowych, ale dodatkowo, jeśli dane wejściowe przechodzą walidację, to również wykonuję wstawienie nowych rekordów i zapisanie w form.vars.id unikalnego "id" nowego rekordu.

Obiekt SQLFORM również automatycznie zajmuje się polami "upload" dokonując zapisu przesyłanych plików w folderze "uploads" (po zmianie nazwy pliku w celach uniknięcia konfliktów i zabezpieczenia przed atakami „directory traversal”) i przechowując ich nazwy (nowe nazwy) w odpowiednim polu w bazie danych. Nowa nazwa pliku zostaje udostępniona, po przetworzeniu formularza, w form.vars.fieldname (czyli zastępuje obiekt cgi.FieldStorage w request.vars.fieldname), więc można łatwo odwoływać się do nowej nazwy.

SQLFORM wyświetla wartości "logiczne" w polach wyboru (checkbox), wartości "tekstowe" w obszarach tekstowych (textarea), natomiast wartości, które muszą się znajdować w określonym zestawie lub bazie danych w listach rozwijanych (drop-downs) a pola "przesyłanych danych" w odnośnikach umożliwiających użytkownikom pobieranie plików. Ukrywa on pola "blob", ponieważ muszą być obsługiwane inaczej, co omówione jest dalej.

Dla przykładu przeanalizujmy następujący model:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('married', 'boolean'),
    Field('gender', requires=IS_IN_SET(['Male', 'Female', 'Other'])),
    Field('profile', 'text'),
    Field('image', 'upload'))

W tym przypadku, SQLFORM(db.person) wygeneruje następujący formularz:

image

Konstruktor SQLFORM umożliwia różnorakie przystosowywania formularza, takie jak wyświetlanie tylko podzestawu pól, zmianę etykiet, dodawanie wartości do opcjonalnej trzeciej kolumny lub tworzenie formularzy UPDATE i DELETE, a nie standardowych formularzy INSERT. SQLFORM jest obiektem, który na tle innych obiektów web2py, potrafi najbardziej oszczędzić czas programisty.

Klasa SQLFORM została zdefiniowana w "gluon/sqlhtml.py". Można ją łatwo rozszerzyć nadpisując jej metodę xml serializującą obiekty, tak aby zmienić jej wyjście.

fields
labels
Sygnatura konstruktora SQLFORM jest następująca:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
  • Opcjonalny drugi argument zamienia formularz INSERT na formularz UPDATE dla określonego rekordu (patrz następny podrozdział).
    showid
    delete_label
    id_label
    submit_button
  • Jeśli deletable jest ustawiony na True, formularz UPDATE wyświetla pole wyboru "Zaznacz aby skasować". Wartość etykiety tego pola jest ustawiana w argumencie delete_label.
  • submit_button wartość przycisku zatwierdzania.
  • id_label ustawia etykietę "id" rekordu.
  • "Id" rekordu nie jest pokazywany, jeśli argument showid jest ustawiony na False.
  • fields jest opcjonalną listą nazw pól, które chce się wyświetlić. Jeśli lista zostaje dostarczona, to wyświetlane będą tylko pola z tej listy. Na przykład:
1
fields = ['name']
  • labels jest słownikiem etykiet pól. Kluczami słownika są nazwy pól a wartościami, to co ma być wyświetlone jako etykieta pola. Jeśli etykieta nie jest określona, web2py wywodzi etykietę z nazwy pola (nazwa pola jest kapitalizowana a znaki podkreślenia są zamieniane na spacje). Na przykład:
1
labels = {'name':'Your Full Name:'}
  • col3 jest słownikiem wartości dla trzeciej kolumny. Na przykład:
1
2
col3 = {'name':A('what is this?',
      _href='http://www.google.com/search?q=define:name')}
  • linkto i upload są opcjonalnymi ścieżkami URL do kontrolerów zdefiniowanych przez użytkownika, które umożliwiają, aby formularz radził sobie z polami referencyjnymi. Jest to omówione w dalszej części rozdziału.
  • readonly. Jeśli ustawiony na True, formularz jest wyświetlany jako tylko do odczytu.
  • comments. Jeśli ustawiony na False, nie będą wyświetlane komentarze w col3.
  • ignore_rw. Zwykle, w formularzu tworzącym i aktualizującym wyświetlane są tylko pola oznaczone jako writable=True a w formularzu tylko do odczytu pola oznaczone jako readable=True. Ustawienie ignore_rw=True spowoduje, że ograniczenia te będą ignorowane i wyświetlone zostaną wszystkie pola. Jest to stosowane głównie w interfejsie appadmin do wyświetlenia wszystkich pól każdego tabeli, przesłaniając to co wskazuje model.
  • formstyle
    formstyle określa styl jaki będzie użyty podczas serializowania formularza do kodu HTML. Może to być "table3cols" (domyślnie), "table2cols" (jeden wiersz dla etykiety i komentarza i jeden wiersz dla pola wejściowego), "ul" (wytwarza nieuporządkowana listę pól wejsciowych), "divs" (reprezentuje formularz wykorzystujący przyjazne bloki div CSS, dla dowolnego dostosowania), "bootstrap" wykorzystujący klasę bootstrap formularza o wartości "form-horizontal". Argument formstyle może również być funkcją generującą wszystko wewnątrz znacznika FORM. Można przekazać do konstruktora funkcję z dwoma argumentami, form i fields. Wskazówki można znaleźć w pliku kodu źródłowego sqlhtml.py (proszę zobaczyć do funkcji o nazwie formstyle_).
  • buttons
    buttons jest listą helperów INPUT lub TAG.button (choć teoretycznie może być dowolna kombinacją helperów), które mają być dodane do DIV w którym znajduje się przycisk zatwierdzający. Na przykład, dodanie przycisku powrotnego opartego na URL (dla formularza wielostronicowego) i zmiana nazwy przycisku zatwierdzającego:
1
2
buttons = [TAG.button('Back',_type="button",_onClick = "parent.location='%s' " % URL(...),
             TAG.button('Next',_type="submit")]
lub przycisku, który łączy do innej strony:
1
buttons = [..., A("Go to another page",_class='btn',_href=URL("default","anotherpage"))]
  • separator
    separator ustawia ciąg znakowy, który oddziela etykiety formularza od pól wejściowych.
  • Opcjonalnie attributes są argumentami rozpoczynającymi się od znaku podkreślenia, które przekazuje się do znacznika FORM w celu odpowiedniego renderowania obiektu SQLFORM. Oto przykład:
1
2
_action = '.'
_method = 'POST'

Istnieje specjalny atrybut hidden. Gdy słownik jest przekazywany jako hidden, jego elementy są tłumaczone na ukryte pole INPUT (zobacz przykład helpera FORM w rozdziale 5):

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

co powoduje, że ukryte pola są przekazywane w zgłoszeniu, nic więcej, nic mniej. Argumenty form.accepts(...) nie są przeznaczone do odczytywania odebranych ukrytych pól i trzeba przenieść je do form.vars. Powodem tego jest bezpieczeństwo. Ukryte pola mogą być naruszane, więc trzeba jawnie przenieść ukryte pola z żądania do formularza:

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

Metoda process

W SQLFORM dostępna jest metoda process (podobnie jak forms).

Jeśli w SQLFORM chce się wykorzystać argument keepvalues, trzeba przekazać go w metodzie process:

1
if form.process(keepvalues=True).accepted:

SQLFORM a insert/update/delete

Gdy formularz jest akceptowany, SQLFORM tworzy nowy rekord. Zakładając, że form=SQLFORM(db.test), to identyfikator ostatnio utworzonegi rekordu będzie dostępny w myform.vars.id.

usuwanie rekordu

Jeśli przekaże się rekord jako drugi argument konstruktora SQLFORM, formularz stanie się formularzem UPDATE dla tego rekordu. Oznacza to, że podczas składania formularza aktualizowany jest istniejący rekord a nowy rekord jest wstawiany. Jeśli ustawi się deletable=True, formularz UPDATE wyświetla pole wyboru "zaznacz aby usunąć". jeśli zaznaczysz, rekord zostanie usunięty.

Przy składaniu formularza, jeśli zaznaczone jest pole wyboru usuwania, atrybut form.deleted zostaje ustawiany na True.

Zmodyfikujmy kontroler z poprzedniego przykładu, tak aby przekazywał dodatkowy argumentu całkowitoliczbowy w ścieżce URL, na przykład:

1
/test/default/display_form/2

i jeśli istnieje rekord z odpowiednim identyfikatorem, to SQLFORM generuje dla tego rekordu formularz UPDATE/DELETE:

1
2
3
4
5
6
7
8
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

Linia w odnajduje rekord a linia 3 wykonuje formularz UPDATE/DELETE form. Linia 4 realizuje całe przetwarzanie odpowiedniego formularza.

Formularz aktualizujący jest bardzo podobny do utworzonego formularza z tym, że jest on wstępnie wypełniony danymi bieżącego rekordu oraz do podgladu obrazów. Mamy domyślnie ustawione deletable = True co oznacza, że formularz aktualizujący będzie wyświetlał opcję "delete record".

Formularze edycyjne zawierają również ukryte pole INPUT z name="id", które jest używane do identyfikacji rekordu. Identyfikator ten jest również przechowywany po stronie serwera dla dodatkowego bezpieczeństwa i jeśli odwiedzajacy będzie coś "majstrował" z wartością tego pola, UPDATE nie zostanie wykonane a web2py zgłosi SyntaxError, "user is tampering with form".

Gdy pole jest oznaczone z writable=False, nie będzie pokazywane w tworzonym formularzu a w formularzach aktualizujących będzie wyświetlane jako pole tylko do odczytu. Jeśli pole jest oznaczone jako writable=False i readable=False, to nie będzie wyświetlane w ogóle, nawet w formularzach aktualizujących.

Formularze tworzone przez

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

ignorują atrybuty readable i writable oraz zawsze pokazują wszystkie pola. Formularze w appadmin ignoruja domślnie te zachowanie.

Formularze tworzone przez

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

wyświetlają wszystkie pola w trybie tylko do odczytu i nie mogą być akceptowane.

Oznakowanie pola przez writable=False zapobiega włączeniu tego pola w strukturę formularza i powoduje przetwarzanie formularza z pominięciem wartości request.vars.field. Jednakże, jeśli przypisze się wartość form.vars.field, to ta wartość stanie się częścią polecenia wstawiającego lub aktualizującego podczas przetwarzania formularza. Umożliwia to zmianę wartości pól, które z jakichś powodów nie chcemy umieszczać w formularzu.

SQLFORM w HTML

Czasem chce się użyć SQLFORM, aby wykorzystać jego generator formularzy i prztwarzanie, ale potrzebny jest większy poziom dostosowania kodu HTML formularza, który nie można osiągnąć z parametrami obiektu SQLFORM, wieć trzeba zaprojektować formularz używając HTML.

Edytujmy teraz poprzedni kontroler i dodajmy nową akcję:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def display_manual_form():
    form = SQLFORM(db.person)
    if form.process(session=None, formname='test').accepted:
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    # Note: no form instance is passed to the view
    return dict()

oraz wstawmy formularz w związany widok "default/display_manual_form.html":

1
2
3
4
5
6
7
8
{{extend 'layout.html'}}
<form action="#" enctype="multipart/form-data" method="post">
<ul>
  <li>Your name is <input name="name" /></li>
</ul>
  <input type="submit" />
  <input type="hidden" name="_formname" value="test" />
</form>

Proszę zauważyć, że ta akcja nie zwraca formularza, ponieważ nie trzeba przekazywać go do widoku. Widok zawiera formularz utworzony ręcznie w HTML. Formularz zawiera ukryte pole "_formname", którego wartość musi być taka sama jak formname określonego jako argument accepts w akcji. web2py używa tej nazwy formularza w przypadku, gdy istnieje wiele formularzy na tej samej stronie, do określenia, który z nich został zgłoszony. Jeśli strona zawiera tylko pojedynczy formularz, można ustawić formname=None i pominąc w widoku ukryte pole.

Metoda form.accepts będzie wyszukiwać wewnątrz response.vars danych odpowiadających polom w tabeli db.person. Pola te są deklarowane w formacie HTML

1
<input name="field_name_goes_here" />

Proszę zauważyć, że w podanym przykładzie, zmienne formularza bedą przekazywane jako argumenty w adresie URL. Jeśli nie jest to pożądane, musi się określić protokół POST. Proszę ponadto mieć na uwadze, że jeśli określi się pola upload, formularz będzie musiał być ustawiony na zezwolenie tych pól. Tutaj są pokazane obie opcje:

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

SQLFORM a pola plikowe

Pola typu upload (pola plikowe) są specjalne. Są renderowane jako pola INPUT type="file". Jeśli nie określi sie tego inaczej, przesyłany na serwer plik jest strumieniowany do używanej pamięci podręcznej i przechowywany w folderze "uploads" aplikacji z użyciem nowej bezpiecznej nazwy, przypisywanej automatycznie. Nazwa tego pliku jest następnie zapisywana do pól typu upload.

Dla przykładu rozważmy następujący model:

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

Użyjemy tej samej akcji kontrolera, "display_form", pokazanej powyżej.

Po wstawieniu nowego rekrdu, formularz pozwoli na przegladanie pliku. Wybierz, na przykład, obraz jpg. Plik ten zostanie pobrany i zapisany jako:

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

"XXXXXX" jest losowym identyfikatorem przypisywanym plikowi przez web2py.

content-disposition
Proszę zauważyć, że domyślnie, oryginalna nazwa przesłanego pliku, to b16encoded i jest ona użyta do budowania nowej nazwy pliku. Nazwa ta jest pobierana przez domyślną akcję "download" i używana jest do ustawienia zawartości nagłówka disposition na oryginalną nazwę pliku.

Tylko rozszerzenie pliku jest zachowywane. Jest to wymóg bezpieczeństwa, ponieważ nazwa pliku może zawierać znaki specjalne, które mogą posłużyć odwiedzającym do przeprowdzenie ataków z przeglądaniem katalogów lub do wykonania innych szkodliwych operacje.

Nowa nazwa pliku zostaje zapisana w form.vars.image.

Podczas edytowania rekordu przy użyciu formulara UPDATE, pożądane jest udostępnienie odnośnika do przesłanego pliku i web2py oferuje na to sposób.

Jeśli przekaże się adres URL do konstruktora SQLFORM poprzez argument upload, web2py użyje akcji określonej w tym adresie URL do pobrania pliku. Przyjrzyjmi się następujacej akcji:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
   record = db.person(request.args(0))
   form = SQLFORM(db.person, record, deletable=True,
                  upload=URL('download'))
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

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

Wstaw teraz nowy rekord do adresu URL:

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

Prześlij zdjęcie, zatwierdź formularz i następnie edytuj nowo utworzony rekord, odwiedzając:

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

(zakładamy tu, że ostatani rekord ma id=3). Formularz wyświetli podgląd obrazu, tak jak pokazano niżej:

image

Formularz ten, po serializacji, wygeneruje następujący kod HTML:

1
2
3
4
5
6
7
<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>

Zawiera on odnośnik umożliwiający pobieranie załadowanego pliku z serwera oraz pole wyboru do usuwania pliku z rekordu bazy danych (i tym samym przechowywaniu NULL w polu "image").

Dlaczego ten mechanizm jest udostępniany? Dlaczego trzeba napisać samemu funkcję pobierania? Bo można wymusić jakiś machanizm autoryzacji w funkcji pobierającej. Przykład znajduje się w rozdziale 9.

Normalnie przesyłane pliki są przechowywane a katalogu "app/uploads", ale można określić inną lokalizację:

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

W większości systemów operacyjnych, dostęp do systemu plików może być wolny, gdy w fplderze znajduje się wiele plików. Gdy planuje się przesyłanie więcej niż 1000 plików, można poprosić web2py o przesyłanie plików do podfolderów:

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

Przechowywanie oryginalnych nazw plików

Web2py automatycznie zapsisuje oryginalną nazwę pliku wewnątrz nowej nazwy UUID pliku i pobiera ją, gdy plik jest pobierany z serwera. Po pobraniu pliku z serwera, oryginalna nazwa pliku jest zapisywana w nagłówku content-disposition odpowiedzi HTTP. Dzieje się to w sposób przejrzysty, bez konieczności programowania.

Czasami zachodzi konieczność zapisania oryginalnej nazwy pliku w bazie danych. W takim przypadku trzeba zmodyfikować model i dodać pole do przechowywania tej nazwy:

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

Potem trzeba zmodyfikować kontroler, tak aby to obsługiwał:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
    record = db.person(request.args(0)) or redirect(URL('index'))
    url = URL('download')
    form = SQLFORM(db.person, record, deletable=True,
                   upload=url, fields=['name', 'image'])
    if request.vars.image!=None:
        form.vars.image_filename = request.vars.image.filename
    if form.process().accepted:
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Proszę zauważyć, że SQLFORM nie wyświetla pola "image_filename". Akcja "display_form" przenosi nazwę pliku request.vars.image do form.vars.image_filename, aby można ją było przetworzyć przez accepts i zapisać w bazie danych. Zanim funkcja pobierajaca prześle plik, sprawdzi w bazie danych oryginalną nazwę pliku i użyje ją w nagłówku content-disposition.

Atrybut autodelete

autodelete

SQLFORM, przy usuwaniu rekordu, nie usuwa fizycznie plików przesłanych na serwer do których odnosi się ten rekord. Jest to spowodowane tym, że web2py nie wie czy ten sam plik nie jest używany (zlinkowany) z innym tabelami lub używany w innych celach. Jeśli wie się, że dany plik można bezpiecznie usunąć podczas usuwania związanego z nim rekordu, można wykonać co następuje:

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

Atrybut autodelete jest ustawiany domyślnie na False. Gdy ustawi się go na True, plik zostanie usunięty przy usuwaniu rekordu.

Odnośniki do rekordów

Rozważmy teraz przypadek dwóch tabel połączonych przez referencje:

1
2
3
4
5
6
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 ma psy a każdy pies należy do jakiegoś właściciela, który jest osobą. Właściciel psa jest zobowiazany do odniesienia prawidłowego pola db.person.id przez nazwę '%(name)s'.

Wykorzystajmy interfejs appadmin naszej aplikacji do dodania kilku osób i ich psów.

Podczas edytowania istniejącej osoby, formularz UPDATE appadmin pokazuje odnośnik do strony z wykazem psów należących do tej osoby. Zachowanie takie może być replikowane przy pomocy argumentu linkto. Argument linkto musi wskazywać na lokalizator URL nowej akcji, która otrzymuje ciąg zapytania z SQLFORM oraz listy odpowiednich rekordów. Oto przykład:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)

a to strona:

image

Jest tu odnośnik "dog.owner". Nazwa tego odnośnika może być zmieniona poprzez argument labels, na przykład:

1
labels = {'dog.owner':"This person's dogs"}

Kliknięcie na ten odnośnik spowoduje przekierowanie na:

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

"list_records" jest określoną akcją, gdzie request.args(0) ustawia nazwę przywoływanej tabeli a request.vars.query ustawia ciąg zapytania SQL. Ciag zapytania zawiera w lokalizatorze URL odpowiednio zakodowaną wartość "dog.owner=5" (web2py rozkodowuje to automatycznie podczas parsowania URL).

Można łatwo zaimplementować bardzo ogólną akcję "list_records" w następujacy sposób:

1
2
3
4
5
6
7
8
9
def list_records():
    import re
    REGEX = re.compile('^(\w+).(\w+).(\w+)\=\=(\d+)$')
    match = REGEX.match(request.vars.query)
    if not match:
        redirect(URL('error'))
    table, field, id = match.group(2), match.group(3), match.group(4)
    records = db(db[table][field]==id).select()
    return dict(records=records)

ze związanym widokiem "default/list_records.html":

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

Gdy zwracany jest przez *select* zestaw rekordów i serializowany w widoku, to najpierw jest on konwertowany do obiektu SQLTABLE (nie tego samego co Table) i następnie jest serializowany do tabeli HTML, gdzie każde pole odpowiada kolumnie tabeli.

Wstępne wypełnianie formularzy

Można zawsze wstępnie wypełnić formularz posługujac sie składnią:

1
form.vars.name = 'fieldvalue'

Wyrażenia podobne do powyższego muszą być wstawiane po deklaracji formularza ale przed zakceptowaniem tego formularza, niezaleznie od tego, czy pole ("name" w naszym przykładzie) jest jawnie wizualizowane w formularzu, czy nie.

Dodawanie dodatkowych elementów formularza do SQLFORM

Czasem po utworzeniu formularza zachodzi potrzeba dodania do formularza dodatkowego elementu. Na przykład, można chcieć dodać pole wyboru z potwierdzeniem, ze użytkownik akceptuje regulamin strony internetowej:

1
2
3
form = SQLFORM(db.yourtable)
my_extra_element = TR(LABEL('I agree to the terms and conditions'),                       INPUT(_name='agree',value=True,_type='checkbox'))
form[0].insert(-1,my_extra_element)

Zmienna my_extra_element powinna być dostosowana do *formstyle*. W naszym przykładzie domyślnie przyjęto formstyle='table3cols'.

Po złożeniu formularza form.vars.agree będzie zawierał stan pola wyboru, co może być wykorzystane, na przykład, w funkcji onvalidation.

SQLFORM bez IO bazy danych

Czasem zachodzi potrzeba wygenerowania formularza z tabeli bazy danych przy użyciu SQLFORM i odpowiedniego sprawdzenia zgłoszonego formularza, ale bez jakichkolwiek automatycznych działań INSERT/UPDATE/DELETE na bazie danych. Jest to przypadek występujacy, na przykład, gdy trzeba przetworzyć pola z uwzglednieniem wartości innych pól wejściowych. Przypadek ten występuje również, gdy potrzeba wykonać dodatkową walidację wprowadzonych danych, czego nie można wykonać stosując standardowe walidatory.

Można to łatwo zrobić poprzez rozbicie:

1
2
3
form = SQLFORM(db.person)
if form.process().accepted:
    response.flash = 'record inserted'

na:

1
2
3
4
5
form = SQLFORM(db.person)
if form.validate():
    ### transakcja z jawnym przesłaniem pliku
    form.vars.id = db.person.insert(**dict(form.vars))
    response.flash = 'record inserted'

To samo może być zrealizowane dla formularzy UPDATE/DELETE przez rozbicie:

1
2
3
form = SQLFORM(db.person,record)
if form.process().accepted:
    response.flash = 'record updated'

na:

1
2
3
4
5
6
7
form = SQLFORM(db.person,record)
if form.validate():
    if form.deleted:
        db(db.person.id==record.id).delete()
    else:
        record.update_record(**dict(form.vars))
    response.flash = 'record updated'

W przypadku tabeli zawierającej pole typu "upload" ("fieldname"), zarówno process(dbio=False) jak i validate() domyślnie dotyczą przechowywania przesyłanego pliku, jak w przypadku process(dbio=True).

Nazwę przypisaną przez web2py do przesyłanego pliku można znaleźć w:

1
form.vars.fieldname

Inne typy formularzy

SQLFORM.factory

Istnieją przypadki, gdy potrzeba wygenerować fomularz, który nie jest skojarzony z tabelą bazy danych, ale posiadający pozostałe możliwosci formularza generowanego przez SQLFORM, takie jak ładny wygląd oparty na CSS oraz przesyłanie plików na serwer i zmianianie ich nazwy.

Można do zrobić przez form_factory. Oto przykład, w którym generowany jest formularz, wykonywana jest walidacja, przesyłany jest plik i wszystkie dane zostają zapisane w session :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def form_from_factory():
    form = SQLFORM.factory(
        Field('your_name', requires=IS_NOT_EMPTY()),
        Field('your_image', 'upload'))
    if form.process().accepted:
        response.flash = 'form accepted'
        session.your_name = form.vars.your_name
        session.your_image = form.vars.your_image
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Obiekt Field w konstruktorze SQLFORM.factory() jest w pełni udokumentowany w DAL chapter. Technika konstruowania w czasie rzeczywistym w SQLFORM.factory() jest taka:

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

Oto widok "default/form_from_factory.html":

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

W etykietach pola trzeba używać znaku pokreślenia zamiast spacji lub jawnie przekazać słownik labels do form_factory, tak samo jak sie to robi w SQLFORM. Domyślnie SQLFORM.factory generuje formularz, wykorzystując atrybut "id" HTML, wygenerowany tak jakby formularz został wygenerowany z tabeli o nazwie "no_table". Zmiane nazwy tej atrapy tabeli można wykonać stosując atrybut table_name w metodzie factory:

1
form = SQLFORM.factory(...,table_name='other_dummy_name')

Zmiana table_name jest niezbędna, jeśli chce się umieścić dwie "fabryki" generujące formularze w tej samej tabeli i chce się uniknąć konfliktów CSS.

Przesyłanie plików w SQLFORM.factory

Jeden formularz dla wielu tabel

Zdarza się często, że mając dwie tabele (na przykład 'client' i 'address'), które są połączone z sobą przez odniesienie, chce się utworzyć pojedynczy formularz, który umożliwia wstawianie do obu tych tabel (czyli o kliencie i jego adresach). Oto jak to zrobić:

model:

1
2
3
4
5
6
db.define_table('client',
     Field('name'))
db.define_table('address',
    Field('client','reference client',
          writable=False,readable=False),
    Field('street'),Field('city'))

kontroler:

1
2
3
4
5
6
7
8
def register():
    form=SQLFORM.factory(db.client,db.address)
    if form.process().accepted:
        id = db.client.insert(**db.client._filter_fields(form.vars))
        form.vars.client=id
        id = db.address.insert(**db.address._filter_fields(form.vars))
        response.flash='Thanks for filling the form'
    return dict(form=form)

SQLFORM.factory wykonuje wspólny formularz przy użyciu pól publicznych z obu tabel i dziedziczy też ich walidatory. Wspólny formularz akceptuje dwie metody wstawiające, kilka danych z pierwszej tabeli i kilka danych z drugiej.

Działa to tylko wtedy, gdy tabele nie mają wspólnych pól.

Formularze potwierdzające

confirm

Często potrzebny jest formularz potwierdzający wybór. Akceptowanie tego formularza jest ostatecznym potwierdzeniem wyboru. Formularz może mieć dodatkowe opcje, które łaczą z innymi stronami internetowymi. web2py dostarcza prosty sposób na wykonanie tego:

1
2
form = FORM.confirm('Are you sure?')
if form.accepted: do_what_needs_to_be_done()

Formularz potwierdzający nie musi i nie może wywoływać .accepts lub .process, ponieważ jest to wykonywane wewnętrznie. Można dodać przycisk z odnośnikami do formularza potwierdzającego w formularzu słownika {'value':'link'}:

1
2
form = FORM.confirm('Are you sure?',{'Back':URL('other_page')})
if form.accepted: do_what_needs_to_be_done()

Formularz do edytowania słownika

Wyobraźmy sobie system, który przechowuje opcje konfiguracyjne w słowniku:

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

i potrzebny jest formularz umożliwiający odwiedzającym modyfikowanie tego słownika. Można to zrealizować tak:

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

Formularz będzie wyświetlał pole INPUT dla każdej pozycji słownika. Wykorzystuje on klucze słownika jako nazwy i etykiety pól INPUT i bieżące wartości do wnioskowania typów (string, int, double, date, datetime, boolean).

Dział to doskonale, ale pozostawiamy Tobie wykonanie logiki realizującej utrwalenie konfiguracji słownika. Na przykład można przechowywać config w sesji:

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

CRUD

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

Jednym z najnowszych dodatków do web2py jest API CRUD (Create/Read/Update/Delete) stworzony na szczycie SQLFORM. CRUD tworzy SQLFORM, ale upraszcza kodowanie, ponieważ obejmuje tworzenie formularza, przetwarzanie formularza, powiadomienie i przekierowanie - wszystko w jednej funkcji.

Na samym początku trzeba podkreślić, że CRUD różni się od innych interfejsów API web2py, jakie używaliśmy do tej pory tym, że nie jest on domyślnie udostępniany. Jest to ważne. Trzeba go też połączyć z określoną bazą danych. Na przykład:

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

Zdefiniowany powyżej obiekt crud dostarcza następujące API:

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

  • crud.tables() zwraca litę tabel zdefiniowanych w bazie danych.
  • crud.create(db.tablename) zwraca formularz utworzony dla tabeli tablename.
  • crud.read(db.tablename, id) zwraca formularz tylko do odczytu dla tablename i rekordu id.
  • crud.update(db.tablename, id) zwraca formularz aktualizujący dla tablename i rekordu id.
  • crud.delete(db.tablename, id) usuwa rekord.
  • crud.select(db.tablename, query) zwraca listę rekordów wybranych z tabeli.
  • crud.search(db.tablename) zwraca krotkę (form, records) gdzie form jest formularzem wyszukiwania a records jest listą rekordów wybranych na podstawie zgłoszonego formularza wybierającego.
  • crud() zwraca jeden z powyższych wyników w oparciu o request.args().

Na przykład, następujaca akcja:

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

udostępniać będzie następujace adresy URL:

1
2
3
4
5
6
7
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]

Jednakże akcja:

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

udostępniać będzie tylko metodę create:

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

Natomiast akcja:

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

będzie udostępniać tylko metodę update:

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

i tak dalej.

Zachowanie CRUD można dostosować na dwa sposoby: przez ustawienie kilku atrybutów obiektu crud lub przez przekazanie dotakowych parametrów do każdej jego metody.

Ustawienia

Oto kompletny wykaz aktualnych atrybutów CRUD, ich wartości domyślnych i znaczeń:

  • aby wymusić uwierzytelnianie we wszystkich formularzach CRUD:
1
crud.settings.auth = auth

Użycie jest wyjaśnione w rozdziale 9.

  • aby określić kontroler definiujący funkcję data, która zwraca obiekt crud:
1
crud.settings.controller = 'default'
  • aby określić adres URL przekierowania po pomyślnym utworzoniu rekordu:
1
crud.settings.create_next = URL('index')
  • aby określić adres URL przekierowania po pomyślnym zaktualizowaniu rekordu:
1
crud.settings.update_next = URL('index')
  • aby określić adres URL przekierowania po pomyślnym usunięciu rekordu:
1
crud.settings.delete_next = URL('index')
  • aby określić adres URL, który ma być użyty do zlinkowania przesłanych plików:
1
crud.settings.download_url = URL('download')
  • aby określic dodatkowe funkcje, które mają być wykonane po standardowych procedurach walidacyjnych dla formularzy crud.create:
1
crud.settings.create_onvalidation = StorageList()

StorageList jest taki sam jak obiekt Storage, obydwa są zdefiniowane w pliku "gluon/storage.py", ale StorageList ma wartość domyślną [], gdy wartością domyślną Storage jest None. Ma następującą składnię:

1
crud.settings.create_onvalidation.mytablename.append(lambda form:....)
  • aby określić dodatkowe funkcje, które mają być wykonane po standardowych procedurach dla formularzy crud.update:
1
crud.settings.update_onvalidation = StorageList()
  • aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.create:
1
crud.settings.create_onaccept = StorageList()
  • aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.update:
1
crud.settings.update_onaccept = StorageList()
  • aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.update, jeśli rekord został usunięty:
1
crud.settings.update_ondelete = StorageList()
  • aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu crud.delete:
1
crud.settings.delete_onaccept = StorageList()
  • aby określic, czy formularze "update" powinny mieć przycisk "delete":
1
crud.settings.update_deletable = True
  • aby określić, czy formularze "update" powinny pokazywać identyfikator edytowanego rekordu:
1
crud.settings.showid = False
  • aby określić, czy po pomyślnym złożeniu formularzy należy utrzymywać poprzednio wprowadzone wartości, czy też zresetować formularz do wartości domyślnych:
1
crud.settings.keepvalues = False
  • crud zawsze wykrywa, czy rekord był edytowany przez osoby postronne w czasie pomiędzy momentem wyświetlania formularza a czasem zgłoszenia. Zachowanie to jest równoważne z:
form.process(detect_record_change=True)

i jest ustawiane w:

1
crud.settings.detect_record_change = True

i może być zmienione (wyłączone) przez ustawienie zmiennej na False.

  • styl formularza można zmienić przez:
1
crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul'
  • można ustawić separator we wszystkich formularzach CRUD:
1
crud.settings.label_separator = ':'

CAPTCHA

Do formularzy można dodać CAPTCHA, stosując tą sama konwencję wyjaśnioną w rozdziale 9, tak:

1
2
3
crud.settings.create_captcha = None
crud.settings.update_captcha = None
crud.settings.captcha = None

Komunikaty

Oto wykaz możliwych do dostosowania komunikatów:

  • 1
    crud.messages.submit_button = 'Submit'
    

ustawia tekst przycisku "submit" zarówno dla formularzach :create", jak i "update";

  • 1
    crud.messages.delete_label = 'Check to delete:'
    

ustawia etykietę przycisku "delete" w formularzach "update";

  • 1
    crud.messages.record_created = 'Record Created'
    

ustawia komunikat fleszowy przy pomyślnym utworzeniu rekordu;

  • 1
    crud.messages.record_updated = 'Record Updated'
    

ustawia komunikat fleszowy po pomyślnym zaktualizowaniu rekordu;

  • 1
    crud.messages.record_deleted = 'Record Deleted'
    

ustawia komunikat fleszowy po pomyślnym usunięciu rekordu;

  • 1
    crud.messages.update_log = 'Record %(id)s updated'
    

ustawia komunikat dziennika po pomyślnym zaktualizowaniu rekordu;

  • 1
    crud.messages.create_log = 'Record %(id)s created'
    

ustawia komunikat dziennika po pomyślnym utworzeniu rekordu;

  • 1
    crud.messages.read_log = 'Record %(id)s read'
    

ustawia komunikat dziennika po pomyślnym dostępie do odczytu rekordu;

  • 1
    crud.messages.delete_log = 'Record %(id)s deleted'
    

ustawia komunikat dziennika po pomyślnym usunięciu rekordu;

Proszę zwrócić uwagę, że crud.messages należy do klasy gluon.storage.Message, która jest podobna do gluon.storage.Storage, ale automatycznie przekłada swoje wartości, bez potrzeby używania operatora T.

Komunikaty dziennika są używane tylko i wyłącznie, gdy CRUD jest połączony z uwierzytelnianiem, tak jak omówiono to w rozdziale 9. Zdarzenia są rejestrowane w tabeli uwierzytelniania "auth_events".

Metody

Zachowanie metod CRUD można również dostosować dla każdego poszczególnego wywołania. Oto sygnatury metod:

1
2
3
4
5
6
7
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)
  • table jest tabelą DAL lub tablename przedmiotowej metody.
  • record i record_id są identyfikatorami rekordu przedmiotowej metody.
  • next jest adresem URL przekierowania po pomyślnym wykonania metody. Jeśli adres URL zawiera podłańcuch "[id]", to zostanie on zastąpiony przez identyfikator aktualnie utworzonego lub zaktualizowanego rekordu.
  • onvalidation ma tą samą funkcję, co SQLFORM(..., onvalidation)
  • onaccept jest funkcją, która ma być wywołana po rozpatrzeniu i zaakceptowaniu złożenego formularza, ale przed przekierowaniem.
  • log jest komunikatem dziennikowym. Komunikat dziennika w CRUD widzi zmienne w słowniku form.vars, takie jak "%(id)s".
  • message jest to komunikat fleszowy po zaakceptowaniu formularza.
  • ondelete jest wywoływane w miejsce onaccept, gdy rekod zostaje usunięty w formularzu "update".
  • deletable określa, czy formularz "update" powinien mieć opcje usuwania.
  • query jest zapytaniem, które ma być zastosowane do wyboru rekordów.
  • fields jest to lista pól do wyboru.
  • orderby określa, w jakiej kolejności powinny być zestawiane wybierane rekordy (zobacz DAL rozdział).
  • limitby określa zakres wybranych rekordów, które mają być wyświeltlone (zobacz rozdział 6).
  • headers - słownik z nazwami nagłówka tabeli.
  • queries- lista taka jak ['equals', 'not equal', 'contains'] zawierająca metody dopuszczalne w formularzu wyszukiwania.
  • query_labels - słownik taki jak query_labels=dict(equals='Equals') podajacy nazwy metod wyszykiwania.
  • fields - lista listą pól, które maja byc wylistowane w widżecie wyszzukiwania.
  • field_labels - słownik mapujący nazwy pól na etyliety.
  • zero - domyślnie "choose one", jest używane jako domyślna opcja dla pól rozwijanych w widżecie wyszukiwania.
  • showall - ustawienie na True powoduje zwracanie wierszy jak dla każdego zapytania w pierwszym wywołaniu (dodano w wersjach po 1.98.2).
  • chkall - ustawienie na True powoduje włączenie wszystkich pól wyboru w formularzu wyszukiwania (dodano w wersjach po 1.98.2).

Oto przykład zastosowania w pojedynczej funkcji kontrolera:

1
2
3
4
5
6
7
## assuming db.define_table('person', Field('name'))
def people():
    form = crud.create(db.person, next=URL('index'),
           message=T("record created"))
    persons = crud.select(db.person, fields=['name'],
           headers={'person.name': 'Name'})
    return dict(form=form, persons=persons)

Oto następna bardzo ogólna funkcja kontrolera, która pozwala wyszukiwać, tworzyć i edytować każdy rekord z dowolnej tabeli, gdzie nazwa tabeli przekazuje request.args(0):

1
2
3
4
5
6
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)

Proszę zwrócić uwagę na linię table.id.represent=..., która powiadamia web2py, aby zmienił reprezentację pola id i zamiast tego wyświetlił odnośnik do samej strony i przekazał identyfikator jako request.args(1), co włącza utworzoną stronę do strony aktualizacyjnej.

Wersjonowanie rekordów

Zarówno SQLFORM jak i CRUD dostarczają narzędzie dla wersjonowania rekordów bazy danych:

Jeśli chce sie mieć tabelę (db.mytable), która wymaga pełnej historii zmian, można to zrobić następująco:

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

Metoda auth.archive definuje nową tabelę o nazwie db.mytable_archive (nazwa pochodzi od nazwy tabeli do której się odwołuje) i aktualizuje rekord, przechowując kopie rekordu (w postaci z przed aktualizacji) w utworzonej tabeli archiwalnej, w tym odniesienie do bieżącego rekordu.

Ponieważ rekord jest w rzeczywistości aktualizowany (archiwizowany jest tylko jego poprzedni stan), odniesienia nie są nigdy zrywane.

Wszystko dzieje się "pod maską". Jeśli chce się uzyskać dostęp do tabeli archiwalnej, trzeba zdefiniować to w modelu:

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

Proszę zauważyć, że tabela ta rozszerza db.mytable (w tym wszystkie jej pola) i dodaje odniesienia do current_record.

Metoda auth.archive nie wykonuje znacznika czasu zapisywanego rekordu, chyba że oryginalna tabela ma pola znacznika czasu, na przykład:

1
2
3
4
5
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),

Nie ma nic szczególnego w tych polach i można nadawać im dowolną nazwę. Zostają wypełnione przed archiwizacją rekordu i sa archiwizowane w każdej kopii rekordu. Nazwa tabeli archiwalnej i ewentualnie nazwa odnoszonego pola mogą być zmieniane w ten sposób:

1
2
3
4
5
6
7
8
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'))

Niestandardowe formularze

Jeśli formularz jest tworzony z SQLFORM, SQLFORM.factory lub CRUD, to istnieje wiele sposobów na osadzenie formularzy w widoku umożliwiającym różnorakie dostosowanie. Rozważmy dla przykładu następujacy model:

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

i akcję aktualizującą:

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

Najprostrzym sposobem osadzenia formularza w widoku dla upload_image jest:

1
{{=form}}

Prowadzi to do standardowego układu tabeli. Jeśli chce się użyć innego układu, można podzielić formularz na elementy:

1
2
3
4
5
{{=form.custom.begin}}
Name: <div>{{=form.custom.widget.name}}</div>
File: <div>{{=form.custom.widget.source}}</div>
{{=form.custom.submit}}
{{=form.custom.end}}

gdzie form.custom.widget[fieldname] zostanie zserializowane na widżet właściwy dla pola. Jeśli formularz zostanie zgłoszony i wypełniony błędami, są one jak zwykle dołączane poniżej widżetów.

Powyższy formularz pokazuje ta ilustracja:

image

Podobny efekt można by uzyskać bez stosowania własnego formularza:

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

lub w przypadku formularzy CRUD przez zastosowanie następujacych parametrów:

1
crud.settings.formstyle='table2cols'

Inne możliwości dla formstyle, to "table3cols" (domyślnie), "divs" i "ul".

Jeśli nie chce się stosować widżetów serializowanych przez web2py, można zamienić je na kod HTML. Istnieje kilka zmiennych, które można do tego wykorzystać:

  • form.custom.label[fieldname] zawiera etykietę dla pola.
  • form.custom.comment[fieldname] zawiera komentarz dla pola.
  • form.custom.dspval[fieldname] form-type i field-type zależne od reprezentacji wyświetlania pola.
  • form.custom.inpval[fieldname] form-type i field-type zalezne od wartości, jakie maja być uzyte w kodzie pola.

Jeśli formularz ma deletable=True, to powinno się również wstawić:

1
{{=form.custom.delete}}

dla wyświetlania pól wyboru usuwania.

Ważne jest, aby postępować z niżej opisaną konwencją.

Konwencje CSS

Zanaczniki generowane w formularzach przez SQLFORM, SQLFORM.factory i CRUD spełniają ściśle konwencję nazewniczą CSS, która może być (a właściwie powinna być) wykorzystana przy dalszym dostosowywaniu formularzy.

Tabela "mytable" i pole "myfield" typu "string", renderowane domyślnie przez:

1
SQLFORM.widgets.string.widget

będą wyglądać tak:

1
2
<input type="text" name="myfield" id="mytable_myfield"
       class="string" />

Proszę zauważyć, że:

  • klasa znacznika INPUT jest taka sama, jak typ pola. Jest to bardzo ważne dla działania kodu jQuery w "web2py_ajax.html". Daje to pewność, że można mieć tylko liczby w polach typu "integer" i "double" oraz, że pola "time", "date" i "datetime" wyświetlają okienko "calendar/datepicker";
  • id jest nazwą klasy plus nazwa pola, połączone znakiem podkreślenia. Zapewnia to unikatowość odniesienia do pola poprzez, na przykład, jQuery('#mytable_myfield') i manipulowanie arkuszem stylów pola lub powiązanie akcji ze zdarzeniami pola (focus, blur, keyup itd.);
  • name to, jak można sie spodziewać, nazwa pola.

Ukrywanie komunikatów o błędach

hideerror

Czasem zachodzi potrzeba wyłaczenia automatycznego umieszczania komunikatów o błędach i wyświetlania tych komunikatów w innym miejscu niż domyślnie. Można to łatwo zrobić.

  • W przypadku FORM lub SQLFORM, przekaż hideerror=True do metody accepts.
  • W przypadku CRUD, ustaw crud.settings.hideerror=True

Można również zmodyfikować widoki, aby wyświetlać komunikaty błedów (ponieważ nie są one już wyświetlane automatycznie).

Oto przykład, w którym komunikaty błedów sa wyswietlane powyżej formularza a nie w formularzu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{if form.errors:}}
  Your submitted form contains the following errors:
  <ul>
  {{for fieldname in form.errors:}}
    <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li>
  {{pass}}
  </ul>
  {{form.errors.clear()}}
{{pass}}
{{=form}}

Ilustruje to ten obrazek:

image

Mechanizm ten działa również w niestandardowych formularzach.

Walidatory

walidatory

Walidatory, to klasy stosowane do sprawdzania pól wejściowych (w tym formularzy generowanych z tabel bazy danych). W zaawansowanych formularzach pochodzących z SQLFORM, walidatory tworzą widżety takie jak rozwijane menu i podglądy innych tabel.

Oto przykład używania walidatora w FORM:

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

Tu natomiast jest przykład zastosowania walidatora dla pola tabeli:

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

Walidatory zawsze są przypisywane przy zastosowaniu atrybutu requires pola. Pole może mieć pojedynczy walidator lub wiele walidatorów. Wiele walidatorów jest przypisywanych w postaci listy:

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

Walidatory są normalnie wywoływane automatycznie przez funkcję accepts i process w FORM lub inny pomocniczy obiekt HTML zawierający formularz. Wywoływane są one w kolejności, w jakiej są listowane.

Można również wywołać walidatory jawnie w polu:

db.person.name.validate(value)

co zwraca krotkę (value,error), w której error ma wartość None w przypadku braku wartości walidacyjnej.

Wbudowane walidatory mają konstruktory pobierające opcjonalny argument:

1
IS_NOT_EMPTY(error_message='cannot be empty')

error_message pozwala zastąpić domyślny komunikat błędu dla każdego walidatora.

Oto przykład walidatora na tabeli bazy danych:

1
db.person.name.requires = IS_NOT_EMPTY(error_message='fill this!')

gdzie mamy dostępny operator translacji T, aby umożliwić umiędzynarodowienie. Proszę zauważyć, że domyślny komunikat błędów nie jest tłumaczony.

Trzeba pamiętać, że walidatory, które mogą być użyte z polami typu list:, to:

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

Ten ostatni może być używany do zastosowania dowolnego walidatora do poszczególnych elementów w liście. multiple=(1,1000) wymaga wyboru elementów pomiędzy 1 a 1000. Wymusza to wybór co najmniej jednego elementu.

Walidatory formatu tekstowego

IS_ALPHANUMERIC
IS_ALPHANUMERIC

Walidator ten sprawdza, czy pole zawiera znaki alfa-numeryczne w zakresie a-z, A-Z lub 0-9.

1
requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!')
IS_LOWER
IS_LOWER

Ten walidator nie zwraca błędu. Po prostu konwertuje wartość pola na małe litery.

1
requires = IS_LOWER()
IS_UPPER
IS_UPPER

Ten walidator nie zwraca błędu. Po prostu konwertuje wartość pola na duże litery.

1
requires = IS_UPPER()
IS_EMAIL
IS_EMAIL

Sprawdza, czy wartość pola wyglada jak adres e-mail. Nie próbuje w celu sprawdzenia wysyłać wiadomości e-mail.

1
requires = IS_EMAIL(error_message='invalid email!')
IS_MATCH
IS_MATCH

Walidator ten dopasowuje wartość pola do wyrażenia regularnego i zwraca błąd, jeśli dopasowanie nie jest zgodne. Oto przykład zastosowania przy sprawdzeniu poprawności z amerykańskim kodem pocztowym:

1
2
requires = IS_MATCH('^\d{5}(-\d{4})?$',
         error_message='not a zip code')

Oto przykład użycia tego walidatora do sprawdzenia adresu IPv4 address (uwaga: bardziej właściwszym walidatorem do tego celu jest IS_IPV4):

1
2
requires = IS_MATCH('^\d{1,3}(.\d{1,3}){3}$',
         error_message='not an IP address')

W tym przykładzie sprawdzamy poprawność amerykańskiego numeru telefonicznego:

1
2
requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$',
         error_message='not a phone number')

Więcej informacji o wyrażeniach regularnych Pythona można znaleźć w oficjalnej dokumentacji Pythona.

IS_MATCH akceptuje opcjonalny argument strict, z domyślną wartością False. Gdy ustawi się go na True, to porównywany bedzie tylko początek łańcucha:

>>> IS_MATCH('ab', strict=False)('abc')
('abc', None)
>>> IS_MATCH('ab', strict=True)('abc')
('abc', 'Invalid expression')

IS_MATCH ma też opcjonalny argument search, którego domyślna wartość, to False. Gdy ustawi się go na True, to do sprawdzenia wartości będzie stosowana metoda search zamiast metody match.

IS_MATCH('...', extract=True) filtruje i wydziela tylko pierwsze dopasowanie podłańcucha a nie oryginalną wartość.

IS_LENGTH
IS_LENGTH

Sprawdza, czy długość łańcucha wartości pola mieści sie w określonym zakresie. Działa zarówno dla wejściowych pól tekstowych i plikowych.

Jego argumentami są:

  • maxsize: maksymalna długość (wielkość) (domyślnie = 255)
  • minsize: minimalna dopuszczalna długość (wielkośc).

Przykłady: Sprawdzenie, czy łańcuch tekstowy jest krótszy niż 33 znaki:

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

Sprawdzenie, czy hasło jest dłuższe od 5 znaków:

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

Sprawdzenie, czy przesyłany plik ma wielkość pomiędzy 1KB a 1MB:

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

Dla wszystkich typów pól z wyjątkiem plikowych, walidator ten sprawdza długość łańcucha wartości pola. W przypadku plików, wartością jest cookie.FieldStorage, tak więc sprawdzana jest długość danych w pliku, czyli jest to zachowanie, które można się intuicyjnie spodziewać.

IS_URL
IS_URL

Odrzuca łańcuch URL, jeśli wystąpi któryś z poniższych warunków:

  • łańcuch jest pusty lub None;
  • w łańcuchu znajdują sie znaki niedopuszczalne w adresach URL;
  • łańcuch łamie jakiekolwiek reguły składniowe HTTP;
  • schemat określa (jeśli jest określony) coś innego niż 'http' lub 'https';
  • nie istnieje domena najwyższego poziomu (jeśli jest określona nazwa hosta).

Powyższe zasady oparte są na standarcie RFC 2616[RFC2616].

Funkcja ta sprawdza tylko składnię adresów URL. Na przykład, nie sprawdza wskazań URL do rzeczywistego dokumentu lub tego, czy lokalizator URL ma inny sematyczny sens. Dokleja ona automatycznie na początku adresu schemat 'http://' w przypadku skróconego adresu URL (np. 'google.ca').

Gdy zastosowany zostanie parametr mode='generic', to funkcja zmienia zachowanie. Będzie odrzucać łańcuch URL, jeśli wystąpi któryś z poniższych warunków:

  • łańcuch jest pusty lub None;
  • w łańcuchu znajdują sie znaki niedopuszczalne w adresach URL;
  • schemat URL jest nieprawidłowy (jeśli został określony).

Powyższe zasady są oparte na standardzie RFC 2396[RFC2396].

Lista dozwolonych schematów jest dostosowalna przy użyciu parametru allowed_schemes. Jesłi z listy wykluczy się None, to odrzucane będą skrócone adresy URL (bez schematu takiego jak 'http').

Domyślnie schemat adresu URL jest dostosowalny za pomocą parametru prepend_scheme. Jeśli prepend_scheme ustawi się na None, to poprzedzanie adresu URL schematem zostanie wyłączone. Adresy URL wymagające przy parsowaniu dodania schematu nadal będą akceptowane, ale zwracana wartość nie będzie zmodyfikowana.

IS_URL jest kompatybilny ze standardem Internationalized Domain Name (IDN) określonym w dokumencie RFC 3490[RFC3490]. W rezultacie, adresami URL mogą być zwykłe łańcuchy znaków ASCII jak i łańcuchy UNICODE. Jeśli element domenowy adresu URL (np. google.ca) zawiera litery ze zbioru innegi niż US-ASCII, to domena będzie konwertowana do Punycode (określonego w RFC 3492[RFC3492] ). IS_URL idzie troche dalej i pozwala, aby w ścieżce i zapytaniu URL występowały znaki z poza zbioru US-ASCII. Znaki z poza US-ASCII będa kodowane. Na przykład, spacja będzie kodowana jako '%20'. Znak UNICODE 0x4e86 zostanie zakodowany jako '%4e%86'.

Przykłady:

1
2
3
4
5
6
7
requires = IS_URL())
requires = IS_URL(mode='generic')
requires = IS_URL(allowed_schemes=['https'])
requires = IS_URL(prepend_scheme='https')
requires = IS_URL(mode='generic',
                  allowed_schemes=['ftps', 'https'],
                  prepend_scheme='https')
IS_SLUG
IS_SLUG
1
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug')

Jeśli check jest ustawione na True, to sprawdzane jest, czy badana wartość jest nazwą skróconą (ang. slug), dopuszczając tylko znaki alfa-numeryczne i nie powtarzające się myślniki.

Jeśi check jest ustawione na False (domyślnie) to badana wartość jest konwertorowana do nazwy krótkiej.

Walidatory daty i czasu

IS_TIME
IS_TIME

Walidator ten sprawdza, czy pole zawiera wartość będącą czasem w określonym formacie.

1
requires = IS_TIME(error_message='must be HH:MM:SS!')
IS_DATE
IS_DATE

Walidator ten sparwdza, czy pole zawiera wartość będącą datą w określonym formacie. Dobrą praktyka jest stosowanie operatora translacji w celu obsługi różnych lokalizacji.

1
2
requires = IS_DATE(format=T('%Y-%m-%d'),
                   error_message='must be YYYY-MM-DD!')

Pełny opis dyrektyw % znajduje się w opsie walidatora IS_DATETIME.

IS_DATETIME
IS_DATETIME

Walidator ten sprawdza, czy pole zawiera wartość typu datetime w określonym formacie. Dobrą praktyka jest stosowanie operatora translacji w celu obsługi różnych lokalizacji.

1
2
requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'),
                       error_message='must be YYYY-MM-DD HH:MM:SS!')

Poniższe symbole można wykorzystać do formatowania łańcucha datetime (pokazano tu symbol i przykładowy łańcuch):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
%Y  '1963'
%y  '63'
%d  '28'
%m  '08'
%b  'Aug'
%b  'August'
%H  '14'
%I  '02'
%p  'PM'
%M  '30'
%S  '59'
IS_DATE_IN_RANGE
IS_DATE_IN_RANGE

Działa bardzo podobnie do poprzedniego walidatora, ale pozwala określić zakres:

1
2
3
4
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!')

Pełny opis dyrektyw % znajduje sie w opsie walidatora IS_DATETIME.

Walidatory zakresu, zbioru i równości

IS_EQUAL_TO
IS_EQUEL_TO

Sprawdza, czy badana wartość jest równa określonej wartości (która może być zmienną):

1
2
requires = IS_EQUAL_TO(request.vars.password,
                       error_message='passwords do not match')
IS_NOT_EMPTY
IS_NOT_EMPTY

Sprawdza, czy zawartość pola nie jest pustym łańcuchem.

1
requires = IS_NOT_EMPTY(error_message='cannot be empty!')
IS_NULL_OR
IS_NULL_OR

Zdeprecjonowany alias IS_EMPTY_OR opisany poniżej.

IS_EMPTY_OR
IS_EMPTY_OR

Czasem zachodzi potrzeba dopuszczenia pustych wartości w polu razem z innymi wymaganiami. Na przykład pole ,oze być datą, ale może być również puste. Walidator IS_EMPTY_OR to umożliwia:

1
requires = IS_EMPTY_OR(IS_DATE())
IS_EXPR
IS_EXPR

Jego pierwszym argumentem jest łańcuch zawierający wyrażenie logiczne w kontekście wartosci zmiennej. Sprawdza on wartość pola, wyrażenie ewaluuje do True. Na przykład:

1
2
requires = IS_EXPR('int(value)%3==0',
                   error_message='not divisible by 3')

Należy najpierw sprawdzić, czy wartość jest liczbą całkowitą tak, aby nie był zgłaszany wyjątek.

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

Konwertuje dane wejściowe do typu decimal języka Python lub generuje błąd, jeśli liczba dziesiętna sie nie mieści (włącznie) w określonym zakresie. Porówanie jest wykonywane w arytmetyce dziesiętnej języka Python.

Minimalnymi i maksymalnymi ograniczeniami może być odpowiednio None, nie mniej lub nie więcej.

Argument dot jest opcjonalny i pozwala na umiedzynarodowienie symbolu używanego do separacji dziesiętnej.

IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE

Sprawdza, czy wartość pola jest liczbą zmienno-przecinkową w zdefiniowanym zakresie, 0 <= value <= 100 w poniższym przykładzie:

1
2
requires = IS_FLOAT_IN_RANGE(0, 100, dot=".",
         error_message='too small or too large!')

Argument dot jest opcjonalny i pozwala na umiedzynarodowienie symbolu używanego do separacji dziesiętnej.

IS_INT_IN_RANGE
IS_INT_IN_RANGE

Sprawdza, czy wartość pola jest liczbą całkowitą w zdefiniowanym zakresie, 0 <= value < 100 w poniższym przykładzie:

1
2
requires = IS_INT_IN_RANGE(0, 100,
         error_message='too small or too large!')
IS_IN_SET
IS_IN_SET
multiple

W SQLFORM (i siatkach) walidator ten automatycznie ustawia pole formularza na pole z opcjami (czyli na pole rozwijanego menu).

IS_IN_SET sprawdza, czy wartości pól są w zbiorze:

1
2
requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'),
         error_message='must be a or b or c')

Argument zero jest opcjonalny i określa tekst domyślnie wybieranej opcji, która nie została zaakceptowana przez sam walidator IS_IN_SET. Jeśli nie chcesz opcji "choose one", ustaw zero=None.

Elementy walidatora zbioru można łączyć z walidatorem numerycznym, tak długo, jak IS_IN_SET jest pierwszym elementem listy. Spowoduje to wymuszenie konwersji przez walidator do typu numerycznego. Tak więc, IS_IN_SET może poprzedzać IS_INT_IN_RANGE (który konwertuje wartość do liczby całkowitej) lub IS_FLOAT_IN_RANGE (który konwertuje wartość do liczby zmienno-przecinkowej). Na przykład:

1
2
requires = [ IS_IN_SET([2, 3, 5, 7],IS_INT_IN_RANGE(0, 8),
          error_message='must be prime and less than 10')]

Sprawdzanie pól wyboru

Do wymuszenia wypełnienia pola wyboru w formularzu (jak na przykład przy potwierdzaniu regulaminu), można użyć tego:

1
requires=IS_IN_SET(['on'])
Słowniki i krotki z IS_IN_SET

Można również uzyć słownika lub listy krotek do wykonania bardziej opisowej listy rozwijanej:

1
2
3
4
5
Przykład użycia słownika:
requires = IS_IN_SET({'A':'Apple','B':'Banana','C':'Cherry'},zero=None)

Przykład użycia listy krotek:
requires = IS_IN_SET([('A','Apple'),('B','Banana'),('C','Cherry')])
IS_IN_SET a tagowanie

Walidator IS_IN_SET ma opcjonalny atrybut multiple=False. Jeśli ustawi się go na True, to w polu będzie można przechowywać wiele wartości. Pole powinno być typu list:integer lub list:string. Referencje multiple sa obsługiwane automatycznie przy tworzeniu i aktualizowaniu formularzy, ale są niewidoczne dla DAL. Sugerujemy korzystanie z wtyczki multiselect jQuery do renderowania wielowartosciowych pól.

Proszę zauważyć, że gdy multiple=True, walidator IS_IN_SET będzie akceptował zero lub więcej wartości, czyli będzie akceptował pole, gdy nic nie będzie wybrane. Opcja multiple może również być krotką krptka w postacji (a,b) gdzie a i b to minimaln i maksymalna (z wyłączeniem) liczba elementów, które mogą być odpowiednio wybrane.

Walidatory skomplikowania i bezpieczeństwa

IS_STRONG
IS_STRONG

Wymusza wymagania skomplikowania łańcucha w polu (zazwyczaj polu hasła)

Przykład:

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

gdzie

  • min jest minimalna długością łańcucha;
  • special jest minimalną liczbą znaków specjalnych w łańcuchu, takich jak !@#$%^&*(){}[]-+;
  • upper jest minimalna liczbą znaków dużej litery.
CRYPT
CRYPT

Jest to też filtr. Wykonuje bezpieczny skrót z danych wejściowych i jest używany do zabezpieczenia haseł w czasie przekazywania ich do bazy danych.

1
requires = CRYPT()

Domyślnie CRYPT używa 1000 iteracji algorytmu pbkdf2 w połączeniu z SHA512 do wytworzenia 20-bajtowego skrótu. Starsze wersje web2py używały "md5" lub HMAC+SHA512 w zależności od tego, czy klucz został określony czy nie.

Jeśli określi się klucz, CRYPT wykorzysta algorytm HMAC. Klucz może zawierać przedrostek określający algorytm jaki trzeba użyć w HMAC, na przykład SHA512:

1
requires = CRYPT(key='sha512:thisisthekey')

Jest to składnia zalecana. Klucz musi być unikalnym łańcuchem związanym z bazą danych. Klucza nie można nigdy zmienić. Jeśli zgubi się klucz, poprzednie wartości skrótów stają się bezużyteczne.

Domyślnie CRYPT stosuje losową sól, tak więc każdy wynik jest inny. W celu użycia stałej wartości soli trzeba określić jej wartość:

1
requires = CRYPT(salt='mysaltvalue')

lub nie używać soli:

1
requires = CRYPT(salt=False)

Walidator CRYPT miesza swoje wejście, co sprawia że jest dość wyjątkowy. Jeśli zachodzi potrzeba sprawdzenia pola hasła, zanim zostanie zakodowane, można użyć CRYPT w liscie walidatorów, ale trzeba walidator ten umieścić na końcu listy, tak aby został wywołany ostatni. Na przykład:

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

CRYPT pobiera również argument min_length, z wartoscią domyślną 0.

Wynikowy skrót ma postać alg$salt$hash, gdzie alg jest użytym algorytmem mieszania, salt jest łańcuchem soli (który może być pusty) a hash jest wyjściem algorytmu. W konsekwencji, skrót jest samoidentyfikujący się, dzięki czemu algorytm może być zmieniany bez unieważniania poprzednich skrótów. Jednak klucz musi pozostać ten sam.

Walidatory specjalne

IS_LIST_OF
IS_LIST_OF

To właściwie nie jest walidator. Jego przeznaczeniem jest umożliwienie sprawdzania pól, które zwracają wiele wartości. Jest on wykorzystywany w tych rzadkich przypadkach, gdy formularz zawiera wiele pól z o tej samej nazwie lub pole wielokrotnego wyboru. Jego jedynym argumentem jest inny walidator a wszystko co robi, to zastosowanie innego walidatora dla każdego elementu listy. Na przykład, poniższe wyrażenie sprawdza, czy każdy element listy jest liczbą całkowitą w przedziale 0-10:

1
requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))

Nigdy nie zwraca błędu i nie zawiera komunikatu o błędzie. Błędy generuje wewnętrzny walidator.

IS_IMAGE
IS_IMAGE

Walidator ten sprawdza, czy przesyłany poprzez wejście plikowe plik został zapisany w jednym z wybranych formatów graficznych i czy ma rozmiary (szerokość i wysokość) w określonych granicach.

Nie sprawdza on maksymalnego rozmiaru pliku (uzyj w tym celu IS_LENGTH). Ddy żadne dane nie zostaną przesłane walidacja kończy się niepowodzeniem. Obsługuje formaty BMP, GIF, JPEG, PNG i nie wymaga biblioteki Python Imaging Library.

Kod pochodzi z ref.[source1]

Walidator pobiera następujace argumenty:

  • extensions: iterowalna wartość zawierająca dopuszczalne rozszerzenia pliku graficznego, pisane małymi literami;
  • maxsize: iterowalna wartość maksymalna szerokość i wysokość obrazu;
  • minsize: iterowalna wartość zawierająca minimalna szerokość i wysokość obrazu;

Użyj (-1, -1) jako minsize aby ominąć sprawdzanie wielkości obrazu.

Oto kilka przykładów:

  • Sprawdzenie, czy przesyłany plik zgodny jest z którymś z obsługiwanych formatów graficznych:
1
requires = IS_IMAGE()
  • Sprawdzenie, czy przesyłany plik, to JPEG albo PNG:
1
requires = IS_IMAGE(extensions=('jpeg', 'png'))
  • Sprawdzenie, czy przesyłany plik, to PNG z rozmiarem nie większym od 200x200 pikseli:
1
requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200))
  • Uwaga: przy wyświetlaniu formularza edycyjnego dla tabeli zawierającej requires = IS_IMAGE() nie pojawi się pole wyboru delete, ponieważ usunięcie pliku spowodowałoby niepowodzenie walidacji. Dla wyświetlenie pola wyboru delete użyj takiej walidacji:
1
requires = IS_EMPTY_OR(IS_IMAGE())
IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME

Weryfikator ten sprawdza, czy nazwa i rozszerzenia pliku, przesyłanego za pośrednictwem wejścia plikowego, zgadza się z określonymi kryteriami.

W żaden sposób nie sprawdza on typu pliku. Jeśli nie zostaną przesłane żadne dane, to zwraca bład walidacji.

Jego argumentami są:

  • filename: wyrażenie regularne nazwy pliku (przed kropką);
  • extension: wyrażenie regularne rozszerzenia (po kropce);
  • lastdot: ustala, którą kropkę powinno się traktować jako seperator rozszerzenia (nazwy pliku): True wskazuje na ostatnią kropke (np. "file.tar.gz" zostanie podzielona na "file.tar" i "gz"), gdy False oznacza pierwszą kropkę (np. "file.tar.gz" zostanie podzielony na "file" i "tar.gz");
  • case: 0 oznacza utrzymanie stanu; 1 oznacza przekształcenie łańcucha na małe litery (domyślnie); 2 oznacza przekształcenie łańcucha na duże litery.

Jeśli nie ma kropki, sprawdzanie rozszerzenia będzie wykonane dla pustego łańcucha a nazwa pliku zostanie wykonana dla całej wartości.

Przykłady:

Sprawdzenie, czy plik ma rozszerzenie pdf (wielkość liter ma znaczenie):

1
requires = IS_UPLOAD_FILENAME(extension='pdf')

Sprawdzenie, czy plik ma rozszerzenie tar.gz i nazwę rozpoczynająca się od backup:

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

Sprawdzenie, czy plik nie ma rozszerzenia i czy jego nazwa zgadza sie ze słowem README (wielkość liter ma znaczenie):

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

Weryfikator ten sprawdza, czy wartość pola jest adresem IP w wersji 4 w postaci dziesiętnej. można go ustawić na wymuszanie adresów z określonego zakresu.

Wyrażenie regularne dla IPv4 pochodzi z ref.[regexlib] Jego argumentami są:

  • minip - najniższy dopuszczalny adres; akceptuje: łańcuch znaków, np. 192.168.0.1; iterowalna sekwencję liczb, np. [192, 168, 0, 1]; liczbę całkowitą, np. 3232235521;
  • maxip najwyższ dopuszczalny adres; akceptuje wartości jak wyżej.

Wszystkie trzy przykładowe wartości są takie same, ponieważ adresy są przekształcane do liczb całkowitych dla włączenia sprawdzania z następującą funkcją:

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

Przykłady:

Sprawdzenie prawidłowości adresu IPv4:

1
requires = IS_IPV4()

Sprawdzenie prawidłowości adresu dla sieci IPv4:

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

Inne walidatory

CLEANUP
CLEANUP

Jest to filtr. Nigdy nie agłasza bledów, po prostu usuwa wszystkie znaki, których dziesietne kodu ASCII nie znajdują się na liście [10, 13, 32-127].

1
requires = CLEANUP()

Walidatory bazy danych

IS_NOT_IN_DB
IS_NOT_IN_DB
Streszczenie:
IS_NOT_IN_DB(db|set, 'table.field')

Rozważmy następujący przykład:

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

Wymaga to, aby wstawienie nowej osoby było możliwe, tylko jeśli jej nazwa nie jest już w bazie danych.

Zamiast db można użyć set.

Tak jak w przypadku innych walidatorów, to wymaganie jest egzekwowane na poziomie przetwarzanego formularza, nie na poziomie bazy danych. Oznacza to, że istnieje nikłe prawdopodobieństwo, że dwóch odwiedzających będzie próbować jednocześnie wprowadzić rekordy z tą sama wartością dla person.name, co w rezultacie doprowadziłoby do zaakceptowania obu rekordów, mimo sytuacji konfliktowej Dlatego bezpieczniej jest poinformować bazę danych, że pole powinno mieć unikatową wartość:

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

Teraz, jeśli wystąpi konflikt, baza danych zgłosi OperationalError i jedno z tych dwóch wstawień zostanie odrzucone.

Pierwszym argumentem IS_NOT_IN_DB może być połączenie z bazą danych lub obiekt Set. W tym drugim przypadku można by sprawdzić tylko zestwa określony przez obiekt Set.

Kompletna lista argumentów dla IS_NOT_IN_DB() jest następująca:

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

Poniższy kod nie pozwala na rejestrację dwóch osób o tej samej nazwie w przciagu 10 dni:

1
2
3
4
5
6
7
import datetime
now = datetime.datetime.today()
db.define_table('person',
    Field('name'),
    Field('registration_stamp', 'datetime', default=now))
recent = db(db.person.registration_stamp>now-datetime.timedelta(10))
db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name')
IS_IN_DB
IS_IN_DB

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

gdzie trzeci i czwarty argument są opcjonalne.

Możliwe jest również użycie argumentu multiple=, jeśli typem pola jest lista. Domyślnie ma on wartość False. Jeśli ustawi się go na True lub na krotkę (min,max) ograniczy się liczbę wybranych wartości. Tak na przykład, multiple=(1,10) wymusi wybór co najmniej 1 i co najwyżej 10 elementów.

Pozostałe argumenty omówione są poniżej.

Przykład

Rozważmy następujące tabele i wymagania:

1
2
3
4
5
6
db.define_table('person', Field('name', unique=True))
db.define_table('dog', Field('name'), Field('owner', db.person)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 zero=T('choose one'))
*or using a Set*
db.person.name.requires = IS_IN_DB(db(db.person.id>10), 'person.id', '%(name)s')

Jest to wymuszane na poziomie formularzy INSERT/UPDATE/DELETE dla encji 'dog'. W tym przykładzie wymaga się, aby dog.owner był prawidłowym identyfikatorem w polu person.id w bazie danych db. Ze względu na ten walidator pole dog.owner jest prezentowane w postaci rozwijanej listy. Trzeci argumet walidatora jest łańcuchem, który opisuje elementy w rozwijanej liście. Na przykład, można spowodować wyświetlanie na liście nazwy osoby, %(name)s, zamiast identyfikatora %(id)s. %(...)s jest zamieniane przez wartość pola w nawiasach dla każdego rekordu.

Opcja zero działa bardzo podobnie jak w walidatorze IS_IN_SET.

Pierwszym argumentem walidatora może być połączenie z bazą danych lub obiekt Set, tak jak w aIS_NOT_IN_DB. może to być przydatne, na przykład, gdy chce się ograniczyć rekody ujmowane na rozwijanej liscie. W tym przykładzie jest to IS_IN_DB użyty w kontrolerze do dynamicznego ograniczania rekordów, przy każdym wywołaniu kontrolera:

1
2
3
4
5
6
7
def index():
    (...)
    query = (db.table.field == 'xyz') #in practice 'xyz' would be a variable
    db.table.field.requires=IS_IN_DB(db(query),....)
    form=SQLFORM(...)
    if form.process().accepted: ...
    (...)

Jeśli chce się sprawdzać pole, ale nie chce się stosować rozwijanej listy, trzeba umieścić walidator w liście:

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

Czasem potrzeba zastosować rozwijaną listę (więc nie można użyć wyżej podanej składni), ale chce się użyć dodatkowych walidatorów. W tym celu walidator IS_IN_DB ma dodatkowy argument _and, w którym można wskazać listę innych walidatorów, jakie chce się zastosować, jeśli sprawdzana wartość przeszła walidację IS_IN_DB. Na przykład, dla sprawdzenia wszystkich psów właściciela w db, które nie znajdują się w subset:

1
2
3
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 ma domyślny argument distinct, którego domyślną wartością jest False. Po ustawieniu na True zabezpieczy się wartość przed powtarzaniem na rozwijanej liście.

IS_IN_DB pobiera również argument cache, który działa podobnie jak argument cache metody select.

IS_IN_DB a tagowanie
tags
multiple

Walidator IS_IN_DB ma opcjonalny atrybut multiple=False. Jeśli ustawi się go na True, można w jednym polu przechowywać wiele wartości. Pole to powinno być typu list:reference, tak jak opisano to w rozdziale 6. Tam też omówiony jest jasny przykład tagowania (określania znaczników HTML). Referencje multiple są obsługiwane automatycznie przy tworzeniu i aktualizowaniu formularzy, ale są niewidoczne dla DAL. Sugerujemy korzystanie z wtyczki multiselect jQuery do renderowania wielowartościowych pól.

Własne walidatory

własny walidator

Wszystkie walidatory wykorzystują poniższy prototyp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)

Działa to tak. Gdy zostaje wywołana walidacja jakiejś wartości, walidator zwraca krotke (x, y). Jeśli y wynosi None, to wartość przechodzi walidację a x zawiera sparsowana wartość. Na przykład, jeśli walidator wymaga, aby wartość była liczbą całkowitą, x jest konwertowane do int(value). Jeśli wartość nie przechodzi walidacji, to x zawiera wartość wejsciową a y zawiera komunikat błędu wyjaśniający niepowodzenie walidacji. Ten komunikat błędu jest używany do raportowania bledu w formularzu, który nie jest prawidłowy.

Walidatory mogą również zawierać metodę formatter. Musi ona wykonać odwrotną konwersję do tej, wykonanej w metodzie __call__. Na przykład, rozważmy taki kod źródłowy dla IS_DATE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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))

W przypadku sukcesu, metoda __call__ odczytuje z formularza łańcuch daty i konwertuje go do obiektu datetime.date używając łańcucha formatujacego określonego w konstruktorze. Obiekt formatter pobiera obiekt datetime.date i konwertuje go do reprezentacji znakowej z wykorzystaniem tego samego formatu. formatter jest wywołaywany automatycznie w formularzach, ale można również wywołać go jawnie do konwersji obiektów do ich właściwej reprezentacji. Na przykład:

1
2
3
4
5
6
7
>>> db = DAL()
>>> db.define_table('atable',
       Field('birth', 'date', requires=IS_DATE('%m/%d/%Y')))
>>> id = db.atable.insert(birth=datetime.date(2008, 1, 1))
>>> row = db.atable[id]
>>> print db.atable.formatter(row.birth)
01/01/2008

Gdy wymaganych jest wiele walidatorów (i są one przechowywane w liście), to wykonywane są one w kolejności a wyjście jednego jest przekazywane do wejścia nastęþnego walidatora. Łańcuch ten się urywa, gdy walidacja sie nie powiedzie.

Odwrotnie, podczas wywołania metody formatter pola, formatery związanych walidatorów są również łączone, ale w odrotnej kolejności.

Proszę mieć na uwadze, że zamiast indywidualnych walidatorów, można użyć argument onvalidate metody form.accepts(...), form.process(...) i form.validate(...).

Walidatory z zależnościami

Zazwyczaj walidatory są ustawiane raz na zawsze w modelach.

Czasem zachodz potrzeba sprawdzenia pola, ale walidator powinien zależeć od wartortości innego pola. Można to osiągnąć w różny sposób, w modelu lub kontrolerze.

Na przykład, oto strona generujaca formularz rejestracyjny, który pyta o nazwę użytkownika i hasło (dwukrotnie). Żadne z pól nie może być puste a oba hasła muszą być zgodne:

1
2
3
4
5
6
7
8
9
def index():
    form = SQLFORM.factory(
        Field('username', requires=IS_NOT_EMPTY()),
        Field('password', requires=IS_NOT_EMPTY()),
        Field('password_again',
              requires=IS_EQUAL_TO(request.vars.password)))
    if form.process().accepted:
        pass # or take some action
    return dict(form=form)

Ten sam mechanizm można zastosować do obiektów FORM i SQLFORM.

Widżety

Oto lista widżetów dostępnych w web2py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

Pierwsze dziesięć z nich, to domyślne widżety dla odpowiednich typów pól. Widżet "options" używany jest gdy wymaganiami pola są walidatory IS_IN_SET lub IS_IN_DB z opcją multiple=False (domyślne ustawienie). Widżet "multiple" stosowany jest gdy wymaganiami pola są walidatory IS_IN_SET lub IS_IN_DB z opcja multiple=True. Widżety "radio" i "checkboxes" nigdy nie są stosowane domyślnie, ale mogą być ustawione ręcznie. Widżet "autocomplete" jest specjalny i omówiony jest w następnym rozdziale.

Na przykład, aby uzyskać pole "string" reprezentowane przez pole obszaru tekstowego trzeba wykonać:

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

Widżety mogą być również przypisywane do pól, "po fakcie":

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

W pewnych warunkach widżety mogą pobierać dodatkowe argumenty i trzeba określić ich wartości. W takim przypadku można wykorzystać lambda:

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

Widżety są pomocniczymi fabrykami i ich pierwsze dwa argumenty, to zawsze field i value. Pozostałe argumenty mogą zawierać zwykłe atrybuty helperów, takie jak _style, _class itd. Pewne widżety mgą również zawierać specjalne argumenty. W szczególności SQLFORM.widgets.radio i SQLFORM.widgets.checkboxes pobierają argument style (nie mylić z _style), który można ustawić na "table", "ul" lub "divs" w celu dopasowania formstyle formularza.

Można utworzyć nowe widżety lub rozszerzyć widżety istniejące.

SQLFORM.widgets[type] jest klasą a SQLFORM.widgets[type].widget jest statyczną funkcją odpowiedniej klasy. Każda funkcja widżetu pobiera dwa argumenty: obiekt pola i aktualna wartość tego pola. Zwraca ona reprezentację widżetu. Na przykład, widżet 'string' można ponownie zakodować, w ten sposób:

1
2
3
4
5
6
7
8
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)

Wartości id i class muszą być zgodne z konwencją opisaną w następnym rozdziale. Widżet może zawierać swoje własne walidatory, lecz dobra praktyka jest skojarzenie walidatorów z atrybutem "requires" pola i pobieranie widżetu stamtąd.

Widżet autouzupełniania

autouzupełnianie

Istnieją dwa możliwie zastosowania widżetu autouzupełniania: do autouzupełniania pola, które pobiera wartość z listy lub do autouzupełniania pola referencyjnego (gdzie łańcuch mający być autouzupełniony jest reprezentacją odniesienia, które jest implementowane jako id).

Pierwszy przypadek jest prosty:

1
2
3
4
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)

Gdzie limitby instruuje widżet, aby wyświetlał jednorazowo nie więcej niż 10 podpowiedzi a min_length instruuje widżet, aby wykonywał wywołanie zwrotne Ajax do pobierania podpowiedzi tylko po wpisaniu przez użytkownika co najmniej 2 znaki w polu wyszukiwania.

Drugi przypadek jest bardziej skomplikowany:

1
2
3
4
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)

W tym przypadku wartość id_field informuje widżet, że nawet jeśli wartością mająca być uzupełnioną automatycznie jest db.category.name, to wartością mającą być zapisana jest wartość odpowiedniego pola db.category.id. Opcjonalnym parametrem jest orderby, który instruuje widżet, aby posortował podpowiedzi (domyślnie alfabetycznie).

Widżet ten działa poprzez Ajax. A gdzie wywołanie zwrotne Ajax? W tym widżecie działa nie jedna magia. Wywołanie zwrotne jest metodą samego obiektu widżetu. Jak jest udostępniane? W web2py dowolna porcja kodu może wygenerować odpowiedź przez zgłoszenie wyjątku HTTP. Widżet ten wykorzystuje tą możliwość w następujacy sposób: najpierw widżet wywołuje Ajax do tego samego adresu URL, z którego wygenerowano widżet i wstawia specjalny token w request.vars. Jeśli widget pobiera ponownie instancję, to zgłosi wyjątek HTTP, który odpowie na żądanie. Wszystko to odbywa sie "pod maską" i jest ukryte dla programisty.

SQLFORM.grid i SQLFORM.smartgrid

Uwaga: grid i smartgrid były eksperymentalne w wersjach web2py wcześniejszych niż 2.0 i były podatne na wyciek informacji. Obecnie nie są już eksperymentalne, ale w warstwie prezentacji siatki nie mają kompatybilności wstecznej. Kompatybilność ta zapewniona jest tylko w interfejsach API.

Są to dwa obiekty wysokiego poziomu, które tworzą złożone kontrolki CRUD. Zapewniają one stronicowanie, możliwość podgladu, wyszukiwanie, sortowanie, tworzenie, aktualizowanie i usuwanie rekordów z pojedynczego obiektu.

Ponieważ obiekty HTML web2py zbudowne są na podstawie prostszych obiektów, to siatki tworzące formularze SQLFORM umożliwiające oglądanie, edytowanie i tworzenie wierszy. Wiele argumentów siatek jest przekazywanych poprzez ten obiekt SQLFORM. Oznacza to, że dokumentacja dla SQLFORM (i FORM) jest adekwatna. Na przykład, siatka pobiera wywołanie zwrotne onvalidation. Logika przetwarzania siatki ostatecznie przekazuje to do podstawowej metody process() obiektu FORM, co oznacza, że należy siegnąć do dokumentacji onvalidation obiektów FORM.

Siatka może posiadać różne stany, takie jak edytowanie wiersza. Informacja o stanie siatki znajduje się w request.args.

SQLFORM.grid

Najprostszym z tych dwóch obiektów jest SQLFORM.grid. Oto przykład zastosowania:

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

co wytwarza następującą stronę:

image

Pierwszym argumentem SQLFORM.grid może być tabela lub zapytanie. Obiekt siatki zapewnia dostęp do rekordów zgodnych z zapytaniem.

Przed zagłebieniem sie w listę argumentów obiektu siatki, konieczne jest zrozumienie jak to działa. Obiekt ten analizuje request.args w celu zdecydowania co trzeba zrobić (przeglądać, wyszukiwać, tworzyć, aktualizować, usuwać itd.). Każdy przycisk tworzony przez obiekt linkuje tą samą funkcję (w tym przypadku manage_users), ale przekazuje różną listę request.args.

Logowanie wymagane domyślnie dla aktualizowania danych

Domyślnie wszystkie adresy URL generowane przez siatkę są podpisane cyfrowo i zweryfikowane. Oznacza to, że nie można wykonać pewnych akcji (tworzenie, aktualizowanie, usuwanie) bez zalogowania się. Ograniczenia te można złagodzić:

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

ale nie zalecamy tego.

Wiele siatek w jednej funkcji kontrolera

Ze względu na sposób działania siatki, można mieć tylko jedną siatkę w jednej funkcji kontrolera, chyba że zostaną one osadzone jako komponenty poprzez LOAD. Do wykonania domyślnego wyszukiwania, działającego w więcej niż jedna załadowanej siatce trzeba użyć inne formname dla każdej z nich.

Bezpieczne stosowanie requests.args

Ponieważ funkcja kontrolera zawierajaca siatkę może sama manipulować argumentami URL (nazywanymi w web2py jako response.args i response.vars), siatka musi wiedzieć, który argument ma być użyty do obługi siatki a który nie. Oto przykład kodu, który pozwala zarządzać dowolna tabelą:

1
2
3
4
5
6
@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 args siatki grid określa który request.args powinien zostać przekazany dalej a który ignorowany przez grid. W naszym przypadku request.args[:1] jest nazwą tabeli, którą chcemy zarządzać i która jest obsługiwana przez samą funkcje manage, a nie przez grid. Tak więc, args=request.args[:1] informuje siatkę, aby zachowała pierwszy argument URL we wszystkich linkach, które generuje, dołączając wszystkie argumenty specyficzne dla siatki po tym pierwszym argumencie.

Sygnatura SQLFORM.grid

Pełna sygnatura dla tej siatki jest następująca:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SQLFORM.grid(
    query,
    fields=None,
    field_id=None,
    left=None,
    headers={},
    orderby=None,
    groupby=None,
    searchable=True,
    sortable=True,
    paginate=20,
    deletable=True,
    editable=True,
    details=True,
    selectable=None,
    create=True,
    csv=True,
    links=None,
    links_in_grid=True,
    upload='<default>',
    args=[],
    user_signature=True,
    maxtextlengths={},
    maxtextlength=20,
    onvalidation=None,
    oncreate=None,
    onupdate=None,
    ondelete=None,
    sorter_icons=(XML('&#x2191;'), XML('&#x2193;')),
    ui = 'web2py',
    showbuttontext=True,
    _class="web2py_grid",
    formname='web2py_grid',
    search_widget='default',
    ignore_rw = False,
    formstyle = 'table3cols',
    exportclasses = None,
    formargs={},
    createargs={},
    editargs={},
    viewargs={},
    buttons_placement = 'right',
    links_placement = 'right'
    )
  • fields jest listą pól do pobrania z bazy danych. Jest również używana do określenia, czy pola mają być pokazane w widoku siatki. Jednak nie kontroluje, co jest wyświetlane w oddzielnym formularzu używanym do edytowania wierszy. W tym celu trzeba użyć atrybutu readable i writalbe pól bazy danych. Na przykład, w edytowalnej siatce, stłumienie aktualizowania pola można wykonać tak: przed utworzeniem SQLFORM.grid, trzeba ustawić:
1
2
db.my_table.a_field.writable = False
db.my_table.a_field.readable = False
  • field_id musi być polem tabeli, które będzie wykorzystywane jako ID, na przykład db.mytable.id;
  • left jest opcjonalnym wyrażeniem lewego złączenia używanym do budowy ...select(left=...);
  • headers jest słownikiem mapującym 'tablename.fieldname' na odpowienią etykietę nagłówka, np. {'auth_user.email' : 'Email Address'};
  • orderby używany jest do ustawiania domyślnego porządku wierszy; Zobacz rozdział DAL (możliwe jest wykorzystywanie wielu pól);
  • groupby służy do grupowania zestawu. Trzeba użyć tej samej składni co w select(groupby=...);
  • searchable, sortable, deletable, editable, details, create ustala odpowiednio, czy można wyszukiwać, sortować, usuwać, edytować, pokazywać sczegóły i tworzyć nowe rekordy;
  • selectable może zostać wykorzystane do wywołania własnej funkcji na wielu rekordach (pole wyboru zostanie wstawione w każdym wierszu) np.:
    1
       selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids)))
    

lub dla przycisków wielu akcji, stosując listę krotek:

1
 selectable = [('button label1',lambda...),('button label2',lambda ...)]
  • paginate ustawia maksymalną liczbę wierszy na stronie;
  • csv jeśli ustaione na True, pozwala pobrać siatke w różnych formatach (wiecej o tym później);
  • links jest używane do wyświetlania nowych kolumn, które mogą być linkowane z innymi stronami. Argument links musi być listą dict(header='name',body=lambda row: A(...)), gdzie header jest nagłówkiem nowej kolumny a body jest funkcją, która pobiera wiersz i zwraca wartość. Na przykład, wartością jest helper A(...);
  • links_in_grid, jeśli ustawi się go na False, linki będą wyświetlane tylko na stronie "details" i "edit" (a więc nie w głównej siatce);
  • upload jest taki sam jak w SQLFORM. web2py używa akcji z tym adresem URL do pobrania pliku;
  • maxtextlength ustawia maksymalną długość tekstu, jaki ma być wyświetlony dla każdej wartości pola, w widoku siatki. Wartość ta może być nadpisywana dla każdego pola używając maxtextlengths słownika 'tablename.fieldname':length np. {'auth_user.email' : 50};
  • onvalidation, oncreate, onupdate i ondelete są funkcjami wywołań zwrotnych. Wszystkie, ale nie ondelete, pobieraja na wejściu obiekt formularza; ondelete pobiera tabelę i id rekordu; Ponieważ furmularz jest obiektem SQLFORM, który rozszerza FORM, to te wywołania zwrotne są zasadniczo uzywane tak, jak opisano to w rozdziałach dotyczących FORM i SQLFORM;

Oto szkieletowy kod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def myonvalidation(form):
    print "In onvalidation callback"
    print form.vars
    form.errors= True  #this prevents the submission from completing
    
    #...or to add messages to specific elements on the form
    form.errors.first_name = "Do not name your child after prominent deities"   
    form.errors.last_name = "Last names must start with a letter"
    response.flash = "I don't like your submission" 

def myoncreate(form):
    print 'create!'
    print form.vars

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

def myondelete(table, id):
    print 'delete!'
    print table, id

onupdate i oncreate są tymi samymi wywołaniami zwrotnymi dostępnymi w SQLFORM.process();

  • sorter_icons jest listą dwóch łańcuchów (lub helperów), które będą użyte do reprezentowania sortowania opcji dla każdego pola;
  • ui może być ustawiony na wartość równą 'web2py', co spowoduje generowanie przyjaznych dla web2py nazw klas; może też być ustawiony na wartość jquery-ui, co spowoduje generowanie nazw klas przyjaznych dla UI jQuery, ale może to być również własny zestaw nazw klas dla różnych komponentów siatki:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    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 pozwala zastąpić domyślny widżet wyszukiwania i zalecamy zapoznania się z kodem źródłowym w "gluon/sqlhtml.py" w celu poznania szczegółów;
  • showbuttontext pozwala na wyświetlanie przycisku bez tekstu (będzie to tylko ikona)
  • _class jest klasą dla kontenera siatki;
  • exportclasses pobiera słownik krotek: domyślnie jest to zdefiniowane jako:
1
2
3
4
5
6
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 i ExporterTSV są zdefioniowane w gluon/sqlhtml.py. Można je wykorzystać do tworzenia własnych eksporterów. Jeśli przekaże się słownik, taki jak dict(xml=False, html=False), to wyłączy się eksport formatu xml i html.

  • formargs jest przekazywany do wszystkich obiektów SQLFORM używanych przez siatkę, podczas gdy createargs, editargs i viewargs są przekazywane tylko do określonych obiektów SQLFORM (formularzy tworzących, edycyjnych i szczegółowych SQLFORM);
  • formname, ignore_rw i formstyle są przekazywane do obiektów SQLFORM używanych przez siatkę przez formularze tworząće i aktualizujące;
  • buttons_placement i links_placement pobierają parametr ('right', 'left', 'both'), który ma wpływ na to, gdzie będą umieszczone przyciski (lub odnośniki).
  • deletable, editable i details są zwykle wartościami logicznymi, ale ale mogą być tez funkcjami pobierającymi obiekt row i decydują o wyświetlaniu odpowiedniego przycisku.

Wirtualne pola w SQLFORM.grid i smartgrid

W wersjach wyższych web2py od 2.6, wirtualne pola są pokazywan w siatkach jak zwykłe pola, albo domyślnie wyświetlając je wraz ze wszystkimi innymi polami, albo przez włączenie ich do argumentufields. Jednakże wirtualne pola nie są możliwe do sortowania.

W starszych wersjach web2py vwyświetlanie wirtualnych pól wymaga uzycia argumentu links. Jest to nadal obsługiwane w nowszych wersjach. Jeśli tabela db.t1 ma pole o nazwie t1.vfield, które opiera się na wartościach t1.field1 i t1.field2, należy zrobić:

1
2
grid = SQLFORM.grid(db.t1, ..., fields = [t1.field1, t1.field2,...], 
   links = [dict(header='Virtual Field 1',body=lambda row:row.vfield),...] )

We wszystkich przypadkach, ponieważ t1.vfield zależy od t1.field1 i t1.field2, pola te muszą być obecne w wierszu. W powyższym przykładzie, gwarantuje to włączenie t1.field1 i t1.field2 do argumentu fields. Alternatywnie, będzie też działać wyświetlanie wszystkich pól. Można wykluczyć pole z wyświetlania ustawiając atrybut readable na False.

Należy pamiętać, że przy definiowaniu wirtualnego pola, funkcja lambda musi kwalifikować pola z nazwą bazy danych, ale w argumencie links nie jest to konieczne. Tak więc w rozpatrywanym przykladzie pole wirtualne może być zdefiniowane tak:

1
2
3
4
db.define_table('t1',Field('field1','string'),
  Field('field2','string'),
  Field.Virtual('virtual1', lambda row: row.t1.field1 + row.t1.field2),
  ...)

SQLFORM.smartgrid

SQLFORM.smartgrid wyglada bardzo podobnie jak grid, w rzeczywistości zawierając siatkę, ale zostało to zaprojektowane, aby pobierać na wejściu nie zapytanie, lecz tabelę i w celo przegladania tej tabeli oraz wyboru powiązanych tabel.

Rozważmy dla przykładu następującą strukturę tabeli:

1
2
db.define_table('parent',Field('name'))
db.define_table('child',Field('name'),Field('parent','reference parent'))

W SQLFORM.grid można wykazać wszystkich rodziców:

1
SQLFORM.grid(db.parent)

wszystkie dzieci:

1
SQLFORM.grid(db.child)

oraz wszystkich rodziców i wszystkie dzieci w jednej tabeli:

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

W SQLFORM.smartgrid można umieścić wszystkie dane w jednym obiekcie, który wytwarza obie tabele:

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

co wyglada tak:

image

Proszę zwrócić uwagę na dodatkowe liniki "children". Można utworzyć dodatkowe links posługując się zwykłym obiektem grid, ale wskaże on inną akcję. W smartgrid sa one tworzone automatycznie i obsługiwane przez ten sam obiekt.

Proszę też zauważyć, że po kliknięciu na link "children" dla danego rodzica, tylko zostanie pobrana lista dzieci tego rodzica (co jest oczywiste), ale jeśli ktoś teraz będzie próbował dodać nowe dziecko, wartość rodzica dla nowego dziecka zostanie automatycznie ustawiona na wybranego rodzica (wyświetlanie w okruszkach związanych z tym obiektem). Wartość tego pola może być zastąpiona - aby temu zapobiec, trzeba uczynić go niezapisywalnym (tylko do odczytu):

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

Jeśli argument linked_tables nie jest określony, wszystkie powiązanae tabele linkowane są automatycznie. W każdym razie, aby uniknąć przypadkowego ujawnienia danych, zalecamy jawne listowanie tabel, które mają zostać zlinkowane.

Poniższy kod tworzy bardzo zaawansowany intrefejs zarządzania dla wszystkich tabel w systemie:

1
2
3
4
5
6
@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()

Sygnatura smartgrid

Obiekt smartgrid ma te same argumenty co grid oraz kilka dodatkowych, z pewnymi zastrzeżeniami:

  • Pierwszy argument jest tabelą, a nie zapytaniem;
  • Istnieje dodatkowy argument constraints będący słownikiem 'tablename':query, który może być użyty do dalszego ograniczania dostępu do rekordów wyświetlanych w siatce 'tablename';
  • Istnieje dodatkowy argument linked_tables, który jest listą nazw tabel, które powinny zostać powiązane poprzez smartgrid;
  • divider pozwala określić znak, jaki ma być uzywany w łańcuchu nawigacyjnym okruszków; breadcrumbs_class będzie zastosowana do klasy elementu;
  • Wszystkie argumenty, oprócz table, args, linked_tables i user_signatures mogą być słownikami, tak jak wyjaśniono to niżej.

Przyjrzyjmy się poprzedniej siatce:

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

Pozwala ona na dostęp zarówno do db.parent jak i db.child. Poza kontrolkami nawigacyjnymi, dla każdej tabeli, smarttable nie wnosi nic innego niż grid. Oznacza to, że w tym przypadku jeden smartgrid może stworzyć jedną siatkę dla rodziców i jedną dla dzieci. można przekazać do tych siatek różne zestawy parametrów, na przykład, różne zestawy parametrów searchable.

Podczas gdy dla grid przekazujemy wartość logiczną:

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

dla smartgrid przekazujemy słownik z wartościami logicznymi:

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

W ten sposób uczynilismy siatke rodziców możliwa do przeszukiwania, ale dzieci dla poszczególnych rozdziców bez możliwości przeszukiwania (nie potrzebują one widżetu wyszukiwania).

Kontrola dostępu do grid i smartgrid

Obiekty grid i smartgrid nie wymuszają automatycznie kontroli dostępu, tak jak czyni to CRUD, ale można zintegrować je z auth stosując jawnie sprawdzanie uprawnień:

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

lub

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

Liczba mnoga w smartgrid

Obiekt smartgrid jest jedynym obiektem web2py wyswietlajacym nazwę tabeli i potrzebuje obsługi zarówni liczby pojedynczej jak i mnogiej. Na przykład, jeden rodzic może mieć dziecko lub wiele dzieci. Dlatego obiekt table musi znać zarówno nazwy w liczbie pojedynczej, jak i mnogiej. web2py zwykle się je domyśla, ale można ustawić jawnie:

1
db.define_table('child', ..., singular="Child", plural="Children")

lub poprzez:

liczba pojedyncza
liczba mnoga

1
2
3
db.define_table('child', ...)
db.child._singular = "Child"
db.child._plural = "Children"

Powinno sie je również umiędzynarodowić za pomocą operatora T.

Wartości dla liczby pojedynczej i mnogiej są następnie wykorzystywane przez smartgrid do dostarczenia prawidłowych nazw nagłówków i linków.

 top