Chapter 4: Rdzeń

Rdzeń

Opcje linii poleceń

Możliwe jest zrezygnowanie z interfejsu UI i rozpoczęcie pracy z web2py bezpośrednio z linii poleceń przez wpisanie czegoś takiego:

hasło
python web2py.py -a 'your password' -i 127.0.0.1 -p 8000

Podczas uruchomienia web2py tworzony jest plik o nazwie "parameters_8000.py", w którym zapisywane jest szyfrowane hasło. Jeśli użyje się "<ask>" jako hasła, web2py poprosi o podanie hasła.

Dla dodatkowego bezpieczeństwa można wystartować web2py poleceniem:

python web2py.py -a '<recycle>' -i 127.0.0.1 -p 8000

W tym przypadku web2py ponownie wykorzystuje przechowywane zaszyfrowane hasło. Jeśli hasło nie zostało podane lub jeśli plik "parameters_8000.py" został usunięty, nie będzie dostępny internetowy interfejs administracyjny.

PAM

W niektórych systemach Unix/Linux, jeśli hasło to

<pam_user:some_user>

web2py używa hasła PAM konta systemu operacyjnego some_user do uwierzytelniania administratora, chyba że jego konto jest zablokowane w konfiguracji PAM.

Platforma web2py zwykle działa z CPython (implementaja C interpretera Python stworzona przez Guido van Rossum), ale również może działać z PyPy i Jython. Ta ostatnia implementacja umożliwia używanie web2py w kontekście infrastruktury Java EE. W celu umożliwienia stosowania Jythona, wystarczy zamienić wyrażenie "python web2py.py ..." na "jython web2py.py". Szczegółowe informacje o instalacji Jython, modułu zxJDBC wymaganego do dostępu do bazy danych można znaleźć w rozdziale 14.

Skrypt "web2py.py" może przyjmować wiele argumentów linii poleceń określających maksymalną liczbę wątków, udostępniających SSL itd. W celu uzyskania pełnego wykazu wpisz:

linia poleceń
>>> python web2py.py -h
Usage: python web2py.py

