Chapter 7: Formularze i walidacja

Formularze i walidatory

formularze

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

  • Helper 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.
  • Helper SQLFORM dostarcza API wysokiego poziomu do budowania funkcjonalności tworzenia, aktualizowania i usuwania formularzy powiązanych z istniejącymi tabelami bazy danych.
  • Metoda SQLFORM.factory jest abstrakcyjną warstwą na szczycie SQLFORM, wykorzystującą funkcjonalności generowania formularzy helpera SQLFORM, nawet jeśli nie jest dostępna baza danych. Generowanie formularzy jest tu bardzo podobne do SQLFORM, z opisem tabeli, ale bez potrzeby tworzenia tabeli bazy danych.
  • Metody obiektu CRUD są funkcjonalnym równoważnikiem SQLFORM i są oparte na SQLFORM, ale dostarczają bardziej zwartą notację.

Wszystkie te formularze są na tyle inteligentne, że jeśli dane wejściowe nie przejdą walidacji, mogą zostać zmienione przez formularz i moga 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. Helperami tymi można manipulować w podobny sposób jak helperem DIV. Na przykład można ustawić styl formularza:

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

def display_form():
    return dict()

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

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

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

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

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

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

form.vars.name = "Max"

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

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

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

onvalidation
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

process: inxx

validate

Skrótem dla

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

jest

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, że 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

form.validate(...)

równoważny jest wyrażeniu

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

Pola warunkowe