web2py Web Framework startup script. ATTENTION: unless a password
is specified (-a 'passwd'), web2py will attempt to run a GUI.
In this case command line options are ignored.

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -i IP, --ip=IP        IP address of the server (e.g., 127.0.0.1 or ::1);
                        Note: This value is ignored when using the
                        'interfaces' option.
  -p PORT, --port=PORT  port of server (8000)
  -a PASSWORD, --password=PASSWORD
                        password to be used for administration (use -a
                        "<recycle>" to reuse the last password))
  -c SSL_CERTIFICATE, --ssl_certificate=SSL_CERTIFICATE
                        file that contains ssl certificate
  -k SSL_PRIVATE_KEY, --ssl_private_key=SSL_PRIVATE_KEY
                        file that contains ssl private key
  --ca-cert=SSL_CA_CERTIFICATE
                        Use this file containing the CA certificate to
                        validate X509 certificates from clients
  -d PID_FILENAME, --pid_filename=PID_FILENAME
                        file to store the pid of the server
  -l LOG_FILENAME, --log_filename=LOG_FILENAME
                        file to log connections
  -n NUMTHREADS, --numthreads=NUMTHREADS
                        number of threads (deprecated)
  --minthreads=MINTHREADS
                        minimum number of server threads
  --maxthreads=MAXTHREADS
                        maximum number of server threads
  -s SERVER_NAME, --server_name=SERVER_NAME
                        server name for the web server
  -q REQUEST_QUEUE_SIZE, --request_queue_size=REQUEST_QUEUE_SIZE
                        max number of queued requests when server unavailable
  -o TIMEOUT, --timeout=TIMEOUT
                        timeout for individual request (10 seconds)
  -z SHUTDOWN_TIMEOUT, --shutdown_timeout=SHUTDOWN_TIMEOUT
                        timeout on shutdown of server (5 seconds)
  --socket-timeout=SOCKET_TIMEOUT
                        timeout for socket (5 second)
  -f FOLDER, --folder=FOLDER
                        location of the applications folder (also known as directory) 
  -v, --verbose         increase --test verbosity
  -Q, --quiet           disable all output
  -D DEBUGLEVEL, --debug=DEBUGLEVEL
                        set debug output level (0-100, 0 means all, 100 means
                        none; default is 30)
  -S APPNAME, --shell=APPNAME
                        run web2py in interactive shell or IPython (if
                        installed) with specified appname (if app does not
                        exist it will be created). APPNAME like a/c/f (c,f
                        optional)
  -B, --bpython         run web2py in interactive shell or bpython (if
                        installed) with specified appname (if app does not
                        exist it will be created). Use combined with --shell
  -P, --plain           only use plain python shell; should be used with
                        --shell option
  -M, --import_models   auto import model files; default is False; should be
                        used with --shell option
  -R PYTHON_FILE, --run=PYTHON_FILE
                        run PYTHON_FILE in web2py environment; should be used
                        with --shell option
  -K SCHEDULER, --scheduler=SCHEDULER
                        run scheduled tasks for the specified apps: expects a
                        list of app names as -K app1,app2,app3 or a list of
                        app:groups as -K app1:group1:group2,app2:group1 to
                        override specific group_names. (only strings, no
                        spaces allowed. Requires a scheduler defined in the
                        models
  -X, --with-scheduler  run schedulers alongside webserver
  -T TEST_PATH, --test=TEST_PATH
                        run doctests in web2py environment; TEST_PATH like
                        a/c/f (c,f optional)
  -C, --cron            trigger a cron run manually; usually invoked from a
                        system crontab
  --softcron            triggers the use of softcron
  -Y, --run-cron        start the background cron process
  -J, --cronjob         identify cron-initiated command
  -L CONFIG, --config=CONFIG
                        config file
  -F PROFILER_FILENAME, --profiler=PROFILER_FILENAME
                        profiler filename
  -t, --taskbar         use web2py gui and run in taskbar (system tray)
  --nogui               text-only, no GUI
  -A ARGS, --args=ARGS  should be followed by a list of arguments to be passed
                        to script, to be used with -S, -A must be the last
                        option
  --no-banner           Do not print header banner
  --interfaces=INTERFACES
                        listen on multiple addresses: "ip1:port1:key1:cert1:ca
                        _cert1;ip2:port2:key2:cert2:ca_cert2;..."
                        (:key:cert:ca_cert optional; no spaces; IPv6 addresses
                        must be in square [] brackets)
  --run_system_tests    runs web2py tests

Uwaga: Opcja -W, używana do instalacji usługi Windows, została usunięta. Proszę zobaczyć nssm w rozdziale Recepty wdrożeniowe

Opcje pisane małymi literami są używane do konfiguracji serwera internetowego. Opcja -L powiadamia web2py aby czytał opcje konfiguracyjne z pliku, -W instaluje web2py jako usługę Windows, podczas gdy opcje -S, -P i -M uruchamiają interaktywną powłokę Pythona. Opcja -T wyszukuje i uruchamia kontroler doctests w środowisku wykonawczym web2py. Poniższy przykład uruchamia doctests na wszystkich kontrolerach w aplikacji "welcome":

python web2py.py -vT welcome

Jeśli uruchomi się web2py jako usługę Windows przez opcję -W, to nie jest wygodne przekazywanie konfiguracji przy użyciu argumentów linii poleceń. Z tego powodu w folderze web2py znajduje się prosty plik konfiguracyjny "options_std.py" dla wewnętrznego serwera internetowego:

import socket
import os

ip = '0.0.0.0'
port = 80
interfaces = [('0.0.0.0', 80)]
               #,('0.0.0.0',443,'ssl_private_key.pem','ssl_certificate.pem')]
password = '<recycle>'  # ## <recycle> means use the previous password
pid_filename = 'httpserver.pid'
log_filename = 'httpserver.log'
profiler_filename = None
ssl_certificate = None  # 'ssl_certificate.pem'  # ## path to certificate file
ssl_private_key = None  # 'ssl_private_key.pem'  # ## path to private key file
#numthreads = 50 # ## deprecated; remove
minthreads = None
maxthreads = None
server_name = socket.gethostname()
request_queue_size = 5
timeout = 30
shutdown_timeout = 5
folder = os.getcwd()
extcron = None
nocron = None

Plik ten zawiera wartości domyślne web2py. Jeśli edytuje się ten plik, to musi się go importować jawnie z użyciem opcji -L linii poleceń. Działa to tylko gdy uruchomi się web2py jako usługę Windows.

Proces przetwarzania

Proces przetwarzania danych w web2py jest następujący:

  1. Żądanie HTTP dociera do serwera internetowego (wbudowanego serwera Rocket lub innego serwera połączonego z web2py poprzez WSGI lub inny adapter). Serwer internetowy obsługuje każde żądanie w oddzielnym wątku, równolegle.
  2. Nagłówek żądania HTTP jest parsowany i przekazywany do dyspozytora (ang. dispatcher) (wyjaśnionego dalej w tym rozdziale).
  3. Dyspozytor decyduje, która z zainstalowanych aplikacji będzie obsługiwać żądanie i odwzorowuje PATH_INFO z adresu URL na wywołanie funkcji. Każdy adres URL odpowiada jednemu wywołaniu funkcji.
  4. Żądania dla plików w folderze statycznym są obsługiwane bezpośrednio a większe pliki są automatycznie strumieniowane do klienta.
  5. Żądania pozostałe (nie dotyczące plików statycznych) są odwzorowywane na akcje (tj. funkcję w pliku kontrolera w żądanej aplikacji).
  6. Przed wywołaniem akcji dzieje się kilka rzeczy: jeśli żądanie zawiera nagłówek ciasteczka dla aplikacji - pobierany jest obiekt sesji; jeśli nie, tworzony jest identyfikator sesji (ale plik sesji jest zapisywany później); tworzone jest środowisko dla żądania; w tym środowisku wykonywane są modele.
  7. Na koniec, we wstępnie zbudowanym środowisku wykonywana jest akcja kontrolera.
  8. Jeśli akcja zwraca ciąg znakowy, to jest on zwracany klientowi (lub jeśli akcja zwraca obiekt helpera HTML web2py, to jest on serializowany i zwracany klientowi).
  9. Jeśli akcja zwraca element iterowalny, to zostaje na nim zastosowana pętla i strumień danych jest przesyłany klientowi.
  10. Jeśli akcja zwraca słownik, web2py próbuje zlokalizować widok do renderowania słownika. Widok musi mieć tą samą nazwę co akcja (chyba, że określono to inaczej) i to samo rozszerzenie, co żądana strona (domyślnie .html); w przypadku błędu web2py może zastosować widok generyczny (jeśli jest dostępny i włączony). Widok widzi każdą zmienną zdefiniowana w modelach, jak również te zmienne w słowniku zwracane przez akcje, ale nie ma tu zmiennych globalnych zdefiniowanych w kontrolerze.
  11. Cały kod wykonywany jest w pojedynczej transakcji bazy danych, chyba że określono to inaczej.
  12. Jeśli kod użytkownika powiedzie się, transakcja jest zatwierdzana.
  13. Jeśli kod użytkownika nie powiedzie się, w bilecie zostaje zapisany komunikat z ostatniego wywołania (ang. traceback) i użytkownikowi zostaje wysłany identyfikator biletu. Tylko administrator systemu może wyszukiwać i odczytywać komunikat biletu.

Istnieje kilka uwag, o których warto pamiętać:

modele: kolejność wykonywania
  • Modele w tym samym folderze (podfolderze) są wykonywane w alfabetycznej kolejności.
  • Każda zmienna zdefiniowana w modelu będzie widoczna dla innych modeli w kolejności alfabetycznej, dla kontrolerów i widoków.
models_to_run
conditional models

  • Modele w podfolderach są wykonywane warunkowo. Na przykład, jeśli użytkownik wysyła żądanie "/a/c/f", gdzie "a" to aplikacja, "c" to kontroler a "f" to funkcja (akcja), to następnie wykonywane są następujące modele:
  • applications/a/models/*.py
  • applications/a/models/c/*.py
  • applications/a/models/c/f/*.py.
  • Zachowanie takie jest wymuszane domyślnie. Zmieniając listę wyrażeń regularnych response.models_to_run można wymusić takie zachowanie, jakie się chce.
  • Wykonywany jest żądany kontroler i wywoływana jest potrzebna funkcja. Oznacza to, że dla każdego żądania dla określonego kontrolera wykonywany jest również cały kod na najwyższym poziomie w tym kontrolerze.
  • Widok jest wywoływany tylko wtedy, gdy akcja zwraca słownik.
  • Jeżeli widok nie zostanie znaleziony, web2py spróbuje użyć widoku generycznego. Domyślnie, widoki generyczne są wyłączone, lecz aplikacja 'welcome' zawiera linię w /models/db.py do ich włączenie, ale tylko na localhost. Mogą one być włączone dla każdego typu i każdej akcji (za pomocą opcji response.generic_patterns). Ogólnie, widoki generyczne są narzędziem programistycznym i zazwyczaj nie powinny być wykorzystywane w środowisku produkcyjnym. Jeśli chce się aby jakieś akcje wykorzystywały widok generyczny, trzeba umieścić go na liście response.generic_patterns.

Możliwe zachowania akcji są następujące:

akcje: dane wyjściowe

Zwracanie lańcucha znaków

def index():
    return 'data'

Zwracanie słownika dla widoku:

def index():
    return dict(key='value')

Zwracanie zmiennych lokalnych:

def index():
    return locals()

Przekierowanie użytkowników do innej strony:

def index():
    redirect(URL('other_action'))

Zwracanie strony HTTP innej niż "200 OK":

def index():
    raise HTTP(404)

Zwracanie helpera (na przykład FORM):

def index():
    return FORM(INPUT(_name='test'))

Jest to najczęściej stosowane dla wywołań zwrotnych Ajax i komentarzy, patrz rozdział 12.

Gdy akcja zwraca słownik, to może zawierać kod generowany przez helpery, w tym formularze oparte na tabelach bazy danych lub formularze z "fabryki", na przykład:

def index():
    return dict(form=SQLFORM.factory(Field('name')).process())

Wszystkie formularze generowane przez web2py wykorzystują zgłoszenia zwrotne (ang. postback).

Rozdzielanie

mapowanie url
odwzorowywanie url
rozdzielanie

Platforma web2py odwzorowuje adres URL w formie:

http://127.0.0.1:8000/a/c/f.html

na akcję f() w kontrolerze "c.py" w aplikacji "a". Jeśli f nie jest podana, web2py stosuje domyślnie akcję index kontrolera. Jeśli c nie jest podany, web2py stosuje domyślnie kontroler "default.py" a przy braku a, web2py wykorzystuje domyślnie aplikację init. Jeśli nie ma aplikacji init, web2py próbuje uruchomić aplikację welcome. Jest to pokazane na poniższym obrazie:

image

Domyślnie, każde nowe żądanie tworzy nową sesję. Ponadto, do przeglądarki klienta zwracane jest ciasteczko sesji, w celu śledzenia sesji.

Rozszerzenie .html jest opcjonalne i jest rozszerzeniem domyślnym. Rozszerzenie determinuje rozszerzenie widoku, który renderuje wyjście funkcji kontrolera f() i pozwala, aby ta sama zawartość była serwowana w wielu formatach (html, xml, json, rss itd.).

Akcje (funkcje) posiadające argumenty lub których nazwa rozpoczyna się od dwóch znaków podkreślenia nie są publicznie dostępne i mogą tylko być wywoływane przez inne funkcje.

pliki statyczne

Istnieje wyjątek zrobiony dla adresów URL w formie:

http://127.0.0.1:8000/a/static/filename

Nie ma kontrolera o nazwie "static" - web2py interpretuje to jako żądanie pliku o nazwie "filename" w podfolderze "static" aplikacji "a".

PARTIAL CONTENT
IF_MODIFIED_SINCE
Po pobraniu plików statycznych, web2py nie tworzy sesji, ani nie wystawia ciasteczka, ani też nie wykonuje modele. web2py zawsze strumieniuje pliki statyczne kawałkami o rozmiarze 1MB i osyła odpowiedź PARTIAL CONTENT (206), gdy klient wysyła żądanie RANGE dla podzbioru pliku.

Platforma web2py również obsługuje protokół IF_MODIFIED_SINCE i nie wysyła pliku, jeśli jest on już przechowywany w pamięci podręcznej przeglądarki i gdy plik ten nie został zmieniony od czasu przesłania go do przeglądarki.

Podczas linkowania do plików audio lub wideo w folderze statycznym, jeśli chce się wymusić na przeglądarce aby pobierała pliki audio lub wideo, a nie je strumieniowała w odtwarzaczu medialnym, trzeba dodać ?attachment do adresu URL. To poinformuje web2py aby ustawił nagłówek Content-Disposition odpowiedzi HTTP na "attachment". Na przykład:

<a href="/app/static/my_audio_file.mp3?attachment">Download</a>

Po kliknięciu powyższego odnośnika, przeglądarka poprosi użytkownika aby pobrał plik MP3 zamiast go strumieniować w odtwarzaczu audio. Tak jak opisano poniżej, można również bezpośrednio ustawiać nagłówki odpowiedzi HTTP przypisując słownik nazw nagłówków i ich wartości do response.headers.

request.application
request.controller
request.function
GET
POST
request.args
web2py odwzorowuje żądania GET i POST w postaci:

http://127.0.0.1:8000/a/c/f.html/x/y/z?p=1&q=2

na funkcję f w kontrolerze "c.py" w aplikacji a i przechowuje parametry adresu URL w zmiennej request jak niżej:

request.args = ['x', 'y', 'z']

oraz:

request.vars = {'p': 1, 'q': 2}

oraz:

request.application = 'a'
request.controller = 'c'
request.function = 'f'

W powyższym przykładzie zarówno request.args[i] jak i request.args(i) mogą być używane do pobrania i-tego elementu request.args, ale gdy pierwszy zgłasza wyjątek jeśli na liście nie występuje index, to drugi zwraca w takim przypadku None.

request.url
request.url

przechowuje pełny adres URL bieżącego żądania (nie dołączając zmiennych GET).

request.ajax
request.cid

request.ajax

ma domyślnie wartość False, ale True jeśli web2py stwierdzi, że akcja została wywołana przez żądanie Ajax.

Jeśli żądanie jest żądaniem Ajax i jeśli zostało zainicjowane przez komponent web2py, nazwę komponentu można znaleźć w:

request.cid

Komponenty omówione są bardziej szczegółowo w rozdziale 12.

request.get_vars
request.post_vars
request.vars
Jeśli żądanie HTTP jest typu GET, to request.env.request_method zostaje ustawiona na "GET", jeśli natomiast typu POST, to na "POST". Parametry zapytania z adresu URL są przechowywane w zmiennej request.get_vars. Zmienna request.post_vars zawiera wszystkie parametry przekazane do ciała żądania (zazwyczaj POST, PUT lub DELETE). Słownik request.vars zawiera je oba (get_vars i post_vars).

Platforma web2py przechowuje zmienne środowiskowe WSGI i web2py w zmiennej request.env, na przykład:

request.env.path_info = 'a/c/f'

a nagłówki HTTP w zmiennych środowiskowych, na przykład:

request.env.http_host = '127.0.0.1:8000'

Proszę zwrócić uwagę, że web2py sprawdza wszystkie adresy URL, aby zapobiegać atakom z przeglądaniem katalogów (ang. directory travelsal).

Adresy URL mogą zawierać tylko znaki alfanumeryczne, znaki podkreślenia i ukośniki. Parametr args nie może zawierać występujących po sobie kropek. Spacje są zamieniane na znaki podkreślenia przed walidacją. Jeśli składnia adresu URL jest nieprawidłowa, web2py zwraca komunikat błędu 400 HTTP[http-w] [http-o] .

Jeśli adres URL zgodny jest z żądaniem dla plików statycznych, web2py po prostu czyta i zwraca (strumieniuje) żądany plik.

Jeśli adres URL nie żąda pliku statycznego, web2py przetwarza żądanie w następującej kolejności:

  • Analizuje ciasteczka.
  • Tworzy środowisko.
  • Inicjuje obiekty request, response, cache.
  • Otwiera istniejący obiekt session lub tworzy nowy.
  • Wykonuje modele należące do żądanej aplikacji.
  • Wykonuje żądaną funkcję akcji kontrolera.
  • Jeśli funkcja zwraca słownik, wykonuje związany widok.
  • W razie powodzenia zatwierdza wszystkie otwarte transakcje.
  • Zapisuje sesję.
  • Zwraca odpowiedź HTTP.

Kontroler i widok są wykonywane w różnych kopiach tego samego środowiska, dlatego widok nie widzi kontrolera, ale widzi modele i zmienne zwracane przez funkcje akcji kontrolera.

Jeśli zostaje wywołany wyjątek (inny niż HTTP), web2py wykonuje następujące rzeczy:

  • Zapisuje komunikat z ostatniego wywołania (traceback) w pliku błędu i przypisuje mu numer biletu.
  • Wycofuje wszystkie otwarte transakcje bazy danych.
  • Zwraca stronę błędu raportującą numer biletu.

Jeśli wyjątek jest wyjątkiem HTTP, to zakłada się, że jest to zamierzone zachowanie (na przykład, przekierowanie HTTP) i wszystkie otwarte transakcje bazy danych zostaja zatwierdzone. Zachowanie późniejsze jest określone przez sam wyjątek HTTP. Klasa wyjątku HTTP nie jest standardowym wyjątkiem Pythona – jest zdefiniowana przez web2py.

Biblioteki

Biblioteki web2py są dostępne dla aplikacji użytkownika jako obiekty globalne. Na przykład obiekty request, response, session, cache, klasy helperów, walidatorów, API DAL oraz funkcje T i redirect.

Obiekty te są zdefiniowane w następujacych plikach rdzenia:

web2py.py
gluon/__init__.py    gluon/highlight.py   gluon/restricted.py  gluon/streamer.py
gluon/admin.py       gluon/html.py        gluon/rewrite.py     gluon/template.py
gluon/cache.py       gluon/http.py        gluon/rocket.py      gluon/storage.py
gluon/cfs.py         gluon/import_all.py  gluon/sanitizer.py   gluon/tools.py
gluon/compileapp.py  gluon/languages.py   gluon/serializers.py gluon/utils.py
gluon/contenttype.py gluon/main.py        gluon/settings.py    gluon/validators.py
gluon/dal.py         gluon/myregex.py     gluon/shell.py       gluon/widget.py
gluon/decoder.py     gluon/newcron.py     gluon/sql.py         gluon/winservice.py
gluon/fileutils.py   gluon/portalocker.py gluon/sqlhtml.py     gluon/xmlrpc.py
gluon/globals.py     gluon/reserved_sql_keywords.py

Wiele z tych modułów, w szczególności dal (warstwa abstrakcji bazy danych), template (język szablonowania), rocket (serwer internetowy) i html (helpery) nie mają zależności i mogą być używane poza web2py.

Skompresowane przez gzip archiwum tar aplikacji dostarczane wraz z web2py to

welcome.w2p

Aplikacja ta jest tworzona podczas instalacji i nadpisywana podczas aktualizacji.

Podczas pierwszego uruchomienia web2py tworzy dwa nowe foldery: deposit i applications. Folder deposit jest używany jako tymczasowy magazyn dla instalowania i odinstalowywania aplikacji.

Podczas pierwszego uruchomienia web2py i po aktualizacji, aplikacja "welcome" jest pakowana do pliku "welcome.w2p" aby mogła być używana jako aplikacja szkieletowa.

Kiedy web2py jest uaktualniany, to dostarczany jest wraz z plikiem o nazwie "NEWINSTALL". Jeśli web2py odnajdzie ten plik, to rozumie, że uaktualnienie zostało zrealizowane, dlatego usuwa plik "welcome.w2p" i tworzy w jego miejsce nowy, o tej samej nazwie.

Aktualna wersja web2py jest zapisywana w polu "VERSION" i spełnia standardową notację wersjonowania, wg której identyfikator budowania jest znacznikiem czasowym budowania.

Jednostkowe testy web2py znajduja się w

gluon/tests/

Istnieją następujace moduły pomocnicze obsługujące połączenia z różnymi serwerami internetowymi:

cgihandler.py       # nie zalecany
gaehandler.py       # dla Google App Engine
fcgihandler.py      # dla FastCGI
wsgihandler.py      # dla WSGI
isapiwsgihandler.py # dla IIS
modpythonhandler.py # przestarzały

Moduł fcgihandler wywołuje gluon/contrib/gateways/fcgi.py (stworzony przez Allana Saddi) i "anyserver.py", który jest skryptem do współdziałania z wieloma serwerami internetowymi, co opisane zostało w rozdziale 13.

W katalogu "examples" znajdują się trzy przykładowe pliki:

options_std.py
routes.parametric.example.py
routes.patterns.example.py

Wszystkie te pliki można skopiować do głównego katalogu (tam gdzie jest plik web2py.py lub web2py.exe) i można je wówczas edytować, zgodnie ze swoimi potrzebami. Pierwszy jest opcjonalnym plikiem konfiguracyjnym, który może być przekazany do web2py.py przy pomocy opcji -L. Drugi jest przykładem pliku mapującego adres URL. Będzie ładowany automatycznie, jeśli zmieni się jego nazwę na "routes.py". Trzeci jest alternatywną składnią dla mapowania adresów URL i może być też przemianowany na "routes.py" (lub skopiowany do tego pliku).

Pliki

app.example.yaml
queue.example.yaml

są przykładowymi plikami konfiguracyjnymi używanymi do wdrożeń na Google App Engine. Można przeczytać więcej na ten temat w rozdziale 13 i na stronach dokumentacji Google.

Istnieją również dodatkowe biblioteki, niektóre opracowane przez osoby trzecie:

  • feedparser[feedparser] opracowana przez by Marka Pilgrima dla odczytu kanałów RSS i Atom:
gluon/contrib/__init__.py
gluon/contrib/feedparser.py
  • markdown2[markdown2] opracowana przez Trenta Micka dla znaczników wiki:
gluon/contrib/markdown/__init__.py
gluon/contrib/markdown/markdown2.py
  • markmin język znaczników:
gluon/contrib/markmin

(patrz składnia MARKMIN w celu uzyskania wiecej informacji)

  • fpdf stworzona przez Mariano Reingarta dla generowania dokumentów PDF:
gluon/contrib/fpdf
Nie jest ona udokumentowana w tej książce, ale jest hostowana i udokumentowana na stronie:
http://code.google.com/p/pyfpdf/
  • pysimplesoap jest implementacją lekkiego serwerem SOAP stworzona przez Mariano Reingarta:
gluon/contrib/pysimplesoap/
  • simplejsonrpc jest lekkim klientem JSON-RPC stworzonym przez Mariano Reingarta:
    jsonrpc
gluon/contrib/simplejsonrpc.py
  • memcache[memcache] API Pythona stworzone przez Evana Martina:
gluon/contrib/memcache/__init__.py
gluon/contrib/memcache/memcache.py
  • redis_cache
    redis
    jest modułem pamięci podręcznej dla bazy danych redis:
gluon/contrib/redis_cache.py
  • gql, port DAL dla Google App Engine:
gluon/contrib/gql.py
  • memdb, port DAL górnej części memcache:
gluon/contrib/memdb.py
  • gae_memcache to API wykorzystujące memcache dla Google App Engine:
gluon/contrib/gae_memcache.py
  • pyrtf[pyrtf] do generowania dokumentów Rich Text Format (RTF), opracowana przez Simona Cusacka i poprawiona przez Granta Edwardsa:
gluon/contrib/pyrtf/
  • PyRSS2Gen[pyrss2gen] opracowana przez Dalke Scientific Software do generowania kanałów RSS:
gluon/contrib/rss2.py
  • simplejson[simplejson] standardowa biblioteka do parsowania i pisania obiektów JSON, opracowana przez Boba Ippolito:
gluon/contrib/simplejson/
  • Google Wallet [googlewallet] dostarcza przycisk "pay now" linkujący Google jako procesora płatności:
gluon/contrib/google_wallet.py
  • Stripe.com [stripe] dostarcza proste API do przyjmowania płatności kartami kredytowymi::
gluon/contrib/stripe.py
  • AuthorizeNet [authorizenet] dostarcza API do przyjmowania płatności kartami kredytowymi poprzez sieć Authorize.net
gluon/contrib/AuthorizeNet.py
  • Dowcommerce [dowcommerce] API przetwarzania kart kredytowych:
gluon/contrib/DowCommerce.py
  • PaymentTech API przetwarzania kart kredytowych::
gluon/contrib/paymentech.py
  • PAM[PAM] API uwierzytelniania stworzone przez Chrisa AtLee:
gluon/contrib/pam.py
  • Klasyfikator Bayesian do wypełniania bazy danych fikcyjnymi danymi w celach testowych:
gluon/contrib/populate.py
  • Plik z API do uruchamiania na Heroku.com :
    heroku
gluon/contrib/heroku.py
  • Plik umożliwiający interakcje z paskiem zadań w Windows, gdy web2py jest uruchomiony jako usługa:
gluon/contrib/taskbar_widget.py
  • Opcjonalne login_methods i login_forms używane do uwierzytelniania:
gluon/contrib/login_methods/__init__.py
gluon/contrib/login_methods/basic_auth.py
gluon/contrib/login_methods/browserid_account.py
gluon/contrib/login_methods/cas_auth.py
gluon/contrib/login_methods/dropbox_account.py
gluon/contrib/login_methods/email_auth.py
gluon/contrib/login_methods/extended_login_form.py
gluon/contrib/login_methods/gae_google_account.py
gluon/contrib/login_methods/ldap_auth.py
gluon/contrib/login_methods/linkedin_account.py
gluon/contrib/login_methods/loginza.py
gluon/contrib/login_methods/oauth10a_account.py
gluon/contrib/login_methods/oauth20_account.py
gluon/contrib/login_methods/oneall_account.py
gluon/contrib/login_methods/openid_auth.py
gluon/contrib/login_methods/pam_auth.py
gluon/contrib/login_methods/rpx_account.py
gluon/contrib/login_methods/x509_auth.py

Paltforma web2py zawiera również folder z przydatnymi skryptami, w tym

scripts/setup-web2py-fedora.sh
scripts/setup-web2py-ubuntu.sh
scripts/setup-web2py-nginx-uwsgi-ubuntu.sh
scripts/setup-web2py-heroku.sh
scripts/update-web2py.sh
scripts/make_min_web2py.py
...
scripts/sessions2trash.py
scripts/sync_languages.py
scripts/tickets2db.py
scripts/tickets2email.py
...
scripts/extract_mysql_models.py
scripts/extract_pgsql_models.py
...
scripts/access.wsgi
scripts/cpdb.py

Skrypty setup-web2py-* są szczególnie przydatne, ponieważ próbują realizować od podstaw pełną instalację i konfigurację środowiska produkcyjnego web2py. Niektóre z nich zostały omówione w rozdziale 14, ale wszystkie z nich zawierają wewnątrz opis dokumentacyjny, który wyjaśnia ich przeznaczenie i używanie.

Wreszcie web2py zawiera pliki wymagane do budowy dystrybucji binarnej.

Makefile
setup_exe.py
setup_app.py

Są to skrypty konfiguracyjne odpowiednio dla py2exe i py2app i są tylko wymagane przy budowaniu dystrybucji binarnej web2py. NIGDY NIE ZAJDZIE POTRZEBA ICH URUCHOMIENIA.

Aplikacje web2py zawierają dodatkowe pliki, zwłaszcza zewnetrzne biblioteki JavaScript, takie jak jQuery, calendar i Codemirror. Ich autorzy są wymienieni w plikach tych bibliotek.

Aplikacje

Aplikacje stworzone w web2py są złożone z następujących części (znajdujących się w swoich katalogach):

  • models (modele) opisuje reprezentację danych w tabelach bazy danych i relacje pomiędzy tabelami;
  • controllers (kontrolery) opisuje logikę aplikację i proces przetwarzania;
  • views (widoki) opisuje jak dane powinny być prezentowane użytkownikowi przy użyciu HTML i JavaScript;
  • languages (języki) opisują jak przetłumaczyć ciągy tekstowe w aplikacji na różne obsługiwane języki;
  • static files (pliki statyczne) nie wymagają przetwarzania (np. obrazy, arkusze stylów CSS itd.);
  • ABOUT i README - te pliki są oczywiste;
  • errors (błędy) przechowuje się tu raporty o błędach wygenerowane przez aplikację;
  • sessions (sesje) przechowuje się tu informacje odnoszące się do każdego określonego użytkownika;
  • databases (bazy danych) przechowuje się tu bazy danych SQLite i dodatkowe tabelaryczne informacje;
  • cache (pamięc podręczna) przechowuje się tu buforowane elementy aplikacje;
  • modules (moduły) są tu opcjonalne moduły Pythona;
  • private (prywatne) pliki, które są dostępne dla kontrolerów ale nie bezpośrednio dla programisty;
  • uploads (przesłane) pliki są dostępne dla modeli ale nie bezpośrednio dla programisty (np. pliki przesłane na serwer przez użytkowników aplikacji).
  • tests (testy) jest katalogiem do przechowywania skryptów testów, fikstur i makiet.

Modele, widoki, kontrolery, języki i pliki statyczne są dostępne poprzez interfejs administracyjny. Pliki ABOUT i README oraz katalog errors są również dostępne przez interfejs administracyjny za pośrednictwem odpowiednich elementów menu. Pliki sesji, pamięci podręcznej, modułów i pliki prywatne są dostępne przez aplikacje, ale nie za pomocą interfejsu administracyjnego.

Wszystko jest uporządkowane w zrozumiałe struktury katalogów, które są replikowane dla każdej zainstalowanej aplikacji web2py, choć użytkownik nie potrzebuje bezpośredniego dostępu do systemu plików:

o aplikacji
licecja
pamięć podręczna
kontrolery
bazy danych
błędy
języki
modele
moduły
prywatne pliki
sesje
pliki statyczne
testy
pliki przesłane
widoki
__init__.py

__init__.py  ABOUT        LICENSE    models    views
controllers  modules      private    tests     cron
cache        errors       upload     sessions  static

Plik "__init__.py" to pusty plik wymagany w celu umożliwienia Pythonowi (i web2py) importowanie modułów w katalogu modules.

Aplikacja admin po prostu dostarcza interfejs administracyjny dla aplikacji web2py w systemie plików serwera. Aplikacje web2py mogą być również tworzone i programowane z poziomu linii poleceń lub poprzez wybrany edytor tekstowy (np. w IDE) – nie jest konieczne korzystanie z interfejsu administracyjnego w przeglądarce. Nową aplikację można utworzyć ręcznie przez replikowanie powyższej struktury katalogów, np. "applications/newapp/" (lub po prostu rozpakowanie pliku welcome.w2p do katalogu nowej aplikacji). Pliki aplikacji mogą być również stworzone i edytowane z poziomu linii poleceń bez konieczności korzystania z interfejsu administracyjnego.

API

Modele, kontrolery i widoki są wykonywane w środowisku, w którym następujące obiekty są już za nas zaimportowane:

Obiekty globalne:

request
response
session
cache

request, response, session, cache

Umiędzynarodowienie :

T
umiędzynarodowienie

T

Nawigacja:

redirect
HTTP

redirect, HTTP

Helpery:

helpery

XML, URL, BEAUTIFY

A, B, BODY, BR, CENTER, CODE, COL, COLGROUP,
DIV, EM, EMBED, FIELDSET, FORM, H1, H2, H3, H4, H5, H6,
HEAD, HR, HTML, I, IFRAME, IMG, INPUT, LABEL, LEGEND,
LI, LINK, OL, UL, META, OBJECT, OPTION, P, PRE,
SCRIPT, OPTGROUP, SELECT, SPAN, STYLE,
TABLE, TAG, TD, TEXTAREA, TH, THEAD, TBODY, TFOOT,
TITLE, TR, TT, URL, XHTML, xmlescape, embed64

CAT, MARKMIN, MENU, ON

Formularze i tabele

SQLFORM (SQLFORM.factory, SQLFORM.grid, SQLFORM.smartgrid)

Walidatory:

walidatory

CLEANUP, CRYPT, IS_ALPHANUMERIC, IS_DATE_IN_RANGE, IS_DATE,
IS_DATETIME_IN_RANGE, IS_DATETIME, IS_DECIMAL_IN_RANGE,
IS_EMAIL, IS_EMPTY_OR, IS_EXPR, IS_FLOAT_IN_RANGE, IS_IMAGE,
IS_IN_DB, IS_IN_SET, IS_INT_IN_RANGE, IS_IPV4, IS_LENGTH,
IS_LIST_OF, IS_LOWER, IS_MATCH, IS_EQUAL_TO, IS_NOT_EMPTY,
IS_NOT_IN_DB, IS_NULL_OR, IS_SLUG, IS_STRONG, IS_TIME,
IS_UPLOAD_FILENAME, IS_UPPER, IS_URL

Baza danych:

DAL

DAL, Field

W celu zachowania wstecznej kompatybilności dostępne są SQLDB=DAL i SQLField=Field. Zachęcamy do stosowania zamiast tej starej, nowej składni DAL i Field.

W bibliotekach są zdefiniowane inne obiekty i moduły, ale nie są one importowane automatycznie, ponieważ używane są rzadko.

Podstawowe encje API w środowisku wykonawczym web2py, to request, response, session, cache, URL, HTTP, redirect i T. Są one omówione niżej.

Kilka obiektów i funkcji, w tym Auth, Crud i Service są zdefiniowane w "gluon/tools.py" i jeśli są potrzebne, to muszą zostać zaimportowane:

from gluon.tools import Auth, Crud, Service

Są one importowane w db.py w aplikacji szkieletowej.

Dostęp do API z modułów Pythona

Twoje modele lub kontrolery mogą importować moduły Pythona, jeśli potrzebują dostępu do kodu tych modułów. Zazwyczaj są to pliki Pythona przechowywane w katalogu modules aplikacji. Mogą one też potrzebować dostępu do jakichś modułów API web2py. Sposobem na to jest ich zaimportowanie:

from gluon import *

W rzeczywistości, każdy moduł Pythona, jeśli nawet nie został zaimportowany przez aplikację web2py, może importować API web2py, jeśli web2py jest na ścieżce sys.path.

Choć jest jedno zastrzeżenie. Framework web2py definiuje niektóre obiekty globalne (request, response, session, cache, T), które mogą istnieć tylko wtedy, gdy obecn jest żądanie HTTP (lub zostało ono sfałszowane). Dlatego moduły mogą z nich korzystać tylko wtedy, gdy są wywoływane z poziomu aplikacji. Z tego powodu są one umieszczane w kontenerze wywołującym current, który jest obiektem lokalnego wątku. Oto przykład.

Utwórz moduł "/myapp/modules/test.py" zawierający:

from gluon import *
def ip(): return current.request.client

Teraz w kontrolerze "myapp" możesz napisać

import test
def index():
    return "Twój ip to " + test.ip()

Zwróć uwagę na kilka rzeczy:

  • Wyrażenie import test wyszukuje moduł najpierw w folderze bieżącej aplikacji, następnie w folderach wykazanych w sys.path. Dlatego moduły poziomu aplikacji zawsze mają pierwszeństwo przed modułami Pythona. Umożliwia to, aby różne aplikacje korzystały bez konfliktu z różnych wersji swoich modułów.
  • Różni użytkownicy mogą wywoływać równocześnie tą sama akcję index, która wywołuje funkcję w module bez powodowania konfliktu, ponieważ obiekt current.request jest innym obiektem w każdym wątku. Wystarczy zachować ostrożność, aby nie przejść do current.request poza funkcjami lub klasami w module (czyli na najwyższym poziomie).
  • Wyrażenie import test jest skrótem wyrażenia from applications.appname.modules import test. Używając dłuższej składni można zaimportować moduły z innej aplikacji.

W celu zachowania spójności z normalnym zachowaniem Pythona, web2py domyślnie nie przeładowuje modułów po wprowadzeniu zmian. Lecz można to zmienić. Aby włączyć możliwość automatycznego przeładowywania modułów, trzeba użyć funkcję track_changes w sposób następujący (zwykle w pliku modelu, przed jakimkolwiek wyrażeniem import):

from gluon.custom_import import track_changes; track_changes(True)

Od teraz, za każdym razem, moduł jest importowany, importer sprawdza, czy plik źródłowy Pythona (.py) został zmieniony. Jeśli tak, to moduł zostanie przeładowany.

Nie wywołuj track_changes w samych modułach.

Śledzenie zmian obywa się tylko dla modułów, które są przechowywane w aplikacji. Moduły importujące current uzyskują dostęp do:

  • current.request
  • current.response
  • current.session
  • current.cache
  • current.T

oraz każdej innej zmiennej aplikacji wybranej do przechowywania w current. Na przykład. można by zrobić taki model

auth = Auth(db)
from gluon import current
current.auth = auth

co spowoduje, że teraz wszystkie moduły mogą mieć dostęp do current.auth.

Wyrażenia current i import tworzą pełnowartościowy mechanizm do budowania rozszerzalnych i mogących być wielokrotnie używanych modułów dla aplikacji.

Uważaj! Biorąc from gluon import current, dobrze jest użyć current.request i którykolwiek z innych obiektów lokalnego wątku, ale nigdy nie należy przypisywać go do zmiennych globalnych w module, tak jak tu

request = current.request # ŹLE! NIEBEZPIECZNIE!

ani też nie należy przypisywać go do atrybutu klasy

class MyClass:
    request = current.request # ŹLE! NIEBEZPIECZNIE!

To dlatego, że obiekt lokalnego wątku musi być ekstrahowany w czasie wykonania. Natomiast zmienne globalne są definiowane tylko raz, podczas importowania modelu po raz pierwszy.

Zamiast tego, przypisz funkcję wewnątrz.

import * from gluon
...
def a_module_function():
    db = current.db  # assuming you assigned current.db = db in the model db.py
   ...

Inne zastrzeżenie dotyczy operacji z pamięcią podręczną. Nie można używać obiektu cache do dekorowania funkcji w modułach, dlatego że nie zachowuje się on wtedy zgodnie z oczekiwaniami. Dla buforowania funkcji f w module musi się użyć lazy_cache:

from gluon.cache import lazy_cache

@lazy_cache('key', time_expire=60, cache_model='ram')
def f(a, b, c,): ....

Pamiętaj, że klucz jest określany przez użytkownika, ale musi on być jednoznacznie powiązany z funkcją. Jeśli zostanie pominięty, to web2py określi go automatycznie.

Obiekt request

request
Storage
request.cookies
user_agent

Obiekt request jest instancją wszechobecnej klasy web2py o nazwie gluon.storage.Storage, która rozszerza klasę dict Pythona. W zasadzie jest to słownik, ale wartości jego elementów mogą być również dostępne jako atrybuty:

request.vars

jest tym samym, co:

request['vars']

W przeciwieństwie do słownika, jeśli atrybut (lub klucz) nie istnieje, to nie jest zgłaszany wyjątek. Zamiast tego zwracane jest None.

Czasem jest to przydatne do tworzenia własnych obiektów Storage. Można to zrobić następująco:

from gluon.storage import Storage
my_storage = Storage() # pusty obiekt storage
my_other_storage = Storage(dict(a=1, b=2)) # konwersja słownika do Storage

Obiekt request ma następujące elementy (atrybuty), z których niektóre również są dostępne w instancji klasy Storage:

  • request.cookies: obiekt Cookie.SimpleCookie() zawierający ciasteczka przekazywane przez żądania HTTP. Działa on jak słownik ciasteczek. Każde ciasteczko jest obiektem Morsel [morsel];
  • request.env: obiekt Storage zawierający zmienne środowiskowe przekazywane do kontrolera, w tym zmienne nagłówka HTTP z żądania HTTP i standardowe parametry WSGI. Wszystkie zmienne środowiskowe są konwertowane na małe litery a kropki na znaki podkreślenia w celi łatwiejszego zapamiętania;
  • request.application: nazwa żądanej aplikacji;
  • request.controller: nazwa żądanego kontrolera;
  • request.function: nazwa żądanej funkcji;
  • request.extension: rozszerzenie żądanej akcji. Domyślną wartością jest "html". Jeśli kontroler zwraca słownik i nie określa widoku, to jest używany do określenia rozszerzenia pliku widoku, który będzie renderował słownik (parsowany w request.env.path_info);
  • request.folder: słownik aplikacji. Na przykład, jeśli aplikacją jest "welcome", request.folder zostaje ustawione na ścieżkę bezwzględną "/path/to/welcome". W programie należy zawsze używać tej zmiennej oraz funkcji os.path.join do budowy ścieżek do plików, do których potrzeba uzyskać dostęp. Chociaż web2py zawsze używa ścieżek bezwzględnych, to dobrą praktyką jest niezmienianie nigdy jawnie bieżącego folderu roboczego (cokolwiek nim jest) ponieważ nie jest to bezpieczne dla wątku;
  • request.now: obiekt datetime.datetime przechowujący datę i czas bieżącego żądania;
  • request.utcnow: obiekt datetime.datetime przechowujący datę i czas UTC bieżącego żądania;
  • request.args: lista parametrów ścieżki URL występujących po nazwie funkcji kontrolera; ekwiwalent request.env.path_info.split('/')[3:]
  • request.vars: obiekt gluon.storage.Storage zawierający wszystkie parametry żądania;
  • request.get_vars: obiekt gluon.storage.Storage zawierający tylko parametry przekazane do ciąga zapytania (żądanie dla /a/c/f?var1=1&var2=2 będzie się kończyć ciągiem {var1: "1", var2: "2"});
  • request.post_vars: obiekt gluon.storage.Storage zawierający tylko parametry przekazane do ciała żądania (zwykle w żądaniach POST, PUT, DELETE);
  • request.client: adres IP klienta, zgodnie z ustaleniem request.env.http_x_forwarded_for, jeśli obecne, a jeśli nie to przez request.env.remote_addr. Chociaż jest to użyteczne, to jednak nie należy temu ufać, ponieważ http_x_forwarded_for może być sfałszowane;
  • request.is_local: True jeśli klientem jest localhost, w przeciwnym razie False. Powinno działać na proxy, jeśli proxy obsługuje http_x_forwarded_for;
  • request.is_https: True jeśli żądanie używa protokołu HTTPS, w przeciwnym razie False;
  • request.body: strumień plikowy tylko do odczytu zawierający ciało żądania HTTP. Jest on automatycznie parsowany w celu pobrania request.post_vars i następnie przewinięty. Można go odczytać poprzez request.body.read();
  • request.ajax True jeśli funkcja jest wywoływana poprzez żądanie Ajax;
  • request.cid jest to identyfikator komponentu, który wygenerował żądanie Ajax (jeśli miało to miejsce). Więcej informacji na ten temat znajduje się w rozdziale 12;
  • request.requires_https() uniemożliwia dalsze wykonywanie kodu, jeśli żądanie nie jest realizowane poprzez HTTPS i przekierowuje odwiedzającego do bieżącej strony poprzez HTTPS.
  • request.restful jest to nowy i bardzo przydatny dekorator, który może być wykorzystany do zmiany zachowania akcji web2py przez oddzielne żądania GET/POST/PUSH/DELETE. Jest to omówione w rozdziale 10.
  • request.user_agent() analizuje pole user_agent w nagłówku żądania i zwraca informację w formie słownika. Jest to przydatne do wykrywania urządzeń mobilnych. Wykorzystuje kod "gluon/contrib/user_agent_parser.py" stworzony przez Rossa Peoples. Aby zobaczyć jak to działa, spróbuj osadzić w widoku następujący kod:
{{=BEAUTIFY(request.user_agent())}}
  • request.global_settings
    request.global_settings
    zawiera ustawienia ogólnosystemowe web2py. Są one ustawiane automatycznie i nie ma potrzeby zmieniania ich. Na przykład request.global_settings.gluon_parent zawiera pełną ścieżkę do folderu web2py, request.global_settings.is_pypy określa czy web2py jest uruchomiony na PyPy;
  • request.wsgi jest to hak, który umożliwia wywołanie aplikacji WSGI osób trzecich z poziomu akcji.

Ostatnio dołączono:

  • request.wsgi.environ
  • request.wsgi.start_response
  • request.wsgi.middleware

ich użycie jest omówione na końcu tego rozdziału.

Jako przykład, następujące adres na typowym systemie:

http://127.0.0.1:8000/examples/default/status/x/y/z?p=1&q=2

da w wyniku następujący obiekt request:

request
env

zmiennawartość
request.applicationexamples
request.controllerdefault
request.functionstatus
request.extensionhtml
request.viewstatus
request.folderapplications/examples/
request.args['x', 'y', 'z']
request.vars<Storage {'p': 1, 'q': 2}>
request.get_vars<Storage {'p': 1, 'q': 2}>
request.post_vars<Storage {}>
request.is_localFalse
request.is_httpsFalse
request.ajaxFalse
request.cidNone
request.wsgi<hook>
request.env.content_length0
request.env.content_type
request.env.http_accepttext/xml,text/html;
request.env.http_accept_encodinggzip, deflate
request.env.http_accept_languageen
request.env.http_cookiesession_id_examples=127.0.0.1.119725
request.env.http_host127.0.0.1:8000
request.env.http_refererhttp://web2py.com/
request.env.http_user_agentMozilla/5.0
request.env.path_info/examples/simple_examples/status
request.env.query_stringremote_addr:127.0.0.1
request.env.request_methodGET
request.env.script_name
request.env.server_name127.0.0.1
request.env.server_port8000
request.env.server_protocolHTTP/1.1
request.env.server_softwareRocket 1.2.6
request.env.web2py_path/Users/mdipierro/web2py
request.env.web2py_versionVersion 2.4.1
request.env.wsgi_errors<open file, mode 'w' at >
request.env.wsgi_input
request.env.wsgi_url_schemehttp

To jakie zmienne środowiskowe zostaną zdefiniowane zależy od serwera internetowego. Tutaj założyliśmy użycie wbudowanego serwera WSGI Rocket. Ten zestaw zmiennych nie różni zbyt od tego, jaki otrzymuje się przy użyciu serwera internetowego Apache.

Zmienne request.env.http_* z nagłówka żądania HTTP są parsowane.

Zmienne request.env.web2py_* ze środowiska serwera internetowego nie są parsowane, ale są tworzone przez web2py. W przypadku naszej aplikacji trzeba znać lokalizację i wersję oraz czy jest ona uruchomiona na Google App Engine (ponieważ może być konieczna specyficzna optymalizacja).

Warto również zwrócić uwagę na zmienne request.env.wsgi_*. Są one specyficzne dla adaptera wsgi.

Obiekt response

response
response.body
response.cookies
response.download
response.files
response.flash
response.headers
response.meta
response.menu
response.postprocessing
response.render
response.static_version
response.status
response.stream
response.subtitle
response.title
response.toolbar
response.view
response.delimiters
response.js
response.write
response.include_files
response.include_meta
response.optimize_css
response.optimize_js
response._caller
response.models_to_run

Obiekt response jest inną instancją klasy Storage. Zawiera:

  • response.body: obiekt StringIO w którym web2py zapisuje ciało strony wyjściowej. NIGDY NIE ZMIENIAJ TEJ ZMIENNEJ;
  • response.cookies: podobne do request.cookies z tą różnicą, że zamiast ciasteczek przesłanych od klienta zawiera ciasteczka ciasteczka wysłane przez serwer do klienta. Ciasteczko sesji jest obsługiwane automatycznie.
  • response.download(request, db): metoda używana do implementowania funkcji kontrolera umożliwiającej pobieranie przesyłanych plików. Metoda response.download oczekuje aby ostatni arg w request.args był zakodowaną nazwą pliku (np. nazwą pliku generowaną w czasie przesyłania i zapisana w polu upload). Ekstrahuje ona nazwę przesyłanego pliku oraz nazwę tabeli, jak również oryginalną nazwę pliku. Metoda response.download pobiera dwa opcjonalne argumenty: chunk_size ustawia rozmiar w bajtach dla pakietowego przesyłania strumieniowego (domyślnie to 64K) a attachments określa czy pobrany plik powinien być traktowany jako załącznik czy też nie (domyślnie to True). Trzeba mieć na uwadze, że response.download jest specjalnie przeznaczony dla pobieranie plików związanych z polami upload w db. Używaj response.stream (patrz niżej) dla innych typów pobieranych plików i strumieniowania. Nie jest konieczne użycie response.download w celu uzyskania dostępu do plików przesyłanych do folderu /static – pliki statyczne mogą (i na ogół powinny) być dostępne bezpośrednio poprzez adres URL (np. /app/static/files/myfile.pdf);
  • response.files: lista plików .css, .js, .coffee i .less wymaganych przez stronę. Będą one automatycznie linkowane w sekcji standardowego widoku "layout.html" za pomocą "web2py_ajax.html". W celu dołączenia nowych plików CSS, JS, COFFEE lub LESS wystarczy je dołączyć do tej listy. Będą obsługiwane duplikaty. Kolejność jest ważna;
  • response.include_files() generuje znaczniki sekcji head pliku html w celu dołączenia wszystkich response.files (używanych w "views/web2py_ajax.html");
  • response.flash: opcjonalny parametr, który może zostać zawarty w widokach. Zwykle używany do powiadamiania użytkownika o czymś co się stało.
  • response.headers: słownik dla nagłówków odpowiedzi HTTP. Platforma web2py ustawia domyślnie kilka nagłówków, w tym "Content-Length", "Content-Type" i "X-Powered-By" (ustawiany na web2py). Platforma web2py ustawia również nagłówki "Cache-Control", "Expires" i "Pragma" w celu zapobiegania buforowaniu po stronie klienta, z wyjątkiem żądań dla plików statycznych, dla których buforowanie po stronie klienta jest włączone. Nagłówki ustawiane przez web2py mogą być nadpisywane lub usuwane oraz mogą być dodawane nowe nagłówki (np. response.headers['Cache-Control'] = 'private'). Nagłówek może zostać usunięty przez usunięcie jego klucza ze słownika, np. del response.headers['Custom-Header'], jednak domyślne nagłówki web2py zostaną po prostu dodane ponownie przed zwróceniem odpowiedzi. Aby uniknąć tego problemu, potrzeba ustawić wartość nagłówka na None, np. w celu usunięcia domyśłnego nagłówka Content-Type, trzeba ustawić response.headers['Content-Type'] = None;
  • response.menu: opcjonalny parametr, który może być zawarty w widokach, zwykle używany do przekazania nawigacyjnego drzewa menu do widoku. Może być renderowany przez helper MENU;
  • response.meta: obiekt klasy Storage, który zawiera opcjonalne informacje <meta>, takie jak response.meta.author, .description lub .keywords. Zawartość każdej zmiennej meta jest automatycznie umieszczana w odpowiednim znaczniku META przez kod w widoku "views/web2py_ajax.html", który jest dołączany przez domyślny widok "views/layout.html";
  • response.include_meta() generuje ciąg znakowy zawierający wszystkie serializowane nagłówki response.meta (używane w widoku "views/web2py_ajax.html");
  • response.postprocessing: jest to lista funkcji, domyślnie pusta. Funkcje te są wykorzystywane do filtrowania obiektu odpowiedzi na wyjściu akcji, zanim wyjście zostanie zrenderowane przez widok. Listę ta można wykorzystać do implementacji innych języków szablonowania;
  • response.render(view, vars): metoda używana do wywołania widoku w sposób jawny wewnątrz kontrolera. Opcjonalny parametr view jest nazwą pliku widoku, vars jest słownikiem nazwanych wartości przekazywanych do widoku;
  • response.session_file: plik strumienia zawierający sesję;
  • response.session_file_name: nazwa pliku w którym będą zapisywane sesje;
  • response.session_id: identyfikator bieżącej sesji. Jest ustalany automatycznie. NIGDY NIE ZMIENIAJ TEJ ZMIENNEJ;
  • response.session_id_name: nazwa ciasteczka sesji dla tej aplikacji. NIGDY NIE ZMIENIAJ TEJ ZMIENNEJ;
  • response.static_version: numer wersji dla zarządzania statycznymi aktywami;
  • response.status: kod stanu HTTP, który ma być przekazany do odpowiedzi. Domyślnie, to 200 (OK);
  • response.stream(file, chunk_size, request=request, attachment=False, filename=None): gdy kontroler to zwraca, web2py strumieniuje z powrotem zawartość pliku do klienta w blokach o wielkości określonej w chunk_size. Parametr request jest wymagany do korzystania z początku fragmentu w nagłówku HTTP. Parametr filepowinien być ścieżką do pliku (z powodu wstecznej kompatybilności, może to być również obiekt otwartego pliku, lecz nie jest to zalecane). Jak wspomniano powyżej, metoda response.download powinna być używana do pobierania przechowywanych plików poprzez pole upload. Metodę response.stream można wykorzystywać w innych przypadkach, takich jak zwracanie pliku tymczasowego lub obiektu StringIO utworzonego przez kontroler. Jeśli parametr attachment to True, nagłówek Content-Disposition zostanie ustawiony na "attachment", a jeśli filename jest też dostarczone, to zostanie dodany również nagłówek Content-Disposition (ale tylko wtedy, gdy attachment ma wartość True). Jeśli nic nie zawarto w response.headers, to następujące nagłówki odpowiedzi zostaną automatycznie ustawione: Content-Type, Content-Length, Cache-Control, Pragma i Last-Modified (trzy ostatnie będą ustawione w celu umożliwienia przeglądarce buforowania pliku). Aby nadpisać któryś z tych automatycznych ustawień, wystarczy ustawić go w response.headers przed wywołaniem response.stream;
  • response.subtitle: opcjonalny parametr, który może zostać zawarty w widokach. Powinien zawierać podtytuł strony;
  • response.title: opcjonalny parametr, który może zostać zawarty w widokach. Powinien zawierać tytuł strony i powinien być renderowany wewnątrz znacznika title HTML w sekcji header.
  • response.toolbar: funkcja umożliwiająca osadzenie paska narzędziowego na stronie do celów debugowania {{=response.toolbar()}}. Ten pasek narzędziowy wyświetla żądania, odpowiedzi, zmienne sesji oraz czas dostępu do bazy danych w każdym zapytaniu;
  • response._vars: ta zmienna jest dostępna tylko w widoku, nie w akcji. Zawiera wartości zwracane przez akcję do widoku;
  • response._caller: jest to funkcja opakowująca wszystkie wywołania akcji. Domyślnie jest to funkcja identyfikująca, ale może zostać zmodyfikowana w celu wyłapania specjalnych typów wyjątków i ich dodatkowym zarejestrowaniu w dzienniku; response._caller = lambda f: f()
  • response.optimize_css: można ustawić na "concat,minify,inline" w celu łączenia, minifikacji i łączenia w jedną linię kodu plików CSS dołączonych przez web2py;
  • response.optimize_js: można ustawić na "concat,minify,inline" w celu łączenia, minifikacji i łączenia w jedną linię kodu plików CSS dołączonych przez web2py;
  • response.view: nazwa szablonu widoku, który musi renderować stronę. Domyślnie ustawione jest na:
      "%s/%s.%s" % (request.controller, request.function, request.extension)
      
    
    lub jeżeli powyższy plik nie może zostać zlokalizowany, to na
      "generic.%s" % (request.extension)
      
    
    Można zmienić wartość tej zmiennej w celu zmodyfikowania pliku widoku związanego z określona akcją.
  • response.delimiters domyślnie to ('{{','}}'). Pozwala to na zmianę separatora kodu osadzanego w widokach;
  • response.xmlrpc(request, methods): gdy kontroler to zwraca, funkcja udostępnia metody poprzez XML-RPC[xmlrpc] . Funkcja ta jest przestarzała ponieważ dostępne są lepsze mechanizmy opisane w rozdziale 10;
  • response.write(text): metoda do wpisywania tekstu do ciała strony wyjściowej;
  • response.js może zawierać kod JavaScript. Kod ten będzie wykonywany tylko jeśli odpowiedź jest odbierana przez komponent web2py, tak jak opisano to w rozdziale 12;
  • response.models_to_run zawiera listę wyrażeń regularnych wybierających to co modele uruchamiają;
    • Domyślnie jest ustawiane dla ładowania plików /a/models/*.py, /a/models/c/*.py i /a/models/c/f/*.py gdy żądany jest /a/c/f. Można ustawić np. response.models_to_run = ['myfolder/'] aby wymuszać tylko modele wewnątrz podfolderu models/myfolder aplikacji;
    • UWAGA: response.models_to_run jest listą wyrażeń regularnych a nie listą ścieżek do pliku. Wyrażenia regularne są względne w stosunku do folderu models/, tak więc każdemu plikowi modelu odpowiada jedna ścieżka do pliku, który ma być wykonany. Należy również zwrócić uwagę, że to nie może wpływać na żadne modele które zostały już ewaluowane, ponieważ zostały wcześniej posortowane alfabetycznie. Tak jest, jeśli warunkowy model dla kontrolera orange to orange/orange_model.py i ustawiono wyrażenie regularne na [.*], którego zmiany nie wpływają na żaden poprzednio odrzucony model do załadowania, taki jak apple/apple_model.py – dopasowuje to nowe wyrażenie regularne, ale będzie to ewaluowane i odrzucane zanim orange/orange_model.py zmieni wyrażenie regularne;
    • Oznacza to, że jeśli chce się użyć models_to_run w celu udostępnienia warunkowych modeli pomiędzy kontrolerami, to trzeba umieścić model w podfolderze, w którym elementy są sortowane malejąco, taki jak zzz i następnie użycie wyrażenia regularnego 'zzz'.

Ponieważ response jest obiektem gluon.storage.Storage, to może być zastosowane do przechowywania innych atrybutów, które chce się przekazać do widoku. Chociaż nie ma technicznych ograniczeń, naszym zaleceniem jest przechowywanie tylko zmiennych, które mają być renderowane przez wszystkie strony z ogólnym układem ("layout.html").

W każdym razie, zdecydowanie sugerujemy aby utrzymywać nastęþujace zmienne:

response.title
response.subtitle
response.flash
response.menu
response.meta.author
response.meta.description
response.meta.keywords
response.meta.*

ponieważ w ten sposób łatwiej jest zamienić standardowy plik "layout.html", na taki, który stosuje ten sam zestaw zmiennych.

W starszych wersjach web2py używano response.author zamiast response.meta.author i podobne do innych meta atrybutów.

Obiekt session

session
session.connect
session.forget
session.secure
Obiekt session jest jedną z instancji klasy Storage. Cokolwiek zostanie zapisywane w session, na przykład:

session.myvariable = "hello"

może być odzyskane w późniejszym czasie:

a = session.myvariable

tak długo, jak kod jest wykonywany w ramach tej samej sesji przez tego samego użytkownika (o ile użytkownik nie usunie ciasteczek sesji a sesja nie wygasła). Ponieważ session jest obiektem Storage, próba uzyskania dostępu do atrybutu (klucza), który nie został ustawiony nie zgłasza wyjątku, zamiast tego zwraca None.

Obiekt sesji ma trzy ważne metody. Jedna z nich, to forget:

session.forget(response)

Informuje ona web2py, aby nie zapisywał sesji. Powinna być stosowana w tych kontrolerach, których akcje są wywoływane często i nie ma potrzeby śledzenia aktywności użytkownika. Metoda session.forget() zapobiega przed przepisywaniem pliku sesji niezależnie od tego czy był on modyfikowany. Metoda session.forget(response) dodatkowo otwiera i zamyka plik sesji. Rzadko trzeba korzystać z tej metody, ponieważ sesje nie są zapisywana, gdy nie zostały zmienione. Jednakże, jeśli strona wykonuje wiele jednoczesnych żądań Ajax, to dobrym pomysłem na wywoływanie akcji poprzez Ajax jest wywołanie session.forget(response) (zakładając, że sesja nie jest potrzebna dla akcji). W przeciwnym razie każda akcja Ajax będzie musiała czekać na zakończenie poprzedniej akcji (i odblokowanie pliku sesji) przed kontynuowaniem, co spowalnia ładowanie strony. Należy mieć na uwadze, że sesje nie zostają zablokowane, gdy są przechowywane w bazie danych.

Inna metoda to:

session.secure()

który powiadamia web2py, aby ustawił ciasteczko sesji jako bezpieczne ciasteczko. Powinno się to ustawiać, jeśli aplikacja używa połączenia https. Przez ustawienie ciasteczka sesji jako bezpiecznego, serwer prosi przeglądarkę aby nie odsyłał z powrotem ciasteczka do serwera, jeśli nie jest to połączenie https.

Następną metodą jest connect. Domyślnie sesje są przechowywane w systemie plików a ciasteczko jest używane do przechowywania i pobierania session.id. Używając metody connect jest możliwe poinformowanie web2y aby przechowywał sesje w bazie danych albo w ciasteczkach, eliminując konieczność dostępu do systemu plików w zarządzaniu sesją.

Na przykład, aby przechować sesje w bazie danych:

session.connect(request, response, db, masterapp=None)

gdzie db jest nazwą otwartego połączenia z bazą danych (tak jak zwrócił to DAL). Powiadamia to web2py, że chce się przechować sesje w bazie danych a nie w systemie plików. Metoda session.connect musi być wywołana po db=DAL(...), ale przed jakąkolwiek logiką wymagajacą sesji, na przykład, konfiguracją Auth.

web2py utworzy tabelę:

db.define_table('web2py_session',
                 Field('locked', 'boolean', default=False),
                 Field('client_ip'),
                 Field('created_datetime', 'datetime', default=now),
                 Field('modified_datetime', 'datetime'),
                 Field('unique_key'),
                 Field('session_data', 'text'))

i zapisze speklowane sesje w polu session_data.

Opcja masterapp=None powiadamia (domyślnie) web2py aby próbował odzyskać istniejącą sesję z nazwą w request.application, w uruchomionej aplikacji.

Jeśli chce się aby dwie lub więcej aplikacji współdzieliły sesje, trzeba ustawić masterapp na nazwę głównej aplikacji.

Natomiast, aby zapisać sesje w ciasteczkach można zrobić tak:

session.connect(request, response, cookie_key='yoursecret', compression_level=None)

gdzie cookie_key to symetryczny klucz szyfrowania, a compression_level jest opcjonalnym poziomem szyfrowania zlib.

Sesje w ciasteczku są często zalecane z powodu skalowalności, gdyż mają ograniczoną wielkość, ale duże sesje będą powodować załamanie się ciasteczek.

Można sprawdzić stan swojej aplikacji w dowolnym momencie przez wydrukowanie zmiennych systemowych request, session i response. Jednym sposobem na wykonanie tego jest utworzenie dedykowanej akcji:

def status():
    return dict(request=request, session=session, response=response)

W widoku "generic.html" osiąga się to używając {{=response.toolbar()}}.

Nie przechowuj w sesji klas zdefiniowanych przez użytkownika

Zmienne przechowywane w sesji są zachowywane pomiędzy żądaniami po ich serializacji.

Sesje są pobierane przed wykonaniem kodu modułu, a więc przed zdefiniowaniem klas. Dlatego klasy zdefiniowane przez użytkownika nie mogą zostać speklowane.

Klasy zdefiniowane w modułach są również "szarą strefą" i nie powinny być umieszczane w pamięci. Będą one działać przez większość czasu ale w końcu mogą załamać sesję. Tak jest ponieważ, na przykład, jeśli zrestartuje się serwer internetowy w chwili pobierania sesji przez użytkownika, to może się zdarzyć, że moduł zostanie zaimportowany. Ten sam problem wystąpi podczas uruchamiania przez serwer internetowy nowego procesu roboczego oraz w środowisku rozproszonym.

Oddzielne sesje

Jeśli sesje są przechowywane w systemie plików i ma się ich wiele, to system plików może stać się wąskim gardłem. Jedno z rozwiązań jest następujące:

session.connect(request, response, separate=True)

Ustawiając separate=True web2py będzie przechowywał sesje nie w folderze "sessions/" ale w podfolderze folderu "sessions/". Podfolder będzie tworzony automatycznie. Sesje z tym samym przedrostkiem będą zapisywane w tym samym podfolderze. Znowu trzeba mieć na uwadze, że powyższe wyrażenie musi być wywołane przed jakąkolwiek logiką, którą może wymagać sesja.

Obiekt cache

cache
cache.ram
cache.disk
W środowisku uruchomieniowym web2py jest również dostępny globalny obiekt cache. Ma on dwa atrybuty:

  • cache.ram: pamięć podręczna aplikacji w głównej pamięci;
  • cache.disk: pamięć podręczna aplikacji na dysku.

Atrybut cache jest wywoływalny, co pozwala na stosowanie go jako dekoratora akcji lub widoków.

Powyższy przykład buforuje funkcję time.ctime() w RAM:

def cache_in_ram():
    import time
    t = cache.ram('time', lambda: time.ctime(), time_expire=5)
    return dict(time=t, link=A('click me', _href=request.url))

Wyjście funkcji lambda: time.ctime() jest buforowane w RAM przez 5 sekund. Ciąg 'time' został tu użyty jako klucz.

Poniższy przykład buforuje funkcję time.ctime() na dysku:

def cache_on_disk():
    import time
    t = cache.disk('time', lambda: time.ctime(), time_expire=5)
    return dict(time=t, link=A('click me', _href=request.url))

Wyjście funkcji lambda: time.ctime() jest buforowane na dysku (z wykorzystaniem modułów shelve) na 5 sekund.

Trzeba mieć na uwadze, że drugi argument cache.ram i cache.disk musi być funkcją lub wywoływalnym obiektem. Jeśli chce się buforować istniejący obiekt, a nie wyjście funkcji, można po prostu zwrócić to przez funkcję lambda:

cache.ram('myobject', lambda: myobject, time_expire=60*60*24)

Następny przykład buforuje funkcję time.ctime() zarówno w RAM jak i na dysku:

def cache_in_ram_and_disk():
    import time
    t = cache.ram('time', lambda: cache.disk('time',
                       lambda: time.ctime(), time_expire=5),
                       time_expire=5)
    return dict(time=t, link=A('click me', _href=request.url))

Wyjście lambda: time.ctime() jest buforowane na dysku (z wykorzystaniem moduły shelve) a następnie w RAM na 5 sekund. Najpierw web2py przeszukuje pamięć RAM i jeśli nic tam nie znajdzie, to przeszukuje dysk. Jeśli nie ma tego co potrzeba ani w pamieci RAM ani na dysku, to wykonywana jest funkcja lambda: time.ctime() i pamięć podręczna zostaje zaktualizowana. Technika ta jest użyteczna w środowisku wieloprocesorowym. Dwa czasy nie muszą być takie same.

W poniższym przykładzie buforowane jest wyjście kontrolera funkcji (ale nie widoku) w pamięci RAM:

buforowanie kontrolera
@cache(request.env.path_info, time_expire=5, cache_model=cache.ram)
def cache_controller_in_ram():
    import time
    t = time.ctime()
    return dict(time=t, link=A('click me', _href=request.url))

Słownik zwracany przez cache_controller_in_ram jest buforowany w RAM na 5 sekund. Wynik wyboru bazy danych nie może być buforowany bez wykonania najpierw serializacji. Lepszym sposobem jest buforowanie wybranej bazy danych bezpośrednio przy wykorzystaniu argumentu select metody cache.

Poniższy przykład buforuje wyjście funkcji kontrolera (ale nie widoku) na dysk:

@cache(request.env.path_info, time_expire=5, cache_model=cache.disk)
def cache_controller_on_disk():
    import time
    t = time.ctime()
    return dict(time=t, link=A('click to reload',
                              _href=request.url))

Słownik zwracany przez cache_controller_on_disk jest buforowany na dysku przez 5 sekund. Trzeba pamiętać, że web2py nie może buforować słownika, który zawiera niepeklowane obiekty.

Możliwe jest również buforowanie widoku. Renderowanie widoku w funkcji kontrolera to trik polegający na tym, aby kontroler zwracał ciąg znakowy. Jest to realizowane przez zwracanie response.render(d) gdzie d jest słownikiem, który chcemy przekazać do widoku. Poniższy przykład buforuje w RAM wyjście funkcji kontrolera (łącznie z renderowanym widokiem):

buforowanie widoku
@cache(request.env.path_info, time_expire=5, cache_model=cache.ram)
def cache_controller_and_view():
    import time
    t = time.ctime()
    d = dict(time=t, link=A('click to reload', _href=request.url))
    return response.render(d)

Funkcja response.render(d) zwraca zrenderowany widok jako ciąg znakowy, który jest teraz buforowany przez 5 sekund. Jest to najlepszy i najszybszy sposób buforowania.

Zalecamy stosowanie @cache.action obsługiwane w web2py &gt; 2.4.6

Parametr time_expire jest używany do porównania bieżącego czasu z czasem żądanego obiektu, który był ostatnio zapisany w pamięci podręcznej. Nie ma to wpływu na przyszłe żądania. Umożliwia aby time_expire był ustawiany dynamicznie podczas żądania obiektu, a nie ustalany podczas zapisywania obiektu. Na przykład:

message = cache.ram('message', lambda: 'Hello', time_expire=5)

Załóżmy teraz, że następujące wywołanie jest wykonane na 10 sekund przed powyższym wywołaniem:

message = cache.ram('message', lambda: 'Goodbye', time_expire=20)

Ponieważ time_expire jest ustawione na 20 sekund w drugim wywołaniu i tylko 10 sekund upłynęło od komunikaty, który był zapisany pierwszy, wartość "Hello" będzie pobierana z pamięci podręcznej i nie zostanie zaktualizowana na "Goodbye". 5 sekundowa wartość time_expire w pierwszym wywołaniu nie ma wpływu na drugie wywołanie.

Ustawienie time_expire=0 (lub na wartość ujemną) wymusza, aby buforowana pozycja była odświeżana (ponieważ czas jaki upłynął od ostatniego zapisu zawsze będzie > 0) a ustawienie time_expire=None wymusza pobieranie wartości buforowanej, niezależnie od czasu jaki upłynął od ostatnie zapisu (jeśli time_expire ma zawsze wartość None, to element buforowany będzie nigdy nie wygasający).

Z pamięci podręcznej można usunąć jedna lub więcej zmiennych poleceniem

cache clear
cache.ram.clear(regex='...')

gdzie regex jest wyrażeniem regularnym dopasowującym wszystkie klucze, jakie chce się usunąć z pamięci podręcznej. Można również usunąć pojedynczy element stosując:

cache.ram(key, None)

gdzie key jest kluczem buforowanego elementu.

Możliwe jest również określenie innych mechanizmów buforowania, takie jak memcache. Memcache jest dostępny poprzez gluon.contrib.memcache i został omówiony w rozdziale 14.

Bądź ostrożny podczas buforowania, pamiętając, że buforowanie jest zwykle dokonywane na poziomie aplikacji, a nie na poziomie użytkownika. Jeśli potrzeba, na przykład, buforować zawartość specyficzną dla użytkownika, to trzeba wybrać klucz, który zawiera identyfikator użytkownika.

Aplikacja interfejsu administracyjnego aplikacji umożliwia wyświetlanie przycisków pamięci podręcznej (i czyszczenia pamięci podręcznej). Dostęp do nich można uzyskać z poziomu ekranu zarządzania bazą danych.

Dekorator cache.action

W web2py domyślnie zakłada się, że zwracana zawartość nie będzie buforowana, gdyż zmniejsza się ryzyko niewłaściwego buforowania po stronie klienta i unika tego skutków.

Na przykład, gdy wyświetla się formularz użytkownikowi lub listę rekordów, strona internetowa nie powinna być buforowana, gdyż inni użytkownicy mogą w tym czasie wprowadzać nowe rekordy do tabeli, która się wyświetla.

Zamiast tego, jeśli wyświetlana jest strona, której zawartość nie będzie się nigdy zmieniać (lub zmieniać rzadko, np. raz na tydzień), to warto tą strona przechować, ale jeszcze lepiej jest powiadomić klienta, że strona ta jest niezmienna.

Osiąga się to przez wysyłanie wraz ze stroną kilku specjalnych nagłówków. Gdy przeglądarka klienta odbierze taką zawartość, to przechowa ją w pamięci podręcznej przeglądarki i nie będzie żądać ponownie takiej strony od serwera. Jest to główny mechanizm przyśpieszania witryn o dostępie publicznym.

W wersjach web2py > 2.4.6 wprowadzono nowy dekortor cache.action umożliwiający bardziej inteligentną obsługę takich sytuacji. Dekorator cache.action może zostać użyty do:

  • ustawiania inteligentnych nagłówków buforowania;
  • odpowiedniego buforowania wyników.

NB: Jest stosowany dla jednego lub drugiego celu albo obu naraz.

Użycie request.env.path_info jako klucza w buforowaniu widoku poprzez

@cache(request.env.path_info, time_expire=300, cache_model=cache.ram)

sprawia kilka problemów, np.:

  1. Nie są obsługiwane zmienne URL
    • Buforowanie wyniku /app/default/index?search=foo : przez następne 300 sekund /app/default/index?search=bar zwracać będzie dokładnie tą samą rzecz co /app/default/index?search=foo;
  2. Nie jest obsługiwany użytkownik
    • Jakiś użytkownik uzyskuje często dostęp do strony i wybiera ją z pamięci podręcznej. Jednakże, buforowany był wynik /app/default/index przy wykorzystaniu request.env.path_info jako klucz, tak więc inny użytkownik będzie widział stronę, która nie była przeznaczona dla niego;
    • Buforowana została strona użytkownika "Bill", gdy "Bill" uzyskał dostęp do strony z pulpitu. Teraz próbuje on uzyskać dostęp z telefonu komórkowego: jeśli przygotowany był szablon dla użytkowników mobilnych, który jest inny od standardowego, to "Joe" nie zobaczy go;
  3. Nie jest obsługiwany język
    • Podczas buforowania strony, jeśli użyje się funkcji T() dla tych samych elementów, strona zostanie zapisana wraz z ustalonym tłumaczeniem;
  4. Nie jest obsługiwana metoda żądania
    • Gdy buforuje się stronę, to należy je buforować tylko wtedy, gdy jest ona wynikiem operacji GET
  5. Nie jest obsługiwany kod stanu strony
    • Gdy buforuje się stronę po raz pierwszy, czasami może pójść coś nie tak i zwracana jest piękna strona 404. Przecież nie chcesz buforować błędów!

Zamiast pisać dużo kodu szablonowego rozwiązującego te wszystkie problemy, wystarczy utworzyć cache.action.

Dekorator ten używa inteligentnie nagłówki buforowania pozwalając przeglądarce buforować wynik. Jeśli przekaże się do buforowania model, to rozwiązany zostanie automatycznie klucz, w najlepszy sposób, tak że różne wersje tej samej strony mogą być przechowywane i pobierane odpowiednio (np. jedna w języku angielskim a druga w polskim).

Pobiera on kilka parametrów, z inteligentnymi wartościami domyślnymi:

  • time_expire : domyślnie 300 sekund;
  • cache_model : domyślnie None. Oznacza to, że @cache.action będzie tylko zmieniał nagłówki, aby pozwolić przeglądarce klienta buforować tą zawartość;
    • jeśli przekaże się, np. cache.ram, to wynik zostanie też zapisany w pamięci podręcznej;
  • prefix : jeśli chce się poprzedzić klucz auto-generated przedrostkiem (przydatne przy usuwaniu go później przez np. cache.ram.clear(prefix*));
  • session : jeśli chce się wziąść po uwagę sesję, domyślnie False;
  • vars : jeśli chce się wziąć pod uwagę zmienne URL, domyślnie True;
  • lang : jeśli chce się wziąć pod uwagę język, domyślnie True;
  • user_agent : jeśli chce się wziąć pod uwagę rodzaj przeglądarki użytkownika (ang. user agent), domyślnie False;
  • public : jeśli chce się buforować tą samą stronę dla wszystkich odwiedzających, tak że będzie zawsze dostępna, domyślnie True;
  • valid_statuses : domyślnie None. cache.client będzie buforował tylko strony z metodą GET, których kod stanu rozpoczyna się os 1,2 lub 3; Można przekazać listę kodów stanu (jeśli chce się aby strony były buforowane z tymi stanami, np. status_codes=[200] spowoduje buforowanie tylko strony, których kod stanu wynosi 200)
  • quick : domyślnie None, ale można przekazać listę liter w celu ustawienia właściwej funkcjonalności:
    • Session, Vars, Lang, User_agent, Public np. @cache.action(time_expire=300, cache_model=cache.ram, quick='SVP') jest tym samym co @cache.action(time_expire=300, cache_model=cache.ram, session=True, vars=True, public=True)

Obsługiwanie, opisane wyżej, oznacza. że np. dla zmiennych, jeśli chce się buforować różne strony, gdy zmienne są różne, to /app/default/index?search=foo nie będzie takie samo jak /app/default/index?search=bar. To zachowanie jest nadpisywane przez kilka ustawień, tak więc np., jeśli ustawi się session=True, public=True, to drugie wyrażenie zostanie odrzucone.

Stosuj to mądrze!

Funkcja URL

URL
funkcja URL
Funkcja URL jest jedną z najważniejszych funkcji w web2py. Generuje wewnętrzne ścieżki URL dla akcji i plików statycznych.

Oto przykład:

URL('f')

jest mapowane na

/[application]/[controller]/f

Wyjście funkcji URL zależy od nazwy bieżącej aplikacji, wywoływanego kontrolera i innych parametrów. Platforma web2py obsługuje mapowanie URL i odwrotne mapowanie URL. Mapowanie URL pozwala przedefiniować format zewnętrznych adresów URL. Jeśli używa się funkcji URL do generowania wszystkich zewnętrznych adresów URL, to następnie trzeba dodać lub zmienić mapowania URL, co zabezpieczy przed nie działającymi odnośnikami w aplikacji web2py.

Do funkcji URL można przekazać dodatkowe parametry, tj. dodatkowe warunki w ścieżce adresu URL (args) i zmienne zapytań URL (vars):

URL('f', args=['x', 'y'], vars=dict(z='t'))

jest mapowane na

/[application]/[controller]/f/x/y?z=t

Atrybuty args są automatycznie parsowane, dekodowane i w końcu zapisywane w request.args. Podobnie vars są parsowane, dekodowane i zapisywane w request.vars. args i vars dostarczają podstawowe mechanizmy, przy pomocy których web2py wymienia informację z przeglądarką klienta.

Jeśli args zawiera tylko jeden element, to nie ma potrzeby przekazywania go w liście.

Można również użyć funkcji URL do generowania adresów URL dla akcji w innych kontrolerach i aplikacjach:

URL('a', 'c', 'f', args=['x', 'y'], vars=dict(z='t'))

jest mapowane na

/a/c/f/x/y?z=t

Możliwe jest też określenie aplikacji, kontrolera i funkcji przy użyciu nazwanych argumentów:

URL(a='a', c='c', f='f')

Jeśli brak jest nazwy aplikacji a, to przyjmowana jest bieżąca aplikacja.

URL('c', 'f')

Jeśli brak jest nazwy kontrolera c, to przyjmowany jest bieżący kontroler.

URL('f')

Zamiast przekazywania nazwy funkcji kontrolera możliwe jest przekazywanie samej funkcji

URL(f)

Z wyżej omówionych powodów, należy zawsze używać funkcji URL do generowania adresów URL plików statycznych aplikacji. Pliki statyczne przechowywane są w podfolderze static (tam kierowane są pliki podczas przesyłania ich za pośrednictwem interfejsu administracyjnego). Platforma web2py udostępnia wirtualny kontroler 'static', którego zadaniem jest pobieranie plików z podfolderu static, ustalenie typu zawartości i strumieniowanie pliku dla klienta. Poniższy przykład generuje adres URL dla pliku statycznego "image.png":

URL('static', 'image.png')

jest mapowane na

/[application]/static/image.png

Jeśli plik statyczny znajduje się w podfolderze folderu static, można dołączyć podfolder (podfoldery) jako część atrybutu filename. Na przykład, aby wygenerować:

/[application]/static/images/icons/arrow.png

trzeba użyć:

URL('static', 'images/icons/arrow.png')

Nie ma potrzeby kodowania (zabezpieczania znakami ucieczki) argumentów args i vars - jest to czynione automatycznie.

Domyślnie rozszerzenie odpowiadające bieżącemu żądaniu (które można znaleźć w request.extension) jest dołączane do funkcji, chyba że request.extension to html (domyślnie). Można to przesłonić dołączając jawnie rozszerzenie jako część nazwy funkcji URL(f='name.ext') lub jako wartość argumentu extension:

URL(..., extension='css')

Bieżące rozszerzenie może być zmazane:

URL(..., extension=False)

Bezwzględne adresy URL

Domyślnie funkcja URL generuje względne adresy URL. Jednakże, można również generować bezwzględne adresy URL określając argumenty scheme i host (jest to przydatne, na przykład, przy wstawianiu adresów URL w wiadomościach email):

URL(..., scheme='http', host='www.mysite.com')

Można automatycznie dołączyć schemat i host bieżącego żądania przez ustawienie tych argumentów na True.

URL(..., scheme=True, host=True)

Funkcja URL akceptuje również argument port do określania portu serwera, jeśli to konieczne.

Adresy URL z podpisem cyfrowym

adresy URL z podpisem cfrowym

Podczas generowania adresu URL, ma się możliwość jego cyfrowego podpisania. Dołącza to zmienną GET _signature, która może być zweryfikowana przez serwer. Można to zrobić w dwojaki sposób.

Można przekazać do funkcji URL następujące argumenty:

  • hmac_key: klucz do podpisania adresu URL (ciąg znakowy);
  • salt: opcjonalny ciąg znakowy będący solą (ciągiem zaburzającym) haszowanie podpisu;
  • hash_vars: opcjonalna lista nazw zmiennych z ciąga zapytania URL (czyli zmiennych GET), które będą dołączone do sygnatury. Można to również ustawić na True (wartość domyślna) aby dołączać wszystkie zmienne lub na False aby nie dołączać żadnych zmiennych.

Oto przykład zastosowania:

KEY = 'mykey'

def one():
    return dict(link=URL('two', vars=dict(a=123), hmac_key=KEY))

def two():
    if not URL.verify(request, hmac_key=KEY): raise HTTP(403)
    # do something
    return locals()

Sprawia to, że akcja two jest dostępna tylko poprzez adres URL podpisany cyfrowo. Adres URL podpisany cyfrowo wygląda podobnie do tego:

'/welcome/default/two?a=123&_signature=4981bc70e13866bb60e52a09073560ae822224e9'

Trzeba mieć na uwadze, że podpis cyfrowy jest weryfikowany przez funkcje URL.verify. Funkcja URL.verify również pobiera argumenty hmac_key, salt i hash_vars opisane powyżej a ich wartości muszą pasować do wartości, które zostały przekazane do funkcji URL podczas tworzenia podpisu cyfrowego.

Druga i bardziej zaawansowana i bardziej powszechna metoda cyfrowego podpisywania adresów URL to powiązanie tego z uwierzytelnianiem. Najlepiej wyjaśnić to na przykładzie:

@auth.requires_login()
def one():
    return dict(link=URL('two', vars=dict(a=123), user_signature=True)

@auth.requires_signature()
def two():
    # do something
    return locals()

W tym przypadku hmac_key jest generowany automatycznie i udostępniany w sesji. Umożliwia to, aby akcja two delegowała kontrolę dostępu do akcji one. Jeśli odnośnik został wygenerowany i podpisany, to jest prawidłowy, w przeciwnym razie nie. Jeśli odnośnik zostanie skradziony przez innego użytkownika, to będzie nieprawidłowy.

Dobrą praktyka jest podpisywanie cyfrowo zawsze wywołań zwrotnych Ajax. Jeśli używa się funkcji LOAD web2py, to ma się do dyspozycji argument user_signature służący temu celowi:

{{=LOAD('default', 'two', vars=dict(a=123), ajax=True, user_signature=True)}}

HTTP i redirect

HTTP
redirect

Platforma web2py definiuje tylko jeden nowy wyjątek o nazwie HTTP. Ten wyjątek może być wywołany gdziekolwiek w modelu, kontrolerze lub widoku poleceniem:

raise HTTP(400, "my message")

Powoduje to, że przepływ sterowania przechodzi z kodu użytkownika z powrotem do web2py i zwraca odpowiedź HTTP podobną do tego:

HTTP/1.1 400 BAD REQUEST
Date: Sat, 05 Jul 2008 19:36:22 GMT
Server: Rocket WSGI Server
Content-Type: text/html
Via: 1.1 127.0.0.1:8000
Connection: close
Transfer-Encoding: chunked

my message

Pierwszy argument funkcji HTTP to kod stanu HTTP. Drugi argument to ciąg znakowy, który będzie zwracany jako ciało odpowiedzi. Dodatkowe nazwane argumenty są używane do budowy nagłówka odpowiedzi HTTP. Na przykład:

raise HTTP(400, 'my message', test='hello')

generuje:

HTTP/1.1 400 BAD REQUEST
Date: Sat, 05 Jul 2008 19:36:22 GMT
Server: Rocket WSGI Server
Content-Type: text/html
Via: 1.1 127.0.0.1:8000
Connection: close
Transfer-Encoding: chunked
test: hello

my message

Jeśli nie chce się zatwierdzić transakcji otwartej bazy danych, to należy ją wycofać przed wywołaniem wyjątku.

Wszystkie wyjątki inne niż HTTP powodują, że web2py cofa wszystkie otwarte transakcje bazy danych, rejestruje komunikat dotyczący ostatniego wywołania (ang. traceback), wydaje bilet odwiedzającemu i zwraca standardową stronę błędu.

Oznacza to, że dla przepływu sterowania między stronami może zostać użyty tylko wyjątek HTTP. Inne wyjątki muszą zostać wyłapane przez aplikację, w przeciwnym razie są one biletowane przez web2py.

Polecenie:

redirect('http://www.web2py.com')

jest po prostu skrótem dla:

raise HTTP(303,
           'Nastąpi przekierowanie <a href="%s">tutaj</a>' % location,
           Location='http://www.web2py.com')

Nazwane argumenty metody inicjatora HTTP są tłumaczone na dyrektywy nagłówka HTTP, w tym przypadku, docelowe miejsce przekierowania. Wyrażenie redirect pobiera drugi opcjonalny argument, który jest kodem stanu HTTP dla przekierowania (domyślnie 303). Zmień ten numer na 307 dla tymczasowego przekierowania lub na 301 dla stałego przekierowania.

Najczęstszym sposobem użycia przekierowania jest przekierowanie do innej strony tej samej aplikacji i (opcjonalnie) przekazanie parametrów:

redirect(URL('index', args=(1,2,3), vars=dict(a='b')))

W rozdziale 12 omówimy komponenty web2py. Wykonują one żądania Ajax dla akcji web2py. Jeśli wywoływana akcja wykonuje przekierowanie, to można spowodować, że żądanie Ajax nastąpi po przekierowaniu albo że cała strona wykona przekierowanie żądania Ajax. W tym drugim przypadku można ustawić:

redirect(...,client_side=True)

Umiędzynarodowienie i pluralizacja za pomocą T

T
umiędzynarodowienie

Obiekt T jest tłumaczem językowym. Stanowi pojedynczą globalną instancję klasy gluon.language.translator web2py. Wszystkie stałe łańcuchowe (i tylko stałe łańcuchowe) powinny być oznaczone przez T, na przykład:

a = T("hello world")

Ciągi znakowe oznaczone przez T są identyfikowane przez web2py jako wymagające tłumaczenia i będą tłumaczone gdy kod (w modelu, kontrolerze lub widoku) będzie wykonywany. Jeśli ciąg znakowy do tłumaczenia nie jest stałą lecz zmienną, to będzie dodany do pliku tłumaczenia w czasie wykonania (z wyjątkiem GAE) w celu późniejszego przetłumaczenia.

Obiekt T może również zawierać zmienne interpolowane i obsługiwać wiele równoważnych składni:

a = T("hello %s", ('Tim',))
a = T("hello %(name)s", dict(name='Tim'))
a = T("hello %s") % ('Tim',)
a = T("hello %(name)s") % dict(name='Tim')

Zalecana jest ostatnia składnia ponieważ czyni tłumaczenie najłatwiejszym. Pierwszy ciąg jest tłumaczony zgodnie z żądanym plikiem językowym, a zmienna name jest zamieniana niezależnie od języka.

Można łączyć tłumaczone ciągi ze zwykłymi ciągami znakowymi:

T("blah ") + name + T(" blah")

Poniższy kod jest również dozwolony i często stosowany:

T("blah %(name)s blah", dict(name='Tim'))

lub alternatywna składnia

T("blah %(name)s blah") % dict(name='Tim')

W obu przypadkach ciąg tłumaczony występujący przed nazwą zmiennej jest podstawiany w wyrażeniu "%(name)s". Poniższej alternatywy NIE NALEŻY STOSOWAĆ:

T("blah %(name)s blah" % dict(name='Tim'))

ponieważ ciąg tłumaczony będzie występował po podstawieniu.

Ustalenie języka

Żądany język jest określany w polu "Accept-Language" nagłówka HTTP, ale ten wybór może być zastąpiony programowo przez żądanie określonego pliku, na przykład:

T.force('pl-pl')

który odczytyje plik językowy "languages/pl-pl.py". Pliki językowe mogą być tworzone i edytowane poprzez interfejs administracyjny.

Można też wymusić język w każdym tłumaczonym ciągu:

T("Hello World", language="pl-pl")

W przypadku wymagania wielu języków, na przykład "pl-pl, fr-ft", web2py próbuje zlokalizować pliki tłumaczeń "pl-pl.py" i "fr-fr.py". Jeżeli żaden z wymaganych plików nie zostanie znaleziony, to następuje próba odnalezienia plików "pl.py" i "pl.py". Jeżeli te pliki nie będą odnalezione, to wybrany zostanie domyślny plik "default.py". Jeżeli i ten plik nie będzie odnaleziony, to tłumaczenie nie nastąpi. Bardziej ogólna zasada jest taka, że web2py próbuje dopasować kolejno nazwy plików "xx-xy-yy.py", "xx-xy.py", "xx.py", "default.py" dla każdego z akceptowanych języków "xx-xy-yy", próbując znaleźć najbardziej zbliżona nazwę do preferencji językowej odwiedzającego.

Tłumaczenie można całkowicie wyłączyć poprzez

T.force(None)

Zwykle, ciąg tłumaczenia jest ewaluowany leniwie podczas renderowania widoku. Dlatego metoda force translatora nie będzie wywoływana wewnątrz widoku.

Możliwe jest wyłączenie leniwej ewaluacji poprzez

T.lazy = False

W ten sposób ciągi są tłumaczone natychmiast przez operator T na podstawie aktualnie akceptowanego lub wymuszanego języka.

Możliwe jest też wyłączenie leniwej ewaluacji dla pojedynczych ciągów:

T("Hello World", lazy=False)

Powszechnie spotykany problem jest następujący. Oryginalna aplikacja jest w języku angielskim. Załóżmy, że jest plik tłumaczenia (na przykład polski, "pl-pl.py") a klient HTTP deklaruje, że jego akceptowanym językiem jest zarówno angielski (en) jak i polski (pl-pl) w tej kolejności. Ma miejsce niepożądana sytuacja: web2py nie wie, że domyślny plik tłumaczenia ("default.py") został napisany w języku angielskim (en). Dlatego preferować będzie dla wszystkiego tłumaczenie polskie (pl-pl) ponieważ może znaleźć tylko plik "pl-pl.py". Gdyby nie znalazł tego pliku, to wybrałby domyślny plik językowy "default.py" (angielski).

Istnieją dwa rozwiązania tego problemu: utworzenie tłumaczenia dla języka angielskiego, co byłoby powtórzeniem istniejącego pliku "default.py" albo rozwiązanie lepsze – powiadomienie web2py o tym, że powinien zostać zastosowany język angielski (język w którym zakodowano aplikację). Można to zrobić tak:

T.set_current_languages('en', 'en-en')

Wyrażenie to przechowuje w T.current_languages listę języków, które nie wymagają tłumaczenia i wymusza przeładowanie plików językowych.

Trzeba pamiętać, że "pl" i "pl-pl" są różnymi językami z punktu widzenia web2py. Do obsługi obydwu potrzeba dwóch różnych plików językowych, o nazwach zawsze pisanych małymi literami. To samo odnosi się do innych języków.

Aktualnie akceptowany język jest przechowywany w

T.accepted_language

Tłumaczenie zmiennych

T(...) tłumaczy nie tylko łańcuchy tekstowe ale też może tłumaczyć wartości przechowywane w zmiennych:

>>> a = "test"
>>> print T(a)

W tym przypadku słowo "test" jest tłumaczone ale, jeśli jego tłumaczenie nie zostanie znalezione i jeśli system plików jest zapisywalny, to zostanie ono dodane w pliku językowym do listy słów w celu przetłumaczenia.

Proszę zauważyć, że może to dać w wyniku wiele plików IO i że można to wyłączyć:

T.is_writable = False

zabezpieczając T przed dynamiczną aktualizacją plików językowych.

Komentarze i tłumaczenia złożone

Zdarza się, że ten sam łańcuch tekstowy występujący w różnych kontekstach w aplikacji, wymaga różnych tłumaczeń opartych na kontekście. Aby to umożliwić, można dodać komentarze do oryginalnego łańcucha tekstowego. Komentarze nie będą renderowane ale zostaną wykorzystane przez web2py do określenia najbardziej właściwego tłumaczenia. Na przykład:

T("hello world ## first occurrence")
T("hello world ## second occurrence")

Tekst występujący po znakach ##, to komentarz.

Silnik liczby mnogiej

Począwszy od wersji 2.0, web2py zawiera zaawansowany system pluralizacji (PS). Oznacza to, że gdy tekst oznaczony do tłumaczenia zależy od zmiennej numerycznej, to może zostać przetłumaczony na podstawie wartości numerycznej. Na przykład w angielskim możemy zrenderować:

x book(s)

na

a book (x==1)
5 books (x==5)

W języku angielskim jest jedna liczba pojedyncza i jedna liczba mnoga. Formę liczby mnogiej tworzy się przez dodanie końcówki "-s" lub "-es" albo używając formy wyjątkowej. web2py dostarcza sposób definiowania zasad liczby mnogiej dla każdego języka, jak również wyjątków od domyślnych reguł. W rzeczywistości web2py już zna zasady tworzenia liczby mnogiej dla wielu języków. Wie, na przykład, że w języku słoweńskim są 3 formy liczby mnogiej (dla x==1, x==3 albo x==4 i x>4). Zasady te są zakodowane w pliku "gluon/contrib/plural_rules/*.py" i można tworzyć nowe pliki. Jawne zasady tworzenia liczby mnogiej dla słów są tworzone przez edytowanie plików pluralizacyjnych w interfejsie administracyjnym.

Domyślnie PS nie jest aktywowany. Jest on wywoływany przez argument symbols funkcji T. Na przykład:

T("You have %s %%{book}", symbols=10)

Teraz PS jest aktywowany dla słowa "book" i dla liczby 10. W wyniku w języku angielskim otrzyma się: "You have 10 books".

PS składa się z 3 elementów:

  • wyrażenia zastępczego %%{} do zaznaczania słów na wejściu funkcji T;
  • reguły rozstrzygająca, którą formę słowa należy zastosować ("rules/plural_rules/*.py");
  • słownika z formami liczby mnogiej danego słowa ("app/languages/plural-*.py").

Wartością symboli może być pojedyncza zmienna, lista (krotka) zmiennych lub słownik.

Wyrażenie zastępcze %%{} składa się z 3 elementów:

%%{[<modifier>]<word>[<parameter>]},

gdzie:

<modifier>::= ! | !! | !!!
<word> ::= każde słowo lub fraza w liczbie pojedynczej pisane małymi literami (!)
<parameter> ::= [index] | (key) | (number)

Na przykład:

  • %%{word} jest równoważne %%{word[0]} (jeśli nie zostały zastosowane jakieś modyfikatory);
  • %%{word[index]} stosuje się gdy parametr symbols jest krotką. Wyrażenie symbols[index] daje liczbę używaną do podejmowania decyzji o tym, która forma słowa ma być wybrana;
  • %%{word(key)} jest używane do pobierania parametru numerycznego z wyrażenia symbols[key];
  • %%{word(number)} umożliwia bezpośrednie ustawienie number (np.: %%{word(%i)});
  • %%{?word?number} zwraca "word" jeśli number==1, w przeciwnym razie zwraca number;
  • %%{?number} or %%{??number} zwraca number jeśli number!=1, w przeciwnym razie zwraca nothing
T("blabla %s %%{word}", symbols=var)

%%{word} domyślnie oznacza %%{word[0]}, gdzie [0] jest indeksem elementu w krotce symbols.

T("blabla %s %s %%{word[1]}", (var1, var2))

PS zostaje zastosowany odpowiednio do "word" i "var2".

Można użyć kilka wyrażeń zastępczych %%{} o tym samym indeksie:

T("%%{this} %%{is} %s %%{book}", var)

lub

T("%%{this[0]} %%{is[0]} %s %%{book[0]}", var)

Jest generowane na:

var  output
------------------
 1   this is 1 book
 2   these are 2 books
 3   these are 2 books

Podobnie można przekazać słownik do parametru symbols:

T("blabla %(var1)s %(wordcnt)s %%{word(wordcnt)}",
  dict(var1="tututu", wordcnt=20))

co wytworzy:

blabla tututu 20 words

Można zamienić "1" na każde słowo, które chce się okreśłic w wyrażeniu zastępczym %%{?word?number}. Na przykład:

T("%%{this} %%{is} %%{?a?%s} %%{book}", var)

wytworzy:

var  output
------------------
 1   this is a book
 2   these are 2 books
 3   these are 3 books
 ...

Wenątrz %%{...} można też użyć następujące modyfikatory:

  • ! aby kapitalizować tekst (odpowiednik string.capitalize);
  • !! aby kapitalizować każde słowo (odpowiednik string.title)
  • !!! aby kapitalizować każdy znak (odpowiednik string.upper)

Trzeba pamiętać, że można użyć znak ukośnika jako znak ucieczki dla znaków ! i ?.

Tłumaczenia i pluralizacja a MARKMIN

Można również użyć zaawansowanej składni MARKMIN wewnątrz ciągów tłumaczeń przez zamienienie

T("hello world")

na

T.M("hello world")

Teraz łańcuch tekstowy akceptuje znaczniki MARKMIN, jak opisano to w rozdziale 5

Ciasteczka

cookies

Do obsługi ciasteczek web2py wykorzystuje moduły ciasteczek Pythona.

Ciasteczka z przeglądarki przechowywane są w request.cookies a ciasteczka przesłane przez serwer znajdują się w response.cookies.

Ciasteczka można ustawić w następujący sposób:

response.cookies['mycookie'] = 'somevalue'
response.cookies['mycookie']['expires'] = 24 * 3600
response.cookies['mycookie']['path'] = '/'

Druga linia informuje przeglądarkę aby zachowała ciasteczko przez 24 godziny. Trzecia linia informuje przeglądarkę, aby przesłała ciasteczko z powrotem do jakiejś aplikacji (ścieżka URL) w bieżącej domenie. Trzeba pamiętać, że jeśli nie określi się ścieżki do ciasteczka, to przeglądarka przyjmie ścieżkę URL z żądania HTTP. Tak więc gdy żądana jest ta sama ścieżka URL, ciasteczko zwracane jest tylko do serwera.

Ciasteczko można zabezpieczyć przez:

response.cookies['mycookie']['secure'] = True

Informuje to przeglądarkę aby wysłała ciasteczko z powrotem do serwera tylko poprzez HTTPS a nie HTTP.

Ciasteczko można pobrać w ten sposób:

if request.cookies.has_key('mycookie'):
    value = request.cookies['mycookie'].value

Jeżeli sesje są włączone, web2py w tle ustawia następujące ciasteczko i używa go do obsługi sesji:

response.cookies[response.session_id_name] = response.session_id
response.cookies[response.session_id_name]['path'] = "/"

Jeśli pojedyncza aplikacja obsługuje wiele poddomen i chce się udostępnić sesję pomiędzy wszystkimi poddomenami (np. sub1.yourdomain.com, sub2.yourdomain.com itd.), należy jawnie ustawić domenę ciasteczka sesji w następujący sposób:

if not request.env.remote_addr in ['127.0.0.1', 'localhost']:
    response.cookies[response.session_id_name]['domain'] = ".yourdomain.com"

Powyższy sposób może być przydatny, jeśli, na przykład, chce się aby użytkownik mógł się logować równocześnie do wszystkich poddomen.

Aplikacja init

init
aplikacje: init
aplikacja domyślna

Podczas wdrażania web2py zachodzi potrzeba ustawienia domyślnej aplikację, czyli aplikacji, która będzie uruchamiana przy pustej ścieżce URL, tak jak tu:

http://127.0.0.1:8000

Domyślnie, przy pustej ścieżce, web2py wyszukuje aplikację o nazwie init. Jeżeli jej nie znajdzie, to wyszukuje aplikację welcome.

default_application

Nazwę domyślnej aplikacji można zmienić z init na inną, ustawiając odpowiednio default_application w pliku routes.py:

default_application = "myapp"

Uwaga: default_application po raz pierwszy pojawiło się w web2py w wersji 1.83.

Oto cztery sposoby na ustawienie domyślnej aplikacji:

  • Wykonanie własnej aplikacji "init".
  • Ustawienie default_application w routes.py na pożądaną nazwę aplikacji.
  • Wykonanie odwołania symbolicznego z "applications/init" do folderu swojej aplikacji.
  • Użycie przepisania adresu URL, co omówione jest w następnym rozdziale.

Przepisywanie URL

przepisywanie adresów URL
routes_in
routes_out

W web2py istnieje możliwość przepisania ścieżki URL przychodzących żądań przed wywołaniem akcji kontrolera (mapowaniem URL) i odwrotnie, web2py może przepisać ścieżkę URL generowaną przez funkcję URL (odwrotne mapowanie URL). Jednym z powodów dla których trzeba to zrobić jest obsługa starszych adresów URL. Innym powodem może być uproszenie ścieżek i wykonanie skrótów.

Platforma web2py zawiera dwa odrębne systemy przepisywania URL: łatwy w użyciu system oparty na parametrach mający zastosowanie we większości przypadków i elastyczny system oparty na wzorcach dobry dla bardziej skomplikowanych zastosowań. W celu określenia reguł przepisywania URL, trzeba utworzyć nowy plik w folderze "web2py" o nazwie routes.py (zawartość routes.py będzie zależeć od tego, który z tych dwóch systemów przepisywania zostanie wybrany, tak jak opisano to w nastęþnych dwóch rozdziałach). Te dwa systemy przepisywania nie mogą być mieszane.

Trzeba pamiętać, że po edytowaniu pliku routes.py trzeba go przeładować. Można to zrobić jednym z dwóch sposobów: przez ponowne uruchomienie serwera internetowego albo przez klikniecie przycisku Przeładuj w interfejsie administracyjnym. Jeśli popełniony został jakiś błąd trasowania, to nie nastąpi przeładowanie.

System trasowania oparty na parametrach

Router oparty na parametrach (parametryczny) zapewnia łatwy dostęp do różnych gotowych metod przepisywania ścieżek URL. Jego możliwości obejmują:

  • Pominięcie w zewnętrznej ścieżce URL (tej tworzonej przez funkcję URL) nazw domyślnej aplikacji, kontrolera i funkcji;
  • Odwzorowanie domen (i ewentualnie portów) do aplikacji lub kontrolerów;
  • Osadzenie selektora językowego w adresie URL;
  • Usuwanie stałych przedrostków z nadchodzących adresów URL i dodawanie ich z powrotem w wychodzących adresach URL;
  • Odwzorowywanie plików głównych, takich jak /robots.txt do katalogu plików statycznych (static) aplikacji.

Parametryczny router zapewnia również nieco bardziej elastycznej walidacji przychodzących adresów URL.

Załóżmy, że napisaliśmy aplikację o nazwie myapp i uczyniliśmy ją aplikacją domyślną, tak więc nazwa tej aplikacji nie jest już częścią adresu URL widzianego przez użytkownika. Nasz domyślny kontroler to ciągle default. Chcemy też usunąć nazwę kontrolera z adresu URL widzianego przez użytkownika. Oto co należy wpisać w pliku routes.py:

routers = dict(
  BASE  = dict(default_application='myapp'),
)

To jest to. Parametryczny router jest wystarczająco inteligentny, aby wiedzieć jak właściwie przekształcić takie adresy URL jak te:

http://domain.com/myapp/default/myapp

lub

http://domain.com/myapp/myapp/index

gdzie normalnie skracana ścieżka byłaby dwuznaczna. Jeśli ma się dwie aplikacje, myapp i myapp2, to można uzyskać ten sam efekt i dodatkowo ze ścieżki URL aplikacji myapp2 zostanie usunięta nazwa domyślnego kontrolera, gdy będzie to bezpieczne (co ma miejsce w większości przypadków).

umiędzynarodowienie: rozróżnianie języka w oparciu o URL

Oto inny przypadek: załóżmy, że chcemy obsługiwać języki w oparciu o URL, gdzie ścieżki URL wyglądają tak:

http://myapp/en/some/path

lub (przepisane)

http://en/some/path

Oto jak to zrobić:

routers = dict(
  BASE  = dict(default_application='myapp'),
  myapp = dict(languages=['en', 'it', 'jp'], default_language='en'),
)

Teraz przychodzący adres URL, wyglądający tak:

http:/domain.com/it/some/path

będzie kierowany do /myapp/some/path, a request.uri_language zostanie ustawiony na 'it', wymuszając w ten sposób tłumaczenie. Można również obsługiwać statyczne pliki specyficzne dla języka:

http://domain.com/it/static/filename

zostanie mapowane do:

applications/myapp/static/it/filename

jeśli ten plik istnieje. Jeśli nie, to adresy URL takie jak:

http://domain.com/it/static/base.css

będą nadal przekształcane na:

applications/myapp/static/base.css

(ponieważ nie istnieje static/it/base.css).

Tak więc, jeśli potrzeba, można mieć statyczne pliki specyficzne językowo, włączając w to obrazy, jeśli potrzeba. Obsługiwane jest również mapowanie domenowe:

routers = dict(
  BASE  = dict(
      domains = {
          'domain1.com' : 'app1',
          'domain2.com' : 'app2',
      }
  ),
)

robi to, czego oczekiwaliśmy.

routers = dict(
  BASE  = dict(
      domains = {
          'domain.com:80'  : 'app/insecure',
          'domain.com:443' : 'app/secure',
      }
  ),
)

odwzorowuje http://domain.com na kontroler o nazwie insecure, ponieważ dostęp przez HTTPS następuje przez kontroler secure. Alternatywnie można odwzorować różne porty na różne aplikacje, w oczywisty sposób.

W celu uzyskania więcej informacji proszę zapoznać się z plikiem "routes.parametric.example.py" umieszczonym w folderze "examples" standardowej dystrybucji web2py.

Uwaga: System trasowania oparty na parametrach został wprowadzony po raz pierwszy w wersji 1.92.1 web2py.

System trasowania oparty na wzorcach

Pomimo, że właśnie opisany system trasowania oparty na parametrach powinien być wystarczający dla większości, alternatywny system trasowania oparty na wzorcach zapewnia pewną dodatkową elastyczność w bardziej złożonych przypadkach. Aby użyć system oparty na wzorcach, zamiast definiowania routerów jako słownika parametrów trasowania, definiuje się dwie listy (lub krotki) dwóch krotek, routes_in i routes_out. Każda krotka zawiera dwa elementy: wzorzec, który ma zostać zamieniony i ciąg znakowy go zastępujący. Na przykład:

routes_in = (
  ('/testme', '/examples/default/index'),
)
routes_out = (
  ('/examples/default/index', '/testme'),
)

Przy tych trasach adres URL:

http://127.0.0.1:8000/testme

jest odwzorowywany na:

http://127.0.0.1:8000/examples/default/index

Dla odwiedzającego wszystkie odnośniki URL do strony wyglądają podobnie do /testme.

Wzorce mają taką samą składnie jak wyrażenia regularne Pythona. Na przykład:

  ('.*.php', '/init/default/index'),

odwzorowuje wszystkie adresy URL kończące się na ".php" na stronę index.

W drugim warunku reguły można również dokonać przekierowania do innej strony:

  ('.*.php', '303->http://example.com/newpage'),

Tutaj 303 jest kodem stanu HTTP do przekierowania odpowiedzi.

Czasem zachodzi potrzeba pozbycia się przedrostka z adresów URL, ponieważ chce się udostępnić tylko jedna aplikację. Można to osiągnąć za pomocą:

routes_in = (
  ('/(?P<any>.*)', '/init/\g<any>'),
)
routes_out = (
  ('/init/(?P<any>.*)', '/\g<any>'),
)

Istnieje również alternatywna składnia umożliwiająca mieszanie notacji wyrażeń regularnych z powyższą składnią. Składa się ona ze zmiennej $name zamiast (?P<name>\w+) czy \g<name>. Na przykład:

routes_in = (
  ('/$c/$f', '/init/$c/$f'),
)

routes_out = (
  ('/init/$c/$f', '/$c/$f'),
)

Eliminuje to również przedrostek aplikacji "/example" we wszystkich adresach URL.

Używając notacji ze zmienną $name, można automatycznie odwzorowac routes_in na routes_out, pod warunkiem, że nie używa się wyrażeń regularnych. Na przykład:

routes_in = (
  ('/$c/$f', '/init/$c/$f'),
)

routes_out = [(x, y) for (y, x) in routes_in]

Jeśli istnieje wiele tras, wykonywana jest pierwsza dopasowana ścieżka URL. Jeśli żaden wzorzec nie zostanie dopasowany, tp ścieżka pozostaje nie zmieniona.

Można użyć zmiennej $anything aby dopasować cokolwiek (.*) aż do końca linii.

Oto minimalna treść pliku "routes.py" dla obsługi żądań pliku favicon.ico i robots.txt:

favicon
robots

routes_in = (
  ('/favicon.ico', '/examples/static/favicon.ico'),
  ('/robots.txt', '/examples/static/robots.txt'),
)
routes_out = ()

Oto bardziej złożony przykład udostępniający pojedynczą aplikację "myapp", bez zbędnych przedrostków, ale również interfejsy admin i appadmin oraz katalog static:

routes_in = (
  ('/admin/$anything', '/admin/$anything'),
  ('/static/$anything', '/myapp/static/$anything'),
  ('/appadmin/$anything', '/myapp/appadmin/$anything'),
  ('/favicon.ico', '/myapp/static/favicon.ico'),
  ('/robots.txt', '/myapp/static/robots.txt'),
)
routes_out = [(x, y) for (y, x) in routes_in[:-2]]

Ogólna składnia trasy jest bardzie skomplikowana, niż widzieliśmy to do tej pory. Oto bardziej ogólny i reprezentatywny przykład:

routes_in = (
 ('140.191.\d+.\d+:https?://www.web2py.com:post /(?P<any>.*).php',
  '/test/default/index?vars=\g<any>'),
)

Odwzorowuje to http lub https żądań POST (ale nie pisane małymi literami "post") dla hosta www.web2py.com ze zdalnego IP pasującego do wyrażenia regularnego

'140.191.\d+.\d+'

żądana strona pasująca do wyrażenia regularnego

'/(?P<any>.*).php'

na

'/test/default/index?vars=\g<any>'

gdzie \g<any> jest zamieniane przez dopasowanie wyrażenia regularnego.

Ogólna składnia to

'[remote address]:[protocol]://[host]:[method] [path]'

Jeśli brakuje pierwszej sekcji wzorca (wszystko z wyjątkiem [path]), to web2py dostarcza domyślnie:

'.*?:https?://[^:/]+:[a-z]+'

Całe wyrażenie jest dopasowywane jako wyrażenie regularne, tak więc znak kropki (.) musi być poprzedzony znakiem ucieczki (znakiem lewego ukośnika) i każde dopasowywane podwyrażenie można przedstawić przy użyciu (?P<...>...) wykorzystując składnię wyrażenia regularnego Pythona. Metoda żądania (zwykle GET lub POST) musi być pisana małymi literami. Adres URL, który jest dopasowywany musi mieć jakiekolwiek wyrażenie ucieczkowe %xx ujęte w znaki cudzysłowu.

Umożliwia to przekierowanie żądań opartych na adresie IP klienta lub domenie, na typie żądania, na metodzie i na ścieżce. Umożliwia to również, aby web2py odwzorowywał różne wirtualne hosty w różnych aplikacjach. Każde dopasowane podwyrażenie może być użyte do budowy docelowej ścieżki URL i ostatecznie przekazane jako zmienna GET.

Wszystkie główne serwery internetowe, takie jak Apache czy lighttpd, mają również zdolność przepisywania adresów URL. W środowisku produkcyjnym może to być wykorzystane zamiast routes.py. Cokolwiek się wybierze, zalecamy aby nie kodować na sztywno wewnętrznych ścieżek URL w aplikacji ale do ich generowania używać funkcji URL. Sprawi to, że aplikacja będzie bardziej przenośna w przypadku zmiany tras.

Przepisywanie adresów URL specyficzne dla aplikacji
routes_app

Gdy stosuje się system trasowania oparty na wzorcach, aplikacja może ustawiać swoje własne trasy w pliku routes.py zlokalizowanym w podstawowym folderze danej aplikacji. Jest to możliwe przez konfigurację routes_app w pliku podstawowym routes.py, tak aby określić skąd ma być wybrana nazwa przychodzącego adresu URL aplikacji. Gdy to nastąpi, będzie używany plik routes.py danej aplikacji w miejsce podstawowego pliku routes.py.

Format routes_app jest identyczny z routes_in z tym wyjątkiem, że wymienialny wzorzec jest tylko nazwą aplikacja. Jeśli zastosowanie routes_app do przychodzącego adresu URL z dopasowaniem nazwy aplikacji nie da rezultatu lub wynikowy plik routes.py określonej aplikacji nie zostanie znaleziony, to zostanie użyty bazowy plik routes.py.

Uwaga: routes_app został zastosowany po raz pierwszy w wersji 1.83 web2py.

Domyślna aplikacja, kontroler i funkcja
default_application
default_controller
default_function

Gdy używa się systemu trasowania opartego na wzorcach nazwa domyślnej aplikacji, kontrolera i funkcji może być zmieniona z init, default czy index odpowiednio na inną nazwę, przez odpowiednie ustawienie wartości w routes.py:

default_application = "myapp"
default_controller = "admin"
default_function = "start"

Uwaga: Elementy te po raz pierwszy zastosowano w wersji 1.83 web2py.

Trasy do stron błędu

routes_onerror

Można również wykorzystać routes.py do zmiany trasy żądań dla określonych akcji, w przypadku gdy pojawi się na serwerze błąd. Można określić to trasowanie globalnie dla całej aplikacji, dla każdego kodu błędu lub dla każdej aplikacji i każdego kodu błędu. Oto przykład:

routes_onerror = [
  ('init/400', '/init/default/login'),
  ('init/*', '/init/static/fail.html'),
  ('*/404', '/init/static/cantfind.html'),
  ('*/*', '/init/error/index')
]

Dla każdej krotki, porównywany jest pierwszy ciąg ze wzorcem "[app name]/[error code]". Jeśli pasuje, to błędne żądanie jest przekierowywane na ścieżkę URL określoną w drugim ciągu dopasowanej krotki. Jeśli ścieżka URL obsługiwanego błędu nie prowadzi do pliku statycznego, to do akcji błędu dopasowana zostanie następująca zmienna GET:

  • code: kod stanu HTTP (np. 404, 500);
  • ticket: w postaci "[app name]/[ticket number]" (lub "None" jeśli nie ma biletu);
  • requested_uri: odpowiednik request.env.request_uri;
  • request_url: odpowiednik request.url.

Zmienne te są dostępne dla akcji obsługującej błędy poprzez request.vars i mogą być użyte do generowania odpowiedzi o błędzie. W szczególności, dobrym pomysłem jest aby akcja błędu zwracała oryginalny kod błędu HTTP zamiast domyślny kod stanu 200 (OK). Można to zrobić przez ustawienie response.status = request.vars.code. Jest też możliwe, aby akcja błędu wysyłała (lub kolejkowała) wiadomości email do administratora, w tym odnośnik do biletu w interfejsie admin.

Niedopasowane błędy wyświetlą domyślną stronę błędów. Tą domyślną stronę błędów można również tutaj dostosować (zobacz "routes.parametric.example.py" i "routes.patterns.example.py" w folderze "examples"):