pola warunkowe`:inxx 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 pola ukryte:inxx 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: <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> :code 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 dać możliwość tworzenia stron, które zawierają i przetwarzają wiele formularzy. W web2py identyfikuje się i rozróżnia 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: <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> :code Proszę zwrócić uwagę na występowanie DIV klasy "error" w serializowanym formularzu. Kod 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 acceptsokreś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 klasą "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:inxx Opcjonalny argument keepvalues powiadamia web2py, co zrobić gdy formularz został zaakceptowany i nie ma przekierowania, czego skutkiem jest to, że 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 dbiojest 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 (rolą programisty będzie wyświetlenie jakoś tych błędów ze zmiennej form.errors). Argument onvalidation jest omówiony poniżej. #### Argument onvalidation
onvalidation:inxx 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: 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)

:code #### Wykrywanie zmian w rekordzie 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:

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)

:code #### Formularze a przekierowania przekierowania:inxx 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: 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() :code W celu ustawienia komunikatu fleszowego na następnej stronie, a nie na bieżącej, trzeba zastosować session.flash zamiast response.flash. Spowoduje to przeniesie wykonania pierwszego wyrażenia na następną stronę, po przekierowaniu. Trzeba mieć na uwadze, że używanie session.flash wymaga niestosowania 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: 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) :code a to jest wyprodukowane wyjście: [[image http://www.web2py.com/books/default/image/42/pl6100.png center 300px]] 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. #### Współdzielenie 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: 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() :code Proszę zauważyć, że ponieważ "page_one" oraz "page_two" używają ten sam obiektform, 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=Nonew process, inaczej 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:inxx Można to zrobić przy wykorzystując metodę add_button: form.add_button('Wstecz', URL('other_page')) :code Do formularza można dodawać więcej niż jeden przycisk. Argumentami metodyadd_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

SQLFORM:inxx Przejdźmy teraz do następnego poziomu i stwórzmy aplikację z plikiem takiego oto modelu: db = DAL('sqlite://storage.sqlite') db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))

:code Zmodyfikujmy kontroler w ten sposób:

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) :code Widoku nie potrzebujemy zmieniać. W nowym kontrolerze nie potrzebujemy budowania FORM, ponieważ konstruktorSQLFORM zbudował jeden formularz z danymi z tabeli db.person, zdefiniowanej w modelu. Jest to nowy formularz, który po zserializowaniu do kodu HTML będzie wyglądał następująco: <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> :code 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łędó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. ----- UWAGA: Domyślna długość pola wynosi 512 znaków. Gdy jakiś system plików nie obsługuje tak długich nazw plików, może być to powodem błędów związanych z generowaniem nieprawidłowych nazw. Rozwiązaniem może być ustawienie Field(...,length=...)na pożądaną wartość. Proszę mieć też na uwadze, że może to powodować skrócenie kodowania oryginalnej nazwy pliku i uniemożliwić odzyskanie pliku po jego pobraniu lub przesłaniu. ----- Helper 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: 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')) :code W tym przypadku, SQLFORM(db.person) wygeneruje następujący formularz: [[image http://www.web2py.com/books/default/image/42/pl6200.png center 300px]] 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:inxx labels:inxx Sygnatura konstruktora SQLFORM jest następująca: 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)

:code - Opcjonalny drugi argument zamienia formularz INSERT na formularz UPDATE dla określonego rekordu (patrz następny podrozdział). showid:inxx delete_label:inxx id_label:inxx submit_button:inxx - 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: fields = ['name'] :code - 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: labels = {'name':'Your Full Name:'} :code - col3 jest słownikiem wartości dla trzeciej kolumny. Na przykład: col3 = {'name':A('what is this?', _href='http://www.google.com/search?q=define:name')} :code - 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:inxx 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 wejściowych), "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:inxx buttons jest listą helperów INPUT lub TAG.button

  (choć teoretycznie może być dowolną 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:
-----------------  
buttons = [TAG.button('Back',_type="button",_onClick = "parent.location='%s' " % URL(...), TAG.button('Next',_type="submit")]

:code lub przycisku, który łączy do innej strony:

buttons = [..., A("Go to another page",_class='btn',_href=URL("default","anotherpage"))] :code ----------------- - separator:inxx 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: _action = '.' _method = 'POST' :code 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):
form = SQLFORM(...,hidden=...) :code 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: form.vars.a = request.vars.a form = SQLFORM(..., hidden=dict(a='b')) :code #### Metoda processW SQLFORM dostępna jest metoda process (podobnie jak forms). Jeśli w SQLFORM chce się wykorzystać argument keepvalues, trzeba przekazać go w metodzie process: if form.process(keepvalues=True).accepted::code #### SQLFORM a insert, update i delete Gdy formularz jest akceptowany, SQLFORM tworzy nowy rekord. Zakładając, żeform=SQLFORM(db.test), to identyfikator ostatnio utworzonego rekordu będzie dostępny w myform.vars.id. usuwanie rekordu:inxx 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, atrybutform.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: /test/default/display_form/2 :code i jeśli istnieje rekord z odpowiednim identyfikatorem, to SQLFORM generuje dla tego rekordu formularz UPDATE/DELETE: def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) form = SQLFORM(db.person, record) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code Linia 2 odnajduje rekord a linia 3 wykonuje formularz UPDATE/DELETE. 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 ma podgląd 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 odwiedzający 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 form = SQLFORM(...,ignore_rw=True) :code ignorują atrybuty readable i writable oraz zawsze pokazują wszystkie pola. Formularze w appadmin ignorują domyślnie to zachowanie. Formularze tworzone przez form = SQLFORM(table,record_id,readonly=True) :code 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 zachodzi potrzeba zastosowania SQLFORM, tak aby wykorzystać jego generator formularzy i przetwarzanie, ale z możliwoością uzyskania większego poziomu dostosowania kodu HTML formularza, który nie można osiągnąć z parametrami obiektu SQLFORM, co wymusza konieczność zaprojektowania formularza przy użyciu HTML. Edytujmy teraz poprzedni kontroler i dodajmy nową akcję: def display_manual_form(): form = SQLFORM(db.person) if form.process(session=None, formname='test').accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' else: response.flash = 'please fill the form'

Note: no form instance is passed to the view return dict()

:code oraz wstawmy formularz związany z widokiem "default/display_manual_form.html":

{{extend 'layout.html'}} <form action="#" enctype="multipart/form-data" method="post"> <ul> <li>Your name is <input name="name" /></li> </ul> <input type="submit" /> <input type="hidden" name="_formname" value="test" /> </form> :code 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 wartość argumentuformname określonego w akcji. Nazwa ta jest wykorzystywana w web2py, gdy istnieje wiele formularzy na tej samej stronie i trzeba ustalić, który z nich został zgłoszony. Jeśli strona zawiera tylko pojedynczy formularz, można ustawićformname=None i pominąć w widoku ukryte pole. Metoda form.accepts będzie wyszukiwać wewnątrz response.vars dane odpowiadające polom w tabeli db.person. Pola te są deklarowane w formacie HTML<input name="field_name_goes_here" /> :code Proszę zauważyć, że w podanym przykładzie, zmienne formularza będą 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: <form enctype="multipart/form-data" method="post"> :code #### SQLFORM a pola plikowe Pola typu ''upload'' (pola plikowe) są specjalne. Są renderowane jako pola INPUTtype="file". Jeśli nie określi się 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: db.define_table('person', Field('name', requires=IS_NOT_EMPTY()), Field('image', 'upload'))

:code Użyjemy tej samej akcji kontrolera, "display_form", pokazanej powyżej. Po wstawieniu nowego rekrdu, formularz pozwoli na przeglądanie pliku. Wybierz, na przykład, obraz jpg. Plik ten zostanie pobrany i zapisany jako:

applications/test/uploads/person.image.XXXXX.jpg :code "XXXXXX" jest losowym identyfikatorem przypisywanym plikowi przez web2py. content-disposition:inxx ------- 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 formularza 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: def display_form(): record = db.person(request.args(0)) form = SQLFORM(db.person, record, deletable=True, upload=URL('download')) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form)

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

:code Wstaw teraz nowy rekord do adresu URL:

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

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

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

:code (zakładamy tu, że ostatani rekord ma id=3). Formularz wyświetli podgląd obrazu, tak jak pokazano niżej: [[image http://www.web2py.com/books/default/image/42/pl6300.png center 300px]] Formularz ten, po serializacji, wygeneruje następujący kod HTML:

<td><label id="person_image__label" for="person_image">Image: </label></td> <td><div><input type="file" id="person_image" class="upload" name="image" />[<a href="/test/default/download/person.image.0246683463831.jpg">file</a>| <input type="checkbox" name="image__delete" />delete]</div></td><td></td></tr> <tr id="delete_record__row"><td><label id="delete_record__label" for="delete_record" >Check to delete:</label></td><td><input type="checkbox" id="delete_record" class="delete" name="delete_this_record" /></td>

:code 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, gdy w folderze znajduje się wiele plików,
dostęp do systemu plików może być powolny. Gdy planuje się przesyłanie więcej niż
1000 plików, można w web2py wymusić przesyłanie plików do podfolderów:

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



#### Przechowywanie oryginalnych nazw plików

W web2py automatycznie zapsisuje się oryginalną nazwę pliku wewnątrz nowej nazwy
UUID pliku i udostępnia się 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:

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

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

def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) url = URL('download') form = SQLFORM(db.person, record, deletable=True, upload=url, fields=['name', 'image']) if request.vars.image!=None: form.vars.image_filename = request.vars.image.filename if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code ------SQLFORM nie wyświetla pola "image_filename". Akcja "display_form" przenosi nazwę pliku request.vars.image doform.vars.image_filename, aby można ją było przetworzyć przez acceptsi 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:inxx 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: db.define_table('image', Field('name', requires=IS_NOT_EMPTY()), Field('source','upload',autodelete=True)) :code 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: db.define_table('person', Field('name', requires=IS_NOT_EMPTY())) db.define_table('dog', Field('owner', 'reference person'), Field('name', requires=IS_NOT_EMPTY())) db.dog.owner.requires = IS_IN_DB(db,db.person.id,'%(name)s') :code Osoba ma psy a każdy pies należy do jakiegoś właściciela, który jest osobą. Dla każdego właściciela psa odnosimy własciwe pole db.person.idprzez 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: def display_form(): record = db.person(request.args(0)) or redirect(URL('index')) url = URL('download') link = URL('list_records', args='db') form = SQLFORM(db.person, record, deletable=True, upload=url, linkto=link) if form.process().accepted: response.flash = 'form accepted' elif form.errors: response.flash = 'form has errors' return dict(form=form) :code a oto strona: [[image http://www.web2py.com/books/default/image/42/pl6400.png center 300px]] Jest tu odnośnik "dog.owner". Nazwa tego odnośnika może być zmieniona poprzez argument labels, na przykład: labels = {'dog.owner':"This person's dogs"}

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

/test/default/list_records/dog?query=db.dog.owner%3D%3D5 :code "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: def list_records(): import re REGEX = re.compile('^(\w+).(\w+).(\w+)\=\=(\d+)$') match = REGEX.match(request.vars.query) if not match: redirect(URL('error')) table, field, id = match.group(2), match.group(3), match.group(4) records = db(db[table][field]==id).select() return dict(records=records)

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

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

:code 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 się składnią:

form.vars.name = 'fieldvalue' :code Wyrażenia podobne do powyższego muszą być wstawiane po deklaracji formularza, ale przed zakceptowaniem tego formularza, niezależnie 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, że użytkownik
akceptuje regulamin strony internetowej:
form = SQLFORM(db.yourtable) my_extra_element = TR(LABEL('I agree to the terms and conditions'), INPUT(_name='agree',value=True,_type='checkbox')) form[0].insert(-1,my_extra_element) :code 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 I/O bazy danych Czasem zachodzi potrzeba wygenerowania formularza z tabeli bazy danych przy użyciuSQLFORM 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 uwzględnieniem 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: form = SQLFORM(db.person) if form.process().accepted: response.flash = 'record inserted'

:code na:

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'

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

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

:code na:

form = SQLFORM(db.person,record) if form.validate(): if form.deleted: db(db.person.id==record.id).delete() else: record.update_record(**dict(form.vars)) response.flash = 'record updated' :code W przypadku tabeli zawierającej pole typu "upload" ("fieldname"), zarównoprocess(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: form.vars.fieldname :code ### Inne typy formularzy #### SQLFORM.factory

SQLFORM.factory:inxx 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 : def form_from_factory(): form = SQLFORM.factory( Field('your_name', requires=IS_NOT_EMPTY()), Field('your_image', 'upload')) if form.process().accepted: response.flash = 'form accepted' session.your_name = form.vars.your_name session.your_image = form.vars.your_image elif form.errors: response.flash = 'form has errors' return dict(form=form) :code Obiekt Field w konstruktorze SQLFORM.factory() jest w pełni udokumentowany w [[rozdziale DAL ../06#field_constructor]]. Technika konstruowania w czasie rzeczywistym w SQLFORM.factory() jest następująca: fields = [] fields.append(Field(...)) form=SQLFORM.factory(*fields)

:code Oto widok "default/form_from_factory.html":

{{extend 'layout.html'}} {{=form}} :code W etykietach pola trzeba używać znaku pokreślenia zamiast spacji lub jawnie przekazać słownik labels do form_factory, tak samo jak się 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_namew metodzie factory: form = SQLFORM.factory(...,table_name='other_dummy_name') :code Zmienna 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 [TODO] #### 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: db.define_table('client', Field('name')) db.define_table('address', Field('client','reference client', writable=False,readable=False), Field('street'),Field('city'))

:code kontroler:

def register(): form=SQLFORM.factory(db.client,db.address) if form.process().accepted: id = db.client.insert(db.client._filter_fields(form.vars)) form.vars.client=id id = db.address.insert(db.address._filter_fields(form.vars)) response.flash='Thanks for filling the form' return dict(form=form) :code 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:inxx formularze potwierdzające:inxx 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: form = FORM.confirm('Are you sure?') if form.accepted: do_what_needs_to_be_done() :code 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'}: form = FORM.confirm('Are you sure?',{'Back':URL('other_page')}) if form.accepted: do_what_needs_to_be_done()

:code #### Formularz do edytowania słownika Wyobraźmy sobie system, który przechowuje opcje konfiguracyjne w słowniku:

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

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

form = SQLFORM.dictform(config) if form.process().accepted: config.update(form.vars) :code 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: session.config or dict(color='black', language='English') form = SQLFORM.dictform(session.config) if form.process().accepted: session.config.update(form.vars) :code ### CRUDCRUD:inxx crud.create:inxx crud.update:inxx crud.select:inxx crud.search:inxx crud.tables:inxx crud.delete:inxx 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: from gluon.tools import Crud crud = Crud(db) :code Zdefiniowany powyżej obiekt crud dostarcza następujące API: crud.tables:inxx crud.create:inxx crud.read:inxx crud.update:inxx crud.delete:inxx crud.select:inxx . - crud.tables() zwraca listę tabel zdefiniowanych w bazie danych. - crud.create(db.tablename) zwraca formularz tworzący rekord w tabeli tablename. - crud.read(db.tablename, id) zwraca formularz odczytujący rekord id w tabeli tablename i. - crud.update(db.tablename, id) zwraca formularz aktualizujący rekordu id w tablename. - 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: def data(): return dict(form=crud())

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

http://.../[app]/[controller]/data/tables http://.../[app]/[controller]/data/create/[tablename] http://.../[app]/[controller]/data/read/[tablename]/[id] http://.../[app]/[controller]/data/update/[tablename]/[id] http://.../[app]/[controller]/data/delete/[tablename]/[id] http://.../[app]/[controller]/data/select/[tablename] http://.../[app]/[controller]/data/search/[tablename]

:code Jednakże akcja:

def create_tablename(): return dict(form=crud.create(db.tablename)) :code udostępniać będzie tylko metodę create: http://.../[app]/[controller]/create_tablename

:code Natomiast akcja:

def update_tablename(): return dict(form=crud.update(db.tablename, request.args(0))) :code będzie udostępniać tylko metodę update: http://.../[app]/[controller]/update_tablename/[id] :code 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: crud.settings.auth = auth :code Użycie jest wyjaśnione w rozdziale 9. - aby określić kontroler definiujący funkcję data, która zwraca obiekt crud: crud.settings.controller = 'default'

:code - aby określić adres URL przekierowania po pomyślnym utworzoniu rekordu:

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

:code - aby określić adres URL przekierowania po pomyślnym zaktualizowaniu rekordu:

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

:code - aby określić adres URL przekierowania po pomyślnym usunięciu rekordu:

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

:code - aby określić adres URL, który ma być użyty do zlinkowania przesłanych plików:

crud.settings.download_url = URL('download') :code - aby określic dodatkowe funkcje, które mają być wykonane po standardowych procedurach walidacyjnych dla formularzy crud.create: crud.settings.create_onvalidation = StorageList() :code 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ę: crud.settings.create_onvalidation.mytablename.append(lambda form:....) :code - aby określić dodatkowe funkcje, które mają być wykonane po standardowych procedurach dla formularzy crud.update: crud.settings.update_onvalidation = StorageList() :code - aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.create: crud.settings.create_onaccept = StorageList() :code - aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.update: crud.settings.update_onaccept = StorageList() :code - aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu działania formularzy crud.update, jeśli rekord został usunięty: crud.settings.update_ondelete = StorageList() :code - aby określić dodatkowe funkcje, które mają być wykonane po zakończeniu crud.delete: crud.settings.delete_onaccept = StorageList()

:code - aby określic, czy formularze "update" powinny mieć przycisk "delete":

crud.settings.update_deletable = True

:code - aby określić, czy formularze "update" powinny pokazywać identyfikator edytowanego rekordu:

crud.settings.showid = False

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

crud.settings.keepvalues = False :code - 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:
crud.settings.detect_record_change = True :code i może być zmienione (wyłączone) przez ustawienie zmiennej na False. - styl formularza można zmienić przez: crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul'

:code - można ustawić separator we wszystkich formularzach CRUD:

crud.settings.label_separator = ':'

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

crud.settings.create_captcha = None crud.settings.update_captcha = None crud.settings.captcha = None :code #### Komunikaty Oto wykaz możliwych do dostosowania komunikatów: - crud.messages.submit_button = 'Submit':code ustawia tekst przycisku "submit" zarówno dla formularzach :create", jak i "update"; - crud.messages.delete_label = 'Check to delete:':code ustawia etykietę przycisku "delete" w formularzach "update"; - crud.messages.record_created = 'Record Created':code ustawia komunikat fleszowy przy pomyślnym utworzeniu rekordu; - crud.messages.record_updated = 'Record Updated':code ustawia komunikat fleszowy po pomyślnym zaktualizowaniu rekordu; - crud.messages.record_deleted = 'Record Deleted':code ustawia komunikat fleszowy po pomyślnym usunięciu rekordu; - crud.messages.update_log = 'Record %(id)s updated':code ustawia komunikat dziennika po pomyślnym zaktualizowaniu rekordu; - crud.messages.create_log = 'Record %(id)s created':code ustawia komunikat dziennika po pomyślnym utworzeniu rekordu; - crud.messages.read_log = 'Record %(id)s read':code ustawia komunikat dziennika po pomyślnym dostępie do odczytu rekordu; - crud.messages.delete_log = 'Record %(id)s deleted':code ustawia komunikat dziennika po pomyślnym usunięciu rekordu; ------ Obiekty crud.messages należy do klasy gluon.storage.Message, która jest podobna do gluon.storage.Storage, ale automatycznie tłumaczą 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: crud.tables() crud.create(table, next, onvalidation, onaccept, log, message) crud.read(table, record) crud.update(table, record, next, onvalidation, onaccept, ondelete, log, message, deletable) crud.delete(table, record_id, next, message) crud.select(table, query, fields, orderby, limitby, headers, **attr) crud.search(table, query, queries, query_labels, fields, field_labels, zero, showall, chkall) :code - table 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ł ../06#orderby]]). - 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:

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) :code 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): def manage(): table=db[request.args(0)] form = crud.update(table,request.args(1)) table.id.represent = lambda id, row: A('edit:',id,_href=URL(args=(request.args(0),id))) search, rows = crud.search(table) return dict(form=form,search=search,rows=rows) :code 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 się mieć tabelę (db.mytable), która wymaga pełnej historii zmian, można to zrobić następująco: form = SQLFORM(db.mytable, myrecord).process(onsuccess=auth.archive)

:code

form = crud.update(db.mytable, myrecord, onaccept=auth.archive) :code 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: db.define_table('mytable_archive', Field('current_record', 'reference mytable'), db.mytable) :code 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: db.define_table('mytable', Field('created_on', 'datetime', default=request.now, update=request.now, writable=False), Field('created_by', 'reference auth_user', default=auth.user_id, update=auth.user_id, writable=False),

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

db.define_table('myhistory', Field('parent_record', 'reference mytable'), db.mytable)

...

form = SQLFORM(db.mytable,myrecord) form.process(onsuccess = lambda form:auth.archive(form, archive_table=db.myhistory, current_record='parent_record'))

:code ### 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:

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

:code i akcję aktualizującą:

def upload_image(): return dict(form=SQLFORM(db.image).process()) :code Najprostrzym sposobem osadzenia formularza w widoku dla upload_image jest: {{=form}}

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

{{=form.custom.begin}} Name: <div>{{=form.custom.widget.name}}</div> File: <div>{{=form.custom.widget.source}}</div> {{=form.custom.submit}} {{=form.custom.end}} :code 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 http://www.web2py.com/books/default/image/42/pl6500.png center 300px]] Podobny efekt można by uzyskać bez stosowania własnego formularza: SQLFORM(...,formstyle='table2cols')

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

crud.settings.formstyle='table2cols' :code 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ć: {{=form.custom.delete}}

:code 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 ściśle spełniają 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:

SQLFORM.widgets.string.widget

:code będą wyglądać tak:

<input type="text" name="myfield" id="mytable_myfield" class="string" /> :code 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 się spodziewać, nazwa pola. #### Ukrywanie komunikatów o błędachhideerror:inxx 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łędów (ponieważ
nie są one już wyświetlane automatycznie).

Oto przykład, w którym komunikaty błędów są wyswietlane powyżej formularza
a nie w formularzu.
{{if form.errors:}} Your submitted form contains the following errors: <ul> {{for fieldname in form.errors:}} <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li> {{pass}} </ul> {{form.errors.clear()}} {{pass}} {{=form}} :code Ilustruje to ten obrazek: [[image http://www.web2py.com/books/default/image/42/pl6600.png center 300px]] Mechanizm ten działa również w niestandardowych formularzach. ### Walidatorywalidatory:inxx 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: INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))

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

db.define_table('person', Field('name')) db.person.name.requires = IS_NOT_EMPTY() :code 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: db.person.name.requires = [IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.name')] :code Walidatory są normalnie wywoływane automatycznie przez funkcję accepts i processw 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:
IS_NOT_EMPTY(error_message='cannot be empty') :code error_message pozwala zastąpić domyślny komunikat błędu dla każdego walidatora. Oto przykład walidatora na tabeli bazy danych: db.person.name.requires = IS_NOT_EMPTY(error_message='fill this!') :code 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:inxx Walidator ten sprawdza, czy pole zawiera znaki alfa-numeryczne w zakresie a-z, A-Z lub 0-9. requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!') :code ##### IS_LOWER
IS_LOWER:inxx Ten walidator nie zwraca błędu. Po prostu konwertuje wartość pola na małe litery. requires = IS_LOWER() :code ##### IS_UPPER
IS_UPPER:inxx Ten walidator nie zwraca błędu. Po prostu konwertuje wartość pola na duże litery. requires = IS_UPPER() :code ##### IS_EMAIL
IS_EMAIL:inxx Sprawdza, czy wartość pola wygląda jak adres e-mail. Nie próbuje w celu sprawdzenia wysyłać wiadomości e-mail. requires = IS_EMAIL(error_message='invalid email!') :code ##### IS_MATCH
IS_MATCH:inxx 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: requires = IS_MATCH('^\d{5}(-\d{4})?$', error_message='not a zip code')

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

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

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

requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$', error_message='not a phone number') :code 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:inxx Sprawdza, czy długość łańcucha wartości pola mieści się 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: INPUT(_type='text', _name='name', requires=IS_LENGTH(32))

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

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

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

INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024)) :code 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:inxx 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ą się 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 2616RFC2616:cite. 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ą się znaki niedopuszczalne w adresach URL; - schemat URL jest nieprawidłowy (jeśli został określony). Powyższe zasady są oparte na standardzie RFC 2396RFC2396:cite. 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 3490RFC3490:cite. 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 3492RFC3492:cite ). 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: requires = IS_URL()) requires = IS_URL(mode='generic') requires = IS_URL(allowed_schemes=['https']) requires = IS_URL(prepend_scheme='https') requires = IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https') :code ##### IS_SLUG
IS_SLUG:inxx requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug') :code 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:inxx Walidator ten sprawdza, czy pole zawiera wartość będącą czasem w określonym formacie. requires = IS_TIME(error_message='must be HH:MM:SS!') :code ##### IS_DATE
IS_DATE:inxx 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. requires = IS_DATE(format=T('%Y-%m-%d'), error_message='must be YYYY-MM-DD!') :code Pełny opis dyrektyw % znajduje się w opsie walidatora IS_DATETIME. ##### IS_DATETIME
IS_DATETIME:inxx 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. requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'), error_message='must be YYYY-MM-DD HH:MM:SS!')

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

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

IS_DATE_IN_RANGE:inxx Działa bardzo podobnie do poprzedniego walidatora, ale pozwala określić zakres: requires = IS_DATE_IN_RANGE(format=T('%Y-%m-%d'), minimum=datetime.date(2008,1,1), maximum=datetime.date(2009,12,31), error_message='must be YYYY-MM-DD!') :code Pełny opis dyrektyw % znajduje się w opsie walidatora IS_DATETIME. #### Walidatory zakresu, zbioru i równości ##### IS_EQUAL_TO
IS_EQUEL_TO:inxx Sprawdza, czy badana wartość jest równa określonej wartości (która może być zmienną): requires = IS_EQUAL_TO(request.vars.password, error_message='passwords do not match') :code ##### IS_NOT_EMPTY
IS_NOT_EMPTY:inxx Sprawdza, czy zawartość pola nie jest pustym łańcuchem. requires = IS_NOT_EMPTY(error_message='cannot be empty!') :code ##### IS_NULL_OR
IS_NULL_OR:inxx Zdeprecjonowany alias IS_EMPTY_OR opisany poniżej. ##### IS_EMPTY_OR
IS_EMPTY_OR:inxx 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: requires = IS_EMPTY_OR(IS_DATE()) :code ##### IS_EXPR
IS_EXPR:inxx 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: requires = IS_EXPR('int(value)%3==0', error_message='not divisible by 3')

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

requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')] :code ##### IS_DECIMAL_IN_RANGE

IS_DECIMAL_IN_RANGE:inxx INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10, dot=".")) :code Konwertuje dane wejściowe do typu decimal języka Python lub generuje błąd, jeśli liczba dziesiętna się 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 umiędzynarodowienie symbolu używanego do separacji dziesiętnej. ##### IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE:inxx Sprawdza, czy wartość pola jest liczbą zmienno-przecinkową w zdefiniowanym zakresie,0 <= value <= 100 w poniższym przykładzie: requires = IS_FLOAT_IN_RANGE(0, 100, dot=".", error_message='too small or too large!') :code Argument dot jest opcjonalny i pozwala na umiędzynarodowienie symbolu używanego do separacji dziesiętnej. ##### IS_INT_IN_RANGE
IS_INT_IN_RANGE:inxx Sprawdza, czy wartość pola jest liczbą całkowitą w zdefiniowanym zakresie, 0 <= value < 100 w poniższym przykładzie: requires = IS_INT_IN_RANGE(0, 100, error_message='too small or too large!') :code ##### IS_IN_SET
IS_IN_SET:inxxmultiple:inxx 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: requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'), error_message='must be a or b or c') :code 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) lubIS_FLOAT_IN_RANGE (który konwertuje wartość do liczby zmienno-przecinkowej). Na przykład: requires = [ IS_IN_SET([2, 3, 5, 7],IS_INT_IN_RANGE(0, 8), error_message='must be prime and less than 10')]

:code [[checkbox_validation]] ###### Sprawdzanie pól wyboru Do wymuszenia wypełnienia pola wyboru w formularzu (jak na przykład przy potwierdzaniu regulaminu), można użyć tego:

requires=IS_IN_SET(['on'])

:code ###### Słowniki i krotki z IS_IN_SET Można również uzyć słownika lub listy krotek do wykonania bardziej opisowej listy rozwijanej:

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')]) :code ##### 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 multiplesa 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 złożoności i bezpieczeństwa ##### IS_STRONG

IS_STRONG:inxx Wymusza pewną złożoność łańcucha w polu (zazwyczaj polu hasła) Przykład: requires = IS_STRONG(min=10, special=2, upper=2) :code 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:inxx 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. requires = CRYPT() :code 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: requires = CRYPT(key='sha512:thisisthekey') :code 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ść: requires = CRYPT(salt='mysaltvalue')

:code lub nie używać soli:

requires = CRYPT(salt=False) :code 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: requires = [IS_STRONG(),CRYPT(key='sha512:thisisthekey')] :code 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:inxx 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 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: requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10)) :code Nigdy nie zwraca błędu i nie zawiera komunikatu o błędzie. Błędy generuje wewnętrzny walidator. ##### IS_IMAGE
IS_IMAGE:inxx 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:cite 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: requires = IS_IMAGE()

:code - Sprawdzenie, czy przesyłany plik, to JPEG albo PNG:

requires = IS_IMAGE(extensions=('jpeg', 'png'))

:code - Sprawdzenie, czy przesyłany plik, to PNG z rozmiarem nie większym od 200x200 pikseli:

requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200)) :code - 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:
requires = IS_EMPTY_OR(IS_IMAGE()) :code ##### IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME:inxx 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łąd 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): requires = IS_UPLOAD_FILENAME(extension='pdf')

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

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

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

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

IS_IPV4:inxx 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:cite 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ą: number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]

:code Przykłady: Sprawdzenie prawidłowości adresu IPv4:

requires = IS_IPV4()

:code Sprawdzenie prawidłowości adresu dla sieci IPv4:

requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255') :code #### Inne walidatory ##### CLEANUP

CLEANUP:inxx 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]. requires = CLEANUP() :code #### Walidatory bazy danych ##### IS_NOT_IN_DB
IS_NOT_IN_DB:inxx ######Streszczenie:IS_NOT_IN_DB(db|set, 'table.field')

Rozważmy następujący przykład:
db.define_table('person', Field('name')) db.person.name.requires = IS_NOT_IN_DB(db, 'person.name') :code 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ść: db.define_table('person', Field('name', unique=True)) db.person.name.requires = IS_NOT_IN_DB(db, 'person.name') :code 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: IS_NOT_IN_DB(dbset, field, error_message='value already in database or empty', allowed_override=[], ignore_common_filters=True)

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

import datetime now = datetime.datetime.today() db.define_table('person', Field('name'), Field('registration_stamp', 'datetime', default=now)) recent = db(db.person.registration_stamp>now-datetime.timedelta(10)) db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name') :code ##### IS_IN_DB

IS_IN_DB:inxx [[validate_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:
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') :code 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 poledog.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:
def index(): (...) query = (db.table.field == 'xyz') #in practice 'xyz' would be a variable db.table.field.requires=IS_IN_DB(db(query),....) form=SQLFORM(...) if form.process().accepted: ... (...)

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

db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')] :code _and:inxx 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_DBma 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: subset=db(db.person.id>100) db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s', _and=IS_NOT_IN_DB(subset,'person.id')) :code IS_IN_DB ma domyślny argument distinct, którego domyślną wartością jestFalse. 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 argumentcache metody select. ##### IS_IN_DB a tagowanietags:inxxmultiple:inxx 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:inxx Wszystkie walidatory wykorzystują poniższy prototyp: class sample_validator: def __init__(self, *a, error_message='error'): self.a = a self.e = error_message def __call__(self, value): if validate(value): return (parsed(value), None) return (value, self.e) def formatter(self, value): return format(value) :code Działa to tak. Gdy zostaje wywołana walidacja jakiejś wartości, walidator zwraca krotkę (x, y). Jeśli y wynosi None, to wartość przechodzi walidację a x zawiera sparsowaną 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ść wejściową a y zawiera komunikat błędu wyjaśniający niepowodzenie walidacji. Ten komunikat błędu jest używany do raportowania błędu 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: class IS_DATE(object): def __init__(self, format='%Y-%m-%d', error_message='must be YYYY-MM-DD!'): self.format = format self.error_message = error_message def __call__(self, value): try: y, m, d, hh, mm, ss, t0, t1, t2 = time.strptime(value, str(self.format)) value = datetime.date(y, m, d) return (value, None) except: return (value, self.error_message) def formatter(self, value): return value.strftime(str(self.format)) :code 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:
>>> db = DAL() >>> db.define_table('atable', Field('birth', 'date', requires=IS_DATE('%m/%d/%Y'))) >>> id = db.atable.insert(birth=datetime.date(2008, 1, 1)) >>> row = db.atable[id] >>> print db.atable.formatter(row.birth) 01/01/2008 :code 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 się 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ć argumentu 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 generująca 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: def index(): form = SQLFORM.factory( Field('username', requires=IS_NOT_EMPTY()), Field('password', requires=IS_NOT_EMPTY()), Field('password_again', requires=IS_EQUAL_TO(request.vars.password))) if form.process().accepted: pass # or take some action return dict(form=form)

:code Ten sam mechanizm można zastosować do obiektów FORM i SQLFORM. ### Widżety Oto lista widżetów dostępnych w web2py:

SQLFORM.widgets.string.widget SQLFORM.widgets.text.widget SQLFORM.widgets.password.widget SQLFORM.widgets.integer.widget SQLFORM.widgets.double.widget SQLFORM.widgets.time.widget SQLFORM.widgets.date.widget SQLFORM.widgets.datetime.widget SQLFORM.widgets.upload.widget SQLFORM.widgets.boolean.widget SQLFORM.widgets.options.widget SQLFORM.widgets.multiple.widget SQLFORM.widgets.radio.widget SQLFORM.widgets.checkboxes.widget SQLFORM.widgets.autocomplete :code 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_SETlub IS_IN_DB z opcją multiple=False (domyślne ustawienie). Widżet "multiple" stosowany jest gdy wymaganiami pola są walidatory IS_IN_SETlub 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ć: Field('comment', 'string', widget=SQLFORM.widgets.text.widget)

:code 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 fieldi 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: def my_string_widget(field, value): return INPUT(_name=field.name, _id="%s_%s" % (field._tablename, field.name), _class=field.type, _value=value, requires=field.requires)

Field('comment', 'string', widget=my_string_widget) :code 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łnianiaautouzupełnianie:inxx 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: db.define_table('category',Field('name')) db.define_table('product',Field('name'),Field('category')) db.product.category.widget = SQLFORM.widgets.autocomplete( request, db.category.name, limitby=(0,10), min_length=2) :code 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: db.define_table('category',Field('name')) db.define_table('product',Field('name'),Field('category')) db.product.category.widget = SQLFORM.widgets.autocomplete( request, db.category.name, id_field=db.category.id) :code 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ść podglądu, 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 przeglą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: @auth.requires_login() def manage_users(): grid = SQLFORM.grid(db.auth_user) return locals() :code co wytwarza następującą stronę: [[image http://www.web2py.com/books/default/image/42/pl6700.png center 480px]] Pierwszym argumentem SQLFORM.grid może być tabela lub zapytanie. Obiekt siatki zapewnia dostęp do rekordów zgodnych z zapytaniem. Przed zagłebieniem się 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ć: def manage_users(): grid = SQLFORM.grid(db.auth_user,user_signature=False) return locals() :code 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ą: @auth.requires_login() def manage(): table = request.args(0) if not table in db.tables(): redirect(URL('error')) grid = SQLFORM.grid(db[table],args=request.args[:1]) return locals() :code Argument args siatki grid określa który request.args powinien zostać przekazany dalej a który ignorowany przez grid. W naszym przypadkurequest.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:
SQLFORM.grid( query, fields=None, field_id=None, left=None, headers={}, orderby=None, groupby=None, searchable=True, sortable=True, paginate=20, deletable=True, editable=True, details=True, selectable=None, create=True, csv=True, links=None, links_in_grid=True, upload='<default>', args=[], user_signature=True, maxtextlengths={}, maxtextlength=20, onvalidation=None, oncreate=None, onupdate=None, ondelete=None, sorter_icons=(XML('&#x2191;'), XML('&#x2193;')), ui = 'web2py', showbuttontext=True, _class="web2py_grid", formname='web2py_grid', search_widget='default', ignore_rw = False, formstyle = 'table3cols', exportclasses = None, formargs={}, createargs={}, editargs={}, viewargs={}, buttons_placement = 'right', links_placement = 'right' ) :code - fields 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ć: db.my_table.a_field.writable = False db.my_table.a_field.readable = False

:code - 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 ../06#orderby]] (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.: selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids))):code lub dla przycisków wielu akcji, stosując listę krotek: selectable = [('button label1',lambda...),('button label2',lambda ...)] :code - 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 używane tak, jak opisano to w rozdziałach dotyczących FORM i SQLFORM; Oto szkieletowy kod: def myonvalidation(form): print "In onvalidation callback" print form.vars form.errors= True #this prevents the submission from completing

...or to add messages to specific elements on the form form.errors.first_name = "Do not name your child after prominent deities" form.errors.last_name = "Last names must start with a letter" response.flash = "I don't like your submission"

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

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

def myondelete(table, id): print 'delete!' print table, id :code onupdate 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: ui = dict( widget=, header=, content=, default=, cornerall=, cornertop=, cornerbottom='', button='button', buttontext='buttontext button', buttonadd='icon plus', buttonback='icon leftarrow', buttonexport='icon downarrow', buttondelete='icon trash', buttonedit='icon pen', buttontable='icon rightarrow', buttonview='icon magnifier') :code - search_widget 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: csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'), csv=(ExporterCSV, 'CSV'), xml=(ExporterXML, 'XML'), html=(ExporterHTML, 'HTML'), tsv_with_hidden_cols=(ExporterTSV, 'TSV (Excel compatible, hidden cols)'), tsv=(ExporterTSV, 'TSV (Excel compatible)')) :code ExporterCSV, ExporterXML, ExporterHTML 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 użycia argumentulinks. 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 it1.field2, należy zrobić: grid = SQLFORM.grid(db.t1, ..., fields = [t1.field1, t1.field2,...], links = [dict(header='Virtual Field 1',body=lambda row:row.vfield),...] ) :code 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łączeniet1.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: db.define_table('t1',Field('field1','string'), Field('field2','string'), Field.Virtual('virtual1', lambda row: row.t1.field1 + row.t1.field2), ...) :code ### SQLFORM.smartgrid SQLFORM.smartgrid wygląda 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 celu przeglądania tej tabeli oraz wyboru powiązanych tabel. Rozważmy dla przykładu następującą strukturę tabeli: db.define_table('parent',Field('name')) db.define_table('child',Field('name'),Field('parent','reference parent'))

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

SQLFORM.grid(db.parent)

:code wszystkie dzieci:

SQLFORM.grid(db.child)

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

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

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

@auth.requires_login() def manage(): grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) return locals() :code co wygląda tak: [[image http://www.web2py.com/books/default/image/42/pl6800.png center 480px]] Proszę zwrócić uwagę na dodatkowe liniki "children". Można utworzyć dodatkowelinks posługując się zwykłym obiektem grid, ale wskaże on inną akcję. W smartgrid są 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): @auth.requires_login(): def manage(): db.child.parent.writable = False grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) return locals() :code 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: @auth.requires_membership('managers'): def manage(): table = request.args(0) or 'auth_user' if not table in db.tables(): redirect(URL('error')) grid = SQLFORM.smartgrid(db[table],args=request.args[:1]) return locals() :code #### 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ć używany 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:
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child']) :code 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ą: grid = SQLFORM.grid(db.parent,searchable=True) :code dla smartgrid przekazujemy słownik z wartościami logicznymi: grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'], searchable= dict(parent=True, child=False)) :code W ten sposób uczyniliśmy siatkę rodziców możliwą do przeszukiwania, ale dzieci poszczególnych rozdziców nie mają 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ń: grid = SQLFORM.grid(db.auth_user, editable = auth.has_membership('managers'), deletable = auth.has_membership('managers'))

:code lub

grid = SQLFORM.grid(db.auth_user, editable = auth.has_permission('edit','auth_user'), deletable = auth.has_permission('delete','auth_user')) :code ### 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: db.define_table('child', ..., singular="Child", plural="Children") :code lub poprzez: liczba pojedyncza:inxx liczba mnoga:inxx db.define_table('child', ...) db.child._singular = "Child" db.child._plural = "Children" :code Powinno się 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