error_message = '<html><body><h1>%s</h1></body></html>'
error_message_ticket = '''<html><body><h1>Internal error</h1>
     Ticket issued: <a href="/admin/default/ticket/%(ticket)s"
     target="_blank">%(ticket)s</a></body></html>'''

Pierwsza zmienna zawiera komunikat błędu na wypadek żądania nieprawidłowej aplikacji lub funkcji. Druga zmienna zawiera komunikat błędu na wypadek wystawienia biletu.

Zmienna routes_onerror działa w obydwu mechanizmach trasowania.

error_handler

W "routes.py" można też określić akcję odpowiedzialną za obsługę błedów:

error_handler = dict(application='error',
                      controller='default',
                      function='index')

Jeśli zmienna error_handler określa akcją, to jest ona wywoływana bez przekierowywania użytkownika i za obsługę błędu będzie odpowiedzialna akcja handlera. W przypadku, gdy sama strona obsługuje zwracane błędy, web2py powróci do swoich starych odpowiedzi statycznych.

Zarządzanie statycznymi aktywami

Od wersji 2.1.0, web2py ma możliwość zarządzania aktywami statycznymi.

Gdy aplikacja jest w fazie tworzenia, pliki statyczne można zmieniać często dlatego, że web2py wysyła pliki statyczne z nagłówkami niebuforowania. Efektem ubocznym jest "wymuszania" na przeglądarce żądania plików statycznych przy każdym żądaniu HTTP. Powoduje to niską wydajność ładowania strony.

W środowisku "produkcyjnym", można serwować pliki statyczne z nagłówkami cache w celu przeciwdziałania zbytecznym pobraniom, gdyż pliki statyczne nie zmieniają się.

Nagłówki cache pozwalają przeglądarce pobrać plik tylko raz, oszczędzając w ten sposób przepustowość i zmniejszając czas ładowania.

Jednak jest pewien problem. Co powinny deklarować nagłówki buforowania? Kiedy należy wygasić buforowanie plików? Gdy plik jest serwowany po raz pierwszy, serwer nie jest w stanie przewidzieć, kiedy nastąpi zmiana tego pliku.

Podręcznikowe podejście do przechowywania plików statycznych nakazuje tworzenie podfolderów dla każdej odrębnej wersji plików statycznych. Na przykład jakaś wcześniejsza wersja "layout.css" może być dostępna na ścieżce URL "/myapp/static/css/1.2.3/layout.css". Gdy wprowadza się zmiany w tym pliku, tworzy się nowy podfolder "/myapp/static/css/1.2.4/layout.css" i zmienia się odnośniki.

Ta procedura działa, ale jest uciążliwa, ponieważ przy każdej zmianie w pliku CSS, trzeba pamiętać aby przenieść go do innego folderu, zmienić ścieżki URL pliku w layout.html i to wdrożyć.

Zarządzanie statycznymi aktywami rozwiązuje problem umożliwiając programiście deklarować wersję grupy plików statycznych, co sprawi, że będą one ponownie żądane tylko gdy zmieni się numer wersji. Numer wersji aktywa jest częścią adresu URL tak jak w poprzednim przykładzie. Różnica w stosunku do poprzedniego podejścia polega na tym, że numer wersji pojawia się tylko w adresie URL a nie w systemie plików.

Jeśli chce się serwować plik "/myapp/static/layout.css" z nagłówkiem buforowania, wystarczy zawrzeć plik ze zmienioną ścieżką URL, która zawiera numer wersji:

/myapp/static/_1.2.3/layout.css

Numer wersji jest określany w ścieżce URL i nie pojawia się on gdzie indziej.

Ścieżka URL rozpoczyna się tutaj od "/myapp/static/", po czym następuje numer wersji złożony ze znaku pokreślenia i 3 liczb całkowitych rozdzielonych kropką (tak jak opisano to w SemVer), a następnie nazwa pliku. Trzeba podkreślić, że nie musi się tworzyć folderu "_1.2.3/".

Za każdym razem gdy plik statyczny zostanie zażądany z numerem wersji w adresie URL, to zostanie on zaserwowany z nagłówkiem buforowania "o dalekiej przyszłości", tak jak tu:

Cache-Control : max-age=315360000
Expires: Thu, 31 Dec 2037 23:59:59 GMT

Oznacza to, przeglądarka będzie pobierać takie pliki tylko raz i będą one zapisane w pamięci przeglądarki "na zawsze".

Za każdym razem, gdy w adresie URL znajduje się komponent wersjonowania "_1.2.3/filename", web2py usuwa ze ścieżki część dotyczącą wersji i serwuje plik z nagłówkiem wskazującym na daleka przyszłość, tak więc plik ten będzie zawsze buforowany. Jeśli został zmieniony numer wersji w adresie URL, to ten wybieg spowoduje, że przeglądarka zażąda innego (wg niej) pliku i plik zostanie pobrany ponownie.

Można użyć numerów "_1.2.3", "_0.0.0", "_999.888.888" – jedyna ograniczenie, to rozpoczęcie numeru wersji znakiem podkreślenia i struktura składająca się z trzech ciągów cyfr rozdzielonych kropką.

Podczas prac programistycznych, można użyć response.files.append(...) w celu zlinkowania adresów URL plików statycznych. W takim przypadku można dopisać ręcznie część "_1.2.3/" lub wykorzystać nowy parametr obiektu odpowiedzi: response.static_version. Wystarczy zawrzeć pliki w zwykły sposób, na przykład

{{response.files.append(URL('static','layout.css'))}}

a w modułach ustawić

response.static_version = '1.2.3'

To przepisze automatycznie każda ścieżkę URL "/myapp/static/layout.css" do "/myapp/static/_1.2.3/layout.css", dla każdego pliku, łącznie z response.files.

Często w środowisku produkcyjnym sprawia się, że odpowiednie serwowanie plików statycznych powierza się serwerowi internetowemu (Apache, Nginx itd.). Trzeba wówczas dostosować konfigurację w taki sposób, aby "przeskoczyć" część "_1.2.3/".

Na przykład, w Apache zmień:

AliasMatch ^/([^/]+)/static/(.*)    /home/www-data/web2py/applications/$1/static/$2

na:

AliasMatch ^/([^/]+)/static/(?:_[\d]+.[\d]+.[\d]+/)?(.*)    /home/www-data/web2py/applications/$1/static/$2

Podobnie w Nginx zmień:

location ~* /(\w+)/static/ {
    root /home/www-data/web2py/applications/;
    expires max;
}

na:

location ~* /(\w+)/static(?:/_[\d]+.[\d]+.[\d]+)?/(.*)$ {
   alias /home/www-data/web2py/applications/$1/static/$2;
   expires max;
}

Uruchamianie zadań w tle

W web2py każde żądanie HTTP jest obsługiwane w swoim własnym wątku. Wątki są odnawiane w celu efektywności i zarządzane przez serwer internetowy. Dla bezpieczeństwa, serwer internetowy ustawia limit czasu dla każdego żądania. Oznacza to, że akcje nie powinny uruchamiać zadań, które trwają zbyt długo, nie powinny tworzyć nowych wątków i nie powinny rozwidlać procesów (jest to możliwe, ale nie zalecane).

Właściwym sposobem na uruchomienie czasochłonnych zadań jest uruchamianie ich w tle. Jest kilka sposobów na to, ale tu opisujemy trzy mechanizmy wbudowane w web2py: cron, własne kolejkowanie zadań i scheduler.

Przez cron rozumiemy tu funkcjonalność web2py a nie mechanizm Cron systemów uniksowych. Cron web2py działa też na systemie Windows.

Cron web2py ma zastosowanie, jeśli potrzeba, aby zadania były uruchamiane w tle o ściśle zaplanowanym czasie i jeśli zadania te mają stosunkowo krótki czas wykonania w stosunku do przerw pomiędzy kolejnymi uruchomieniami. Każde zadanie uruchamiane jest w jego ostatnim procesie i można uruchomiać jednocześnie wiele zadań, lecz nie ma kontroli nad tym, jak wiele zadań jest uruchomionych. Jeśli jedno z zadań pokrywa się z innym, to może to doprowadzić do blokady bazy danych i skoku zużycia pamięci.

W schedulerze web2py przyjęto inne podejście. Liczba uruchamianych procesów jest stała i mogą one działać na różnych komputerach. Każdy proces jest wywoływany przez worker. Każdy worker wybiera zadanie, gdy jest ono dostępne i wykonywane jest ono niezwłocznie po upływie zaplanowanego terminu uruchomienia, ale nie koniecznie dokładnie o tym czasie. Nie może być uruchomionych więcej procesów niż liczba zaplanowanych zadań i dlatego nie nastąpi skok pamięci. Zadania schedulera mogą być definiowane w modelach i są przechowywane w bazie danych. Scheduler web2py nie implementuje rozproszonej kolejki, ponieważ zakłada się, że czas potrzebny do podzielenia zadań jest znikomy w porównaniu z czasem uruchomionych zadań. Workery pobierają zadania z bazy danych.

Własne kolejkowanie zadań może być w niektórych przypadkach prostszą alternatywą dla schedulera web2py.

Cron

cron

Cron web2py umożliwia, aby aplikacje wykonywały zadania w określonym czasie, w sposób niezależny od systemu operacyjnego.

Funkcjonalność cron jest określana w każdej aplikacji w pliku crontab:

app/cron/crontab

Stosuje się w nim zwykłą składnię opisaną w informatorze [cron] (z pewnymi rozszerzeniami, specyficznymi dla web2py).

Przed wersją 2.1.1 web2py, cron był domyślnie włączony i mógł być wyłączony opcją -N polecenia terminalowego. Od wersji 2.1.1, cron jest domyślnie wyłączony i może być włączony opcja -Y. Zmiana ta została podyktowana chęcią nakłonienia użytkowników do stosowania nowego schedulera (który jest lepszy od mechanizmu cron) a także dlatego, żecron może mieć negatywny wpływ na wydajność.

Oznacza to, że każda aplikacja może mieć oddzielną konfigurację cron i że konfiguracja cron może być zmieniana bez wpływu na cron systemu operacyjnego.

Oto przykład:

0-59/1  *  *  *  *  root python /path/to/python/script.py
30      3  *  *  *  root *applications/admin/cron/db_vacuum.py
*/30    *  *  *  *  root **applications/admin/cron/something.py
@reboot root    *mycontroller/myfunction
@hourly root    *applications/admin/cron/expire_sessions.py

Dwie ostatnie linie w tym przykładzie wykorzystują rozszerzenia zwykłej składni cron aby zapewnić dodatkową funkcjonalność web2py.

Plik "applications/admin/cron/expire_sessions.py" faktycznie istnieje i jest dostarczany wraz z aplikacja admin. Zawarty tam kod sprawdza czy sesje wygasły i usuwa je. Kod w "applications/admin/cron/crontab" uruchamia to zadanie co godzinę.

Jeśli nazwa zadania (skryptu) jest poprzedzona znakiem gwiazdki (*) i kończy się rozszerzeniem .py, to zadanie to będzie wykonane w środowisku web2py. Oznacza to, że ma się do dyspozycji wszystkie kontrolery i modele. Jeśli użyje się dwóch gwiazdek (**), modele nie będą wykonywane. Jest to zalecany sposób wywoływania, ponieważ ma mniejsze obciążenie i pozwala uniknąć potencjalne problemy.

Proszę zwrócić uwagę, że skrypty (funkcje) wykonywane w środowisku web2py wymagają ręcznego wykonania db.commit() na końcu funkcji, inaczej transakcja zostanie odwrócona.

Framework web2py nie generuje biletów lub jednoznacznych komunikatów o ostatnich wywołaniach (ang. traceback) w trybie powłoki, w której uruchomiony jest cron. Tak więc trzeba się upewnić czy kod web2py został uruchomiony bez błędów, zanim ustawi się go jako zadanie cron, ponieważ nie będzie się w stanie stwierdzić ewentualnych błędów po uruchomieniu zadania cron. Ponadto należy uważać, w jaki sposób korzysta się z modeli: podczas wykonania zdarza się, że w oddzielnym procesie muszą być wzięte pod uwagę blokady bazy danych, aby uniknąć stron oczekujących na zadania cron, które mogą blokować bazę danych. Użyj składni **, jeśli nie musisz używac bazy danych w swoim zadaniu cron.

Można również wywołać funkcję kontrolera, w którym to przypadku nie potrzeba określać ścieżki. Kontroler i funkcja będą przywoływać aplikację. Trzeba zachować szczególną ostrożność wykazując powyższe. Przykład:

*/30  *  *  *  *  root *mycontroller/myfunction

Jeśli określi się @reboot w pierwszym polu pliku crontab, podane zadanie zostanie wykonane tylko raz, przy uruchomieniu web2py. Można skorzystać z tej funkcjonalności, jeśli chce się wstępnie buforować jakieś dane, sprawdzić je lub zainicjować dane przy uruchomieniu web2py. Trzeba pamiętać, że zadania cron są wykonywane równolegle z aplikacją. Jeśli aplikacja nie jest gotowa do obsługi żądań do czasu zakończenia zadań cron, trzema implementować sprawdzenie tego stanu i jego uwzględnienie. Przykład:

@reboot  root *mycontroller/myfunction

W zależności od sposobu wywołania web2py, są cztery tryby pracy cron web2py.

  • miękki cron: dostępny we wszystkich trybach wykonania;
  • twardy cron: dostępny, jeśli używa się wbudowanego serwera internetowego (bezpośrednio lub za pośrednictwem mod_proxy Apache);
  • zewnetrzny cron: dostępny, jeśli ma się możliwość korzystania z usługi cron systemu operacyjnego;
  • brak cronu.

Gdy używa się wbudowanego serwera internetowego domyślnym trybem jest twardy cron. We wszystkich inych przypadkach, domyślnym trybem jest miękki cron. Miękki cron jest też domyślnym trybem, jeśli używa się CGI, FASTCGI lub WSGI (ale trzeba mieć na uwadze, że miękki cron nie jest domyślnie włączony w standardowym pliku wsgihandler.py dostarczanym w web2py).

Zadania cron są wykonywane od pierwszego wywołania (załadowania strony) aż do czasu określonego w crontab, ale tylko po przetworzeniu strony, tak więc użytkownik nie zauważy żadnego opóźnienia. Oczywiście istnieje pewien stopień niepewności co do dokładności czasu w który zadanie będzie wykonane, co uzależnione jest od ruchu na witrynie. Ponadto zadanie cron może zostać przerwane, jeśli serwer internetowy ma ustawiony limit ładowania strony. Jeśli te ograniczenia nie są dopuszczalne, to należy zastosować zewnętrzny cron. Miękki cron powinien być stosowany w ostateczności, gdy inne tryby nie mogą być użyte.

Twardy cron jest trybem domyślnym, jeśli używa się wbudowanego serwera internetowego (bezpośrednio lub za pośrednictwem mod_proxy Apache). Twardy cron jest wykonywany w równoległym wątku, inaczej niż ma to miejsce w przypadku miękkiego crona. W trybie tym nie ma żadnych ograniczeń związanych z czasem uruchamiania lub dokładnością czasu wykonania.

Zewnętrzny cron nie jest domyślny w jakimkolwiek scenariuszu, ale wymaga dostępu do systemowej usługi cron. Jest uruchamiany w równoległym procesie, tak więc nie ma ograniczeń występujących w miękkim cronie. Jest to zalecany sposób użycia crona w WSGI lub FASTCGI.

Przykład linii kodu dodającego systemowy crontab, (zwykle to /etc/crontab):

0-59/1 * * * * web2py cd /var/www/web2py/ && python web2py.py -J -C -D 1 >> /tmp/cron.output 2>&1

W zewnętrznym cron, trzeba się upewnić, że dodany jest parametr -J (albo --cronjob, co jest tym samym) jak pokazano to powyżej, tak aby web2py wiedział, że to zadanie jest wykonywane przez zewnętrzny cron. Web2py przestawia to wewnętrznie z miękkiego i twardego cron.

Własne kolejkowanie zadań

Chociaż cron jest przydatny do uruchamiania zadań w regularnych odstępach czasu, to nie zawsze jest najlepszym rozwiązaniem do uruchamiania zadań w tle. Z tych powodów w web2py udostępnia się możliwość uruchamiania dowolnych skryptów Pythona, tak jakby znajdowały się w kontrolerze:

python web2py.py -S app -M -R applications/app/private/myscript.py -A a b c

gdzie -S app powiadamia web2py aby uruchamiał "myscript.py" jako "app", -M powiadamia aby wykonał modele a -A a b c przekazuje opcjonalne argumenty linii poleceń sys.args=['a','b','c'] do "myscript.py".

Tego typu proces tła nie powinien być wykonywany za pośrednictwem cron (może z wyjątkiem wyrażenie @reboot crona), ponieważ trzeba mieć pewność, że w danym czasie działa nie więcej niż jedna instancja. W cronie możliwe jest, że proces rozpoczyna pierwszą iteracje crona i nie jest zakańczany przez iterację drugą, tak więc cron uruchamia go ponownie i ponownie i ponownie – zagłuszając tym samym serwer poczty, jeśli zadanie dotyczy wysyłania wiadomości email.

W rozdziale 8 znajdują się przykłady pokazujące jak używać powyższej metody do wysyłania wiadomości email.

Scheduler

scheduler

Przed wersją 2.6.0 scheduler był traktowany jako kod eksperymentalny. Począwszy od wersji 2.6.0 udokumentowane API jest stabilne. Stabilne API składa się z następujących funkcji:

  • disable()
  • resume()
  • terminate()
  • kill()
  • queue_task(),
  • task_status()
  • stop_task()

Scheduler web2py działa bardzo podobnie do kolejkowania zadań, opisanego w poprzednim podrozdziale z pewnymi różnicami:

  • Zapewnia standardowy mechanizm dla tworzenia, harmonogramowania i monitorowania zadań.
  • Nie jest pojedynczym procesem wykonywanym w tle, ale zestawem workerów.
  • Zadania węzłów wykonawczych (ang. job of worker nodes) mogą być monitorowane, ponieważ ich stan, jak również stan samych zadań jest przechowywany w bazie danych.
  • Działa to bez web2py ale nie jest to udokumentowane.

Scheduler web2py nie wykorzystuje crona, choć można użyć wyrażenia @reboot crona aby rozpocząć działanie węzłów wykonawczych.

Więcej informacji o wdrażaniu schedulera pod Linuksem i Windows znajduje się w rozdziale 13 "Receptury wdrożeniowe".

W schedulerze zadanie jest po prostu funkcją zdefiniowaną w modelu (lub w module i importowana przez model). Na przykład:

def task_add(a, b):
    return a + b

Zadania będą zawsze wywoływane w tym samym środowisku, jakie widzą kontrolery i dlatego są dla nich dostępne wszystkie zmienne globalne zdefiniowane w modelach, w tym połączenia z bazą danych (db). Zadania różnią się od akcji kontrolera, ponieważ nie są powiązane z jakimś żądaniem HTTP i dlatego nie ma request.env. Ponadto, zadania mogą mieć dostęp do zmienej innego środowiska, która nie występuje w zwykłych żądaniach: W2P_TASK. Zmienna W2P_TASK.id posiada scheduler_task.id i W2P_TASK.uuid pola scheduler_task.uuid uruchomionego zadania.

Trzeba pamiętać, aby wywołać db.commit() na końcu każdego zadania, jeśli zadanie dotyczy operacji zapisu danych do bazy danych. Wprawdzie web2py zatwierdza domyślnie transakcję na koniec udanej akcji, ale zadania schedulera nie są akcjami.

Aby udostępnić scheduler należy w modelu utworzyć instancję klasy Scheduler. Zalecanym sposobem włączenia schedulera do aplikacji jest utworzenie pliku modelu o nazwie scheduler.py i zdefiniowanie tam swojej funkcji. Po funkcji można umieścić w modelu następujący kod:

from gluon.scheduler import Scheduler
scheduler = Scheduler(db)

Jeśli zadania są zdefiniowane w modelu (w przeciwieństwie do modelu), to może być konieczne ponowne uruchomienie workerów.

Planowanie zadań dokonuje się za pomocą

scheduler.queue_task(task_add, pvars=dict(a=1, b=2))
Parametry

Pierwszym argumentem klasy Scheduler musi być baza danych używana przez scheduler do komunikacji z workerami. Może to być db aplikacji lub inny dedykowany obiekt db, być może jeden ze współdzielonych przez wiele aplikacji. Jeśli używa się SQLite, to zaleca się stosowanie oddzielnych baz danych – odrębnej dla aplikacji i dla schedulera, tak aby utrzymać responsywność aplikacji. Po zdefiniowaniu zadań i utworzeniu instancji Scheduler, wszystko co trzeba zrobić, to uruchomić workery. Można to zrobić na kilka sposobów:

python web2py.py -K myapp

uruchamia workera dla aplikacji myapp. Jeśli chce się uruchomić wiele workerów dla tej samej aplikacji, można to zrobić przekazując po prostu myapp,myapp. Można przekazać również group_names (przesłaniając jedno z ustawień w modelu) poprzez

python web2py.py -K myapp:group1:group2,myotherapp:group1

Jeśli ma się model o nazwie scheduler.py można uruchomić (zatrzymać) workery z domyślnego okna web2py (tego używanego do ustawiania adresu IP i portu).

Wdrożenie schedulera

Jeden ostatni miły dodatek: w przypadku korzystania z wbudowanego serwera internetowego, można uruchomić ten serwer i scheduler za pomocą jednej linii kodu (jeśli nie chce się, aby wyskakiwało okno web2py, to można użyć zamiast tego menu "Schedulers")

python web2py.py -a yourpass -K myapp -X

Można przekazać zwykłe parametry (-i, -p, tutaj -a zabezpiecza okno przed wyświetlaniem), przekazać cokolwiek do aplikacji w parametrze -K i dołączyć -X. Scheduler będzie działać obok serwera internetowego!

Użytkownicy Windows poszukujący informacji o tworzeniu usługi schedulera powinni szukać jej w rozdziale 13 "Recepty wdrożeniowe".

Kompletna sygnatura schedulera

Pełna sygnatura schedulera to:

Scheduler(
    db,
    tasks=None,
    migrate=True,
    worker_name=None,
    group_names=None,
    heartbeat=HEARTBEAT,
    max_empty_runs=0,
    discard_results=False,
    utc_time=False
)

Omówmy ją po kolei:

  • db to instancja DAL bazy danych, w której chce się umieścić tabele schedulera;
  • tasks to słownik odwzorowujący nazwy zadań na funkcje. Jeśli nie przekaże się tego parametru, to funkcja będzie wyszukiwana w środowisku aplikacji;
  • worker_name domyślnie None. Jak tylko zostanie uruchomiony worker, generowana jest jego nazwa jako uuid. Jeśli chce się ją określić samemu, to trzeba być pewnym, że wybrana nazwa jest unikalna.
  • group_names domyślnie ustawiony jest na [main]. Wszystkie zadania mają parametr group_name ustawiony domyślnie na main. Workery mogą tylko wybierać zadania przypisane do ich grupy.

NB: Jest to przydatne, jeśłi ma się różne instancje workerów (np. na różnych maszynach) i chce się przypisać zadania do określonego workera.

NB2: Jest możliwe, aby przypisać workerowi więcej grup i mogą być one również wszystkie takie same jak ['mygroup','mygroup']. Zadania zostaną rozdzielone, przy uwzględnieniu, że worker z group_names ['mygroup','mygroup'] jest zdolny przetworzyć dwa razy więcej zadań niż worker z nazwą ['mygroup'].

  • heartbeat domyślnie jest ustawiony na 3 sekundy. Parametr ten kontroluje jak często scheduler ma sprawdzać swój stan w tabeli scheduler_worker i stwierdzić, czy są jakieś zadania przypisane do jego procesu;
  • max_empty_runs domyślnie to 0, co oznacza, że worker będzie kontynuować przetwarzanie zadań dopóki są PRZETWARZANE. Jeśli ustawi się to na jakąś wartość inną niż 0, powiedzmy na 10, worker przestanie działać automatycznie po dziesięciu pętlach, jeśli jest AKTYWNE i żadne zadania nie są PRZYPISANE. Pętla wykonywana jest kiedy worker wyszukuje zadania, co 3 sekundy (lub o interwale ustawionym w heartbeat);
  • discard_results domyślnie False. Jeśli ustawione na True, nie zostaną utworzone żadne rekordy scheduler_run.

NB: rekordy scheduler_run będą tworzone jak poprzednio dla stanów zadań BŁĘDNE, PO CZASIE i ZATRZYMANE.

  • utc_time domyślnie False. Jeśli zachodzi potrzeba koordynacji workerów znajdujących się w różnych strefach czasowych lub nie ma się problemu z czasem solar/DST, dostarczającym danych czasowych dla różnych krajów itd., można ustawić ten parametr na True. Scheduler będzie uznawał czas UTC i działać będzie pozostawiając czas lokalny na boku. Uwaga: trzeba planować zadania w czasach UTC (dla start_time, stop_time i tak dalej).

Mamy już teraz ustawioną infrastrukturę: zdefiniowane zadania, poinformowany scheduler o nich, uruchomiony worker (workery). Pozostaje właściwe zaplanowanie zadań

Zadania

Zadania mogą być planowane programowo lub poprzez interfejs administracyjny. W rzeczywistości planowanie zadania odbywa się poprzez dodanie wpisu do tabeli "scheduler_task", do której dostęp można uzyskać poprzez interfejs administracyjny:

http://127.0.0.1:8000/myapp/appadmin/insert/db/scheduler_task

Znaczenia pól tej tabeli są oczywiste. Pola "args" i "vars"" są wartościami przekazywanymi do zadania w formacie JSON. W przypadku poprzednio rozpatrywanego "task_add", przykładem "args" i "vars" może być:

args = [3, 4]
vars = {}

lub

args = []
vars = {'a': 3, 'b': 4}

Zadania organizowane są w tabeli scheduler_task.

W celu dodania zadań poprzez API, trzeba zastosować

scheduler.queue_task('mytask', ...)

co jest udokumentowane poniżej .

Cykl życia zadania

Wszystkie zadania mają następujący cykl życia

scheduler tasks

Domyślnie podczas wysyłania zadania do schedulera ma ono status QUEUED. Jeżeli zachodzi potrzeba późniejszego wykonania tego zadania, to stosuje się parametr start_time (domyślnie now). Jeśli z jakichś powodów musi się mieć pewność, że zadanie nie zostanie wykonane później niż jakiś moment czasu (na przykład, do usługi internetowej, która zostaje zamknięta o 1AM, poczty, która musi być wysłana po godzinach pracy itd.) można ustawić dla niego parametr stop_time (domyślnie None). Jeśli zadanie NIE zostało pobrane przez workera przed czasem określonym w stop_time, będzie ustawione jako EXPIRED. Zadania z nie ustawionym parametrem stop_time lub pobrane PRZED czasem stop_time są ASSIGNED do workera. Gdy workery pobierają zadanie, jego stan jest ustawiany na RUNNING.

Zadania uruchomione (status RUNING) można zakończyć ze statusem:

  • TIMEOUT gdy upłynęło więcej niż n sekund od czasu przekazanego przez parametr timeout (domyślnie 60 sekund);
  • FAILED gdy wykryty został wyjątek;
  • COMPLETED gdy zadanie zostało zakończone pomyślnie.

Wartości dla start_time i stop_time powinny być obiektami datetime. W celu zaplanowania zadania "mytask", tak aby uruchamiane było, na przykład, co 30 sekund od chwili obecnej, można wykonać co następuje:

from datetime import timedelta as timed
scheduler.queue_task('mytask',
    start_time = request.now + timed(seconds=30))

Dodatkowo można kontrolować ile razy zadanie powinno być powtarzane (czyli trzeba zgrupować kilka danych określających interwały czasowe). Aby to wykonać, trzeba ustawić parametr repeats (domyślnie 1, 0 – nieograniczona ilość razy). Można wpłynąć na to, ile sekund powinno upłynąć pomiędzy poszczególnymi wykonaniami ustawiając parametr period (domyślnie 60 sekund).

Domyślnie, czas nie jest obliczany między końcem pierwszego cyklu a początkiem następnego, ale pomiędzy początkiem pierwszego cyklu a początkiem następnego cyklu). Może to powodować gromadzenie się 'dryftu' (opóźnień) w momencie rozpoczęcia zadania. Po wersji 2.8.2 został dodany nowy parametr prevent_drift z domyślną wartością False. Po ustawieniu na True, przy kolejnym wykonaniu zadania, parametr start_time będzie uzyskiwał pierwszeństwo, zabezpieczając przed dryftem.

Można również ustawić, ile razy funkcje mogą zgłaszać wyjątki (np. w przypadku żądania danych z powolnych serwisów internetowych) i ponownie kolejkować je zamiast zatrzymywać ze statusem FAILED, wykorzystując parametr retry_failed (domyślnie 0, -1 - nieograniczone).

task repeats

Podsumowując, można ustawić

  • period i repeats aby automatycznie pobierać zmienioną funkcję;
  • timeout aby się upewnić, że funkcja nie przekracza okreśłonej ilości czasu;
  • retry_failed aby kontrolować ile razy zadanie może być "błędne";
  • start_time i stop_time aby zaplanować funkcje w wymaganym okresie czasu.
queue_task i task_status

Metoda:

scheduler.queue_task(
    function,
    pargs=[],
    pvars={},
    start_time=now, 		#datetime
    stop_time=None,		#datetime
    timeout=60,               #seconds
    prevent_drift=False,
    period=60,                  #seconds
    immediate=False,
    repeats = 1
)

pozwala kolejkować zadania, które mają być wykonywane przez workery. Metoda ta zwraca wiersz (patrz tutaj) i pobiera następujące parametry:

  • function (obowiazkowy): może to być nazwa zadania lub referencje do aktualnej funkcji;
  • pargs: są argumentami, które będą przekazywane do zadania, zapisywane jako lista Pythona;
  • pvars : są nazwanymi argumentami, które mają być przekazane do zadania, zapisane jako słownik Pythona;
  • wszystkie inne kolumny scheduler_task mogą być przekazywane jako argumenty kluczowe – najważniejsze są wyświetlane.

Na przykład:

scheduler.queue_task('demo1', [1, 2])

robi dokładnie to samo, co

scheduler.queue_task('demo1', pvars={'a': 1, 'b': 2})

jako

st.validate_and_insert(function_name='demo1', args=json.dumps([1, 2]))

i jako:

st.validate_and_insert(function_name='demo1', vars=json.dumps({'a': 1,'b': 2}))

Oto bardziej złożony kompletny przykład:

def task_add(a,b):
    return a + b

scheduler = Scheduler(db, tasks=dict(demo1=task_add))

scheduler.queue_task('demo1', pvars=dict(a=1, b=2),
                     repeats = 0, period = 180)

Od wersji 2.4.1, jeśli przekaże się dodatkowy parametrimmediate=True, to zostanie wymuszone, aby główny worker ponownie powiązał zadania. Do wersji 2.4.1 worker sprawdzał dla nowego zadania co każde 5 cykli (a więc co 5*heartbeats sekund). Jeśli ma się aplikację, która potrzebuje często sprawdzać nowe zadania, aby uzyskać prędkie zachowanie, trzeba wymusić wyższy parametr heartbeat, stawiając niepotrzebnie bazę danych pod presją. Z parametrem immediate=True można wymusić sprawdzanie nowych zadań – trwać to będzie co najwyżej taką ilość sekund, jaką przekazano w heartbeat.

Wywołanie scheduler.queue_task zwraca id zadania i uuid zadania, które oczekuje w kolejce (może być tym, co zostało przekazane lub tym, co zostało wygenerowane automatycznie) i ewentualnie errors:

<Row {'errors': {}, 'id': 1, 'uuid': '08e6433a-cf07-4cea-a4cb-01f16ae5f414'}>

Jeśli wystąpią jakieś błędy (zazwyczaj błąd składni lub błędy sprawdzania danych wejściowych), to zwrócony będzie wynik walidacji, a identyfikatory id i uuid będą miały wartość None

<Row {'errors': {'period': 'enter an integer greater than or equal to 0'}, 'id': None, 'uuid': None}>
Wyniki i dane wyjściowe

Tabela "scheduler_run" przechowuje statusy wszystkich uruchomionych zadań. Każdy rekord odpowiada zadaniu, jakie zostało wybrane przez workera. Jedno zadanie może mieć wiele uruchomień. Na przykład, zadanie zaplanowane do powtórzenia 10 razy w ciągu godziny będzie przypuszczalnie mieć 10 uruchomień (chyba, że jedno z nich zawiedzie lub trwają one dłużej niż 1 godzinę). Trzeba pamiętać, że jeśli zadanie nie ma zwracanych wartości, to jest usuwane z tabeli scheduler_run jak tylko zakończy działanie.

Możliwe statusy, to:

RUNNING, COMPLETED, FAILED, TIMEOUT

Jeśli uruchomienie zostało zakończone, nie zgłoszone zostały jakieś wyjątki i nie ma zadań z przekroczonym limitem czasu, uruchomienie jest oznaczane jako COMPLETED a samo zadanie jako QUEUED lub COMPLETED, w zależności od tego czy jest przeznaczone do ponownego uruchomienia w późniejszym czasie. Dane wyjściowe zadania są serializowane do formatu JSON i zapisywane w rekordzie uruchomienia.

Gdy uruchomione zadanie (ze statusem RUNNING) zgłasza wyjątek, uruchomienie i samo zadanie zostają oznaczone jaki błędne (status FAILED). Komunikat o ostatnim wywołaniu (traceback) zostaje zapisane w rekordzie uruchomienia.

Podobnie, gdy uruchomienie przekroczy określony limit czasu, to zostanie zatrzymane i oznaczone jako TIMEOUT, tak samo jak zadanie.

W każdym przypadku, standardowy strumień wyjścia stdout jest przechwytywany i również rejestrowany w rekordzie uruchomienia.

Używając interfejsu administracyjnego, można sprawdzić wszystkie uruchomione zadania (ze statusem RUNNING), dane wyjściowe zadań zakończonych (ze statusem COMPLETED), błąd zadań błędnych (ze statusem FAILED) itd.

Scheduler również tworzy jeszcze jedną tabele o nazwie "scheduler_worker", która przechowuje dane parametru heartbeat workerów i ich statusy.

Zarządzanie procesami

Zarządzanie workerami jest trudne. Moduł ten stara się obsłużyć każdą platformę (Mac, Win, Linux).

Po uruchomieniu workera, można później potrzebować go:

  • zabić "bez względu na to co robi";
  • zabić tylko wtedy, gdy żadne zadanie nie jest przetwarzane;
  • uśpić.

Załóżmy, że mamy jeszcze jakieś zadania w kolejce i chcemy zaoszczędzić trochę zasobów. Chcemy te zadania przetworzyć co godzinę, tak więc możemy:

  • przetworzyć wszystkie zadania w kolejce i automatycznie je zlikwidować.

Wszystkie te rzeczy są możliwe do zrobienia w zarządzaniu parametrami klasy Scheduler lub tabelą scheduler_worker. Aby być bardziej precyzyjnym, dla rozpoczętych workerów można zmienić wartość status każdego workera, aby wpłynąć na jego zachowanie. Co do zadań, workery mogą się znajdować w jednym z następujących stanów: ACTIVE, DISABLED, TERMINATE lub KILLED.

Stany ACTIVE i DISABLED są stanami "trwałymi", podczas gdy TERMINATE i KILL, jak sugeruje sama nazwa, są bardziej poleceniami niż stanami. Użycie w terminalu (w którym uruchomiono workera) klucza Ctrl+C jest równoważne ustawieniu workera w stan KILL.

workers statuses

Począwszy 2.4.1 dostępnych jest kilka narzędziowych funkcji:

scheduler.disable()
scheduler.resume()
scheduler.terminate()
scheduler.kill()

Każda z tych funkcji przyjmuje opcjonalny parametr, który może być łańcuchem lub listą, w celu zarządzania workerami na podstawie group_names. Domyśłnie jest to parametr group_names zdefiniowany w obiekcie schedulera.

Przykład jest lepszy niż tysiące słów: scheduler.terminate('high_prio') zakończy działanie wszystkich workerów, które przetwarzają zadania high_prio, podczas gdy scheduler.terminate(['high_prio', 'low_prio']) zakończy działanie wszystkich workerów z zadaniami high_prio i low_prio.

Uważaj: jeśli ma się worker przetwarzający high_prio i low_prio, scheduler.terminate('high_prio') będzie usuwał każdy taki worker, nawet jeśli nie chce się usunąć workerów z low_prio.

Wszystko co można zrobić z poziomu interfejsu administracyjnego, można zrobić programowo przez wstawienie i zaktualizowanie rekordów w tych tabelach.

W każdym razie, nie powinno się aktualizować rekordów dotyczących zadań ze statusem RUNNING, gdyż może spowodować to nieoczekiwane zachowanie się kodu. Najlepszą praktyką jest kolejkowanie zadań przy wykorzystaniu metody "queue_task".

Na przykład:

scheduler.queue_task(
    function_name='task_add',
    pargs=[],
    pvars={'a':3,'b':4},
    repeats = 10, # run 10 times
    period = 3600, # every 1h
    timeout = 120, # should take less than 120 seconds
    )

Pola "times_run", "last_run_time" i "assigned_worker_name" nie są udostępniane w czasie planowania, ale są wypełniane automatycznie przez workery.

Można też pobrać dane wyjściowe zakończonych zadań:

completed_runs = db(db.scheduler_run.run_status='COMPLETED').select()

Scheduler jest jeszcze uważany za eksperymentalny, ponieważ może ulec zmianie struktura tabeli, jak też mogą zostać dodane nowe możliwości.

Procentowe raportowanie

W wyrażeniach print funkcji można użyć specjalne"słowo" czyszczące wszystkie poprzednie dane wyjściowe. Słowem tym jest !clear!. To w połączeniu z parametrem sync_output umożliwia procentowe raportowanie.

Oto przykład:

def reporting_percentages():
    time.sleep(5)
    print '50%'
    time.sleep(5)
    print '!clear!100%'
    return 1

Funkcja reporting_percentages śpi przez 5 sekund, wyprowadza 50%. Następnie, znowu śpi przez 5 sekund i wyprowadza 100%. Dane wyjściowe w tabeli scheduler_run są synchronizowane co 2 sekundy i że drugie wyrażenie print, zawierające !clear!100%, dostaje wyczyszczone wyjście 50% i zamienia to na 100%.

scheduler.queue_task(reporting_percentages,
                     sync_output=2)

Moduły zewnętrzne

import

Framework web2py jest napisany w Pythonie, tak więc można go importować i używać jak każdy moduł Pythona, podobnie jak moduły osób trzecich (zewnetrzne). Wystarczy, że Python będzie miał możliwość odnalezienia ich. Tak jak w przypadku każdej aplikacji Pythona, moduły mogą być instalowane w oficjalnym ogólnowitrynowym katalogu pakietów Pythona, a następnie importowane w dowolnym miejscu kodu.

Moduły w witrynowym katalogu pakietów są, jak sama nazwa wskazuje, pakietami poziomu witryny internetowej. Aplikacje wymagające pakietów witrynowych nie są przenośne, chyba że moduły te są zainstalowane oddzielnie. Zaleta posiadania modułów w witrynowym katalogu pakietów jest to, że korzystać z nich może wiele aplikacji. Przyjrzyjmy się, na przykład, pakietowi kreślarskiemu o nazwie "matplotlib". Można zainstalować go z poziomu powłoki używając polecenia easy_install [easy-install] (lub jego nowszy zamiennik pip [PIP] ):

easy_install py-matplotlib

a następnie można zaimportować go do dowolnego modelu, kontrolera czy widoku poleceniem:

import matplotlib

Dystrybucja źródłowa web2py i dystrybucja binarna Windows mają pakiety witrynowe w folderze głównego poziomu. Dystrybucja binarna Mac ma folder pakietów witrynowych w folderze:

web2py.app/Contents/Resources/site-packages

Problem z wykorzystaniem pakietów witrynowych jest taki, że trudno jest używać różne wersje tego samego modułu w tym samym czasie. Na przykład nie może być dwóch aplikacji, w których każda używa inną wersję tego samego pliku. W tym przykładzie, sys.path nie poze być zmienione ponieważ ma to wpływ na obie aplikacje.

Dla tego rodzaju sytuacji web2py dostarcza inny sposób importu modułów w taki sposób, że globalna zmienna sys.path jest niezmienna – przez umieszczenie tych modułów w folderze "modules" aplikacji. Jedną z zalet tego rozwiązania jest to, że moduł będzie automatycznie kopiowany i dystrybuowany z aplikacją.

Po umieszczeniu modułu "mymodule.py" w folderze "modules/" aplikacji, może on zostać zaimportowany z dowolnego miejsca aplikacji web2py (bez potrzeby zmiany sys.path):

import mymodule

Środowisko wykonawcze

exec_environment

Chociaż wszystko co tu opisano działa, to jednak zamiast tego rozwiązania zalecamy zbudowanie swojej aplikacji przy wykorzystaniu komponentów omówionych w rozdziale 12.

Pliki modelu i kontrolera web2py różnią się od typowych modułów Pythona tym, że nie mogą być importowane przy użyciu wyrażenia import Pythona. Powodem jest to, że modele i kontrolery są zaprojektowane do ich wykonywania w przygotowanym środowisku, które zostało wstępnie wypełnione globalnymi obiektami web2py (żądanie, odpowiedź, sesja, pamięć podręczna i T) oraz funkcjami pomocniczymi (helperami). Jest to konieczne, ponieważ Python jest językiem o statycznym zakresie leksykalnym, podczas gdy środowisko web2py jest tworzone dynamiczne.

Framework web2py dostarcza funkcję exec_environment umożliwiającą bezpośredni dostęp do modeli i kontrolerów. Funkcja exec_environment tworzy środowisko wykonawcze web2py, ładując do niego plik, a następnie zwracając obiekt Storage zawierający środowisko. Obiekt Storage obsługuje również mechanizm przestrzeni nazw. Każdy plik Pythona, zaprojektowany jako wykonywany w środowisku wykonawczym, może być ładowany przy użyciu exec_environment. Zastosowanie exec_environment obejmuje:

  • Uzyskiwanie dostępu do danych (modeli) z innych aplikacji.
  • Dostęp do obiektów globalnych z innych modeli lub kontrolerów.
  • Wykonywanie funkcji kontrolera z poziomu innych kontrolerów.
  • Ładowanie bibliotek pomocniczych dla całej witryny.

Następujący przykład odczytuje wiersze z tabeli user w aplikacji cas:

from gluon.shell import exec_environment
cas = exec_environment('applications/cas/models/db.py')
rows = cas.db().select(cas.db.user.ALL)

Inny przykład. Załóżmy, że mamy kontroler "other.py", który zawiera:

def some_action():
    return dict(remote_addr=request.env.remote_addr)

Oto jak można wywołać ta akcję z poziomu innego kontrolera (lub z powłoki web2py):

from gluon.shell import exec_environment
other = exec_environment('applications/app/controllers/other.py', request=request)
result = other.some_action()

W linii 2, wyrażenie request=request jest opcjonalne. Jego efektem jest przekazanie bieżącego żądania do środowiska "other". Bez tego argumentu środowisko powinno zawierać nowy i pusty obiekt żądania (oprócz request.folder). Możliwe jest również przekazanie obiektu odpowiedzi i sesji do exec_environment. Trzeba jednak być bardzo ostrożnym przekazując obiekty żądania, odpowiedzi i sesji --- modyfikacja przez wywołanie akcji lub zależności kodowe w wywołanej akcji mogą doprowadzić do nieoczekiwanych efektów.

Wywołanie funkcji w linii 3 nie wywołuje widoku. Zwraca po prostu słownik, chyba że response.render zostanie wywołany jawnie przez "some_action".

Jedna końcowa uwaga: nie należy używać exec_environment niewłaściwie. Jeśli chce się otrzymywać wyniki akcji w innej aplikacji, to prawdopodobnie trzeba zaimplementować XML-RPC API (implementacja API XML-RPC w web2py jest niemal trywialna). Nie należy używać exec_environment jako mechanizmu przekierowań – w takim przypadku trzeba wykorzystać helper redirect.

Współdziałanie aplikacji

współdziałanie aplikacji

Istnieje kilka postaci współdziałania aplikacji:

  • Aplikacje mogą łączyć się z tą samą bazą danych i współdzielić tabele. Nie jest konieczne, aby wszystkie tabele w bazie danych były definiowane przez wszystkie aplikacje, ale muszą być zdefiniowane w tych aplikacjach, które je używają. Wszystkie aplikacje mogą używać tej samej tabeli, ale jedna musi ją definiować z parametrem migrate=False.
  • Aplikacje mogą osadzać komponenty z innych aplikacji, przy użyciu helpera LOAD (opisanego w rozdziale 12).
  • Aplikacje mogą współdzielić sesje.
  • Aplikacje mogą zdalnie wywoływać akcję każdej innej aplikacji poprzez XML-RPC.
  • Aplikacje mogą uzyskiwać dostęp do plików każdej innej aplikacji poprzez system plików (zakładając, że współdzielą ten sam system plików).
  • Aplikacje mogą lokalnie wywoływać akcje każdej innej aplikacji w sposób wyżej omówiony.
  • Aplikacje mogą importować moduły każdej innej aplikacji używając składni:
from applications.otherapp.modules import mymodule

lub

import applications.otherapp.modules.othermodule
  • Aplikacje mogą importować dowolny moduł zlokalizowany na ścieżce wyszukiwania PYTHONPATH, sys.path.

W jedna aplikacji można załadować sesję innej aplikacji używając polecenia:

session.connect(request, response, masterapp='appname', db=db)

Tutaj appname jest nazwą aplikacji nadrzędnej, która ustawia w ciasteczku początkowy parametr session_id. Parametr db jest połączeniem z bazą danych zawierającą tabele sesji (web2py_session). Wszystkie aplikacje, które współdzielą sesje muszą użyć tej samej bazy danych do przechowywania sesji.

Rejestrowanie zdarzeń

Python dostarcza API rejestrowania zdarzeń a web2py dostarcza mechanizm konfigurujący ten interfejs, tak aby aplikacje mogły go używać.

W aplikacji można utworzyć rejestrator, na przykład w modelu:

import logging
logger = logging.getLogger("web2py.app.myapp")
logger.setLevel(logging.DEBUG)

i można go zastosować do rejestracji komunikatów o różnej wadze

logger.debug("Just checking that %s" % details)
logger.info("You ought to know that %s" % details)
logger.warn("Mind that %s" % details)
logger.error("Oops, something bad happened %s" % details)

logging to standardowy moduł Pythona, opisany na stronie http://docs.python.org/library/logging.html.

Łańcuch "web2py.app.myapp" definiuje rejestrator poziomu aplikacji.

Jednak aby działał on poprawnie, potrzebny jest plik konfiguracyjny dla rejestratora. Jeden jest dostarczany przez web2py w folderze "examples" i ma nazwę "logging.example.conf". Trzeba skopiować ten plik do katalogu web2py i zmienić jego nazwę na "logging.conf" i dostosować w razie potrzeby.

Plik ten zawiera opis dokumentacyjny, tak więc wystarczy go otworzyć i przeczytać.

W celu utworzenia konfigurowalnego rejestratora dla aplikacji "myapp", należy dodać nazwę myapp do listy kluczy [loggers]:

[loggers]
keys=root,rocket,markdown,web2py,rewrite,app,welcome,myapp

oraz dodać sekcję [logger_myapp], wzorując się na [logger_welcome].

[logger_myapp]
level=WARNING
qualname=web2py.app.myapp
handlers=consoleHandler
propagate=0

Dyrektywa "handlers" określa typ rejestrowania i tutaj wskazuje na rejestrowanie "myapp" do konsoli.

WSGI

WSGI

Platformę web2py i WSGI łączy związek miłości i nienawiści. Z naszej perspektywy WSGI został opracowany jako protokół do łączenia serwera internetowego z aplikacjami w przenośny sposób i używamy go do tego celu. Platforma web2py w istocie jest aplikacją WSGI - gluon.main.wsgibase. Niektórzy programiści wykorzystują WSGI w ograniczony sposób jako protokół komunikacyjnej warstwy pośredniej tworząc aplikację internetową jak cebulę z wielu warstw (każda warstwa stanowi warstwę pośrednią WSGI niezależnie od całego frameworka). Platforma web2py nie wprowadza takiej struktury wewnętrznej. To dlatego, że naszym zdaniem, rdzenna funkcjonalność frameworka (obsługa ciasteczek, sesji, błędów, transakcji, rozdzielania) może być lepiej zoptymalizowana pod względem prędkości, jeśli jest obsługiwana kompleksowo przez pojedynczą warstwę.

A jednak web2py umożliwia wykorzystywanie obcych aplikacji WSGI (stworzonych przez osoby trzecie) i warstwy pośredniej na trzy sposoby (i ich kombinację):

  • Można edytować plik "wsgihandler.py" i dołączyć dowolna obcą warstwę pośrednią WSGI.
  • Można połączyć obcą warstwę pośrednią WSGI do konkretnej akcji w swojej aplikacji.
  • Można wywołać obcą aplikację WSGI z poziomu swojej aplikacji.

Jedynym ograniczeniem jest to, że nie można zastosować obcej warstwy pośredniej do zamiany funkcji rdzenia web2py.

Zewnętrzna warstwa pośrednia

Rozważmy plik "wsgibase.py":

#...
LOGGING = False
#...
if LOGGING:
    application = gluon.main.appfactory(wsgiapp=gluon.main.wsgibase,
                                        logfilename='httpserver.log',
                                        profilerfilename=None)
else:
    application = gluon.main.wsgibase

Gdy LOGGING jest ustawiona na True, gluon.main.wsgibase zostaje opakowywana przez funkcje warstwy pośredniej gluon.main.appfactory. Zapewnia to rejestrowanie zdarzeń w pliku "httpserver.log". W podobny sposób można dodać dowolna warstwę pośrednią. Polecamy zapoznać się z oficjalna dokumentacją WSGI w celu poznania szczegółów.

Wewnętrzna warstwa pośrednia

Każda obca aplikacja warstwy pośredniej (na przykład MyMiddleware, która konwertuje dane wyjściowe na duże litery), może być użyta jako dekorator web2py tworzący warstwę pośrednia dla dowolnej akcji kontrolera (na przykład index). Oto przykład:

class MyMiddleware:
    """converts output to upper case"""
    def __init__(self,app):
        self.app = app
    def __call__(self, environ, start_response):
        items = self.app(environ, start_response)
        return [item.upper() for item in items]

@request.wsgi.middleware(MyMiddleware)
def index():
    return 'hello world'

Nie możemy gwarantować, że wszystkie obce warstwy pośredniej będą działać z tym mechanizmem.

Wywoływanie aplikacji WSGI

Aplikacja WSGI może być łatwo wywołana z poziomu akcji web2py. Oto przykład:

def test_wsgi_app(environ, start_response):
    """this is a test WSGI app"""
    status = '200 OK'
    response_headers = [('Content-type','text/plain'),
                        ('Content-Length','13')]
    start_response(status, response_headers)
    return ['hello world!\n']

def index():
    """a test action that calls the previous app and escapes output"""
    items = test_wsgi_app(request.wsgi.environ,
                          request.wsgi.start_response)
    for item in items:
        response.write(item,escape=False)
    return response.body.getvalue()

W tym przypadku akcja index wywołuje test_wsgi_app i przed zwróceniem wartości zabezpiecza dane wyjściowe znakami ucieczki. Akcja index nie jest sama w sobie aplikacją WSGI i musi być wykorzystane zwykłe API web2py (takie jak response.write do zapisu do gniazda).

 top