Professional Documents
Culture Documents
yd
an
ie
II
Flask
TWORZENIE APLIKACJI INTERNETOWYCH W PYTHONIE
Miguel Grinberg
Tytuł oryginału: Flask Web Development: Developing Web Applications with Python, 2nd Edition
ISBN: 978-83-283-6384-7
© 2020 Helion SA
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Flask Web Development,
the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording or by any information storage
retrieval system, without permission from the Publisher.
Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne
i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym
ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion SA nie ponoszą również
żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce.
Helion SA
ul. Kościuszki 1c, 44-100 Gliwice
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/flask2_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dla Alicji
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Spis treści
Wstęp ....................................................................................................................... 11
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
3. Szablony ................................................................................................................... 41
Mechanizm szablonów Jinja2 41
Renderowanie szablonów 42
Zmienne 43
Struktury sterujące 44
Integracja Bootstrapa z Flask-Bootstrap 45
Niestandardowe strony błędów 48
Łącza 51
Pliki statyczne 51
Lokalizowanie dat i czasu za pomocą pakietu Flask-Moment 52
6 Spis treści
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
6. Wiadomości e-mail ................................................................................................... 87
Obsługa e-mail za pomocą rozszerzenia Flask-Mail 87
Wysyłanie wiadomości e-mail z powłoki Pythona 88
Integrowanie wiadomości e-mail z aplikacją 89
Asynchroniczne wysyłanie e-maila 90
Spis treści 7
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
9. Role użytkowników ................................................................................................. 129
Reprezentacja ról w bazie danych 129
Przypisanie ról 132
Weryfikacja roli 133
8 Spis treści
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
14. Interfejsy programowania aplikacji ..........................................................................189
Wprowadzenie do architektury REST 189
Zasoby są wszystkim 190
Metody żądania 190
Treści żądań i odpowiedzi 191
Kontrola wersji 192
Flask i usługi sieciowe typu REST 193
Tworzenie schematu interfejsu API 193
Obsługa błędów 194
Uwierzytelnianie użytkownika za pomocą Flask-HTTPAuth 195
Uwierzytelnianie za pomocą tokenów 198
Serializacja zasobów do i z formatu JSON 199
Implementacja punktów końcowych dla zasobów 202
Podział dużych kolekcji zasobów na strony 204
Testowanie usług internetowych za pomocą HTTPie 205
Spis treści 9
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Kontenery na platformie Docker 240
Instalowanie Dockera 240
Budowanie obrazu kontenera 241
Uruchamianie kontenera 244
Sprawdzanie działającego kontenera 245
Przekazywanie obrazu kontenera do rejestru zewnętrznego 246
Korzystanie z zewnętrznej bazy danych 247
Orkiestracja kontenerów za pomocą Docker Compose 248
Sprzątanie starych kontenerów i obrazów 251
Korzystanie z platformy Docker podczas produkcji 252
Tradycyjne wdrożenia 252
Konfiguracja serwera 253
Importowanie zmiennych środowiskowych 253
Konfigurowanie protokołowania 254
10 Spis treści
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Wstęp
Flask wyróżnia się na tle innych frameworków, ponieważ pozwala programiście przejąć całkowitą
kontrolę i dowolnie budować całą swoją aplikację. Każdy z pewnością zetknął się już z powiedzeniem
o „walce z frameworkiem”. Takie sytuacje zdarzają się w większości frameworków, jeżeli decydujesz
się zastosować rozwiązanie odmienne od oficjalnego. Być może chcesz użyć nieodpowiedniej bazy
danych albo innej metody uwierzytelniania użytkowników. Zejście ze ścieżki wytyczonej przez
twórców frameworka powoduje powstanie wielu dziwacznych problemów.
Flask jest zupełnie inny. Lubisz używać relacyjnych baz danych? Świetnie, Flask pozwala na ich
stosowanie. A może wolisz skorzystać z bazy danych NoSQL? Żaden problem! Flask świetnie sobie
z nią radzi. Sądzisz, że lepiej się sprawdzi Twój własny mechanizm przechowywania danych?
W ogóle nie potrzebujesz bazy danych? Nie widzę przeciwwskazań. Flask umożliwia swobodne
dobieranie komponentów aplikacji, a nawet tworzenie własnych w razie potrzeby. Nikt niczego nie
narzuca.
Tak wielka wolność jest możliwa dlatego, że Flask od samego początku był projektowany z myślą
o rozszerzeniach. Zbudowany jest na bazie bardzo solidnego rdzenia udostępniającego bardzo pod-
stawowe funkcje, jakich potrzebują wszystkie aplikacje WWW, a wszystkie pozostałe elementy
muszą być realizowane za pomocą zewnętrznych rozszerzeń tworzonych w ramach ekosystemu.
Oczywiście każdy może dodać własne rozszerzenie.
W tej książce prezentuję mój własny sposób pracy przy tworzeniu aplikacji na bazie frameworka
Flask. Nie twierdzę, że jest to jedyny, prawidłowy sposób tworzenia aplikacji. Przedstawione tutaj
metody należy traktować jako rekomendacje, a nie prawdy objawione.
Większość książek o tworzeniu oprogramowania prezentuje małe programy przykładowe, skupiające
się na prezentowaniu konkretnych funkcji i rozwiązań w całkowitym oderwaniu od pozostałych
elementów aplikacji. Brakuje w nich „kleju” spajającego poszczególne, niezależne od siebie funkcje
w pełnoprawną aplikację. Te „szczegóły” czytelnik musi już uzupełnić samodzielnie. W tej książce
przyjąłem zupełnie inne podejście. Wszystkie prezentowane przeze mnie przykłady są częścią jednej
aplikacji, która na początku jest bardzo prosta i w każdym rozdziale jest coraz bardziej rozbudowy-
wana. Życie tej aplikacji zaczyna się od kilku prostych wierszy kodu, aby na zakończenie urosnąć
do pełnoprawnej aplikacji do tworzenia blogów i sieci społecznościowych.
11
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dla kogo jest ta książka?
Aby najlepiej skorzystać z tej książki, należy mieć już jakieś doświadczenie w programowaniu w Py-
thonie. Co prawda zakładam, że nie masz żadnych doświadczeń z frameworkiem Flask, ale takie
koncepcje języka Python jak pakiety, moduły, funkcje, dekoratory oraz programowanie obiektowe
powinny być dla Ciebie zrozumiałe. Bardzo przydatna będzie też znajomość obsługi wyjątków oraz
metod diagnozowania problemów na podstawie stosów wywołań.
Pracując z przykładami z tej książki, wiele czasu spędzisz przy wierszu poleceń. Zakładam zatem, że
nie masz problemów z pracą w wierszu poleceń swojego systemu operacyjnego.
W nowoczesnych aplikacjach dla sieci WWW nie da się uniknąć używania języków HTML, CSS
i JavaScript. Przykładowa aplikacja, którą będziemy rozwijali w tej książce, również będzie korzy-
stać z tych technologii, ale nie będziemy zagłębiać się w sposoby ich wykorzystywania. Pożądana
będzie jednak choć częściowa znajomość tych języków, jeżeli masz w planach samodzielne przygoto-
wanie aplikacji bez pomocy programistów zajmujących się technologiami działającymi po stronie
klienta.
Kod źródłowy aplikacji towarzyszącej tej książce udostępniam na GitHubie. Choć GitHub pozwala na
pobranie aplikacji w postaci plików ZIP lub TAR, to zdecydowanie zalecam zainstalowanie klienta Git
i zapoznanie się z metodami kontroli wersji kodu źródłowego (przynajmniej w zakresie podstawo-
wych poleceń służących do klonowania i pobierania różnych wersji aplikacji bezpośrednio z repo-
zytorium). Skróconą listę niezbędnych poleceń podaję w podrozdziale „Jak pracować z przykładowym
kodem?”. Podczas pracy nad własnymi projektami na pewno przyda Ci się system kontroli wersji,
dlatego możesz wykorzystać tę książkę jako sposób nauczenia się pracy z Gitem.
Na koniec muszę zaznaczyć, że ta książka nie jest wyczerpującym podręcznikiem opisującym cały fra-
mework Flask. Prezentuję tutaj większość jego funkcji, ale informacje z tej książki należy zawsze uzu-
pełniać lekturą oficjalnej dokumentacji (https://palletsprojects.com/p/flask/).
12 Wstęp
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W części II, „Przykład: Aplikacja do blogowania społecznościowego”, zaczniemy budować aplikację
do blogowania i tworzenia sieci społecznościowych, którą przygotowałem na potrzeby tej książki:
W rozdziale 8. przygotujemy system uwierzytelniania użytkowników.
W rozdziale 9. zaimplementujemy różne role i uprawnienia.
W rozdziale 10. utworzymy strony profilowe użytkowników.
W rozdziale 11. zajmiemy się interfejsem do blogowania.
W rozdziale 12. dodamy funkcję obserwowania użytkowników.
W rozdziale 13. damy użytkownikom możliwość komentowania wpisów na blogach.
W rozdziale 14. udostępnimy własny interfejs programowania aplikacji (API).
W części III, „Ostatnie kroki”, opisywać będę pewne ważne zadania, które nie wiążą się bezpośrednio
z tworzeniem kodu aplikacji, ale trzeba o nich pamiętać przed opublikowaniem swojego dzieła:
Rozdział 15. zostanie poświęcony różnym strategiom tworzenia testów jednostkowych.
W rozdziale 16. przyjrzymy się różnym technikom analizy wydajności aplikacji.
W rozdziale 17. będę opisywał proces wdrażania aplikacji Flaska, rozważając osobno rozwią-
zania tradycyjne, chmurowe i kontenerowe.
W rozdziale 18. znajdzie się lista dodatkowych źródeł informacji i nie tylko.
Polecenie git clone powoduje pobranie kodu źródłowego z GitHuba i umieszczenie go w katalogu
flasky2, jaki zostanie utworzony w aktualnym katalogu. Utworzony folder nie zawiera wyłącznie kodu
źródłowego, ale całą kopię repozytorium Git wraz z całą historią zmian, jakie zostały wprowadzone
w aplikacji.
1
Materiały w repozytorium dostępne są w języku angielskim. Ostateczna i spolszczona wersja przykładowej aplikacji
jest dostępna na serwerze FTP wydawnictwa Helion pod adresem: ftp://ftp.helion.pl/przyklady/flask2.zip.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W rozdziale 1. poproszę cię o checkout początkowego wydania aplikacji, a następnie w różnych miej-
scach w książce będę prosił o przesunięcie się do przodu w historii zmian. Polecenie Gita, które
pozwala na przenoszenie się w różne miejsca w historii zmian projektu, to: git checkout. Oto przy-
kład takiego polecenia:
$ git checkout 1a
Użyte w tym polecenie oznaczenie 1a jest tak zwanym znacznikiem (ang. tag), czyli nazwą pewne-
go punktu w historii zmian projektu. W całym repozytorium przygotowałem wiele znaczników dosto-
sowanych do rozdziałów tej książki. I tak znacznik 1a użyty w tym przykładzie powoduje przywrócenie
plików projektu do stanu początkowego, używanego w rozdziale 1. Z wieloma rozdziałami powiąza-
nych jest kilka znaczników. Na przykład znaczniki 5a, 5b itd. oznaczają kolejne wersje kodu używane
w rozdziale 5.
Po zastosowaniu takiego polecenia git checkout jak pokazane wyżej Git wyświetli komunikat
z ostrzeżeniem, że repozytorium znajduje się w stanie „detached HEAD”. Oznacza to, że nie mamy
żadnej konkretnej gałęzi, w której moglibyśmy umieszczać nowe zmiany, ale mamy za to wgląd
w wybrany stan plików w środku historii projektu. Nie ma powodu, żeby się przejmować tym komu-
nikatem. Trzeba jednak pamiętać, że po wprowadzeniu jakichkolwiek zmian w dowolnym pliku
próba wydania polecenia git commit zakończy się niepowodzeniem, ponieważ Git nie będzie wiedział,
co ma zrobić z tymi zmianami. W takiej sytuacji dalsza praca z projektem będzie wymagać przywróce-
nia wszystkich zmienionych plików do ich oryginalnej postaci. Najprościej można to osiągnąć za
pomocą polecenia git reset:
$ git reset --hard
Polecenia git fetch używane są do zaktualizowania historii zmian oraz znaczników w lokalnym
repozytorium na podstawie zdalnego repozytorium GitHub. Taka aktualizacja nie powoduje jeszcze
żadnych zmian w lokalnych plikach źródłowych. Te pojawią się dopiero po użyciu polecenia git
reset. Przypominam, że za każdym razem, gdy wprowadzasz polecenie git reset, tracisz wszystkie
wprowadzone wcześniej i niezachowane lokalne zmiany.
Kolejną przydatną operacją jest przegląd różnic wstępujących pomiędzy dwoma wersjami aplikacji.
Często ważne jest, żeby móc dokładnie ocenić wszystkie wprowadzone zmiany. W wierszu poleceń
14 Wstęp
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
można w tym celu użyć polecenia git diff. Na przykład, chcąc zobaczyć różnice między rewizjami 2a
i 2b, należy wpisać polecenie:
$ git diff 2a 2b
Wszystkie tak wyznaczone różnice prezentowane są jako łatka (ang. patch). Jeżeli nie masz doświad-
czenia w pracy z plikami łatek, to zapewne uznasz, że nie jest to najbardziej intuicyjna forma prezen-
towania zmian. Z pewnością znacznie czytelniejsze będą graficzne prezentacje zmian dostępne na
GitHubie. Na przykład różnice między rewizjami 2a i 2b można zobaczyć na GitHubie pod adresem:
https://github.com/miguelgrinberg/flasky/compare/2a...2b.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Stała szerokość z kursywą lub nawiasy ostre (<>)
Oznaczać będzie tekst, który należy zastąpić wartością podaną przez użytkownika albo warto-
ściami wynikającymi z kontekstu.
Podziękowania
Nie zdołałbym napisać tej książki samodzielnie. Wiele wsparcia otrzymałem od swojej rodziny,
współpracowników, starych przyjaciół oraz tych nowych, których poznałem przy okazji pisania
książki.
Brendanie Kohlerze, muszę Ci podziękować za doskonałe recenzje techniczne oraz za pomoc przy
nadawaniu kształtu rozdziałowi na temat interfejsów programowania aplikacji. David Baumgold,
Todd Brunhoff, Cecil Rock i Matthew Hugues — recenzenci mojego manuskryptu na różnych
etapach tworzenia — dziękuję za wszystkie porady dotyczące zakresu poruszanych tematów i sposobu
organizowania całego materiału. Jestem Waszym dłużnikiem.
Tworzenie przykładowego kodu na potrzeby tej książki wymagało niemałego wysiłku, w którym
wspomagał mnie Daniel Hofmann, wykonując dokładny przegląd całego kodu aplikacji i wska-
zując różne możliwości jej poprawienia. Z kolei Dylan Grinberg (mój nastoletni syn) na kilka
weekendów ograniczył swoje uzależnienie od Minecrafta i pomagał mi testować ten kod na wielu
różnych platformach. Jestem mu za to niezwykle wdzięczny.
Wydawnictwo O’Reilly prowadzi wspaniały program o nazwie Early Release, który umożliwia niecier-
pliwym czytelnikom dostęp do książek w czasie, gdy są jeszcze tworzone. Kilku moich czytelników
będących częścią tego programu wdało się ze mną w bardzo przydatną dyskusję na temat swoich
pierwszych kontaktów z tą książką, co doprowadziło do wprowadzenia niejednej ważnej zmiany. Mu-
szę wspomnieć tutaj o kilku osobach, których wkład w tę książkę jest nieoceniony: Sundeep Gupta,
Dan Caron, Brian Wisti oraz Cody Scott.
Pracownicy wydawnictwa O’Reilly zawsze bardzo mnie wspierali. Muszę podziękować tutaj Meghan
Blanchette za jej nieustające wsparcie, porady i udzielaną pomoc. Meghan sprawiła, że proces tworze-
nia książki stał się niezapomnianym doświadczeniem.
A ponad to wszystko chciałbym serdecznie podziękować całej społeczności frameworka Flask.
16 Wstęp
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Podziękowania do wydania drugiego
Chciałbym tu podziękować Ally MacDonald — redaktor drugiego wydania mojej książki, a także
Susan Conant, Rachel Roumeliotis i całemu zespołowi wydawnictwa O’Reily za nieustającą pomoc
z ich strony.
Recenzentki techniczne tej książki — Lorena Mesa, Diane Chen i Jesse Smith — świetnie się spisały,
wskazując mi obszary wymagające poprawy i prezentując mi nowe perspektywy. Chcę zatem podzię-
kować im za ich wkład w tę książkę przez ich podpowiedzi i sugestie. W podziękowaniach nie mogę
też pominąć mojego syna — Dylana Grinberga, który w pocie czoła testował kod wszystkich
przykładów z tej książki.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
18 Wstęp
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
CZĘŚĆ I
Wprowadzenie do Flaska
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 1.
Instalacja
Według wszelkich definicji Flask jest frameworkiem, choć jest na tyle mały, że można nazywać go
„mikroframeworkiem”. Jest wystarczająco niewielki, że po zapoznaniu się z nim najprawdopodobniej
będziesz w stanie przeczytać i zrozumieć cały jego kod źródłowy.
Jednak to, że jest mały, nie oznacza, że ma mniejsze możliwości niż inne frameworki. Flask był od
początku projektowany jako framework korzystający z rozszerzeń. Sam Flask udostępnia zbiór
podstawowych usług, podczas gdy rozszerzenia zajmują się całą resztą. Dzięki temu, że możesz
dobrać potrzebne Ci pakiety rozszerzeń, końcowy produkt nie jest przerośnięty i spełnia posta-
wione mu wymagania.
Flask ma trzy główne zależności: podsystemy routingu, debugowania oraz WSGI (ang. Web Server
Gateway Interface) pochodzą z biblioteki Werkzeug (http://werkzeug.pocoo.org/), mechanizmy
obsługi szablonów realizowane są przez pakiet Jinja2 (http://jinja.pocoo.org/), natomiast system
integracji z wierszem poleceń pochodzi z pakietu Click (http://click.pocoo.org). Autorem tych wszyst-
kich pakietów jest Armin Ronacher, który jest też autorem frameworka Flask.
We Flasku nie znajdziemy mechanizmów obsługi baz danych, sprawdzania poprawności formularzy,
uwierzytelniania użytkowników ani żadnych innych mechanizmów wysokiego poziomu. Te oraz wiele
innych kluczowych usług stosowanych we współczesnych aplikacjach WWW jest realizowanych
w ramach rozszerzeń, które są integrowane z głównym pakietem. Każdy programista takich aplikacji
może sobie zatem dobierać te rozszerzenia, które najlepiej będą sprawdzały się w jego projekcie, a jeżeli
zajdzie taka potrzeba, może też napisać własne rozszerzenia. Jest to ogromna zmiana względem
większych frameworków, w których wszystkie decyzje zostały podjęte odgórnie, a ich zmiana jest bar-
dzo trudna, a czasem zupełnie niemożliwa.
W tym rozdziale dowiesz się, jak można zainstalować framework Flask. Jedynym wymaganiem
jest komputer z zainstalowanym Pythonem.
Przykłady z tej książki na pewno działają w Pythonie 3.5 i 3.6. Jeżeli to konieczne,
możesz też używać Pythona 2.7, ale ze względu na to, że ta wersja języka zostanie
porzucona po 2020 roku, bardzo zalecam korzystanie z wersji 3.x.
21
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeżeli planujesz korzystać z komputera z systemem Microsoft Windows podczas
pracy z przykładowymi programami, to musisz zdecydować, czy chcesz stosować
narzędzia przygotowane specjalnie dla systemów Windows, czy też skonfigurować
swój system w sposób umożliwiający używanie powszechniej stosowanych narzędzi
uniksowych. Kod podawany w tej książce jest w większości zgodny z oboma rozwiąza-
niami. W tych przypadkach, w których konieczne jest użycie odmiennych rozwiązań,
podawane jest rozwiązanie dla systemów uniksowych, a to dla systemów Windows
jest tylko pokrótce omawiane.
Jeżeli zdecydujesz się stosować metody dla systemów uniksowych, to masz do wyboru
kilka opcji. Jeżeli korzystasz z systemu Windows 10, to możesz włączyć podsystem
WSL (ang. Windows Subsystem for Linux). Jest to oficjalna funkcja systemu tworząca
instalację Linuksa Ubuntu, działającą równolegle z samym systemem Windows. W ten
sposób uzyskuje się dostęp do powłoki bash oraz pełnego zbioru narzędzi uniksowych.
Jeżeli w Twoim systemie nie jest dostępny podsystem WSL, to możesz skorzystać
z otwartoźródłowego projektu Cygwin (https://www.cygwin.com/), który zajmuje się
emulacją stosowanego w Uniksie podsystemu POSIX i udostępnia wiele dostosowa-
nych wersji narzędzi uniksowych.
Jeżeli nie masz ochoty korzystać z Gita i wolisz samodzielnie wpisywać albo kopiować kod przykła-
dów, to możesz po prostu utworzyć pusty katalog aplikacji za pomocą tych instrukcji:
$ mkdir flasky
$ cd flasky
Wirtualne środowiska
Skoro utworzyłeś już katalog dla aplikacji, możesz przystąpić do instalowania Flaska. Najwygod-
niejszą metodą będzie tu skorzystanie ze środowiska wirtualnego. Takie środowisko jest kopią inter-
pretera Pythona, w której możesz prywatnie instalować różne pakiety, nie wpływając na globalny
interpreter Pythona zainstalowany w całym systemie.
Wirtualne środowiska są bardzo przydatne, ponieważ pozwalają unikać zaśmiecania pakietami
systemowego interpretera Pythona oraz przeciwdziałają powstawaniu konfliktów wersji. Utworzenie
wirtualnego środowiska dla każdego projektu sprawia, że aplikacje mają dostęp wyłącznie do tych
22 Rozdział 1. Instalacja
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
pakietów, z których faktycznie korzystają, podczas gdy globalny interpreter nadal jest niezaśmie-
cony i stanowi podstawę do tworzenia kolejnych środowisk wirtualnych. Dodatkową zaletą jest to,
że środowiska wirtualne mogą być tworzone i zarządzane bez uprawnień administracyjnych,
podczas gdy zarządzanie systemowym interpreterem Pythona wymaga takich uprawnień.
Opcja -m venv uruchamia pakiet venv z biblioteki standardowej, jako niezależny skrypt, przekazując
mu w parametrze nazwę środowiska.
Teraz przystąpimy do tworzenia wirtualnego środowiska w katalogu flasky. Powszechnie stosowana
konwencja tworzenia wirtualnych środowisk nakazuje nadawać im nazwę venv, ale możesz zastosować
dowolną inną, wybraną przez siebie nazwę. Upewnij się, że aktualny katalog to katalog flasky, i wpro-
wadź poniższe polecenie:
$ python3 -m venv venv
Po zakończeniu pracy tego polecenia w katalogu flasky zobaczysz nowy podkatalog o nazwie venv,
w którym znajdzie się nowo utworzone wirtualne środowisko zawierające interpreter Pythona do wy-
łącznego użytku w tym projekcie.
Jeżeli używasz systemu Microsoft Windows, to upewnij się, że okienko wiersza poleceń zostało
uruchomione z uprawnieniami administratora, a następnie zastosuj to polecenie:
$ pip install virtualenv
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Polecenie virtualenv przyjmuje w parametrze nazwę wirtualnego środowiska. Upewnij się, że aktual-
nym katalogiem jest katalog flasky, a następnie uruchom poniższe polecenie, aby utworzyć wirtualne
środowisko o nazwie venv:
$ virtualenv venv
New python executable in venv/bin/python2.7
Also creating executable in venv/bin/python
Installing setuptools, pip, wheel...done.
Utworzony zostanie podkatalog o nazwie venv, w którym znajdą się wszystkie pliki związane z po-
wstałym właśnie wirtualnym środowiskiem.
Po zakończeniu pracy z wirtualnym środowiskiem należy wpisać w wierszu poleceń instrukcję deactivate,
aby przywrócić pierwotną postać zmiennej środowiskowej PATH.
24 Rozdział 1. Instalacja
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Instalowanie pakietów Pythona za pomocą narzędzia pip
Pakiety Pythona instalowane są za pomocą menedżera pakietów pip, który dołączany jest do każdego
wirtualnego środowiska. Podobnie jak ma to miejsce w przypadku polecenia python, wpisanie w wier-
szu poleceń polecenia pip spowoduje wyświetlenie numeru wersji narzędzia, które związane jest
z aktywnym wirtualnym środowiskiem.
Aby zainstalować framework Flask w swoim wirtualnym środowisku, upewnij się, że jest ono aktywne,
a następnie wprowadź poniższe polecenie:
(venv) $ pip install flask
Po uruchomieniu tego polecenia menedżer pip zainstaluje nie tylko sam framework Flask, ale też
wszystkie jego zależności. W dowolnym momencie możesz sprawdzić, jakie pakiety są zainstalowane
w wirtualnym środowisku, wprowadzając polecenie pip freeze:
(venv) $ pip freeze
click==6.7
Flask==0.12.2
itsdangerous==0.24
Jinja2==2.9.6
MarkupSafe==1.0
Werkzeug==0.12.2
W danych wypisywanych przez polecenie pip freeze znajduje się dokładny numer wersji każdego
z zainstalowanych pakietów. Najprawdopodobniej numery wersji pakietów w Twoim systemie będą
różniły się od tych podanych powyżej.
Możesz też skontrolować, czy framework Flask został prawidłowo zainstalowany. W tym celu należy
uruchomić interpreter Pythona i spróbować zaimportować odpowiedni pakiet:
(venv) $ python
>>> import flask
>>>
Jeżeli nie pojawią się żadne komunikaty o błędach, to możesz sobie pogratulować: przygotowania
zostały zakończone i możesz przejść do następnego rozdziału, w którym napiszesz swoją pierwszą
aplikację WWW.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
26 Rozdział 1. Instalacja
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 2.
Podstawowa struktura aplikacji
W tym rozdziale poznasz różne części aplikacji Flaska. Napiszesz i uruchomisz też swoją pierwszą
aplikację internetową Flask.
Inicjalizacja
Wszystkie aplikacje Flaska muszą utworzyć instancję aplikacji (ang. application instance). Serwer WWW
przekazuje wszystkie żądania otrzymane od klientów do tego obiektu obsługującego, używając
przy tym protokołu o nazwie Web Server Gateway Interface (WSGI, wymawiane „wiz-gii”). Instancja
aplikacji jest obiektem klasy Flask, który zwykle tworzony jest w ten sposób:
from flask import Flask
app = Flask(__name__)
Jedynym wymaganym argumentem konstruktora klasy Flask jest nazwa głównego modułu lub
pakietu tej aplikacji. W przypadku większości aplikacji w Pythonie poprawną wartością dla tego
argumentu jest zmienna __name__.
Troszkę później poznasz bardziej złożone sposoby inicjalizowania aplikacji, ale w przypadku prostych
aplikacji to naprawdę wszystko, czego potrzeba.
27
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Najwygodniejszym sposobem zdefiniowania trasy w aplikacji Flaska jest użycie dekoratora
app.route, który jest udostępniany przez instancję aplikacji. Poniższy przykład pokazuje, jak można
zadeklarować trasę przy użyciu tego dekoratora:
@app.route('/')
def index():
return '<h1>Witaj, świecie!</h1>'
Poprzedni przykład rejestruje funkcję index() jako funkcję obsługi głównego adresu URL aplikacji.
Co prawda dekorator app.route jest zalecaną metodą rejestrowania funkcji widoku, ale Flask
udostępnia również bardziej tradycyjny sposób konfigurowania tras w aplikacji za pomocą metody
app.add_url_rule().W swojej najbardziej podstawowej formie funkcja ta przyjmuje trzy argu-
menty: adres URL, nazwę punktu końcowego oraz funkcję widoku. W poniższym przykładzie
użyto metody app.add_url_rule() do zarejestrowania funkcji index(). Przedstawiony tu kod
działa dokładnie tak samo jak ten pokazany wyżej:
def index():
return '<h1>Witaj, świecie!</h1>'
app.add_url_rule('/', 'index', index)
Funkcje obsługujące adresy URL aplikacji, takie jak funkcja index(), nazywane są funkcjami widoku
(ang. view functions). Jeżeli aplikacja zostanie umieszczona na serwerze z przypisaną domeną
www.przyklad.pl, wówczas wpisanie w przeglądarce adresu http://www.przyklad.pl/ spowoduje wywo-
łanie na serwerze funkcji index(). Wartość zwracana przez funkcję widoku jest odpowiedzią prze-
syłaną do klienta. Jeśli klient jest przeglądarką internetową, to odpowiedzią jest dokument wyświetlany
użytkownikowi w oknie przeglądarki. Odpowiedź zwrócona przez funkcję widoku może być pro-
stym ciągiem znaków zawierającym kod HTML, ale może również przybierać bardziej złożone
formy, o czym będziemy mówić później.
Jeśli zwrócisz uwagę na to, jak zbudowane są adresy URL usług, z których korzystasz na co dzień,
to zauważysz, że wiele z nich ma zmienne sekcje. Na przykład adres URL strony profilu na Facebooku
ma format https://www.facebook.com/<twoja-nazwa>, który zawiera Twoją nazwę użytkownika,
dzięki czemu adres jest inny dla każdego użytkownika. Flask obsługuje te rodzaje adresów URL
przy użyciu specjalnej składni dekoratora app.route. Poniższy przykład definiuje trasę, która ma
składnik dynamiczny:
@app.route('/user/<name>')
def user(name):
return '<h1>Witaj, {}!</h1>'.format(name)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Część adresu URL trasy, która zawarta jest w nawiasach ostrokątnych, to właśnie część dynamiczna.
Wszystkie adresy URL pasujące do części statycznych zostaną zmapowane na tę trasę, a po wywołaniu
funkcji widoku komponent dynamiczny zostanie przekazany jako jej argument. W poprzednim
przykładzie argument name służył do wygenerowania odpowiedzi zawierającej spersonalizowane
powitanie.
Dynamiczne komponenty w trasach są domyślnie ciągami znaków, ale mogą mieć też inne typy.
Na przykład trasa /user/<int:id> będzie pasowała tylko do adresów URL, które mają liczbę całkowitą
w segmencie dynamicznym id, taką jak na przykład /user/123. Flask obsługuje dla tras takie typy
jak string, int, float i path. Typ path to specjalny typ ciągu znaków, który w przeciwieństwie do
typu string może zawierać ukośniki.
Kompletna aplikacja
W poprzednich podrozdziałach dowiedziałeś się o różnych częściach aplikacji internetowej Flask,
a teraz nadszedł czas, aby napisać pierwszą taką aplikację. Skrypt aplikacji hello.py pokazany na li-
stingu 2.1 definiuje instancję aplikacji oraz pojedynczą trasę i funkcję widoku w sposób, w który
opisano to już wcześniej.
Jeśli sklonowałeś repozytorium Git naszej przykładowej aplikacji z GitHuba, możesz te-
raz uruchomić polecenie git checkout 2a, aby pobrać tę wersję aplikacji.
Natomiast w przypadku użytkowników systemu Microsoft Windows jedyną różnicą jest sposób usta-
wienia zmiennej środowiskowej FLASK_APP:
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
(venv) $ set FLASK_APP=hello.py
(venv) $ flask run
* Serving Flask app "hello"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Po uruchomieniu serwer wchodzi w pętlę, która przyjmuje żądania i obsługuje je. Pętla ta jest wyko-
nywana do momentu zatrzymania aplikacji przez naciśnięcie kombinacji klawiszy Ctrl+C.
Przy uruchomionym serwerze otwórz przeglądarkę internetową i wpisz w pasku adresu
http://localhost:5000/. Na rysunku 2.1 przedstawiam, co zobaczysz po połączeniu z aplikacją.
Natomiast jeśli wpiszesz coś innego po podstawowym adresie URL, aplikacja nie będzie wiedziała,
jak sobie z tym poradzić, i zwróci przeglądarce błąd o kodzie 404 — jest to znany błąd, który pojawia
się, gdy próbujesz przejść do nieistniejącej strony internetowej.
Instrukcja flask run sprawia, że takie działanie nie jest już potrzebne, jednak
w niektórych przypadkach metoda app.run() może okazać się nadal przydatna. Na
przykład podczas tworzenia testów jednostkowych, o których dowiedziesz się więcej
w rozdziale 15.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Trasy dynamiczne
Nowa wersja aplikacji, pokazana na listingu 2.2, definiuje drugą trasę, która tym razem jest dynamiczna.
Po wprowadzeniu w przeglądarce dynamicznego adresu URL pojawi się spersonalizowane powi-
tanie zawierające imię podane w adresie.
@app.route('/')
def index():
return '<h1>Witaj, świecie!</h1>'
@app.route('/user/<name>')
def user(name):
return '<h1>Witaj, {}!</h1>'.format(name)
Jeśli sklonowałeś repozytorium Git naszej aplikacji z GitHuba, możesz teraz uruchomić
polecenie git checkout 2b, aby pobrać wersję aplikacji.
W celu przetestowania dynamicznej trasy upewnij się, że serwer jest uruchomiony, a następnie
wprowadź w przeglądarce adres http://localhost:5000/user/Dave. Aplikacja odpowie spersonalizo-
wanym powitaniem, wykorzystując dynamiczny argument name. Spróbuj użyć różnych imion
w adresie URL, aby zobaczyć, w jaki sposób funkcja widoku wygeneruje odpowiedź na podstawie
podanego imienia. Przykład działania aplikacji przedstawiam na rysunku 2.2.
Trasy dynamiczne 31
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tryb debugowania
Aplikacje Flaska można opcjonalnie uruchamiać w trybie debugowania (ang. debug mode). W tym
trybie domyślnie włączone są dwa bardzo wygodne moduły serwera roboczego, o nazwie reloader
i debugger.
Gdy włączony jest moduł reloader, Flask obserwuje wszystkie pliki kodu źródłowego Twojego
projektu i automatycznie restartuje serwer, gdy którykolwiek z plików zostanie zmodyfikowany.
Serwer działający z włączonym reloaderem jest niezwykle przydatny podczas programowania, ponie-
waż za każdym razem, gdy zmodyfikujesz i zapiszesz plik źródłowy, serwer automatycznie ponownie
się uruchomi i pobierze zmianę.
Debugger to narzędzie internetowe, które pojawia się w przeglądarce, gdy aplikacja zgłosi nieobsłu-
żony wyjątek. Okno przeglądarki internetowej przekształca się w interaktywny stos wywołań, który
umożliwia sprawdzanie kodu źródłowego aplikacji i skontrolowanie wartości wyrażeń w dowolnym
miejscu stosu wywołań. Na rysunku 2.3 można zobaczyć, jak wygląda taki debugger.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Domyślnie tryb debugowania jest wyłączony. Aby go włączyć, przed wywołaniem polecenia flask
run zdefiniuj zmienną środowiskową FLASK_DEBUG=1:
(venv) $ export FLASK_APP=hello.py
(venv) $ export FLASK_DEBUG=1
(venv) $ flask run
* Serving Flask app "hello"
* Forcing debug mode on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 273-181-528
Jeśli korzystasz z systemu Microsoft Windows, użyj instrukcji set zamiast export, aby zdefiniować
zmienną środowiskową.
Provides commands from Flask, extensions, and the application. Loads the
application defined in the FLASK_APP environment variable, or from a
wsgi.py file. Setting the FLASK_ENV environment variable to 'development'
will enable debug mode.
$ export FLASK_APP=hello.py
$ export FLASK_ENV=development
$ flask run
Options:
--version Show the flask version
--help Show this message and exit.
Commands:
routes Show the routes for the app.
run Run a development server.
shell Run a shell in the app context.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Instrukcja flask shell służy do uruchamiania sesji powłoki Pythona w kontekście aplikacji. Tej
sesji można użyć do uruchamiania zadań konserwacyjnych lub testów albo wykorzystać ją do debu-
gowania problemów. Przykłady sytuacji, w których to polecenie jest naprawdę przydatne, przed-
stawię później, w kilku następnych rozdziałach.
Znasz już instrukcję flask run, która — jak sama nazwa wskazuje — uruchamia aplikację z roboczym
serwerem WWW. To polecenie ma wiele różnych opcji:
(venv) $ flask run --help
Usage: flask run [OPTIONS]
This server is for development purposes only. It does not provide the
stability, security, or performance of production WSGI servers.
Options:
-h, --host TEXT The interface to bind to.
-p, --port INTEGER The port to bind to.
--cert PATH Specify a certificate file to use HTTPS.
--key FILE The key file to use when specifying a
certificate.
--reload / --no-reload Enable or disable the reloader. By default
the reloader is active if debug is enabled.
--debugger / --no-debugger Enable or disable the debugger. By default
the debugger is active if debug is enabled.
--eager-loading / --lazy-loader
Enable or disable eager loading. By default
eager loading is enabled if the reloader is
disabled.
--with-threads / --without-threads
Enable or disable multithreading.
--extra-files PATH Extra files that trigger a reload on change.
Multiple paths are separated by ':'.
--help Show this message and exit.
Szczególnie przydatny jest argument --host, ponieważ mówi serwerowi, na jakim interfejsie siecio-
wym ma nasłuchiwać połączeń od klientów. Domyślnie roboczy serwer WWW Flaska nasłuchuje
połączeń na hoście lokalnym (ang. localhost), więc przyjmowane są wyłącznie połączenia pochodzące
z tego samego komputera. Poniższe polecenie sprawia, że serwer WWW nasłuchuje połączeń
w publicznym interfejsie sieciowym, przyjmując również połączenia od innych komputerów w tej
samej sieci:
(venv) $ flask run --host 0.0.0.0
* Serving Flask app "hello"
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Serwer WWW powinien być teraz dostępny z dowolnego komputera w sieci http://a.b.c.d:5000,
gdzie a.b.c.d to adres IP komputera, na którym działa ten serwer.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Opcje --reload, --no-reload, --debugger i --no-debugger zapewniają większą kontrolę nad ustawie-
niami trybu debugowania. Na przykład jeśli tryb debugowania jest włączony, to można użyć opcji
–no-debugger, aby wyłączyć debugger przy jednoczesnym zachowaniu aktywnego trybu debugo-
wania i reloadera.
@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return '<p>Twoją przeglądarką jest {}</p>'.format(user_agent)
Zwróć proszę uwagę, że w tej funkcji widoku zmienna request jest używana tak, jakby była zmienną
globalną. W rzeczywistości zmienna request nie może być zmienną globalną. Na serwerze wielowąt-
kowym kilka wątków może jednocześnie pracować nad obsługą żądań od różnych klientów, więc
każdy wątek musi widzieć inny obiekt w zmiennej request. Konteksty umożliwiają Flaskowi globalne
udostępnienie wątkowi pewnych zmiennych bez ingerencji w inne wątki.
Flask ma dwa konteksty: kontekst aplikacji i kontekst żądania (ang. application context, request
context). Tabela 2.1 pokazuje zmienne udostępniane przez każdy z tych kontekstów.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 2.1. Zmienne globalne w kontekstach Flaska
Flask aktywuje (lub przekazuje) konteksty aplikacji i żądania przed wysłaniem żądania do aplika-
cji i usuwa je po jego obsłużeniu. Po przekazaniu kontekstu aplikacji w wątku stają się dostępne
zmienne current_app i g. Podobnie przekazanie kontekstu żądania sprawia, że dostępne są zmienne
request i session. Jeśli nastąpi próba dostępu do dowolnej z tych zmiennych bez aktywnego
kontekstu aplikacji lub żądania, to wygenerowany zostanie błąd. Nie martw się, jeśli nie jesteś
w stanie zrozumieć, do czego mogą się przydać te zmienne. Wszystkie cztery zmienne kontekstu
zostaną szczegółowo omówione w tym i w następnych rozdziałach.
Poniższa sesja powłoki Pythona pokazuje, w jaki sposób działa kontekst aplikacji:
>>> from hello import app
>>> from flask import current_app
>>> current_app.name
Traceback (most recent call last):
...
RuntimeError: working outside of application context
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name
'hello'
>>> app_ctx.pop()
W tym przykładzie, gdy w sesji kontekst aplikacji nie jest aktywny, wywołanie current_app.name
nie działa, ale nagle zaczyna działać, gdy tylko kontekst aplikacji zostanie jej przekazany. Zwróć
uwagę na to, jak kontekst aplikacji można uzyskać poprzez wywołanie w instancji aplikacji metody
app_context().
Przesyłanie żądania
Gdy aplikacja otrzyma żądanie od klienta, musi dowiedzieć się, która funkcja widoku będzie właściwa
do obsługi tego żądania. W tym celu Flask poszukuje adresu URL podanego w żądaniu w mapie
adresów URL aplikacji, która odwzorowuje adresy URL na obsługujące je funkcje widoku. Flask bu-
duje tę mapę przy użyciu informacji podawanych w dekoratorach app.route lub działającej w ten
sam sposób funkcji app.add_url_rule().
Aby zobaczyć, jak wygląda mapa adresów URL w aplikacji Flaska, możesz w powłoce Pythona przej-
rzeć mapę utworzoną dla pliku hello.py. Zanim spróbujesz, upewnij się, że środowisko wirtualne jest
aktywowane:
(venv) $ python
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
>>> from hello import app
>>> app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,
<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
<Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
Trasy / oraz /user/<name> zostały zdefiniowane w aplikacji za pomocą dekoratora app.route. Nato-
miast trasa /static/<filename> to specjalna trasa dodana przez Flaska, aby zapewnić dostęp do
plików statycznych. Nieco więcej o plikach statycznych dowiesz się w rozdziale 3.
Elementy (HEAD, OPTIONS, GET) pokazane w mapie adresów URL to metody żądania (ang. request
methods) obsługiwane przez daną trasę. Specyfikacja HTTP określa, że wszystkie żądania są wysyłane
za pomocą metody, która samodzielnie wskazuje rodzaj działania, jakiego klient żąda od serwera.
Flask dołącza metodę żądania do każdej trasy, dzięki czemu różne metody żądania wysyłane pod
ten sam adres URL mogą być obsługiwane przez różne funkcje widoku. Flask automatycznie zarządza
metodami HEAD i OPTIONS, więc w praktyce można powiedzieć, że w tej aplikacji trzy trasy na mapie
adresów URL są powiązane z metodą GET, która jest używana, gdy klient zażąda informacji, takich
jak na przykład strona internetowa. W rozdziale 4. dowiesz się więcej, jak tworzyć trasy dla innych
metod żądań.
Obiekt żądania
Dowiedziałeś się już wcześniej, że Flask udostępnia obiekt żądania jako zmienną kontekstu o nazwie
request. Jest to niezwykle przydatny obiekt, gdyż zawiera wszystkie informacje, które klient zawarł
w żądaniu HTTP. W tabeli 2.2 przedstawiono najczęściej używane atrybuty i metody obiektu żą-
dania Flask.
Hooki w żądaniach
Czasami przydatne jest wykonanie pewnego kodu przed obsłużeniem każdego żądania lub po
tym. Na przykład na początku każdego żądania może być konieczne utworzenie połączenia z bazą
danych lub uwierzytelnienie użytkownika wysyłającego żądanie. Zamiast w każdej funkcji widoku
powielać kod wykonujący te czynności, możemy zarejestrować typowe funkcje, które będą wywoły-
wane przed przekazaniem żądania do obsługi lub po wykonaniu tej czynności.
Hooki w żądaniach są implementowane jako dekoratory. Oto cztery hooki obsługiwane przez Flaska:
before_request
Rejestruje funkcję do uruchomienia przed każdym żądaniem.
before_first_request
Rejestruje funkcję do uruchomienia tylko przed obsłużeniem pierwszego żądania. Może to być
wygodny sposób realizowania zadań związanych z inicjowaniem serwera.
after_request
Rejestruje funkcję do uruchomienia po każdym żądaniu, ale tylko wtedy, gdy nie wystąpiły
nieobsłużone wyjątki.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 2.2. Metody żądania obiektu Flask
teardown_request
Rejestruje funkcję do uruchomienia po każdym żądaniu, nawet jeśli wystąpiły nieobsłużone
wyjątki.
Często stosowanym sposobem współdzielenia danych między funkcjami hooków w żądaniu a funk-
cjami widoku jest użycie zmiennej g będącej częścią globalnego kontekstu. Na przykład funkcja
before_request może załadować zalogowanego użytkownika z bazy danych i zapisać go w zmiennej
g.user. Wywoływana później funkcja widoku może stamtąd pobrać dane użytkownika.
Odpowiedzi
Gdy Flask wywołuje funkcję widoku, oczekuje, że jej wartość zwracana będzie odpowiedzią na
żądanie. W większości przypadków odpowiedź jest prostym ciągiem znaków, który jest odsyłany
do klienta jako strona HTML.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ale protokół HTTP wymaga, aby odpowiedź na żądanie była czymś więcej niż tylko ciągiem znaków.
Bardzo ważną częścią odpowiedzi HTTP jest kod statusu (ang. status code), której Flask domyślnie
przypisuje wartość 200. Jest to kod wskazujący, że żądanie zostało pomyślnie obsłużone.
W przypadku gdy funkcja widoku musi odpowiedzieć innym kodem statusu, może dodać odpowied-
nią liczbę jako drugą wartość zwracaną obok tekstu odpowiedzi. Na przykład poniższa funkcja
widoku zwraca kod statusu 400, który jest kodem nieprawidłowego żądania:
@app.route('/')
def index():
return '<h1>Nieprawidłowe żądanie</h1>', 400
Odpowiedzi zwrócone przez funkcje widoku mogą zawierać też trzeci element — tak zwany słownik
nagłówków (ang. dictionary of headers), który jest dodawany do odpowiedzi HTTP. Przykład nie-
standardowych nagłówków odpowiedzi przedstawię w rozdziale 14.
Funkcje widoku mogą też zwracać obiekt odpowiedzi (ang. response object) zamiast jednej, dwu lub
trzech wartości zebranych w krotce. Funkcja make_response() przyjmuje jeden, dwa lub trzy argu-
menty (są to te same wartości, które można zwrócić z funkcji widoku) i zwraca przygotowany już
obiekt odpowiedzi. Czasami przydatne jest wygenerowanie takiego obiektu w funkcji widoku, a na-
stępnie użycie jego metod w ramach dalszej konfiguracji odpowiedzi. Poniższy przykład tworzy obiekt
odpowiedzi, po czym umieszcza w nim plik cookie:
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>Ten dokument zawiera plik cookie!</h1>')
response.set_cookie('odpowiedz', '42')
return response
W tabeli 2.3 prezentuję najczęściej używane atrybuty i metody dostępne w obiektach odpowiedzi.
Istnieje specjalny rodzaj odpowiedzi zwany przekierowaniem (ang. redirect). Ta odpowiedź nie ma
żadnego dokumentu strony; po prostu podaje przeglądarce nowy adres URL. Z przekierowań bardzo
często korzysta się podczas pracy z formularzami internetowymi, o czym dowiesz się już w rozdziale 4.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Przekierowanie zwykle składa się z kodu statusu odpowiedzi 302 oraz docelowego adresu URL poda-
nego w nagłówku Location. Odpowiedź przekierowania może być wygenerowana ręcznie w trój-
elementowej wartości zwracanej lub z obiektem odpowiedzi. Ze względu na częste używanie
przekierowań Flask zapewnia funkcję pomocniczą redirect(), która pozwala na utworzenie tego
typu odpowiedzi:
from flask import redirect
@app.route('/')
def index():
return redirect('http://www.przyklad.com')
Kolejna specjalna odpowiedź może zostać przygotowana za pomocą funkcji abort(), która używana
jest do obsługi błędów. Poniższy przykład zwraca kod statusu 404, jeśli dynamiczny argument id,
podany w adresie URL, nie jest związany z poprawnym użytkownikiem:
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id)
if not user:
abort(404)
return '<h1>Witaj, {}</h1>'.format(user.name)
Zauważ, że funkcja abort() nie wraca już do naszej funkcji, ponieważ zgłasza wyjątek.
Rozszerzenia Flaska
Flask został zaprojektowany do obsługi rozszerzeń. Celowo nie realizuje tak ważnych funkcji jak
obsługa baz danych lub uwierzytelnianie użytkowników, dając Ci w ten sposób swobodę wyboru
pakietów, które będą najlepiej pasowały do Twojej aplikacji. Nic nie stoi też na przeszkodzie, żeby
napisać własne pakiety.
Społeczność stworzyła szeroki wybór rozszerzeń Flaska przeznaczonych do wielu różnych celów,
a jeśli to nie wystarczy, można również użyć dowolnego standardowego pakietu lub biblioteki
Pythona. Pierwszego rozszerzenia Flaska użyjesz już w rozdziale 3.
W niniejszym rozdziale wprowadzono pojęcie odpowiedzi na żądania, ale o odpowiedziach można
by opowiadać znacznie więcej. Flask zapewnia bardzo dobre wsparcie przy generowaniu odpowiedzi
za pomocą szablonów (ang. templates), a stanowi to tak ważny temat, że został temu poświęcony
następny rozdział.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 3.
Szablony
Kluczem do tworzenia łatwych w utrzymaniu aplikacji jest pisanie czystego i dobrze zorganizowanego
kodu. Przykłady, które widzieliście do tej pory, były zbyt proste, aby to zademonstrować, jednak funk-
cje widoku mają dwa całkowicie niezależne zadania, co stwarza tu pewien problem.
Oczywistym zadaniem funkcji widoku jest wygenerowanie odpowiedzi na żądanie, jak mogliśmy
to zobaczyć w przykładach pokazanych w rozdziale 2. W przypadku najprostszych żądań to wystarczy,
ale w wielu innych kwestiach żądanie powoduje również zmianę stanu aplikacji. I taką zmianę
musi wygenerować funkcja widoku.
Weźmy na przykład użytkownika, który rejestruje nowe konto w witrynie internetowej. Użytkownik
wpisuje adres e-mail i hasło w formularzu, a następnie klika przycisk Prześlij. Na serwerze pojawia się
żądanie z danymi dostarczonymi przez użytkownika, a Flask wysyła je do funkcji widoku, która
obsługuje żądania rejestracji. Funkcja widoku musi teraz komunikować się z bazą danych, aby dodać
do niej nowego użytkownika, a następnie wygenerować odpowiedź (która zawiera komunikat o sukce-
sie lub niepowodzeniu) i odesłać ją z powrotem do przeglądarki. Te dwa rodzaje zadań są formalnie
nazywane, odpowiednio, logiką biznesową i logiką prezentacji (ang. business logic, presentation logic).
Mieszanie logiki biznesowej z logiką prezentacji prowadzi do powstania kodu, który jest trudny do
zrozumienia i utrzymania. Wyobraź sobie, że musisz zbudować kod HTML dla dużej tabeli, łącząc
informacje uzyskane z bazy danych z niezbędnymi literałami ciągów znaków HTML. Sposobem
na ułatwienie konserwacji takich aplikacji jest przeniesienie logiki prezentacji do szablonów (ang.
templates).
Szablon to plik zawierający tekst odpowiedzi ze zmiennymi umieszczonymi w miejscach części dyna-
micznych, które będą konkretyzowane w kontekście żądania. Proces, który zastępuje zmienne rze-
czywistymi wartościami i zwraca końcowy ciąg znaków odpowiedzi, nazywa się renderowaniem (ang.
rendering). Do renderowania szablonów Flask wykorzystuje potężny mechanizm szablonów o nazwie
Jinja2.
41
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 3.1. Szablon Jinja2: templates/index.html
<h1>Witaj, świecie!</h1>
Odpowiedź zwracana przez funkcję widoku user() z listingu 2.2 zawiera składnik dynamiczny,
który jest reprezentowany przez zmienną. Na listingu 3.2 przedstawiam szablon implementujący
taką odpowiedź.
Renderowanie szablonów
Domyślnie Flask szuka szablonów w podkatalogu templates głównego katalogu aplikacji. W kolejnej
wersji pliku hello.py musisz utworzyć podkatalog templates i umieścić w nim pliki szablonów
index.html i user.html zdefiniowane na listingach 3.1 i 3.2.
Aby wyświetlić te szablony, należy zmodyfikować funkcje widoku w aplikacji. Odpowiednie zmiany
przedstawiam na listingu 3.3.
# ...
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
Udostępniana przez Flaska funkcja render_template() integruje mechanizm szablonów Jinja2 z naszą
aplikacją. Funkcja jako swój pierwszy argument przyjmuje nazwę pliku szablonu. Wszelkie dodatkowe
argumenty to pary klucz – wartość, reprezentujące rzeczywiste wartości zmiennych, do których
odwołuje się szablon. W tym przykładzie drugi szablon pobiera zmienną name.
W poprzednim listingu argumenty słów kluczowych, takie jak name=name, są dość powszechne, ale
jeśli nie masz doświadczenia w ich stosowaniu, to mogą wydawać Ci się mylące i trudne do zro-
zumienia. Słowo „name” znajdujące się po lewej stronie reprezentuje argument name, który jest uży-
wany w symbolu zastępczym zapisanym w szablonie. Natomiast słowo „name” zapisane po prawej
stronie jest zmienną w aktualnym zakresie, przekazującą wartość argumentu o tej samej nazwie.
Chociaż jest to powszechnie stosowany sposób zapisu, to jednak używanie tej samej nazwy zmiennej
po obu stronach nie jest wymagane.
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, to możesz pobrać nową wersję
aplikacji za pomocą polecenia git checkout 3a.
42 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Zmienne
Konstrukcja {{ name }} zastosowana w szablonie pokazanym na listingu 3.2 odwołuje się do zmien-
nej, specjalnego symbolu zastępczego, który informuje mechanizm szablonu, że wartość, jaka ma
się znaleźć w tym miejscu, powinna zostać uzyskana z danych dostarczonych w momencie rende-
rowania szablonu.
Jinja2 rozpoznaje zmienne dowolnego typu, nawet typu złożonego, takiego jak lista, słownik i obiekt.
Oto kilka przykładów zmiennych używanych w szablonach:
<p>Wartość ze słownika: {{ mydict['key'] }}.</p>
<p>Wartość z listy: {{ mylist[3] }}.</p>
<p>Wartość z listy ze zmiennym indeksem: {{ mylist[myintvar] }}.</p>
<p>Wartość z metody obiektu: {{ myobj.somemethod() }}.</p>
Zmienne można modyfikować za pomocą filtrów, które są dodawane za nazwą zmiennej z wykorzy-
staniem znaku potoku (|) jako separatora. Na przykład poniższy szablon wyświetla wartość zmiennej
name, zaczynając od wielkiej litery:
Witaj, {{ name|capitalize }}
W tabeli 3.1 można zobaczyć listę często używanych filtrów, które są dostępne w Jinja2.
Specjalnego omówienia wymaga tutaj interesujący filtr safe. Domyślnie Jinja2, ze względów bezpie-
czeństwa, odpowiednio interpretuje zawartość zmiennych. Na przykład jeśli zmienna ma wartość
'<h1>Cześć</h1>', to Jinja2 wyświetli ten ciąg znaków jako '<h1>Cześć</h1>', co spo-
woduje wyświetlenie tekstu znacznika h1, który nie będzie interpretowany przez przeglądarkę. Ze
względu na to, że nierzadko konieczne jest wyświetlanie kodu HTML przechowywanego w zmien-
nych, w takich właśnie przypadkach używany jest filtr safe.
Nigdy nie używaj filtra safe wobec wartości, które nie są zaufane, na przykład wo-
bec takich wartości jak tekst wprowadzany przez użytkowników w formularzach
internetowych.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Struktury sterujące
Jinja2 udostępnia kilka struktur sterujących, których można użyć do zmiany przepływu w szablonie.
W tym punkcie przedstawiam niektóre z najprzydatniejszych struktur wraz z prostymi przykła-
dami ich użycia.
Poniższy przykład pokazuje, jak można umieścić w szablonie instrukcje warunkowe:
{% if user %}
Witaj, {{ user }}!
{% else %}
Witaj, nieznajomy!
{% endif %}
Równie często zachodzi potrzeba wyświetlenia w szablonach listy elementów. Poniższy przykład
pokazuje, jak można to zrobić za pomocą pętli for:
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
Jinja2 obsługuje również makra (ang. macros), które są podobne do funkcji w kodzie Pythona.
Weźmy na przykład:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
Makra można przechowywać w osobnych plikach, tak aby dało się ich używać wielokrotnie. Takie
pliki z makrami będą następnie importowane do stosujących je szablonów:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
Części kodu szablonu, które należy powtórzyć w kilku różnych miejscach, można zapisać w osobnym
pliku i dołączyć do poszczególnych szablonów za pomocą instrukcji:
{% include 'common.html' %}
Jeszcze innym skutecznym sposobem ponownego użycia kodu jest dziedziczenie szablonów, które
jest podobne do dziedziczenia klas w kodzie Pythona. I tak, najpierw tworzony jest szablon podsta-
wowy o nazwie base.html:
<html>
<head>
{% block head %}
44 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
<title>{% block title %}{% endblock %} - Moja aplikacja</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
Szablony bazowe definiują bloki, które można zastąpić w szablonach wywiedzionych. Dyrektywy
block i endblock definiują bloki zawartości, która są dodawane do szablonu bazowego. W naszym
przykładzie są to bloki o nazwach head, title i body. Zwróć, proszę, uwagę na to, że blok title jest
zawarty w bloku head. Poniższy przykład jest szablonem wywiedzionym z szablonu bazowego:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Witaj, świecie!</h1>
{% endblock %}
Dyrektywa extends wskazuje, że ten szablon został wywiedziony z szablonu base.html. Po tej dyrekty-
wie pojawiają się nowe definicje trzech bloków, zdefiniowanych w szablonie bazowym, które są
w nim umieszczane w odpowiednich miejscach. Jeśli blok ma pewną zawartość zarówno w szablonie
bazowym, jak i wywiedzionym, to używana jest treść z szablonu wywiedzionego. W każdym bloku
szablon wywiedziony może wywoływać funkcję super(), aby odwoływać się do zawartości bloku
w szablonie bazowym. W poprzednim przykładzie takie wywołanie znajduje się w bloku head.
Rzeczywiste zastosowania wszystkich struktur sterujących przedstawionych w tym podrozdziale
zostanie zaprezentowane później. Będzie zatem okazja, żeby przekonać się, jak działają.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rozszerzenie to nazywa się Flask-Bootstrap i można je zainstalować za pomocą polecenia pip:
(venv) $ pip install flask-bootstrap
Rozszerzenia Flaska są inicjowane w momencie tworzenia instancji aplikacji. Na listingu 3.4 po-
kazano przykład kodu inicjalizującego Flask-Bootstrap.
Rozszerzenie jest zwykle importowane z pakietu flask_<nazwa>, gdzie <nazwa> jest nazwą rozsze-
rzenia. Większość rozszerzeń Flaska stosuje jeden z dwóch wzorców inicjalizacji. Na listingu 3.4
rozszerzenie jest inicjowane przez przekazanie instancji aplikacji jako argumentu w konstruktorze.
W rozdziale 7. poznasz bardziej zaawansowaną metodę inicjalizowania rozszerzeń, odpowiednią
dla większych aplikacji.
Po zainicjowaniu rozszerzenia Flask-Bootstrap aplikacja uzyskuje dostęp do szablonu bazowego,
który zawiera już ogólną strukturę i wszystkie pliki Bootstrapa. Następnie aplikacja korzysta z dziedzi-
czenia szablonów Jinja2, aby rozszerzyć szablon bazowy. Na listingu 3.5 przedstawiam nową wersję
pliku user.html jako szablonu wywiedzionego.
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Przełącz pasek nawigacji</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Strona główna</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Witaj, {{ name }}!</h1>
</div>
</div>
{% endblock %}
46 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dyrektywa extends uruchamia dziedziczenie szablonu, odwołując się do pliku bootstrap/base.html
z pakietu Flask-Bootstrap. Szablon bazowy tworzy szkielet strony internetowej, która zawiera już
wszystkie pliki CSS i JavaScript frameworka Bootstrap.
Szablon user.html definiuje trzy bloki o nazwach title, navbar i content. Są to bloki, które eksportuje
szablon bazowy, umożliwiając ich przedefiniowanie w szablonach wywiedzionych. Blok title jest
bardzo prosty, a jego zawartość pojawi się między znacznikami <title> w nagłówku renderowanego
dokumentu HTML. Natomiast bloki navbar i content powinny zawierać pasek nawigacji na stronie
oraz jej główną treść.
W tym szablonie blok navbar definiuje prosty pasek nawigacji wykorzystujący komponenty Boot-
strapa. W bloku content znalazł się kontener <div> zawierający nagłówek strony. Wiersz powitania,
który był w poprzedniej wersji szablonu, znajduje się teraz w nagłówku strony. Wygląd aplikacji
po tych wszystkich zmianach można zobaczyć na rysunku 3.1.
Szablon base.html z pakietu Flask-Bootstrap definiuje kilka innych bloków, które można wykorzy-
stać w szablonach wywiedzionych. W tabeli 3.2 przedstawiam pełną listę dostępnych bloków.
Wiele bloków przedstawionych w tabeli 3.2 jest używanych przez sam pakiet Flask-Bootstrap,
więc ich bezpośrednie zastąpienie spowodowałoby problemy. Na przykład bloki styles i scripts
to miejsca, w których deklarowane są pliki CSS i JavaScript programu Bootstrap. Jeśli aplikacja
musi dodać własną treść do bloku, który już zawiera pewną zawartość, należy użyć funkcji super().
Na przykład aby dodać nowy plik JavaScript do dokumentu, musimy w szablonie wywiedzionym
napisać blok scripts w taki oto sposób:
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 3.2. Bloki szablonu bazowego
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
Procedury obsługi błędów zwracają odpowiedź, podobnie jak funkcje widoku, ale muszą również
zwrócić liczbowy kod stanu odpowiadający błędowi, który Flask przyjmuje jako drugą wartość
zwracaną.
48 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Trzeba jeszcze napisać szablony wymienione w procedurach obsługi błędów. Powinny one mieć ten
sam układ co zwykłe strony, więc w tym przypadku będą miały pasek nawigacyjny i nagłówek strony
z komunikatem o błędzie.
Najprostszym sposobem napisania tych szablonów jest skopiowanie pliku templates/user.html do
plików templates/404.html i templates/500.html, a następnie zmienienie elementów nagłówka strony
w tych dwóch nowych plikach na odpowiednie komunikaty o błędach. Niestety spowoduje to powsta-
nie dużej ilości zduplikowanego kodu.
Z pomocą przychodzi nam tutaj dziedziczenie szablonów Jinja2. Podobnie jak pakiet Flask-Bootstrap
zapewnia szablon bazowy z podstawowym układem strony, aplikacja również może zdefiniować
własny szablon bazowy z jednolitym układem strony, w którym znajdzie się pasek nawigacyjny,
pozostawiając zawartość strony do zdefiniowania w szablonach wywiedzionych. Na listingu 3.7
został pokazany plik template/base.html. Jest to nowy szablon, który dziedziczy po szablonie bootstrap/
base.html i definiuje wygląd paska nawigacji, ale sam jest szablonem bazowym drugiego poziomu dla
pozostałych szablonów aplikacji, takich jak templates/user.html, templates/404.html i templates/500.html.
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Przełącz pasek nawigacji</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Strona główna</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
Blok content tego szablonu składa się z jednego elementu kontenera <div>, otaczającego nowy
pusty blok o nazwie page_content, który może być definiowany przez szablony wywiedzione.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Szablony aplikacji będą teraz dziedziczyć z tego szablonu zamiast bezpośrednio z szablonu pakietu
Flask-Bootstrap. Na listingu 3.8 można zobaczyć, jak łatwo buduje się niestandardową stronę błędu
404, która będzie dziedziczyć po pliku templates/base.html. Strona błędu 500 jest podobna i można ją
znaleźć w repozytorium GitHub dla naszej aplikacji.
Listing 3.8. templates/404.html: Niestandardowa strona błędu 404 korzystająca z dziedziczenia szablonów
{% extends "base.html" %}
{% block page_content %}
<div class="page-header">
<h1>Nie znaleziono</h1>
</div>
{% endblock %}
Szablon templates/user.html można teraz uprościć, dziedzicząc go po szablonie bazowym, tak jak
pokazano na listingu 3.9.
{% block page_content %}
<div class="page-header">
<h1>Witaj, {{ name }}!</h1>
</div>
{% endblock %}
50 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Łącza
Każda aplikacja, która ma więcej niż jedną trasę, z całą pewnością musi zawierać łącza prowadzące
do różnych stron. Takie łącza mogą znajdować się na pasku nawigacyjnym.
Zapisywanie adresów URL jako odnośników umieszczanych bezpośrednio w szablonie jest możliwe
w przypadku prostych tras, ale gdy korzystasz z tras dynamicznych ze zmiennymi elementami, to
umieszczanie adresów URL bezpośrednio w szablonie może być znacznie trudniejsze. Ponadto
adresy URL zapisane jawnie tworzą niepożądaną zależność od tras zdefiniowanych w kodzie. W przy-
padku przeorganizowania tras wszystkie łącza zawarte w szablonach mogą zostać pozrywane.
Flask pozwala uniknąć tych problemów, udostępniając funkcję pomocniczą url_for(), która generuje
adresy URL na podstawie informacji przechowywanych w mapie adresów URL aplikacji.
W najprostszym wariancie funkcja przyjmuje nazwę funkcji widoku (lub nazwę punktu końcowego
dla tras zdefiniowanych za pomocą funkcji app.add_url_route()) jako jedyny argument i zwraca
adres URL. Na przykład w aktualnej wersji pliku hello.py wywołanie url_for('index') zwróci adres /,
czyli główny adres URL aplikacji. Natomiast wywołanie url_for('index',_external=True) zwró-
ciłoby bezwzględny adres URL, który w tym przykładzie wygląda tak: http://localhost:5000/.
Dynamiczne adresy URL można generować za pomocą instrukcji url_for(), przekazując części dy-
namiczne jako argumenty słów kluczowych. Na przykład adres url_for('user', name ='janusz',
_external=True) zwróci http://localhost:5000/user/janusz.
Argumenty słów kluczowych przekazywane funkcji url_for() nie ograniczają się do argumentów
używanych w trasach dynamicznych. Wszystkie argumenty, które nie są dynamiczne, zostaną
przez tę funkcję umieszczone w ciągu znaków zapytania. Na przykład wywołanie url_for('user',
name='janusz', page=2, version=1) zwróci adres /user/janusz?page=2&version=1.
Pliki statyczne
Aplikacje internetowe nie są zbudowane z samego kodu Pythona i szablonów. Większość aplikacji ko-
rzysta również z plików statycznych, takich jak obrazy, pliki źródłowe JavaScript i pliki CSS, do
których odwołuje się kod HTML zawarty w szablonach.
Być może pamiętasz, że kiedy w rozdziale 2. przeglądaliśmy mapę adresów URL aplikacji hello.py,
pojawiła się na niej pozycja static. Flask automatycznie obsługuje pliki statyczne, dodając do
aplikacji specjalną trasę zdefiniowaną zapisem /static/<filename>. Na przykład wywołanie funkcji
url_for('static', filename='css/styles.css', _external=True) zwróci adres http://localhost:5000/
static/css/styles.css.
Pliki statyczne 51
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W swojej domyślnej konfiguracji Flask szuka plików statycznych w podkatalogu o nazwie static,
znajdującym się w folderze głównym aplikacji. W razie potrzeby pliki mogą być rozmieszczane
w podkatalogach tego folderu. Gdy serwer otrzymuje adres URL pasujący do trasy statycznej, generuje
odpowiedź, na którą składa się zawartość wskazanego pliku, zapisanego w systemie plików.
Kod z listingu 3.10 pokazuje, jak aplikacja może dołączyć ikonę favicon.ico do szablonu bazowego,
aby przeglądarki mogły wyświetlać ją w pasku adresu.
Deklaracja ikony jest wstawiana na końcu bloku head. Zauważ, że funkcja super() jest tutaj używana
w celu zachowania treści bloku zdefiniowanych w szablonach bazowych.
52 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rozszerzenie jest inicjowane w podobny sposób jak pakiet Flask-Bootstrap. Wymagany do tego
kod pokazano na listingu 3.11.
Pakiet Flask-Moment oprócz pliku Moment.js potrzebuje także pliku jQuery.js. Te dwie biblioteki
muszą zostać dołączone do dokumentu HTML. Można to zrobić bezpośrednio, co pozwala Ci
wybrać wersję biblioteki, z której chcesz skorzystać. Ale możesz też użyć funkcji pomocniczych
udostępnionych przez to rozszerzenie, które odwołują się do przetestowanych już wersji bibliotek
z systemu dostarczania treści CDN (ang. Content Delivery Network). Dzięki temu, że pakiet Boot-
strap zawiera już plik jQuery.js, musimy już tylko dodać plik Moment.js. Na listingu 3.12 pokazano,
jak można załadować tę bibliotekę w bloku scripts naszego szablonu, zachowując przy tym orygi-
nalną zawartość tego bloku dostarczoną przez szablon bazowy. Zwróć uwagę na to, że jest to blok
predefiniowany w szablonie bazowym pakietu Flask-Bootstrap, a zatem lokalizacja tego bloku w pliku
templates/base.html nie ma żadnego znaczenia.
@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
Funkcja format('LLL') renderuje datę i godzinę zgodnie ze strefą czasową i ustawieniami regionalny-
mi na komputerze klienta. Argument określa styl renderowania, począwszy od 'L' aż do 'LLLL'
dla czterech różnych poziomów szczegółowości. W funkcji format() można też wykorzystać dowolne
z długiej listy niestandardowych specyfikatorów formatu.
Styl renderowania fromNow(), pokazany w drugim wierszu, renderuje względny znacznik czasu i au-
tomatycznie odświeża go w miarę upływu czasu. Początkowo znacznik ten będzie wyświetlany jako
„kilka sekund temu”, ale opcja refresh=True będzie go aktualizować w miarę upływu czasu, więc
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
jeśli pozostawisz otwartą stronę na kilka minut, zobaczysz tekst zmieniający się na „minutę temu”,
a następnie na „2 minuty temu” i tak dalej.
Na rysunku 3.3 pokazano, jak będzie wyglądała trasa http://localhost:5000/ po dodaniu dwóch znacz-
ników czasu do szablonu index.html.
Znaczniki czasu renderowane przez Flask-Moment mogą być zlokalizowane w wielu językach. Język
można wybrać w szablonie, przekazując dwuliterowy kod tego języka (https://pl.wikipedia.org/
wiki/ISO_3166-1) do funkcji locale() zaraz po dołączeniu biblioteki Moment.js. Na przykład w ten
sposób można skonfigurować tę bibliotekę do używania języka polskiego:
54 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.locale('pl') }}
{% endblock %}
Dzięki wszystkim technikom omówionym w tym rozdziale powinieneś już być w stanie zbudować
nowoczesne i przyjazne strony internetowe dla swojej aplikacji. Następny rozdział będzie dotyczyć
aspektu szablonów, które do tej pory nie zostały jeszcze omówione, takie jak sposoby interakcji z użyt-
kownikiem za pomocą formularzy internetowych.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
56 Rozdział 3. Szablony
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 4.
Formularze internetowe
Konfiguracja
W przeciwieństwie do większości innych rozszerzeń Flask-WTF nie musi być inicjowane na poziomie
aplikacji, ale oczekuje, że aplikacja będzie miała skonfigurowany tajny klucz (ang. secret key). Tajny
klucz jest ciągiem znaków zawierającym dowolną losową i unikatową treść, która jest używana jako
klucz szyfrujący lub podpisujący. Taki klucz jest wykorzystywany na kilka sposobów w celu podniesie-
nia poziomu bezpieczeństwa aplikacji. Flask używa tego klucza do ochrony zawartości sesji użytkow-
nika przed niewłaściwym użyciem. W każdej nowo budowanej aplikacji powinno się przygotowywać
inny tajny klucz, jednocześnie upewniając się, że nie zostanie on nikomu ujawniony. Na listingu 4.1
pokazuję, jak można skonfigurować tajny klucz w aplikacji Flaska.
57
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 4.1. hello.py: Konfiguracja rozszerzenia Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = 'trudny do odgadnięcia ciąg znaków'
Słownik app.config jest miejscem używanym przez Flaska do przechowywania zmiennych konfi-
guracyjnych ogólnego przeznaczenia, wykorzystywanych przez rozszerzenia lub przez samą aplikację.
Wartości konfiguracyjne można dodać do obiektu app.config przy użyciu standardowej składni
słownika. Ten obiekt konfiguracji zawiera również metody importowania wartości konfiguracji
z plików lub ze środowiska. Praktyczniejszy sposób zarządzania wartościami konfiguracji dla większej
aplikacji zostanie omówiony w rozdziale 7.
Rozszerzenie Flask-WTF wymaga skonfigurowania w aplikacji tajnego klucza, ponieważ ten klucz
jest częścią mechanizmu używanego przez to rozszerzenie do zabezpieczania wszystkich formularzy
przed atakami typu CSRF (ang. cross-site request forgery). Atak CSRF ma miejsce, gdy złośliwa witryna
wysyła żądania do serwera aplikacji, na którym użytkownik jest aktualnie zalogowany. Flask-WTF
generuje tokeny bezpieczeństwa dla wszystkich formularzy i przechowuje je w sesji użytkownika,
chronionej za pomocą podpisu kryptograficznego generowanego przy użyciu tajnego klucza.
Klasy formularzy
Gdy korzystamy z rozszerzenia Flask-WTF, każdy formularz internetowy jest reprezentowany na
serwerze przez klasę, która dziedziczy po klasie FlaskForm. Taka klasa definiuje listę pól w formularzu,
z których każde jest reprezentowane przez osobny obiekt. Do każdego obiektu pola można dołą-
czyć jeden lub więcej walidatorów (ang. validators). Walidator jest funkcją sprawdzającą poprawność
danych przesłanych przez użytkownika.
Na listingu 4.2 przedstawiam prosty formularz z polem tekstowym i przyciskiem wysyłania.
class NameForm(FlaskForm):
name = StringField('Jak masz na imię?', validators=[DataRequired()])
submit = SubmitField('Wyślij')
Pola w formularzu są zdefiniowane jako zmienne klasy, a każdej takiej zmiennej przypisany jest obiekt
odpowiedni dla typu pola. W tym przykładzie formularz NameForm ma pole tekstowe o nazwie name
i przycisk przesyłania o nazwie submit. Klasa StringField reprezentuje element HTML <input>
z atrybutem type="text". Z kolei klasa SubmitField reprezentuje element HTML <input> z atrybutem
type="Submit". Pierwszym argumentem konstruktorów pól jest tekst etykiety, który będzie używany
podczas renderowania formularza w języku HTML.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Opcjonalny argument validators, umieszczony w wywołaniu konstruktora klasy StringField,
definiuje listę walidatorów, które zostaną zastosowane do skontrolowania danych przesłanych
przez użytkownika, jeszcze przed ich przyjęciem do aplikacji. Walidator DataRequired() ma tutaj
zapewniać, że pole nie zostanie przesłane bez zawartości.
Bazowa klasa FlaskForm jest zdefiniowana przez rozszerzenie Flask-WTF, jest więc
importowana z pakietu flask_wtf. Jednak klasy pól oraz walidatory są importowane
bezpośrednio z pakietu WTForms.
Lista standardowych pól HTML obsługiwanych przez pakiet WTForms znajduje się w tabeli 4.1.
Tabela 4.1. Standardowe pola HTML WTForms
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 4.2. Walidatory WTForms
Walidator Opis
DataRequired Sprawdza, czy po konwersji typu pole zawiera dane.
Email Sprawdza poprawność adresu e-mail.
EqualTo Porównuje wartości dwóch pól; jest to przydatne, gdy żądasz od użytkownika dwukrotnego
wprowadzenia hasła w celu potwierdzenia.
InputRequired Sprawdza, czy pole zawiera dane przed konwersją typu.
IPAddress Sprawdza poprawność adresu sieciowego IPv4.
Length Sprawdza długość wprowadzonego ciągu znaków.
MacAddress Sprawdza poprawność adresu MAC.
NumberRange Sprawdza, czy wprowadzona wartość liczbowa mieści się w podanym zakresie.
Optional Pozwala na przesłanie pola bez wartości, pomijając dodatkowe walidatory.
Regexp Sprawdza poprawność danych wejściowych względem wyrażenia regularnego.
URL Sprawdza poprawność adresu URL.
UUID Sprawdza identyfikator UUID.
AnyOf Sprawdza, czy dane z pola są jedną z pozycji z listy możliwych wartości.
NoneOf Sprawdza, czy dane z pola nie są żadną z pozycji na liście możliwych wartości.
Zauważ, proszę, że oprócz pól name i submit formularz zawiera także element form.hidden_tag().
Ten element definiuje dodatkowe, ukryte pole formularza używane przez Flask-WTF do zaimple-
mentowania ochrony przed atakami CSRF.
Oczywiście przygotowany w ten sposób formularz jest wyjątkowo prymitywny. Wszystkie argumenty
słów kluczowych umieszczone w wywołaniach funkcji renderujących pola są konwertowane na atry-
buty HTML danego pola. Na przykład możesz dodać do pola atrybuty id lub class, a następnie
zdefiniować dla nich style CSS:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
Mimo to wysiłek niezbędny do wyrenderowania formularza w ten sposób i poprawienia jego wyglądu
(nawet przy użyciu atrybutów HTML) jest znaczący. Jeśli to tylko możliwe, lepiej jest wykorzystać
zestaw stylów formularzy z pakietu Bootstrap. Rozszerzenie Flask-Bootstrap udostępnia funkcję
pomocniczą wysokiego poziomu, która renderuje cały formularz Flask-WTF przy użyciu wstępnie
zdefiniowanych stylów Bootstrapa. I to wszystko za pomocą jednego wywołania. Wykorzystując
pakiet Flask-Bootstrap, poprzedni formularz można renderować tak:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
Dyrektywa import działa w taki sam sposób jak zwykłe skrypty w języku Python. Pozwala to na im-
portowanie różnych elementów szablonów i ich użycie w niezależnych szablonach. Zaimportowany
plik bootstrap/wtf.html definiuje funkcje pomocnicze, które wyświetlają formularze Flask-WTF
przy użyciu Bootstrapa. Funkcja wtf.quick_form() przyjmuje w argumencie obiekt formularza
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Flask-WTF i renderuje go za pomocą domyślnych stylów Bootstrapa. Kompletny szablon z pliku
hello.py przedstawiam na listingu 4.3.
{% block page_content %}
<div class="page-header">
<h1>Witaj, {% if name %}{{ name }}{% else %}nieznajomy{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
Obszar treści szablonu ma teraz dwie sekcje. Pierwsza z tych sekcji to nagłówek strony z powitaniem.
Tutaj stosowany jest szablon warunkowy. Warunki w Jinja2 mają format {% if warunek %}...{%
else %}...{% endif %}. Jeśli warunek ma wartość True, wówczas do renderowanego szablonu doda-
wany jest kod znajdujący się między dyrektywami if i else. Jeśli warunek ma wartość False, to
zamiast tego renderowany jest kod znajdujący się pomiędzy dyrektywami else i endif. Ma to na celu
wypisanie przywitania Witaj, {{ name }}!, kiedy zdefiniowana jest zmienna szablonu name, a w prze-
ciwnym razie wypisanie ciągu znaków Witaj, nieznajomy!. Druga część treści szablonu renderuje
formularz NameForm za pomocą wywołania funkcji wtf.quick_form().
Listing 4.4. hello.py: Obsługa formularza z wykorzystaniem metod żądań GET i POST
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)
Argument methods dodany do dekoratora app.route mówi Flaskowi, aby w mapie adresów URL
zarejestrował tę funkcję widoku jako funkcję obsługi żądań GET i POST. Jeśli nie dodalibyśmy argu-
mentu methods, to funkcja widoku zostałaby zarejestrowana tylko do obsługi żądań typu GET.
Dodanie metody POST do tej listy jest konieczne, ponieważ obsługa danych przesyłanych z formularzy
jest znacznie wygodniejsza w przypadku żądań POST. Oczywiście możliwe jest przesłanie formula-
rza w ramach żądania typu GET, ale ze względu na to, że żądania tego typu nie mają treści, dane są do-
łączane do adresu URL jako zapytanie i stają się widoczne w pasku adresu przeglądarki. Z tego i jeszcze
kilku innych powodów przesyłanie formularzy prawie zawsze odbywa się jako żądanie POST.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Lokalna zmienna name służy do przechowywania nazwy otrzymanej z formularza. Jeżeli nazwa
formularza nie jest znana, wówczas zmienna jest inicjowana wartością None. Funkcja widoku tworzy
instancję pokazanej już wcześniej klasy NameForm, która będzie reprezentować formularz. Dostęp-
na w formularzu metoda validate_on_submit() zwraca wartość True po przesłaniu formularza i po
zaakceptowaniu danych przez wszystkie walidatory pola. We wszystkich innych przypadkach
funkcja validate_on_submit() będzie zwracała wartość False. Wartość zwracana przez tę metodę
pozwala skutecznie ustalić, czy formularz ma zostać jedynie zrenderowany, czy też musi być również
obsłużony.
Gdy użytkownik pierwszy raz przejdzie do aplikacji, serwer otrzyma żądanie GET bez danych for-
mularza, więc funkcja validate_on_submit() zwróci wartość False. Treść instrukcji if zostanie
pominięta, a żądanie zostanie obsłużone przez renderowanie szablonu. W tym celu szablon pobierze
obiekt formularza i przypisze zmiennej name wartość None. Użytkownicy zobaczą w przeglądarce
pusty formularz.
Po przesłaniu formularza przez użytkownika serwer otrzymuje żądanie POST z danymi. Wywołanie
metody validate_on_submit() spowoduje wywołanie walidatora DataRequired() dołączonego do
pola name. Jeśli pole nie jest puste, to metoda sprawdzająca poprawność danych je zaakceptuje i funkcja
validate_on_submit() zwróci wartość True. Od teraz tekst wprowadzony przez użytkownika jest
dostępny jako atrybut pola data. W ramach instrukcji if tekst z formularza jest przypisywany do
zmiennej lokalnej name, a pole formularza jest czyszczone przez przypisanie atrybutowi data pustego
ciągu znaków dzięki czemu pole jest puste, gdy formularz jest ponownie renderowany na stronie.
Wywołanie metody render_template() w ostatnim wierszu renderuje szablon, ale tym razem ar-
gument name zawiera już tekst z formularza, dzięki czemu powitanie zostanie spersonalizowane.
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić teraz pole-
cenie git checkout 4a, aby pobrać tę wersję aplikacji.
Na rysunku 4.1 można zobaczyć, jak będzie wyglądał formularz w oknie przeglądarki, gdy użytkownik
po raz pierwszy wejdzie na stronę. Gdy użytkownik poda swoje imię, aplikacja odpowie sperso-
nalizowanym powitaniem. Formularz nadal będzie się pojawiać pod spodem, więc, jeśli to tylko
będzie konieczne, użytkownik będzie mógł go przesłać wiele razy z wpisanymi różnymi imionami.
Natomiast na rysunku 4.2 pokazano wygląd formularza po przesłaniu danych.
Jeśli użytkownik prześle formularz bez podania swojego imienia, walidator DataRequired() wyłapie
ten błąd, co pokazano na rysunku 4.3. Zwróć, proszę, teraz uwagę, ile funkcji jest udostępnianych
automatycznie. To świetny przykład możliwości, jakie dają nam dobrze zaprojektowane rozszerzenia,
takie jak Flask-WTF i Flask-Bootstrap.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 4.1. Formularz internetowy Flask-WTF
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 4.3. Wygląd formularza po błędzie zgłoszonym przez walidator
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ze względy na to, że obsługa żądania POST jest zakończona za pomocą przekierowania, aplikacja
będzie musiała przechowywać ten tekst podany przez użytkownika, aby po przekierowaniu można
było go użyć do zbudowania faktycznej odpowiedzi.
Aplikacje mogą „zapamiętywać” dane przechodzące z jednego żądania do drugiego, przechowując
je w sesji użytkownika (ang. user session) — prywatnej pamięci dostępnej dla każdego podłączo-
nego klienta. O sesji użytkownika wspominałem już w rozdziale 2., wymieniając ją jako jedną ze
zmiennych powiązanych z kontekstem żądania. Taka zmienna nazywa się session i można z niej
korzystać jak ze standardowego słownika w Pythonie.
Na listingu 4.5 można zobaczyć nową wersję funkcji widoku index(), która implementuje przekiero-
wania i sesje użytkownika.
W poprzedniej wersji aplikacji zmienna lokalna name była używana do przechowywania nazwy wpro-
wadzonej przez użytkownika w formularzu. Zmienna ta jest teraz umieszczana w sesji użytkownika
jako session['name'], dzięki czemu jest zapamiętywana poza granice jednego żądania.
Obsługa żądań przesyłanych z poprawnymi danymi pochodzącymi z formularza kończy się teraz
wywołaniem funkcji pomocniczej redirect(), która generuje odpowiedź przekierowania HTTP.
Funkcja redirect() przyjmuje jako argument adres URL, na który ma nastąpić przekierowanie.
W tym przypadku używamy podstawowego adresu naszej aplikacji, więc odpowiedź mogła zostać
napisana bardziej zwięźle jako redirect('/'). Zamiast tego użyto funkcji generatora adresów
URL url_for(), omawianej już wcześniej w rozdziale 3.
Pierwszym i jedynym wymaganym argumentem funkcji url_for() jest nazwa punktu końcowego
(ang. endpoint), czyli wewnętrzna nazwa każdej trasy. Domyślnie punktem końcowym trasy jest
nazwa dołączonej do niej funkcji widoku. W tym przykładzie funkcją widoku, która obsługuje
podstawowy adres URL, jest funkcja index(), więc funkcji url_for() przekazujemy w argumencie
nazwę index.
Ostatnia zmiana dotyczy funkcji render_template(), która teraz uzyskuje argument name bezpośred-
nio z sesji przy użyciu wywołania session.get('name'). Podobnie jak w przypadku zwykłych słowni-
ków użycie metody get() w celu pobrania danych związanych z podanym kluczem słownika pozwala
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
uniknąć powstania wyjątku w razie podania nieznanego klucza. Dla brakujących kluczy metoda get()
zwraca wartość domyślną None.
W wersji aplikacji możesz zauważyć, że odświeżenie strony w przeglądarce zawsze powoduje jej ocze-
kiwane zachowanie.
Wyświetlanie komunikatów
Czasami, po zakończeniu obsługi żądania, warto przekazać użytkownikowi informacje o stanie apli-
kacji. Może to być komunikat potwierdzający, ostrzeżenie lub błąd. Typowym przykładem jest
sytuacja, w której przesyłamy formularz logowania do witryny internetowej zawierający błędne
dane. W takim przypadku serwer ponownie wyświetla formularz logowania uzupełniony komuni-
katem, który informuje nas, że nazwa użytkownika lub hasło są nieprawidłowe.
Flask udostępnia nam taką możliwość jako jeden ze swoich elementów podstawowych. W kodzie
z listingu 4.6 pokazano, jak można w tym celu użyć funkcji flash().
W tym przykładzie za każdym razem, gdy z formularza przesyłane jest wprowadzone imię, jest ono
porównywane z imieniem zapisanym w sesji użytkownika. W sesji zapisane jest imię, które zostało
tam umieszczone podczas poprzedniego przesłania tego samego formularza. Jeśli te dwa imiona
są różne, to wywoływana jest funkcja flash() z komunikatem wyświetlanym w następnej odpowiedzi
wysyłanej do klienta.
W celu wyświetlenia komunikatu nie wystarczy samo wywołanie funkcji flash(). Szablony używane
przez aplikację muszą jeszcze te komunikaty renderować. Najlepszym miejscem do renderowania
takich wiadomości jest szablon bazowy, ponieważ umożliwi to wyświetlanie takich wiadomości
na wszystkich stronach. Flask udostępnia szablonom funkcję get_flashed_messages(), zajmującą
się pobieraniem wiadomości i ich renderowaniem, tak jak pokazano to na listingu 4.7.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 4.7. templates/base.html: Renderowanie komunikatów
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
W tym przykładzie wiadomości są renderowane przy użyciu stylów CSS dla alertów Bootstrapa prze-
znaczonych do tworzenia komunikatów ostrzegawczych (jeden z nich pokazano na rysunku 4.4).
Używana jest tutaj pętla, ponieważ w kolejce może się znajdować wiele komunikatów do wyświetlenia,
po jednym dla każdego wywołania funkcji flash() w cyklu poprzedniego żądania. Wiadomości
pobrane przez metodę get_flashed_messages() nie zostaną zwrócone przy następnym wywołaniu
tej funkcji, więc wyskakujące komunikaty pojawiają się tylko raz, a następnie są usuwane.
Wyświetlanie komunikatów 67
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
68 Rozdział 4. Formularze internetowe
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 5.
Bazy danych
Baza danych (ang. database) przechowuje dane aplikacji w zorganizowany sposób. Następnie aplika-
cja wysyła zapytania w celu pobrania określonych, potrzebnych w danym momencie części danych.
W aplikacjach internetowych najczęściej stosowanymi bazami danych są te oparte na modelu relacyj-
nym, zwane również bazami danych SQL, co odnosi się do używanego przez nie języka zapytań
strukturalnych — Structured Query Language. Jednak w ostatnich latach popularną alternatywą
stały się bazy danych zorientowane na dokumenty (ang. document-oriented) i bazy danych typu
klucz-wartość (ang. key-value), nieformalnie znane jako bazy danych NoSQL.
69
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 5.1. Przykład relacyjnej bazy danych
Tabela users zawiera listę użytkowników, z których każdy ma swój własny unikatowy numer id. Oprócz
klucza głównego id tabela roles ma kolumnę name, a tabela users ma kolumny username i password.
Kolumna role_id w tabeli użytkowników to klucz obcy. Linia łącząca kolumny roles.id
i users.role_id reprezentuje relację pomiędzy dwiema tabelami. Symbole dołączone do linii na
każdym końcu opisują liczność (ang. cardinality) relacji. Po stronie roles.id linia ma tylko „jeden
koniec” — jedną relację, a po stronie users.role_id ma „wiele końców”, co stanowi reprezentację
wielu relacji. Schemat ten przedstawia relację jeden do wielu (ang. one-to-many), wskazując, że każdy
wiersz z tabeli roles może być powiązany z wieloma wierszami z tabeli users.
Jak widać w tym przykładzie, relacyjne bazy danych skutecznie przechowują dane i unikają powie-
lania. Zmiana nazwy roli użytkownika w tej bazie danych jest prosta, ponieważ nazwy ról zapisa-
ne są w jednym miejscu. Natychmiast po zmianie nazwy w tabeli roles tę aktualizację zobaczą
wszyscy ci użytkownicy, którzy mają pole role_id odwołujące się do tego zmienionej roli.
Z drugiej strony podział danych na wiele tabel może być tu komplikacją. Utworzenie listy użytkowni-
ków z ich rolami stanowi niewielki problem, gdyż dane użytkowników i ich ról muszą zostać odczytane
z dwóch tabel i złączone ze sobą. Dopiero potem będzie można wyświetlić listę. Relacyjne bazy
danych zapewniają w razie konieczności obsługę wykonywania operacji złączenia między tabelami.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 5.2. Przykład bazy danych NoSQL
Baza danych o takiej strukturze ma jawnie zapisaną nazwę roli każdego użytkownika. Zmiana nazwy
roli może wówczas okazać się kosztowną operacją, która będzie wymagać aktualizacji dużej liczby
dokumentów.
Mimo to bazy danych NoSQL nie są aż takie złe. Duplikowanie danych pozwala na szybszą obsługę
zapytań. Wyświetlanie listy użytkowników i ich ról jest proste, ponieważ nie wymaga to tworzenia
żadnych złączeń.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Przy wyborze frameworka bazy danych należy ocenić szereg różnych czynników:
Łatwość użycia
Porównując bezpośrednio same bazy danych z ich warstwami abstrakcji, stwierdzimy, że druga
grupa wyraźnie wygrywa. Warstwy abstrakcji, nazywane są również odwzorowaniem obiekt
– relacja (ang. ORM — object-relational mapper) lub odwzorowaniem obiekt – dokument
(ang. ODM — object-document mapper). Zapewniają one przezroczystą konwersję obiektowych
operacji wysokiego poziomu na niskopoziomowe instrukcje bazy danych.
Wydajność
Konwersje, które muszą wykonywać ORM i ODM, aby dokonać translacji z domeny obiek-
towej na domenę bazy danych, związane są z dodatkowym nakładem pracy. W większości
przypadków utrata wydajności jest minimalna, ale nie zawsze możemy sobie na nią pozwolić.
Zasadniczo wzrost produktywności uzyskany dzięki ORM i ODM znacznie przewyższa minimalny
spadek wydajności aplikacji, więc nie jest to argument przemawiający za wyeliminowaniem
ORM i ODM. Dobrym rozwiązaniem jest wybranie warstwy abstrakcji danych, która opcjo-
nalnie umożliwiałaby bezpośredni dostęp do samej bazy danych. Ta możliwość przydaje się
w sytuacji, gdy pewne operacje muszą zostać zoptymalizowane i zrealizowane za pomocą instruk-
cji właściwych dla samej bazy danych.
Przenośność
Należy rozważyć wybór bazy danych spośród dostępnych na używanych przez Ciebie platformach
programistycznych i produkcyjnych. Na przykład jeśli planujesz hostować aplikację na plat-
formie chmurowej, musisz dowiedzieć się, jakie warianty bazy danych oferuje ta usługa.
Kolejny aspekt związany z przenośnością dotyczy mechanizmów ORM i ODM. Chociaż niektóre
z tych frameworków zapewniają warstwę abstrakcji dla jednego rodzaju bazy danych, inne
tworzą jeszcze wyższy poziom abstrakcji, oferując współpracę z różnymi rodzajami baz danych.
Są one wszystkie dostępne poprzez ten sam interfejs obiektowy. Najlepszym przykładem jest
SQL-Alchemy ORM, który obsługuje długą listę różnych relacyjnych baz danych, w tym popularne
MySQL, Postgres i SQLite.
Integracja z Flaskiem
Nie musisz koniecznie wybierać środowiska, które będzie miało gotowe mechanizmy integracji
z Flaskiem, ale taki wybór pozwoli Ci uniknąć samodzielnego pisania kodu takiej integracji.
Integracja z Flaskiem może znacznie uprościć konfigurację i normalną pracę, dlatego zaleca się
używanie pakietu specjalnie zaprojektowanego jako rozszerzenie frameworka Flask.
Na potrzeby przykładów zawartych w tej książce, biorąc pod uwagę wszystkie powyższe rozważania,
wybrałem pakiet Flask-SQLAlchemy (http://pythonhosted.org/Flask-SQLAlchemy/), będący rozszerze-
niem Flaska umożliwiającym współpracę ze SQLAlchemy (http://www.sqlalchemy.org/).
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Zarządzanie bazą danych za pomocą Flask-SQLAlchemy
Flask-SQLAlchemy to rozszerzenie, które upraszcza korzystanie z frameworka SQLAlchemy w aplika-
cjach Flask. SQLAlchemy to potężny framework relacyjnych baz danych, który obsługuje kilka
rodzajów baz danych. Udostępnia zarówno mechanizmy ORM wysokiego poziomu, jak i niskopo-
ziomowy dostęp do bazy danych za pomocą instrukcji SQL.
Podobnie jak większość innych rozszerzeń, Flask-SQLAlchemy jest instalowany za pomocą na-
rzędzia pip:
(venv) $ pip install flask-sqlalchemy
We Flask-SQLAlchemy baza danych jest podawana jest jako adres URL. W tabeli 5.1 przedstawiam
format adresów URL trzech najpopularniejszych baz danych.
W tych adresach URL nazwa_hosta (ang. hostname) odnosi się do serwera obsługującego usługę
bazy danych, którym może być host lokalny (ang. localhost) lub zdalny serwer. Serwery baz danych
mogą obsługiwać kilka różnych baz, dlatego baza_danych wskazuje nazwę bazy danych, której
chcemy używać. W przypadku baz danych wymagających uwierzytelnienia nazwa_użytkownika i hasło
są danymi uwierzytelniającymi użytkownika.
Bazy danych SQLite nie mają serwera, więc elementy nazwa_hosta, nazwa_użytkownika
i hasło są pomijane i zastępowane nazwą pliku z bazą danych znajdującego się na dysku.
Adres URL bazy danych używanej przez aplikację musi być zapisany jako klucz SQLALCHEMY_
DATABASE_URI w obiekcie konfiguracyjnym Flaska. Dokumentacja pakietu Flask-SQLAlchemy sugeruje
również przypisanie kluczowi SQLALCHEMY_TRACK_MODIFICATIONS wartości False, aby zmniejszyć
zużycie pamięci, chyba że potrzebujesz sygnałów o zmianie stanu obiektów. Informacje na temat in-
nych opcji konfiguracji można znaleźć w dokumentacji pakietu Flask-SQLAlchemy. Na listingu 5.1
pokazano, jak zainicjować i skonfigurować prostą bazę danych SQLite.
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obiekt db utworzony jako instancja klasy SQLAlchemy reprezentuje bazę danych i zapewnia dostęp
do wszystkich funkcji Flask-SQLAlchemy.
Definicja modelu
Termin model jest używany w odniesieniu do trwałych encji używanych przez aplikację. W kon-
tekście ORM model jest zazwyczaj klasą Pythona z atrybutami odpowiadającymi kolumnom
przypisanej mu tabeli bazy danych.
Instancja bazy danych z pakietu Flask-SQLAlchemy udostępnia klasę bazową dla modeli, a także
zestaw klas pomocniczych i funkcji, które są używane do definiowania struktury modeli. Tabele roles
i users z rysunku 5.1 można zdefiniować jako modele Role i User, tak jak pokazano to na listingu 5.2.
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
Zdefiniowana w klasie zmienna __tablename__ określa nazwę tabeli w bazie danych. Flask-
SQLAlchemy przypisuje tabelom domyślną nazwę, jeśli nazwa nie zostanie podana w zmiennej
__tablename__. Niestety te domyślne nazwy nie są zgodne z popularną konwencją używania liczby
mnogiej dla nazw tabel, więc najlepiej nazwać tabele jawnie. Pozostałe zmienne klas są atrybutami
modelu, zdefiniowanymi jako instancje klasy db.Column.
Pierwszym argumentem podanym konstruktorowi db.Column jest typ kolumny bazy danych i atrybut
modelu. Tabela 5.2 zawiera listę niektórych dostępnych typów kolumn wraz z typami Pythona
użytymi w modelu.
Pozostałe argumenty dla konstruktora db.Column określają opcje konfiguracji dla każdego atrybutu.
W tabeli 5.3 przedstawiam niektóre z dostępnych opcji.
Chociaż nie jest to absolutnie konieczne, oba modele zawierają metodę __repr__(), wypisującą
informacje o obiekcie, która może być używana do celów debugowania i testowania.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 5.2. Najpopularniejsze typy kolumn SQLAlchemy
Relacje
Relacyjne bazy danych ustanawiają połączenia między wierszami w różnych tabelach za pomocą
relacji. Schemat relacji pokazany na rysunku 5.1 wyraża prostą relację między użytkownikami a ich
rolami. W tym przypadku jest to relacja typu jeden do wielu, ponieważ jedna rola może być przypi-
sana do wielu użytkowników, ale każdy użytkownik może mieć tylko jedną rolę.
Na listingu 5.3 pokazuję, jak w modelu klas można zapisać relację typu jeden do wielu (pokazaną
już wcześniej na rysunku 5.1).
Relacje 75
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 5.3. hello.py: Relacje w modelach baz danych
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
Jak widać na rysunku 5.1, relacja łączy dwa wiersze za pomocą klucza obcego. Kolumna role_id
dodana do modelu User jest definiowana jako klucz obcy, który ustanawia relację. Argument
'roles.id' w wywołaniu funkcji db.ForeignKey() określa, że podana kolumna powinna przechowy-
wać jedynie wartości z kolumny id w tabeli roles.
Atrybut users dodany do modelu Role reprezentuje obiektowy widok relacji widziany z „jednej”
strony. W przypadku instancji klasy Role atrybut users zwróci listę użytkowników powiązanych z tą
rolą (tj. stronę „wiele”). Pierwszy argument funkcji db.relationship() wskazuje, który z modeli
znajduje się po drugiej stronie relacji. Klasa modelu może zostać podana jako ciąg znaków, o ile zo-
stanie zdefiniowana w dalszej części modułu.
Argument backref w wywołaniu funkcji db.relationship() definiuje odwrotny kierunek relacji
poprzez dodanie atrybutu role do modelu User. Tego atrybutu można użyć zamiast klucza obcego
role_id w dowolnej instancji klasy User, aby w ten sposób uzyskać dostęp do modelu Role jako
obiektu.
W większości przypadków funkcja db.relationship() może samodzielnie zlokalizować klucz obcy
relacji, ale czasami nie może określić, której kolumny użyć jako klucza obcego. Na przykład jeżeli
model User miałby co najmniej dwie kolumny zdefiniowane jako klucze obce modelu Role, wówczas
SQLAlchemy nie wiedziałby, której kolumny użyć. Ilekroć konfiguracja klucza obcego jest niejedno-
znaczna, należy podać funkcji db.relationship() dodatkowe argumenty. W tabeli 5.4 przedsta-
wiam niektóre typowe opcje konfiguracji, których można użyć do definiowania relacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie
git checkout 5a, aby pobrać tę wersję aplikacji.
Oprócz relacji jeden do wielu istnieją jeszcze inne typy relacji. Relację jeden do jednego można zapisać
tak samo jak relację jeden do wielu (co opisałem już wcześniej), ale w definicji db.relationship()
należy przypisać wartość False do opcji uselist. Dzięki temu strona „wiele” staje się stroną „jeden”.
Jeśli tabele są odwrócone, to relację wiele do jednego można wyrazić również jako relację jeden do
wielu. Można też użyć klucza obcego i definicji db.relationship() po stronie „wiele”. Najbardziej
złożony typ relacji, wiele do wielu, wymaga dodatkowej tabeli zwanej tabelą asocjacji (ang. association
table) lub tabelą połączeń (ang. junction table). O relacjach typu wiele do wielu dowiesz się więcej
w rozdziale 12.
Tworzenie tabel
Pierwszą rzeczą do zrobienia jest poinstruowanie Flask-SQLAlchemy, aby utworzył bazę danych
na podstawie klas modeli. Funkcja db.create_all() wyszukuje wszystkie podklasy klasy db.Model
i na ich podstawie tworzy w bazie danych odpowiednie tabele:
(venv) $ flask shell
>>> from hello import db
>>> db.create_all()
Jeśli teraz sprawdzisz katalog aplikacji, zobaczysz nowy plik o nazwie data.sqlite. Ta nazwa została
nadana bazie danych SQLite podczas jej konfiguracji. Funkcja db.create_all() nie utworzy po-
nownie ani nie zaktualizuje tabeli bazy danych, jeśli ta już istnieje w bazie danych. Jest to pewną
niedogodnością w sytuacji, gdy modele są modyfikowane, a zmiany należy zastosować w istniejącej już
bazie danych. Rozwiązanie siłowe, stosowane do aktualizacji istniejących już tabel bazy danych do
nowego schematu, polega na wstępnym usunięciu starych tabel za pomocą instrukcji:
>>> db.drop_all()
>>> db.create_all()
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Wstawianie wierszy
Poniższy przykład tworzy kilka ról i użytkowników:
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='jan', role=admin_role)
>>> user_susan = User(username='zuza', role=user_role)
>>> user_david = User(username='dawid', role=user_role)
Konstruktory modeli przyjmują wartości początkowe atrybutów modelu jako argumenty słów klu-
czowych. Zauważ, że można użyć atrybutu role, nawet jeśli nie będzie to rzeczywista kolumna bazy
danych, ale wysokopoziomowa reprezentacja relacji jeden do wielu. Nie przypisuje się wartości
atrybutowi id nowo tworzonych obiektów: w wielu przypadkach kluczami głównymi zarządza
sama baza danych. Jak na razie obiekty istnieją tylko po stronie Pythona, ale nie zostały jeszcze
zapisane w bazie danych. Z tego powodu atrybutom id nie zostały jeszcze przypisane wartości:
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
Wszystkie zmiany wprowadzane w bazie danych są zarządzane przez sesję (ang. session) bazy danych,
którą Flask-SQLAlchemy udostępnia w ramach zmiennej db.session. Aby przygotować obiekty
do zapisania w bazie danych, należy je dodać do sesji:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
Aby zapisać obiekty w bazie danych, sesja musi zostać zatwierdzona przez wywołanie metody commit():
>>> db.session.commit()
Po zatwierdzeniu danych sprawdź ponownie atrybuty id, aby zobaczyć, czy mają teraz wartość:
>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Sesja bazy danych w zmiennej db.session nie ma nic wspólnego z obiektem session
frameworka Flask, który omówiliśmy już w rozdziale 4. Sesje bazy danych są również
nazywane transakcjami (ang. transactions).
Sesje bazy danych są niezwykle przydatne w utrzymywaniu spójności bazy danych. Operacja zatwier-
dzenia atomowo zapisuje wszystkie obiekty, które zostały dodane do sesji. Jeśli podczas zapisywa-
nia sesji wystąpi błąd, to cała sesja zostanie odrzucona. Jeśli zawsze zatwierdzasz powiązane ze sobą
zmiany w ramach jednej sesji, masz gwarancję uniknięcia niespójności bazy danych wynikających
z wykonywania częściowych aktualizacji.
Sesję bazy danych można również cofnąć. Jeśli wywołana zostanie funkcja db.session.
rollback(), wszystkie obiekty dodane do sesji bazy danych zostaną przywrócone
do stanu, jaki miały w bazie danych.
Modyfikowanie wierszy
Metodę add() sesji bazy danych można również wykorzystać do aktualizowania modeli. W tej samej
sesji powłoki poniższy przykład zmienia nazwę roli z Admin na Administrator:
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
Usuwanie wierszy
Sesja bazy danych ma również metodę delete(). Poniższy przykład usuwa z bazy danych rolę Moderator:
>>> db.session.delete(mod_role)
>>> db.session.commit()
Należy pamiętać, że operacje usunięcia, tak jak i operacje wstawiania i aktualizacji, są wykonywane
tylko po zatwierdzeniu sesji bazy danych.
Zapytanie o wiersze
Flask-SQLAlchemy w każdej klasie modelu udostępnia obiekt zapytania (query). Najbardziej podsta-
wowe zapytanie dla modelu jest uruchamiane za pomocą metody all(), która zwraca całą zawartość
tabeli powiązanej z modelem:
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>]
>>> User.query.all()
[<User 'jan'>, <User 'zuza'>, <User 'dawid'>]
Obiekt zapytania można skonfigurować tak, aby przeprowadzał bardziej szczegółowe wyszukiwania
w bazie danych za pomocą filtrów. W poniższym przykładzie odnajdowani są wszyscy użytkownicy,
którym przypisano rolę User:
>>> User.query.filter_by(role=user_role).all()
[<User 'zuza'>, <User 'dawid'>]
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Możliwe jest również przejrzenie kodu zapytania SQL generowanego przez SQLAlchemy dla danego
zapytania. W tym celu należy użyć konwersji obiektu zapytania na ciąg znaków:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id \nFROM users \nWHERE :param_1 = users.role_id'
Jeśli wyjdziesz z sesji powłoki, obiekty utworzone w poprzednim przykładzie przestaną istnieć jako
obiekty Pythona, ale nadal będą istnieć jako wiersze w odpowiednich tabelach bazy danych. Jeśli
następnie rozpoczniesz nową sesję powłoki, musisz ponownie utworzyć obiekty Pythona na podstawie
istniejących już wierszy bazy danych. Poniższy przykład przedstawia zapytanie, które ładuje rolę
użytkownika o nazwie User:
>>> user_role = Role.query.filter_by(name='User').first()
Zwróć uwagę na to, że w tym przypadku zapytanie zostało wysłane za pomocą metody first(), a nie
metody all(). Podczas gdy metoda all() zwraca wszystkie wyniki zapytania w formie listy, to
metoda first() zwraca tylko pierwszy wynik lub wartość None, jeśli nie ma żadnych wyników.
Jest to zatem wygodny sposób tworzenia zapytań, o których wiadomo, że mogą zwrócić co najwyżej
jeden wynik.
Takie filtry jak filter_by() są wywoływane w obiekcie zapytania i zwracają nowe, poprawione
zapytanie. Dzięki temu można po kolei wywoływać wiele filtrów, dopóki zapytanie nie zostanie
skonfigurowane zgodnie z wymaganiami.
W tabeli 5.5 przedstawiam niektóre z popularniejszych filtrów dostępnych dla zapytań, natomiast
pełną ich listę możesz znaleźć w dokumentacji SQLAlchemy (http://docs.sqlalchemy.org).
Opcja Opis
filter() Zwraca nowe zapytanie, które dodaje kolejny filtr do pierwotnego zapytania.
filter_by() Zwraca nowe zapytanie, które dodaje dodatkowy filtr równości do pierwotnego zapytania.
limit() Zwraca nowe zapytanie, które ogranicza liczbę wyników pierwotnego zapytania do podanej liczby.
offset() Zwraca nowe zapytanie, które wprowadza przesunięcie do listy wyników pierwotnego zapytania.
order_by() Zwraca nowe zapytanie, które sortuje wyniki pierwotnego zapytania według podanych kryteriów.
group_by() Zwraca nowe zapytanie grupujące wyniki pierwotnego zapytania zgodnie z podanymi kryteriami.
Po zastosowaniu w zapytaniu żądanych filtrów wywołanie metody all() spowoduje jego wykonane
i zwróci wyniki zapytania w postaci listy. Istnieją także inne sposoby uruchomienia zapytania oprócz
wykorzystania metody all(). W tabeli 5.6 przedstawiam inne metody wykonywania zapytań.
Relacje działają podobnie do zapytań. Poniższy przykład wysyła zapytania do relacji jeden do wielu
między rolami a użytkownikami, odpytując ją z obu stron:
>>> users = user_role.users
>>> users
[<User 'zuza'>, <User 'dawid'>]
>>> users[0].role
<Role 'User'>
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 5.6. Najpopularniejsze moduły wykonujące zapytania SQLAlchemy
Opcja Opis
all() Zwraca wszystkie wyniki zapytania jako listę.
first() Zwraca pierwszy wynik zapytania lub None, jeśli nie ma żadnych wyników.
first_or_404() Zwraca pierwszy wynik zapytania, a jeśli nie ma żadnych wyników, to przerywa żądanie i jako odpowiedź
wysyła błąd 404.
get() Zwraca wiersz pasujący do podanego klucza głównego lub None, jeśli nie znaleziono pasującego wiersza.
get_or_404() Zwraca wiersz pasujący do podanego klucza głównego, a jeśli klucz nie zostanie znaleziony, przerywa
żądanie i jako odpowiedź wysyła błąd 404.
count() Zwraca liczbę wyników zapytania.
paginate() Zwraca obiekt Pagination, który zawiera określony zakres wyników.
Zapytanie user_role.users ma tutaj mały problem. Niejawne zapytanie, które jest uruchamiane
po wywołaniu wyrażenia user_role.users, wywołuje funkcję all()zwracającą listę użytkowników.
Ze względu na to, że obiekt zapytania jest ukryty, nie można go dopracować za pomocą dodatko-
wych filtrów. W tym konkretnym przykładzie użyteczne może okazać się żądanie zwrócenia listy
użytkowników w kolejności alfabetycznej. W kodzie z listingu 5.4 konfiguracja relacji jest modyfiko-
wana za pomocą argumentu lazy='dynamic'. W ten sposób decydujemy, że nasze zapytanie nie będzie
wykonywane automatycznie.
Po skonfigurowaniu relacji w ten sposób wyrażenie user_role.users zwróci zapytanie, które nie zo-
stało jeszcze wykonane, więc można dodać do niego filtry:
>>> user_role.users.order_by(User.username).all()
[<User 'dawid'>, <User 'zuza'>]
>>> user_role.users.count()
2
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
db.session.add(user)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False))
W tej zmodyfikowanej wersji aplikacji za każdym razem, gdy przesyłane jest imię, aplikacja
sprawdza je w bazie danych za pomocą filtra filter_by(). Zmienna known jest zapisywana do sesji
użytkownika, dzięki czemu po przekierowaniu informacje mogą zostać przesłane do szablonu, gdzie
będą wykorzystywane do dostosowania powitania. Pamiętaj jednak, że aplikacja będzie działała
poprawnie pod warunkiem, że tabele bazy danych zostaną utworzone w powłoce Pythona, jak poka-
zano to już wcześniej.
Nowa wersja powiązanego szablonu jest pokazana na listingu 5.6. Ten szablon wykorzystuje argument
known, aby dodać do powitania drugi wiersz, który będzie wyglądał inaczej w zależności od tego,
czy użytkownik już istnieje w bazie danych, czy jest zupełnie nowy.
{% block page_content %}
<div class="page-header">
<h1>Witaj, {% if name %}{{ name }}{% else %}nieznajomy{% endif %}!</h1>
{% if not known %}
<p>Miło mi cię poznać!</p>
{% else %}
<p>Miło cię znowu widzieć!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Aby dodać obiekty do listy importu, należy utworzyć procesor kontekstu powłoki (ang. shell context
processor) i zarejestrować go za pomocą dekoratora app.shell_context_processor. Zostało to pokazane
w kodzie znajdującym się na listingu 5.7.
Funkcja procesora kontekstu powłoki zwraca słownik zawierający instancję bazy danych oraz modele.
Polecenie flask shell automatycznie importuje te elementy do powłoki, dodając je do obiektu
app, który jest importowany domyślnie:
$ flask shell
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'hello.User'>
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Kod z listingu 5.8 pokazuje, jak inicjowane jest to rozszerzenie.
W celu udostępnienia instrukcji migracji bazy danych Flask-Migrate dodaje instrukcję flask db z kil-
koma podinstrukcjami. Podczas pracy nad nowym projektem możesz wprowadzić obsługę migracji
baz danych za pomocą podinstrukcji init:
(venv) $ flask db init
Creating directory /home/flask/flasky/migrations...done
Creating directory /home/flask/flasky/migrations/versions...done
Generating /home/flask/flasky/migrations/alembic.ini...done
Generating /home/flask/flasky/migrations/env.py...done
Generating /home/flask/flasky/migrations/env.pyc...done
Generating /home/flask/flasky/migrations/README...done
Generating /home/flask/flasky/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/flask/flasky/migrations/alembic.ini' before proceeding.
To polecenie tworzy katalog migrations, w którym będą przechowywane wszystkie skrypty migracji.
Jeśli śledzisz nasz przykładowy projekt za pomocą instrukcji git checkout, to nie musisz samo-
dzielnie wykonywać tego kroku, ponieważ repozytorium migracji jest już zawarte w repozytorium
GitHuba.
Pliki repozytorium migracji bazy danych należy zawsze dodawać do kontroli wersji
wraz z resztą aplikacji.
Aby wprowadzić zmiany w schemacie bazy danych za pomocą Flask-Migrate, należy postępować
zgodnie z poniższą procedurą:
1. Wprowadź niezbędne zmiany w klasach modeli.
2. Utwórz skrypt automatycznej migracji za pomocą instrukcji flask db migrate.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Automatyczne migracje nie zawsze są dokładne i mogą pomijać pewne niejedno-
znaczne szczegóły. Na przykład jeśli nazwa kolumny zostanie zmieniona, automatycz-
nie wygenerowana migracja może stwierdzać, że kolumna została usunięta i doda-
no nową kolumnę o innej nazwie. Pozostawienie migracji w takiej postaci spowoduje
utratę danych w tej kolumnie! Z tego powodu automatycznie generowane skrypty
migracji powinny być zawsze przeglądane i ręcznie poprawiane, jeśli zawierają jakieś
nieścisłości.
3. Przejrzyj wygenerowany skrypt i dostosuj go tak, aby dokładnie odzwierciedlał zmiany dokonane
w modelach.
4. Dodaj skrypt migracji do kontroli źródeł.
5. Zastosuj migrację na bazie danych za pomocą instrukcji flask db upgrade.
Podinstrukcja flask db migrate utworzy skrypt automatycznej migracji:
(venv) $ flask db migrate -m "wstępna migration"
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'roles'
INFO [alembic.autogenerate] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index
'ix_users_username' on '['username']'
Generating /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...done
Jeśli korzystasz z instrukcji git checkout, aby stopniowo aktualizować przykładową aplikację, nie mu-
sisz wydawać polecenia migrate, ponieważ skrypty migracji są już częścią tagów repozytorium Git.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
(venv) $ flask db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration
Temat projektowania i użytkowania bazy danych jest bardzo ważny; napisano o tym całe książki.
Powinieneś zatem potraktować ten rozdział tylko jako krótki przegląd. Bardziej zaawansowane
tematy zostaną omówione w dalszych rozdziałach. Natomiast następny rozdział zostanie poświęcony
wysyłaniu wiadomości e-mail.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 6.
Wiadomości e-mail
Wiele rodzajów aplikacji musi powiadamiać użytkowników o wystąpieniu określonych zdarzeń, a ty-
pową metodą komunikacji jest metoda elektroniczna. W tym rozdziale dowiesz się, jak wysyłać
wiadomości e-mail z aplikacji Flaska.
Rozszerzenie łączy się z serwerem SMTP (ang. Simple Mail Transfer Protocol) i przesyła do niego
wiadomości e-mail, które ten ma dostarczyć do adresata. Jeśli nie podano konfiguracji, Flask-Mail
łączy się z hostem lokalnym (ang. localhost) na porcie 25 i wysyła wiadomość e-mail bez uwierzytel-
nienia. W tabeli 6.1 przedstawiłem listę kluczy konfiguracyjnych, których można użyć do skonfi-
gurowania serwera SMTP.
Podczas tworzenia aplikacji wygodniejsze może okazać się połączenie z zewnętrznym serwerem
SMTP. Na przykład w kodzie z listingu 6.1 pokazano, jak skonfigurować aplikację do wysyłania
wiadomości e-mail za pośrednictwem konta Google GMail.
87
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 6.1. hello.py: Konfiguracja konta GMail w rozszerzeniu Flask-Mail
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
>>> msg = Message('E-mail testowy', sender='twoj@przyklad.pl',
... recipients=['twoj@przyklad.pl'])
>>> msg.body = 'To jest zwykły tekst'
>>> msg.html = 'To jest treść w <b>HTML</b>'
>>> with app.app_context():
... mail.send(msg)
...
Zauważ, że funkcja send() pakietu Flask-Mail używa zmiennej current_app, więc musi zostać wywo-
łana z aktywnym kontekstem aplikacji.
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@przyklad.pl>'
Funkcja korzysta z dwóch kluczy konfiguracyjnych samej aplikacji, które definiują ciąg znaków
prefiksu tematu wiadomości oraz adres używany jako adres nadawcy. Funkcja send_email() pobiera
adres docelowy, temat wiadomości, szablon treści e-maila oraz listę argumentów słów kluczowych.
Podana nazwa szablonu nie może mieć rozszerzenia, tak aby można było użyć dwóch wersji szablonu
dla zwykłego tekstu i dla treści HTML. Argumenty słów kluczowych przekazane przez wywołujący
kod są przekazywane do funkcji render_template(), dzięki czemu mogą być używane przez szablony
do generowania treści wiadomości e-mail jako zmienne szablonu.
Funkcję widoku index() można łatwo rozbudować, tak aby wysłała wiadomość e-mail do admini-
stratora za każdym razem, gdy w formularzu pojawi się nowe imię. Tę zmianę pokazuję na listingu 6.4.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
if user is None:
user = User(username=form.name.data)
db.session.add(user)
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'Nowy użytkownik',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))
Adresat wiadomości e-mail jest podany w zmiennej środowiskowej FLASKY_ADMIN, która podczas
uruchamiania jest ładowana do zmiennej konfiguracyjnej o tej samej nazwie. Trzeba też utworzyć
dwa pliki szablonów wiadomości e-mail. Jeden dla wersji tekstowej i drugi dla wersji HTML. Te pliki
są przechowywane w podkatalogu mail, znajdującym się w katalogu templates, aby oddzielić je od
zwykłych szablonów. Szablony wiadomości e-mail oczekują, że nazwa użytkownika zostanie podana
jako argument szablonu, a zatem wywołanie funkcji send_email() musi zawierać odpowiedni argu-
ment słowa kluczowego.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 6.5. hello.py: Asynchroniczna obsługa poczty e-mail
from threading import Thread
Ta implementacja podkreśla ciekawy problem. Wiele rozszerzeń Flaska działa przy założeniu, że ist-
nieją aktywne konteksty aplikacji i/lub żądań. Jak wspominałem już wcześniej, funkcja send() z pa-
kietu Flask-Mail używa zmiennej current_app, więc wymaga aktywnego kontekstu aplikacji. Niestety
konteksty są powiązane z danym wątkiem, dlatego gdy funkcja mail.send() działa w innym wątku,
trzeba sztucznie utworzyć kontekst aplikacji za pomocą funkcji app.app_context(). Instancja app
jest przekazywana do wątku jako argument, aby można było w nim utworzyć kontekst.
Jeśli teraz uruchomisz naszą aplikację, to zauważysz, że znacznie lepiej reaguje. Pamiętaj jednak, że
w przypadku aplikacji, które wysyłają dużą liczbę wiadomości e-mail, przygotowanie zadania realizu-
jącego wysyłanie wiadomości jest bardziej odpowiednie od uruchamiania nowego wątku dla każdej
wiadomości z osobna. Na przykład wykonanie funkcji send_async_email() można wysłać do kolejki
zadań tworzonej w projekcie Celery (http://www.celeryproject.org/).
W tym rozdziale przyjrzeliśmy się funkcjom niezbędnym do działania większości aplikacji interneto-
wych. Teraz pojawia się problem związany na tym, że nasz skrypt hello.py zaczyna się rozrastać,
co może utrudniać nam pracę. W następnym rozdziale dowiemy się, jak przygotować strukturę
większej aplikacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
92 Rozdział 6. Wiadomości e-mail
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 7.
Struktura dużej aplikacji
Pomimo że umieszczenie małych aplikacji internetowych w jednym pliku skryptowym może być
bardzo wygodne, to jednak takie podejście nie sprawdza się w większej skali. Gdy aplikacja staje
się coraz bardziej złożona, praca z jednym dużym plikiem źródłowym bywa problematyczna.
W przeciwieństwie do większości innych frameworków internetowych Flask nie narzuca konkretnej
organizacji dużych projektów; sposób strukturyzowania aplikacji pozostawia w gestii programiście.
W tym rozdziale przedstawiam zatem różne sposoby organizowania dużej aplikacji w pakiety i mo-
duły. Przygotowana tu struktura będzie wykorzystywana w pozostałych przykładach z tej książki.
Struktura projektu
Na listingu 7.1 przedstawiam podstawowy układ aplikacji Flaska.
93
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ta struktura ma cztery foldery najwyższego poziomu:
Aplikacja Flaska znajduje się w pakiecie o ogólnej nazwie app.
Folder migrations, tak jak poprzednio, zawiera skrypty migracji bazy danych.
Testy jednostkowe są zapisane w pakiecie tests.
Folder venv, tak jak poprzednio, zawiera środowisko wirtualne Pythona.
Mamy tu również kilka nowych plików:
requirements.txt zawiera listę zależności pakietów, dzięki czemu można łatwo odtworzyć iden-
tyczne środowisko wirtualne na innym komputerze.
config.py przechowuje ustawienia konfiguracji.
flasky.py definiuje instancję aplikacji Flaska, a także zawiera kilka zadań, które pomagają zarządzać
aplikacją.
Aby dokładniej opisać całą tę strukturę, w następnych podrozdziałach przedstawię proces konwersji
naszej aplikacji hello.py.
Opcje konfiguracji
Aplikacje często wymagają kilku zestawów konfiguracyjnych. Najlepszym tego przykładem jest
potrzeba niezależnego używania różnych baz danych na etapie programowania, testowania i na
produkcji, a to wszystko w taki sposób, aby bazy nie przeszkadzały sobie nawzajem.
Zamiast prostej, podobnej do słownika, zmiennej konfiguracji app.config używanej w pliku hello.py
można zastosować całą hierarchię klas konfiguracyjnych. Na listingu 7.2 przedstawiam plik config.py
zawierający wszystkie ustawienia zaimportowane z pliku hello.py.
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'trudny do odgadnięcia tekst'
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@przyklad.pl>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
pass
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
Bazowa klasa Config zawiera ustawienia wspólne dla wszystkich konfiguracji, natomiast poszcze-
gólne podklasy definiują ustawienia specyficzne dla różnych konfiguracji. W razie potrzeby można
dodać dodatkowe konfiguracje.
Chcąc sprawić, aby konfiguracja była bardziej elastyczna i bezpieczna, większość ustawień można
opcjonalnie importować ze zmiennych środowiskowych. Na przykład wartość parametru SECRET_KEY,
ze względu na jego szczególny charakter, można pobierać ze środowiska, a w przypadku gdy ta
wartość nie będzie zdefiniowana w środowisku, to można tutaj zapisać jego wartość domyślną.
Zazwyczaj w trakcie tworzenia oprogramowania można używać tych ustawień z wartościami domyśl-
nymi, ale na serwerze produkcyjnym każde z nich powinno mieć właściwą wartość przygotowaną
w odpowiedniej zmiennej środowiskowej. Wszystkie opcje konfiguracji serwera e-mail również są
importowane ze zmiennych środowiskowych, przy czym, podczas programowania, wartości domyślne
wskazują na serwer GMaila.
Nigdy nie zapisuj haseł ani innych poufnych danych w pliku konfiguracyjnym,
który jest objęty systemem kontroli wersji.
Opcje konfiguracji 95
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W dolnej części skryptu konfiguracji różne konfiguracje są rejestrowane w słowniku config. Jedna
z tych konfiguracji (w tym przypadku programistyczna) jest również zarejestrowana jako domyślna.
Pakiet aplikacji
Pakiet aplikacji to miejsce, w którym znajdują się wszystkie kody aplikacji, szablony i pliki statyczne.
Tutaj będziemy go po prostu nazywać app, ale w razie potrzeby można nadać mu inną nazwę.
Katalogi templates i static są teraz częścią pakietu aplikacji, więc są przenoszone do katalogu app.
Modele bazy danych i funkcje obsługi poczty e-mail są również przenoszone do tego pakietu, każdy
w osobnym module, jako app/models.py i app/email.py.
bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
return app
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ten konstruktor importuje większość obecnie używanych rozszerzeń Flaska, ale ponieważ nie ma in-
stancji aplikacji do ich zainicjowania, tworzy je niezainicjowane, nie przekazując żadnych argumentów
do ich konstruktorów. Funkcja create_app() to funkcja wytwórcza, która jako argument przyj-
muje nazwę konfiguracji używanej dla aplikacji. Ustawienia konfiguracji zapisane w jednej z klas
zdefiniowanych w pliku config.py można zaimportować bezpośrednio do aplikacji za pomocą
metody from_object() dostępnej w obiekcie konfiguracyjnym app.config Flaska. Obiekt konfigu-
racji jest wybierany przez nazwę ze słownika config. Po utworzeniu i skonfigurowaniu aplikacji
rozszerzenia można już zainicjować. Wywołanie metody init_app() dla wcześniej utworzonych
rozszerzeń uzupełnia ich inicjalizację.
Inicjalizacja aplikacji jest teraz wykonywana w funkcji wytwórczej, przy użyciu metody from_object()
z obiektu konfiguracyjnego Flaska, który przyjmuje jako argument jedną z klas konfiguracji zdefinio-
wanych w pliku config.py. Wywoływana jest również metoda init_app() wybranej konfiguracji,
aby umożliwić przeprowadzenie bardziej złożonych procedur inicjalizacji.
Funkcja wytwórcza zwraca utworzoną instancję aplikacji, jednak należy tu pamiętać, że aplikacje
utworzone za pomocą funkcji produkcyjnej w jej bieżącym stanie będą niekompletne, ponieważ
brakuje im tras i niestandardowych procedur obsługi stron błędów. I to właśnie będzie tematem
następnego punktu.
Pakiet aplikacji 97
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Schematy powstają przez utworzenie obiektu klasy Blueprint. Konstruktor tej klasy pobiera dwa
niezbędne argumenty: nazwę projektu oraz moduł lub pakiet, w którym znajduje się schemat.
Podobnie jak w przypadku aplikacji, poprawną wartością drugiego argumentu jest zazwyczaj
zmienna __name__ Pythona.
Trasy aplikacji są przechowywane w pakiecie, w module app/main/views.py, natomiast procedury
obsługi błędów znajdują się w pliku app/main/errors.py. Importowanie tych modułów powoduje,
że trasy i procedury obsługi błędów zostaną powiązane ze schematem. Ważne jest to, aby pamiętać,
że moduły są importowane na zakończenie skryptu app/main/__init__.py, tak aby uniknąć błę-
dów wynikających z zależności cyklicznych. W tym konkretnym przykładzie problem polega na
tym, że moduły app/main/views.py i app/main/errors.py same importują obiekt main schematu,
więc operacja importu zakończy się niepowodzeniem, chyba że taka cykliczna referencja pojawi
się już po zdefiniowaniu obiektu main.
return app
@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
Różnice przy zapisywaniu procedur obsługi błędów w schemacie polegają na tym, że jeśli jest uży-
wany dekorator errorhandler, to procedura ta będzie wywoływana tylko w przypadku błędów,
które pochodzą z tras zdefiniowanych w projekcie. Aby zainstalować metody obsługi błędów dla całej
aplikacji, należy zamiast tego użyć dekoratora app_errorhandler.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Na listingu 7.7 przedstawiam trasę aplikacji zaktualizowanej, tak aby mogła się znaleźć w schemacie.
Podczas pisania funkcji widoku w projekcie trzeba brać pod uwagę dwie główne różnice. Pierwsza
różnica — podobnie jak miało to miejsce wcześniej dla procedur obsługi błędów, dekorator trasy
pochodzi ze schematu, dlatego zamiast instrukcji app.route użyto main.route. Druga różnica po-
lega na użyciu funkcji url_for(). Jak zapewne pamiętacie, pierwszym argumentem tej funkcji jest na-
zwa punktu końcowego trasy, która dla tras opartych na aplikacjach domyślnie przyjmuje nazwę funk-
cji widoku. Na przykład w aplikacji jednoskryptowej adres URL funkcji widoku index() można
uzyskać za pomocą wywołania url_for('index').
W przypadku stosowania schematów różnica polega na tym, że Flask dodaje przestrzeń nazw do
wszystkich punktów końcowych zdefiniowanych w schemacie, dzięki czemu wiele schematów może
definiować funkcje widoku z tymi samymi nazwami punktów końcowych, nie powodując przy
tym kolizji. Przestrzeń nazw to nazwa schematu (pierwszy argument konstruktora klasy Blueprint)
i jest ona oddzielona znakiem kropki od nazwy punktu końcowego. Funkcja widoku index() jest
zatem rejestrowana z nazwą punktu końcowego main.index, a jej adres URL można uzyskać za pomocą
wywołania funkcji url_for('main.index').
Funkcja url_for() obsługuje również w projekcie krótszy format punktów końcowych, w których
pominięto nazwę schematu, na przykład url_for('.index'). Dzięki tej notacji do uzupełnienia
nazwy punktu końcowego używana jest nazwa schematu właściwego dla aktualnego żądania. W efek-
cie oznacza to, że przekierowania w ramach tego samego schematu mogą korzystać z krótszej
formy zapisu, podczas gdy przekierowania między schematami muszą używać pełnej nazwy punktu
końcowego, która zawiera także nazwę schematu.
Aby dokończyć zmiany w pakiecie aplikacji, obiekty formularza również musimy umieścić wewnątrz
schematu w module app/main/forms.py.
Pakiet aplikacji 99
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Skrypt aplikacji
Moduł flasky.py w katalogu najwyższego poziomu to miejsce, w którym zdefiniowano instancję
aplikacji. Odpowiedni skrypt pokazano na listingu 7.8.
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
Skrypt zaczyna się od utworzenia aplikacji. Nazwa konfiguracji jest pobierana ze zmiennej środowi-
skowej FLASK_CONFIG, a jeśli nie jest ona zdefiniowana, to używana jest konfiguracja domyślna.
Następnie inicjowany jest pakiet Flask-Migrate oraz własny kontekst powłoki Pythona.
Głównym skryptem aplikacji nie jest już hello.py, ale flasky.py, dlatego trzeba odpowiednio zaktuali-
zować zmienną środowiskową FLASK_APP, tak aby instrukcja flask mogła zlokalizować instancję
aplikacji. Przydatne będzie też włączenie trybu debugowania Flaska za pomocą ustawienia
FLASK_DEBUG=1. W przypadku systemów Linux i macOS to wszystko odbywa się w następujący
sposób:
(venv) $ export FLASK_APP=flasky.py
(venv) $ export FLASK_DEBUG=1
Plik wymagań
Dobrą praktyką jest dołączanie do aplikacji pliku requirements.txt, który rejestruje wszystkie zależności
pakietu wraz z dokładnymi numerami wersji. Jest to ważne w przypadku, gdy środowisko wirtualne
musi zostać odtworzone na innym komputerze, na przykład takim, na którym aplikacja zostanie
wdrożona do użytku produkcyjnego. Plik ten może zostać wygenerowany automatycznie przez
instrukcję pip za pomocą poniższego polecenia:
(venv) $ pip freeze >requirements.txt
Dobrym pomysłem jest także odświeżanie tego pliku za każdym razem, gdy instalowany lub aktuali-
zowany będzie jakiś pakiet. Przykładowy plik wymagań pokazano tutaj:
alembic==0.9.3
blinker==1.4
click==6.7
dominate==2.3.1
Flask==0.12.2
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Flask-Bootstrap==3.3.7.1
Flask-Mail==0.9.1
Flask-Migrate==2.0.4
Flask-Moment==0.5.1
Flask-SQLAlchemy==2.2
Flask-WTF==0.14.2
itsdangerous==0.24
Jinja2==2.9.6
Mako==1.0.7
MarkupSafe==1.0
python-dateutil==2.6.1
python-editor==1.0.3
six==1.10.0
SQLAlchemy==1.1.11
visitor==0.1.3
Werkzeug==0.12.2
WTForms==2.1
Kiedy będziemy musieli zbudować idealną replikę środowiska wirtualnego, wystarczy utworzyć
nowe środowisko i uruchomić na nim takie polecenie:
(venv) $ pip install -r requirements.txt
Numery wersji w naszym przykładowym pliku requirements.txt prawdopodobnie nie będą już aktualne,
gdy będziesz to czytał. Jeśli chcesz, możesz spróbować użyć nowszych wersji pakietów. Jeśli wystąpią
jakiekolwiek problemy, zawsze możesz wrócić do podanych tutaj wersji, gdyż wiadomo, że są one
kompatybilne z aplikacją.
Testy jednostkowe
Nasza aplikacja jest bardzo mała, więc nie ma w niej jeszcze zbyt wiele do przetestowania, ale jako
przykład można zdefiniować dwa proste testy, takie jak te pokazane na listingu 7.9.
class BasicsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_app_exists(self):
self.assertFalse(current_app is None)
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Testy są pisane przy użyciu standardowego pakietu unittest ze standardowej biblioteki Pythona.
Metody setUp() i tearDown() klasy przypadków testowych są uruchamiane przed każdym testem
i po nim, a wszelkie metody o nazwie rozpoczynającej się od słów test_ są wykonywane jako testy.
Jeśli chcesz dowiedzieć się więcej na temat pisania testów jednostkowych za pomocą
pakietu unittest Pythona, przeczytaj oficjalną dokumentację (https://docs.python.org/
3.6/library/unittest.html).
Metoda setUp() próbuje utworzyć dla testu środowisko zbliżone do tego, w którym działa urucho-
miona aplikacja. Najpierw tworzy aplikację skonfigurowaną do testowania i aktywuje jej kontekst.
Ten krok sprawia, że testy mają dostęp do zmiennej current_app, tak jak zwykłe żądania. Następnie,
przy użyciu metody create_all() z pakietu FlaskSQLAlchemy, tworzona jest nowa baza danych
dla testów. Baza danych i kontekst aplikacji są usuwane w metodzie tearDown().
Pierwszy test sprawdza, czy istnieje instancja aplikacji, natomiast drugi test kontroluje, czy aplikacja
działa w konfiguracji testowej. Aby przekształcić katalog tests w poprawny pakiet, należy dodać
moduł tests/init.py. Może to być pusty plik, ponieważ pakiet unittest przegląda wszystkie moduły
w poszukiwaniu testów.
W celu uruchomienia testów jednostkowych można dodać specjalne polecenie do skryptu flasky.py.
Kod z listingu 7.10 pokazuje, jak dodać polecenie test.
Dekorator app.cli.command ułatwia implementację własnych poleceń. Nazwa funkcji z tym dekorato-
rem jest używana jako nazwa polecenia, a jej dokumentacja jest wyświetlana w komunikatach
pomocy. Implementacja funkcji test() uruchamia procedurę testową z pakietu unittest.
Testy jednostkowe można przeprowadzić w taki sposób:
(venv) $ flask test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
----
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Konfiguracja bazy danych
Przebudowana aplikacja korzysta z innej bazy danych niż wersja ta zbudowana w ramach jednego
skryptu.
Adres URL bazy danych jest przede wszystkim pobierany ze zmiennej środowiskowej, natomiast
alternatywną, domyślną bazą danych jest SQLite. Zmienne środowiskowe i nazwy plików baz danych
SQLite są różne dla każdej z trzech konfiguracji. Na przykład w konfiguracji programistycznej ad-
res URL jest uzyskiwany ze zmiennej środowiskowej DEV_DATABASE_URL, a jeśli nie będzie on zdefinio-
wany, to zostanie użyta baza danych SQLite o nazwie data-dev.sqlite.
Niezależnie od źródła adresu URL bazy danych w każdej nowej bazie muszą zostać utworzone
wszystkie tabele. Podczas pracy z pakietem Flask-Migrate tabele bazy danych można tworzyć lub
aktualizować do najnowszej wersji za pomocą tylko jednego polecenia:
(venv) $ flask db upgrade
Uruchamianie aplikacji
Refaktoryzacja jest teraz zakończona i można już uruchomić naszą aplikację. Upewnij się jednak,
że zaktualizowałeś zmienną środowiskową FLASK_APP, jak omówiono to w podrozdziale „Skrypt
aplikacji”, a następnie uruchom aplikację w standardowy sposób, używając polecenia:
(venv) $ flask run
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
104 Rozdział 7. Struktura dużej aplikacji
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
CZĘŚĆ II
Przykład:
Aplikacja do blogowania społecznościowego
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 8.
Uwierzytelnianie użytkownika
Większość aplikacji musi wiedzieć, kim są ich użytkownicy. Gdy użytkownicy łączą się z aplikacją,
muszą się uwierzytelniać (ang. authenticate), dzięki czemu ujawniają swoją tożsamość. Gdy aplikacja
dowie się, kim jest użytkownik, może zaoferować spersonalizowane funkcje.
Najczęściej stosowana metoda uwierzytelniania wymaga od użytkowników podania dowodu tożsamości,
którym może być adres e-mail albo nazwa użytkownika, a także znany wyłącznie im sekret, który
nazywa się hasłem. W tym rozdziale przygotujemy pełny system uwierzytelniania dla aplikacji Flasky.
Oprócz pakietów związanych z uwierzytelnianiem będą tutaj używane następujące rozszerzenia ogól-
nego przeznaczenia:
Flask-Mail: wysyłanie wiadomości e-mail związanych z uwierzytelnianiem,
Flask-Bootstrap: szablony HTML,
Flask-WTF: formularze internetowe.
Bezpieczeństwo hasła
Podczas projektowania aplikacji internetowych często pomijana jest kwestia bezpieczeństwa informa-
cji o użytkownikach, przechowywanych w bazach danych. Jeśli osoba atakująca będzie w stanie wła-
mać się na serwer i uzyskać dostęp do bazy danych o użytkownikach, ryzykujemy w ten sposób
ich bezpieczeństwo — a to ryzyko jest większe, niż myślisz. Znany jest także fakt, że większość
107
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
użytkowników używa tego samego hasła w wielu różnych witrynach, więc nawet jeśli nie przechowu-
jesz żadnych poufnych informacji, samo przechwycenie haseł przechowywanych w naszej bazie
danych może dać osobie atakującej dostęp do kont użytkowników w innych witrynach.
Klucz do bezpiecznego przechowywania haseł użytkowników w bazie danych polega na tym, że nie
przechowuje się samego hasła, ale jedynie jego skrót (ang. hash). Funkcja generująca skrót hasła
pobiera to hasło jako dane wejściowe i dodaje do nich losowy składnik, czyli sól (ang. salt), a następnie
stosuje kilka jednokierunkowych przekształceń kryptograficznych. Rezultatem tego jest nowa sekwen-
cja znaków, która nie jest podobna do oryginalnego hasła, i nie istnieje znany sposób na prze-
kształcenie jej z powrotem w oryginalne hasło. Takie skróty haseł można kontrolować zamiast
prawdziwych haseł, ponieważ funkcje skrótu są powtarzalne: przy tych samych danych wejściowych
(haśle i soli) otrzymywany wynik zawsze będzie taki sam.
Tworzenie skrótów haseł jest złożonym zadaniem, które trudno poprawnie wykonać.
Zaleca się, aby nie wdrażać własnych rozwiązań, ale polegać na dobrze już znanych
bibliotekach, które zostały sprawdzone przez społeczność. W następnym punkcie
zostaną przedstawione funkcje skrótów hasła udostępniane przez pakiet Werkzeug.
Innymi dobrymi opcjami do haszowania haseł są bcrypt (https://github.com/
pyca/bcrypt/) i Passlib (https://bitbucket.org/ecollins/passlib/wiki/Home). Jeśli chcesz
dowiedzieć się, co wiąże się z generowaniem bezpiecznych skrótów haseł, warto
przeczytać artykuł „Salted Password Hashing — Doing It Right” autorstwa Defuse
Security (http://bit.ly/saltedpass).
class User(db.Model):
# ...
password_hash = db.Column(db.String(128))
@property
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
def password(self):
raise AttributeError('Nie można odczytać atrybutu password.')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
Zwróć, proszę, uwagę, że próba uzyskania dostępu do właściwości password obiektu użytkownika
zwraca błąd typu AttributeError. Ponadto użytkownicy u i u2 mają całkowicie różne skróty haseł, na-
wet jeśli obaj używają tego samego hasła. Aby mieć pewność, że te funkcje będą działały w przyszłości,
wszystkie te wykonywane ręcznie testy należy zapisać jako testy jednostkowe, które będzie można
łatwo powtórzyć. Na listingu 8.2 pokazano nowy moduł, znajdujący się w pakiecie tests. Zapisano
w nim trzy nowe testy, które sprawdzają ostatnie zmiany w modelu User.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.2. tests/test_user_model.py: Testy haszowania hasła
import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):
def test_password_setter(self):
u = User(password = 'cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password = 'cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password = 'cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
.----------------------------------------------------------------------
Ran 6 tests in 0.379s
OK
Ten zestaw testów jednostkowych można uruchomić za każdym razem, gdy chce się potwierdzić,
że wszystko działa zgodnie z naszymi oczekiwaniami. Zastosowanie automatyzacji sprawia, że skon-
trolowanie tej funkcji jest mało pracochłonne, dlatego też testy należy powtarzać często, aby mieć
pewność, że funkcja nie ulegnie awarii podczas dalszych prac.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.3. app/auth/__init__.py: Tworzenie schematu uwierzytelnienia
from flask import Blueprint
@auth.route('/login')
def login():
return render_template('auth/login.html')
Zauważ teraz, że plik szablonu przekazany do funkcji render_template() jest przechowywany w kata-
logu auth. Ten katalog musi zostać utworzony w katalogu app/templates, ponieważ Flask oczekuje,
że ścieżki szablonów będą względne w stosunku do katalogu szablonów aplikacji. Dzięki umieszczeniu
szablonów schematów w ich własnym podkatalogu unikamy ryzyka kolizji nazw ze schematem
main lub innymi schematami, które zostaną dodane w przyszłości.
Schematy można również skonfigurować tak, aby miały własne niezależne katalogi
szablonów. W przypadku skonfigurowania wielu katalogów szablonów funkcja
render_template() najpierw będzie przeszukiwać katalog szablonów dla aplikacji,
a następnie zajmie się przeszukiwaniem katalogów szablonów zdefiniowanych
w schematach.
Schemat auth musi zostać dołączony do aplikacji w funkcji wytwórczej create_app(), tak jak poka-
zano na listingu 8.5.
return app
Argument url_prefix nie jest wymagany podczas rejestrowania schematu. Jeżeli zostanie użyty,
to wszystkie trasy zdefiniowane w schemacie będą rejestrowane z przypisanym mu przedrostkiem.
W tym przypadku będzie to przedrostek /auth. Na przykład trasa /login zostanie zarejestro-
wana jako /auth/login, a jej pełny adres URL na roboczym serwerze będzie miał wtedy postać
http://localhost:5000/auth/login.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Uwierzytelnianie użytkownika za pomocą Flask-Login
Gdy użytkownicy logują się do aplikacji, ich uwierzytelniony stan musi zostać zapisany w sesji
użytkownika, tak aby był dostepny podczas przeglądania kolejnych stron. Flask-Login to małe, ale
niezwykle przydatne rozszerzenie, które specjalizuje się w zarządzaniu tym szczególnym aspek-
tem systemu uwierzytelniania użytkownika, ale nie jest powiązane z żadnym konkretnym mecha-
nizmem uwierzytelniania.
Na początek rozszerzenie musi zostać zainstalowane w środowisku wirtualnym:
(venv) $ pip install flask-login
Właściwość/metoda Opis
is_authenticated Musi mieć wartość True, jeśli użytkownik ma prawidłowe dane logowania, a w przeciwnym razie
— wartość False.
is_active Musi mieć wartość True, jeśli użytkownik może się zalogować, a w przeciwnym razie — wartość
False. W przypadku kont wyłączonych należy użyć wartości False.
is_anonymous Musi mieć zawsze wartość False dla zwykłych użytkowników i wartość True dla specjalnych
obiektów reprezentujących anonimowych użytkowników.
get_id() Musi zwrócić unikatowy identyfikator użytkownika zapisany jako ciąg znaków Unicode.
Listing 8.6. app/models.py: Aktualizacja modelu User w celu obsługi logowania użytkownika
from flask_login import UserMixin
Zauważ, że pojawiło się też pole email. W tej aplikacji użytkownicy będą logować się przy użyciu
swoich adresów e-mail, ponieważ swoje adresy zapominają rzadziej niż nazwy użytkowników. Rozsze-
rzenie Flask-Login jest inicjowane w funkcji wytwórczej aplikacji, co pokazano w kodzie znajdują-
cym się na listingu 8.7.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.7. app/__init__.py: Inicjowanie Flask-Login
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
def create_app(config_name):
# ...
login_manager.init_app(app)
# ...
Atrybut login_view obiektu LoginManager definiuje punkt końcowy strony logowania. Gdy anoni-
mowy użytkownik spróbuje uzyskać dostęp do chronionej części strony, wtedy Flask-Login przekie-
ruje go na stronę logowania. Trasa logowania została zdefiniowana w schemacie, dlatego jej nazwa
musi być poprzedzona nazwą tego schematu.
Na koniec Flask-Login wymaga, aby aplikacja udostępniała funkcję, która ma zostać wywołana,
gdy rozszerzenie będzie musiało załadować dane użytkownika z bazy danych na podstawie jego iden-
tyfikatora. Taka funkcja została pokazana na listingu 8.8.
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Ochrona tras
Rozszerzenie Flask-Login udostępnia dekorator login_required, pozwalający zabezpieczyć trasę
tak, aby dostęp do niej mogli uzyskać tylko uwierzytelnieni użytkownicy. Poniżej przedstawiam
sposób jego użycia:
from flask_login import login_required
@app.route('/secret')
@login_required
def secret():
return 'Dostęp tylko dla zalogowanych użytkowników!'
W tym przykładzie widać, że możliwe jest „łączenie” wielu dekoratorów funkcji. Po dodaniu do
funkcji dwóch lub więcej dekoratorów każdy z nich będzie wpływał nie tylko na samą funkcję, ale
też na te dekoratory, które znajdują się poniżej niego. W tym przykładzie funkcja secret() jest chro-
niona przed nieautoryzowanymi użytkownikami za pomocą dekoratora login_required, a następnie
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
wynikowa funkcja jest rejestrowana we Flasku jako trasa. Odwrócenie tej kolejności spowoduje
niepoprawny wynik, ponieważ nasza funkcja zostanie zarejestrowana jako trasa, zanim jeszcze
otrzyma dodatkowe właściwości od dekoratora login_required.
Dzięki zastosowaniu dekoratora login_required, jeśli nieautoryzowany użytkownik będzie próbował
uzyskać dostęp do tej trasy, to rozszerzenie Flask-Login przechwyci żądanie i skieruje użytkownika
na stronę logowania.
class LoginForm(FlaskForm):
email = StringField('E-mail', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('Hasło', validators=[DataRequired()])
remember_me = BooleanField('Zapamiętaj mnie')
submit = SubmitField('Zaloguj')
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 8.1. Formularz logowania
Używana w warunku zmienna current_user jest definiowana przez rozszerzenie Flask-Login i jest
automatycznie dostępna dla szablonów i funkcji widoku. Zawiera ona dane aktualnie zalogowanego
użytkownika lub obiekt anonimowego użytkownika, jeśli użytkownik nie jest zalogowany. W obiekcie
anonimowego użytkownika właściwość is_authenticated ma wartość False, więc wyrażenie
current_user.is_authenticated stanowi wygodny sposób sprawdzania, czy aktualny użytkownik
jest zalogowany.
Logowanie użytkowników
Implementację funkcji widoku login() przedstawiam na listingu 8.11.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('Nieprawidłowa nazwa użytkownika lub hasło.')
return render_template('auth/login.html', form=form)
Funkcja widoku tworzy obiekt LoginForm i używa go tak jak prostego formularza przedstawionego już
w rozdziale 4. Gdy żądanie jest typu GET, funkcja widoku po prostu renderuje szablon, który wyświetla
sam formularz. Gdy formularz jest przesyłany w żądaniu POST, funkcja validate_on_submit()
z rozszerzenia Flask-WTF kontroluje zmienne z formularza, a następnie próbuje zalogować
użytkownika.
Aby zalogować użytkownika, funkcja zaczyna od załadowania z bazy danych użytkownika wskazanego
przy użyciu email z formularza. Jeśli użytkownik o podanym adresie e-mail już istnieje, to wywo-
ływana jest metoda valid_password(), która sprawdza poprawność hasła otrzymanego z formularza.
Jeśli jest ono prawidłowe, to wywoływana jest funkcja login_user() z rozszerzenia Flask-Login
w celu oznaczenia w sesji użytkownika jako zalogowanego. Funkcja login_user() loguje użytkownika
wraz z opcjonalnym znacznikiem „Zapamiętaj mnie”, który również jest przesyłany wraz z formula-
rzem. Wartość False tego argumentu powoduje, że sesja użytkownika wygasa, gdy okno przeglą-
darki zostanie zamknięte, więc następnym razem użytkownik będzie musiał się ponownie zalogować.
Natomiast wartość True spowoduje utworzenie długoterminowego pliku cookie w przeglądarce,
którego to Flask-Login używa do przywracania sesji użytkownika. Opcjonalnego ustawienia
REMEMBER_COOKIE_DURATION można użyć do zmiany domyślnego czasu trwania pliku cookie (jeden rok).
Musimy jeszcze zaktualizować szablon logowania, aby odpowiednio renderować formularz. Zmiany te
pokazano na listingu 8.12.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.12. app/templates/auth/login.html: Szablon formularza logowania
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
Wylogowywanie użytkowników
Implementację trasy dla operacji wylogowania pokazano na listingu 8.13.
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Zostałeś wylogowany.')
return redirect(url_for('main.index'))
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
2. Użytkownik wprowadza swoją nazwę użytkownika i hasło oraz naciska przycisk Wyślij. Po-
nownie wywoływana jest ta sama procedura obsługi żądania, ale teraz otrzymuje żądanie typu
POST, a nie GET.
a. Procedura sprawdza dane logowania przesłane z formularzem, a następnie wywołuje
funkcję login_user() z rozszerzenia Flask-Login aby zalogować użytkownika.
b. Funkcja login_user() zapisuje w sesji użytkownika jego identyfikator, traktując go jako
ciąg znaków.
c. Funkcja widoku zwraca przekierowanie na stronę główną.
3. Przeglądarka odbiera przekierowanie i wysyła żądanie strony głównej.
a. Wywoływana jest funkcja widoku strony głównej, która uruchamia renderowanie głównego
szablonu Jinja2.
b. Podczas renderowania szablonu Jinja2 po raz pierwszy pojawia się odwołanie do zmien-
nej current_user z rozszerzenia Flask-Login.
c. Do zmiennej kontekstowej current_user nie przypisano jeszcze wartości dla tego żąda-
nia, dlatego wywoływana jest wewnętrzna funkcja Flask-Login o nazwie _get_user(),
aby uzyskać dane użytkownika.
d. Funkcja _get_user() sprawdza, czy w sesji użytkownika jest zapisany identyfikator użyt-
kownika. Jeśli go nie ma, zwraca instancję klasy AnonymousUser. Jeśli jednak identyfikator
istnieje, to wywołuje funkcję zarejestrowaną przez aplikację dekoratorem user_loader,
podając w argumencie jej identyfikator.
e. Zarejestrowana w aplikacji funkcja user_loader odczytuje dane użytkownika z bazy i
zwraca je do rozszerzenia. Flask-Login przypisuje te dane do zmiennej kontekstowej
current_user dla bieżącego żądania.
f. Szablon otrzymuje dane użytkownika przypisane do zmiennej current_user.
Dekorator login_required wykorzystuje zmienną kontekstową current_user, pozwalając na urucho-
mienie powiązanej ze sobą funkcji widoku pod warunkiem, że wyrażenie current_user.
is_authenticated ma wartość True. Funkcja logout_user() po prostu usuwa identyfikator użyt-
kownika z sesji.
Testowanie
W ramach sprawdzania, czy funkcja logowania działa poprawnie, można zaktualizować stronę
główną tak, żeby witała zalogowanego użytkownika, wypisując jego imię. Część szablonu generująca
takie powitanie została pokazana na listingu 8.14.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Po raz kolejny w tym szablonie funkcja current_user.is_authenticated służy do ustalenia, czy
dany użytkownik jest zalogowany.
Ponieważ nie przygotowaliśmy żadnej funkcji rejestrującej nowego użytkownika, to musimy wykonać
tę operację z poziomu powłoki:
(venv) $ $ flask shell
>>> u = User(email='jan@przyklad.pl', username='jan', password='kot')
>>> db.session.add(u)
>>> db.session.commit()
Utworzony w ten sposób użytkownik może się już zalogować. Na rysunku 8.2 pokazuję główną stronę
aplikacji z zalogowanym użytkownikiem.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
from wtforms import ValidationError
from ..models import User
class RegistrationForm(FlaskForm):
email = StringField('E-mail', validators=[DataRequired(), Length(1, 64),
Email()])
username = StringField('Nazwa użytkownika', validators=[
DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Nazwa użytkownika może składać się wyłącznie z liter, cyfr, kropek '
'i znaków podkreślenia')])
password = PasswordField('Hasło', validators=[
DataRequired(), EqualTo('password2', message='Hasła muszą być identyczne.')])
password2 = PasswordField('Potwierdź hasło', validators=[DataRequired()])
submit = SubmitField('Zarejestruj')
Ten formularz używa walidatora Regexp z pakietu WTForms, aby upewnić się, że pole username
zaczyna się od litery i zawiera tylko litery, cyfry, znaki podkreślenia i kropki. Dwa argumenty walidatora
następujące po wyrażeniu regularnym to po kolei: flagi wyrażenia regularnego oraz komunikat
o błędzie wyświetlany w razie wystąpienia niedogodności.
Dla bezpieczeństwa użytkownik musi dwukrotnie wprowadzić swoje hasło, ale to z kolei wymaga
sprawdzenia, czy te dwa pola z hasłami mają tę samą zawartość. Takie sprawdzenie odbywa się za
pomocą innego walidatora pochodzącego z pakietu WTForms, o nazwie EqualTo. Jest on dołączony
do jednego z pól hasła, a nazwa drugiego pola jest podana jako argument.
Formularz ten zawiera również dwa niestandardowe walidatory zaimplementowane jako metody.
Jeżeli klasa formularza definiuje metodę o nazwie zawierającej przedrostek validate_, po którym na-
stępuje nazwa pola, to taka metoda będzie wywoływana jako uzupełnienie standardowo zdefiniowa-
nych walidatorów. W tym przypadku niestandardowe walidatory dla pól email i username gwarantują,
że podane wartości nie są duplikatami. Niestandardowe walidatory zgłaszają błąd walidacji, tworząc
wyjątek typu ValidationError z tekstem komunikatu o błędzie.
Szablon tworzący ten formularz znajduje się w pliku /templates/auth/register.html. Podobnie jak
szablon logowania, ten również renderuje formularz za pomocą wywołania funkcji wtf.quick_form().
Strona rejestracji użytkownika została pokazana na rysunku 8.3.
Łącze do strony rejestracji musi znajdować się na stronie logowania, tak aby użytkownicy, któ-
rzy nie mają jeszcze konta, mogli je łatwo założyć. Odpowiednia zmiana jest pokazana w kodzie
na listingu 8.16.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 8.3. Formularz rejestracji nowego użytkownika
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
flash('Możesz się już zalogować.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
Potwierdzenie konta
W przypadku niektórych rodzajów aplikacji ważne jest to, aby informacje o użytkowniku podane
podczas rejestracji były prawidłowe. Częstym wymogiem jest zapewnienie sprawnego kontaktu
z użytkownikiem za pośrednictwem podanego przez niego adresu e-mail.
Aby zweryfikować adres e-mail podany przez użytkownika podczas rejestracji, aplikacje wysyłają
na ten adres wiadomość z prośbą o potwierdzenie rejestracji. Nowe konto jest początkowo oznaczone
jako niepotwierdzone, dopóki nie zostaną wykonane instrukcje zawarte w wiadomości e-mail, co
dowodzi, że użytkownik ją otrzymał. Procedura potwierdzenia konta zwykle obejmuje kliknięcie
specjalnie przygotowanego linka URL zawierającego token potwierdzający.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'
>>> data = s.loads(token)
>>> data
{'confirm': 23}
Pakiet itsdangerous udostępnia kilka różnych generatorów tokenów. Wśród nich znajduje się klasa
TimedJSONWebSignatureSerializer generująca sygnatury sieciowe JSON Web Signatures (JWS)
z określonymi terminami ważności. Konstruktor tej klasy przyjmuje jako argument klucz szyfrowania,
którym w aplikacji Flaska może być skonfigurowany klucz SECRET_KEY.
Metoda dumps() generuje podpis kryptograficzny dla danych podanych w argumencie, a następ-
nie serializuje dane razem z podpisem do postaci wygodnego ciągu znaków tokena. Argument
expires_in określa czas ważności tokena wyrażony w sekundach.
Funkcja confirm(), oprócz skontrolowania tokena, sprawdza także, czy identyfikator z tokena
pasuje do zalogowanego użytkownika, który jest przechowywany w zmiennej current_user. Zapewnia
to, że token potwierdzający dla danego użytkownika nie będzie mógł zostać użyty do potwierdzenia
innego użytkownika.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Do modelu dodano nową kolumnę przechowującą informację o stanie potwierdzenia
każdego konta, dlatego trzeba teraz wygenerować i zastosować nową migrację bazy
danych.
Dwie nowe metody dodane do modelu User można łatwo przetestować za pomocą testów jednostko-
wych. Odpowiednie testy znajdziesz w repozytorium GitHub dla tej aplikacji.
Pamiętaj jednak, że wywołanie metody db.session.commit() musi znajdować się przed wysłaniem
wiadomości e-mail. Wynika to z faktu, że nowym użytkownikom identyfikator przypisywany jest
dopiero wtedy, gdy ich dane są zapisywane do bazy danych, a to właśnie ten identyfikator jest potrzebny
do wygenerowania tokena potwierdzającego.
Szablony wiadomości e-mail używane w schemacie uwierzytelniania zostaną umieszczone w katalogu
templates/auth/email, tak aby oddzielić je od szablonów stron HTML. Jak omówiono to już wcześniej
w rozdziale 6., do każdego e-maila potrzebne są dwa szablony — jeden dla wersji tekstowej i drugi dla
treści HTML. Na przykład na listingu 8.20 pokazano tekstową wersję szablonu wiadomości e-mail
z prośbą o potwierdzenie. Odpowiadającą jej wersję HTML można znaleźć w repozytorium GitHub.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Funkcja url_for() domyślnie generuje względne adresy URL, dlatego na przykład wywołanie
url_for('auth.confirm', token='abc') zwraca ciąg znaków '/auth/confirm/abc'. To oczywiście
nie jest adres URL w postaci, którą można wysłać w wiadomości e-mail, ponieważ jest to tylko
ścieżka, czyli jeden element pełnego adresu URL. Względne adresy URL działają poprawnie, gdy
są używane w kontekście strony internetowej, ponieważ przeglądarka konwertuje je na bezwzględne
adresy URL, dodając im nazwę hosta i numer portu z aktualnej strony. Jednak wysyłając adres
URL w wiadomości e-mail, nie mamy już takiego kontekstu. Argument _external=True jest dodawany
do wywołania funkcji url_for() w celu żądania pełnego adresu URL zawierającego schemat
(http:// lub https://), nazwę hosta i port.
Funkcja widoku zajmująca się potwierdzaniem kont została pokazana na listingu 8.21.
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('Potwierdziłeś swoje konto. Dzięki!')
else:
flash('Link potwierdzający jest nieprawidłowy lub już wygasł.')
return redirect(url_for('main.index'))
Ta trasa jest chroniona przez dekorator login_required z pakietu Flask-Login, dzięki czemu, gdy
użytkownik kliknie link znajdujący się w otrzymanym e-mailu, zostanie poproszony o zalogowanie
się, zanim przejdzie do funkcji widoku.
Funkcja najpierw sprawdza, czy konto zalogowanego użytkownika jest już potwierdzone, i jeżeli tak
jest, to przekierowuje go na stronę główną, ponieważ nie ma tu już nic do zrobienia. Zapobiega to
niepotrzebnej pracy, jeśli użytkownik przez pomyłkę kilka razy kliknie token potwierdzenia.
Dzięki temu, że faktyczne potwierdzenie tokena odbywa się całkowicie w modelu User, wystarczy
tylko wywołać metodę confirm(), a następnie wysłać komunikat zgodnie z otrzymanym wynikiem.
Gdy potwierdzenie się powiedzie, zostanie zmieniona wartość atrybutu confirmed w modelu User.
Ta zmiana zostanie też dodana do sesji, a następnie zostanie zatwierdzona sesja bazy danych.
Każda aplikacja może zdecydować, co mogą robić użytkownicy, zanim jeszcze potwierdzą swoje
konta. Jedną z możliwości jest zezwolenie niepotwierdzonym użytkownikom na zalogowanie się,
ale wyświetlenie im tylko strony z prośbą o potwierdzenie konta, bez możliwości uzyskania dostępu
do pozostałych elementów aplikacji.
Ten krok można wykonać za pomocą hooka before_request, który został już krótko opisany w roz-
dziale 2. Z punktu widzenia schematu hook before_request dotyczy wyłącznie żądań należących
do tego schematu. Oznacza to, że aby zainstalować hook schematu dla wszystkich żądań aplikacji,
należy użyć dekoratora before_app_request. Na listingu 8.22 pokazuję, jak implementowana jest
odpowiednia metoda obsługi.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.22. app/auth/views.py: Filtrowanie niepotwierdzonych kont za pomocą metody obsługi before_app_request
@auth.before_app_request
def before_request():
if current_user.is_authenticated \
and not current_user.confirmed \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
W tej trasie powtarzamy działania, które zostały wykonane w trasie rejestracji, wykorzystując zmienną
current_user, czyli traktując zalogowanego użytkownika jako użytkownika docelowego. Ta trasa
jest również chroniona za pomocą dekoratora login_required, dzięki czemu żądanie może wysłać je-
dynie uwierzytelniony użytkownik.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 8.4. Niepotwierdzona strona konta
Zarządzanie kontem
Użytkownicy posiadający konta w aplikacji mogą od czasu do czasu wprowadzać w nich jakieś zmia-
ny. Stosując techniki przedstawione w tym rozdziale, do schematu uwierzytelnienia można dodać
następujące zadania:
Aktualizacja hasła
Użytkownicy świadomi tematów związanych z bezpieczeństwem mogą okresowo zmieniać
swoje hasła. Jest to łatwa do zaimplementowania funkcja, ponieważ dopóki użytkownik jest
zalogowany, można bezpiecznie przedstawić formularz z prośbą o podanie starego hasła i nowego
hasła. W repozytorium na GitHubie ta funkcja jest dostępna jako commit 8f. W ramach tej
zmiany łącze Wyloguj na pasku nawigacyjnym zostało przekształcone w menu zawierające łącza
Zmień hasło i Wyloguj.
Zresetowanie hasła
Aby uniknąć blokowania użytkownikom dostępu do aplikacji, gdy zapomną swojego hasła,
można zaoferować im opcję jego resetowania. Aby bezpiecznie zresetować hasło, konieczne
jest użycie tokenów podobnych do tych używanych do potwierdzania kont. Gdy użytkownik
poprosi o zresetowanie hasła, na zarejestrowany adres e-mail zostanie wysłana wiadomość e-mail
z tokenem resetowania. Użytkownik może wtedy kliknąć link znajdujący się w wiadomości,
a po zweryfikowaniu tokena pojawi się formularz, w którym będzie można wprowadzić nowe
hasło. W repozytorium na GitHubie ta funkcja jest dostępna jako commit 8g.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Zmiany adresu e-mail
Użytkownicy mogą mieć możliwość zmiany zarejestrowanego adresu e-mail, ale zanim nowy
adres zostanie zaakceptowany, należy go zweryfikować za pomocą wiadomości e-mail z potwier-
dzeniem. Aby skorzystać z tej funkcji, użytkownik wprowadza nowy adres e-mail w formula-
rzu. Nowy adres musi zostać potwierdzony, a zatem wysyłany jest na niego token potwier-
dzający. Gdy serwer otrzyma ponownie ten token, może zaktualizować obiekt użytkownika.
Podczas gdy serwer czeka na otrzymanie tokena, może zapisać nowy adres e-mail w nowym
polu bazy danych, zarezerwowanym dla oczekujących adresów e-mail, lub może zapisać adres
w tokenie wraz z identyfikatorem użytkownika. W repozytorium na GitHubie ta funkcja jest
dostępna jako commit 8h.
W następnym rozdziale podsystem obsługi użytkowników zostanie rozbudowany poprzez zastosowa-
nie ról użytkownika.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 9.
Role użytkowników
Nie wszyscy użytkownicy aplikacji internetowych są sobie równi. W większości aplikacji niewielki
odsetek użytkowników ma dodatkowe uprawnienia, które pozwalają im pracować nad utrzymaniem
bezproblemowego działania aplikacji. Najlepszym przykładem są administratorzy, ale w wielu
przypadkach istnieją również zaawansowani użytkownicy średniego poziomu, tacy jak na przykład
moderatorzy treści. Zaimplementowanie takich różnic wymaga, żeby każdy użytkownik miał przypi-
saną określoną rolę (ang. role).
Istnieje kilka sposobów zaimplementowania ról w aplikacji. Właściwa metoda implementacji
w dużej mierze zależy od tego, ile ról musi istnieć w aplikacji i jak bardzo są one skomplikowane.
Na przykład prosta aplikacja może wymagać tylko dwóch ról, jednej dla zwykłych użytkowników
i jednej dla administratorów. W takim przypadku wystarczy mieć w modelu User pole is_administrator
typu Boolean. Bardziej złożona aplikacja może wymagać dodatkowych ról o różnych poziomach
uprawnień pomiędzy zwykłymi użytkownikami a administratorami. W niektórych aplikacjach
rozmowa o poszczególnych rolach może nie mieć sensu, a zamiast tego właściwym podejściem
może okazać się dawanie użytkownikom zestawu indywidualnych uprawnień.
Implementacja roli użytkownika przedstawiona w tym rozdziale jest hybrydą pomiędzy odrębnymi
rolami a uprawnieniami. Użytkownicy mają przypisaną rolę dyskretną; każda taka rola określa,
jakie akcje pozwala wykonywać użytkownikom, za pomocą listy jej uprawnień.
129
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
def __init__(self, **kwargs):
super(Role, self).__init__(**kwargs)
if self.permissions is None:
self.permissions = 0
Pole default jest jednym z nowych elementów tego modelu. Tylko jedna rola powinna mieć wartość
True w tym polu, a wszystkie pozostałe powinny mieć wartość False. Rola oznaczona jako do-
myślna zostanie przypisana nowym użytkownikom po ich zarejestrowaniu. Aplikacja będzie przeszu-
kiwać tabelę roles w celu znalezienia domyślnej roli, dlatego w konfiguracji kolumny został dodany jej
indeks, żeby przyspieszyć to wyszukiwanie.
Kolejnym dodatkiem do modelu jest pole uprawnień — permissions. Przechowuje ono liczbę całko-
witą, która w zwarty sposób definiuje listę uprawnień dla roli. Ze względu na to, że SQLAlchemy
domyślnie przypisze temu polu wartość None, dodajemy konstruktor klasy, który przypisze mu war-
tość 0, o ile nie podano wartości początkowej w argumentach konstruktora.
Lista zadań, do których potrzebne są osobne uprawnienia, jest oczywiście specyficzna dla każdej
aplikacji. W przypadku Flasky taka lista została przedstawiona w tabeli 9.1.
Korzyścią wynikającą z zastosowania wartości uprawnień będących potęgami dwójki jest to, że
pozwala łączyć uprawnienia ze sobą. Dzięki temu każda kombinacja uprawnień ma swoją unika-
tową wartość, którą można zapisać w polu permissions wybranej roli. Na przykład w przypadku
roli, która daje użytkownikom uprawnienia do śledzenia innych użytkowników i komentowania
postów, wartość uprawnień będzie wynosiła FOLLOW + COMMENT = 3. Jest to bardzo skuteczny sposób
przechowywania listy uprawnień przypisanych do każdej roli.
Kod reprezentujący zawartość tabeli 9.1 przedstawiono na listingu 9.2.
Po zdefiniowaniu stałych uprawnień można dodać kilka nowych metod do modelu Role, przeznaczo-
nych do zarządzania tymi uprawnieniami. Nowe metody zostały pokazane na listingu 9.3.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 9.3. app/models.py: Zarządzanie uprawnieniami w modelu Role
class Role(db.Model):
# ...
def reset_permissions(self):
self.permissions = 0
W tabeli 9.2 przedstawiam listę ról użytkowników obsługiwanych w naszej aplikacji wraz z kombina-
cjami uprawnień, które zostały przypisane każdej z nich.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ręczne dodawanie ról do bazy danych jest czasochłonne i podatne na błędy, dlatego lepszym
rozwiązaniem będzie przygotowanie metody w klasie Role, takiej jak pokazano to na listingu 9.4.
Ułatwi ona ponowne utworzenie prawidłowych ról i uprawnień na potrzeby testów jednostkowych
oraz, co ważniejsze, na potrzeby serwera produkcyjnego po wdrożeniu aplikacji.
Funkcja insert_roles() nie tworzy bezpośrednio nowych obiektów roli. Zamiast tego próbuje po na-
zwie znaleźć istniejące już role i je zaktualizować. Nowy obiekt roli jest tworzony tylko dla tych
ról, których nie ma jeszcze w bazie danych. Takie działanie umożliwia późniejsze aktualizowanie
listy ról, jeżeli zaszłaby potrzeba wprowadzania w niej zmian. Aby dodać nową rolę lub zmienić
uprawnienia danej roli, wystarczy zmienić słownik roles znajdujący się na początku funkcji, a następ-
nie uruchomić ją ponownie. Pamiętaj, że rola użytkownika anonimowego ("Anonymous") nie musi
być zapisywana w bazie danych, ponieważ jest to rola reprezentująca użytkowników, którzy nie są
jeszcze znani, a zatem nie znajdują się w bazie.
Muszę też zauważyć, że metoda insert_roles() jest metodą statyczną — specjalną metodą, która
nie wymaga tworzenia obiektu, ponieważ można ją wywołać bezpośrednio w klasie, na przykład
tak: Role.insert_roles(). Metody statyczne nie przyjmują argumentu self jak metody instancji.
Przypisanie ról
Gdy użytkownik rejestruje konto w aplikacji, należy mu przypisać odpowiednią dla niego rolę.
Większości użytkowników podczas rejestracji przypisana zostanie rola "User", ponieważ jest to rola
oznaczona jako domyślna. Jedyny wyjątek stanowi administrator, któremu od samego początku
należy przypisać rolę "Administrator". Użytkownik ten jest identyfikowany przez adres e-mail zapisany
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
w zmiennej konfiguracyjnej FLASKY_ADMIN, więc gdy tylko ten adres e-mail pojawi się w żądaniu
rejestracji, można mu przypisać poprawną rolę. Na listingu 9.5 pokazuję, jak robi to konstruktor
modelu User.
Konstruktor klasy User najpierw wywołuje konstruktory klas bazowych, a jeśli obiekt nadal nie ma
zdefiniowanej roli, to w zależności od podanego adresu e-mail przypisuje mu rolę administratora
lub rolę domyślną.
Weryfikacja roli
Aby uprościć implementację ról i uprawnień, do modelu User można dodać metodę pomocniczą,
która sprawdzałaby, czy użytkownicy mają określone uprawnienia w przypisanej sobie roli. Taka im-
plementacja po prostu korzysta z metod dodanych już wcześniej, tak jak pokazano to na listingu 9.6.
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
login_manager.anonymous_user = AnonymousUser
Dodana do modelu User metoda can() zwraca wartość True, jeśli sprawdzane uprawnienie jest
obecne w roli, co oznacza, że użytkownik powinien mieć możliwość wykonania żądanej operacji.
Sprawdzanie uprawnień administracyjnych jest tak częste, że jest ono realizowane jako samodzielna
metoda is_administrator().
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dla dodatkowej wygody tworzona jest także dodatkowa klasa AnonymousUser, która imple-
mentuje metody can() i is_administrator(). Umożliwi to aplikacji swobodne wywoływanie metod
current_user.can() i current_user.is_administrator() bez konieczności sprawdzania, czy użytkow-
nik jest zalogowany. Informujemy też pakiet Flask-Login, żeby używał nowego, anonimowego użyt-
kownika aplikacji, przypisując odpowiednią klasę do atrybutu login_manager.anonymous_user.
W przypadkach, w których cała funkcja widoku musi być dostępna wyłącznie dla użytkowników
z określonymi uprawnieniami, można posłużyć się własnym dekoratorem. Na listingu 9.7 przed-
stawiam implementację dwóch dekoratorów. Jeden z nich służy do ogólnej kontroli uprawnień, a drugi
przeznaczony jest do sprawdzania uprawnień administratora.
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ADMIN)(f)
@main.route('/admin')
@login_required
@admin_required
def for_admins_only():
return "Dla administratorów!"
@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def for_moderators_only():
return "Dla moderatorów komentarzy!"
Zasadniczo, jeżeli używasz wielu dekoratorów funkcji widoku, to dekorator route z Flaska powinien
być podany jako pierwszy. Pozostałe dekoratory należy podawać w kolejności, w jakiej muszą być
sprawdzane w przypadku wywołania funkcji widoku. W tych dwóch przypadkach najpierw należy
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
sprawdzić stan uwierzytelnienia użytkownika, ponieważ użytkownik musi zostać przekierowany
do strony logowania, jeśli okaże się, że nie został jeszcze uwierzytelniony.
W szablonach również zachodzi potrzeba kontrolowania uprawnień, dlatego musi być dla nich
dostępna klasa Permission ze wszystkimi swoimi stałymi. Aby uniknąć konieczności dodawania
argumentu szablonu do każdego wywołania funkcji render_template(), można użyć procesora
kontekstu (ang. context processor). Procesory kontekstu udostępniają zmienne wszystkim szablonom
na czas ich renderowania. Ta zmiana została pokazana na listingu 9.8.
Działanie nowych ról i uprawnień można sprawdzić w testach jednostkowych. Na listingu 9.9
przedstawiam dwa takie testy. W kodzie źródłowym w repozytorium na GitHubie umieściłem po jed-
nym teście dla każdej roli.
def test_user_role(self):
u = User(email='jan@przyklad.pl, password='kot')
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertFalse(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
def test_anonymous_user(self):
u = AnonymousUser()
self.assertFalse(u.can(Permission.FOLLOW))
self.assertFalse(u.can(Permission.COMMENT))
self.assertFalse(u.can(Permission.WRITE))
self.assertFalse(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
Zanim przejdziesz do następnego rozdziału, dodaj nowe role do bazy danych, używając do tego
sesji powłoki:
(venv) $ flask shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>, <Role 'Moderator'>]
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dobrym pomysłem jest również zaktualizowanie listy użytkowników, tak aby wszystkie konta
użytkowników utworzone przed wprowadzeniem ról i uprawnień miały przypisaną rolę. Aby wykonać
taką aktualizację, możesz uruchomić poniższy kod w powłoce Pythona:
(venv) $ flask shell
>>> admin_role = Role.query.filter_by(name='Administrator').first()
>>> default_role = Role.query.filter_by(default=True).first()
>>> for u in User.query.all():
... if u.role is None:
... if u.email == app.config['FLASKY_ADMIN']:
... u.role = admin_role
... else:
... u.role = default_role
...
>>> db.session.commit()
System danych użytkownika jest właściwie ukończony. W następnym rozdziale wykorzystamy go
do przygotowania stron profili użytkowników.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 10.
Profile użytkowników
W tym rozdziale zaimplementujemy profile użytkowników aplikacji Flasky. Wszystkie witryny spo-
łecznościowe udostępniają użytkownikom stronę profilu, na której prezentowane jest podsumo-
wanie działań tego użytkownika w witrynie. Użytkownicy mogą promować swoją obecność w wi-
trynie, udostępniając adres URL swojej strony profilu, dlatego ważne jest to, aby takie adresy URL
były krótkie i łatwe do zapamiętania.
Informacje o profilu
W bazie danych można przechowywać dodatkowe informacje o użytkownikach, co pozwoli
na uatrakcyjnienie ich stron profilowych. W kodzie z listingu 10.1 model użytkownika został
rozszerzony o kilka nowych pól.
Nowe pola przechowują prawdziwe imię użytkownika, jego lokalizację, samodzielnie napisaną biogra-
fię, datę rejestracji i datę ostatniej wizyty. Pole about_me ma przypisany typ db.Text(). Różnica
między db.String a db.Text polega na tym, że db.Text jest polem o zmiennej długości i dlatego
nie wymaga podawania maksymalnej długości.
Dwa znaczniki czasu otrzymują domyślną wartość bieżącego czasu. Zauważ, że w wywołaniu datetime.
utcnow brakuje końcowych nawiasów (). Użyłem takiego zapisu, ponieważ argument default
funkcji db.Column() może przyjąć funkcję jako wartość. Za każdym razem, gdy trzeba wygenerować
wartość domyślną, SQLAlchemy wywołuje funkcję, aby tę wartość wygenerować. Tak uzyskana
wartość domyślna całkowicie wystarcza do zarządzania polem member_since.
Podczas tworzenia obiektu pole last_seen jest również inicjowane aktualną wartością czasu, ale trzeba
je odświeżać za każdym razem, gdy użytkownik uzyska dostęp do witryny. Takie aktualizacje
można wykonywać za pomocą nowej metody w klasie User. Odpowiednią metodę pokazuję na li-
stingu 10.2.
137
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 10.2. app/models.py: Odświeżanie czasu ostatniej wizyty użytkownika
class User(UserMixin, db.Model):
# ...
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
db.session.commit()
Aby stale aktualizować datę ostatniej wizyty wszystkich użytkowników, należy wywoływać metodę
ping() za każdym razem, gdy odbierane jest żądanie od użytkownika. Możemy łatwo wykonać to
zadanie, ponieważ funkcja obsługująca before_app_request w schemacie auth jest uruchamiana przed
rozpoczęciem obsługi każdego żądania. Odpowiedni kod przedstawiam na listingu 10.3.
Nowa trasa została dodana w schemacie main. Strona profilu użytkownika o nazwie jan będzie
dostępna pod adresem http://localhost:5000/user/jan. Podana w adresie URL nazwa użytkownika
jest poszukiwana w bazie danych i jeśli zostanie znaleziona, to jest przekazywana do szablonu
user.html jako jego argument. Niepoprawna nazwa użytkownika wysłana na tę trasę spowoduje
zwrócenie błędu 404. Dzięki metodom pakietu Flask-SQLAlchemy przypadki udanego i błędnego
wyszukiwania można ładnie łączyć w pojedynczą instrukcję za pomocą metody first_or_404()
dostępnej w obiekcie zapytania. Szablon user.html musi przedstawić informacje o użytkowniku,
więc jako argument otrzymuje obiekt użytkownika. Początkowa wersja tego szablonu jest pokazana na
listingu 10.5.
{% block page_content %}
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
<div class="page-header">
<h1>{{ user.username }}</h1>
{% if user.name or user.location %}
<p>
{% if user.name %}{{ user.name }}{% endif %}
{% if user.location %}
From <a href="http://maps.google.com/?q={{ user.location }}">
{{ user.location }}
</a>
{% endif %}
</p>
{% endif %}
{% if current_user.is_administrator() %}
<p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
{% endif %}
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
<p>
Data rejestracji {{ moment(user.member_since).format('L') }}.
Ostatnia aktywność {{ moment(user.last_seen).fromNow() }}.
</p>
</div>
{% endblock %}
Korzystanie z warunkowego łącza do strony profilu jest konieczne, ponieważ pasek nawigacyjny
jest również renderowany dla nieuwierzytelnionych użytkowników, a wtedy łącze do profilu jest
pomijane. Na rysunku 10.1 możesz zobaczyć, jak strona profilu wygląda w przeglądarce. Widoczne
jest tam również nowe łącze do profilu umieszczone na pasku nawigacyjnym.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 10.1. Strona profilu użytkownika
Edytor profilu
Istnieją dwa różne przypadki użycia związane z edytowaniem profili użytkowników. Najbardziej
oczywiste jest to, że użytkownicy muszą mieć dostęp do strony, na której będą mogli wprowadzić
różne informacje o sobie, prezentowane później na ich stronach profilowych. Mniej oczywistym,
ale również ważnym wymogiem jest umożliwienie administratorom edycji profili innych użytkowni-
ków — i to nie tylko ich danych osobowych, ale również innych pól znajdujących się w modelu
User, do których użytkownicy nie mają bezpośredniego dostępu, takich jak rola użytkownika. Ze
względu na to, że te dwa wymagania związane z operacją edytowania profilu mocno różnią się od
siebie, utworzymy na ich potrzeby dwa osobne formularze.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ze względu na to, że wszystkie pola tego formularza są opcjonalne, walidator długości dopuszcza dłu-
gość zerową jako minimum. Definicję trasy używającej tego formularza przedstawiam na listingu 10.8.
Podobnie jak w poprzednich formularzach, dane powiązane z każdym polem formularza są dostępne
w zmiennej form.<nazwa-pola>.data. Jest to przydatne nie tylko do odczytywania wartości poda-
nych przez użytkownika, ale także do wstawiania do pól wartości początkowych, które użytkownik
może dowolnie edytować. Gdy wywołanie funkcji form.validate_on_submit() zwraca wartość
False, trzy pola formularza są inicjowane danymi pobranymi z odpowiadających im pól zmiennej
current_user. Następnie, po przesłaniu formularza, atrybuty data pól formularza zawierają zaktuali-
zowane wartości, więc są one przenoszone do pól obiektu użytkownika. Na zakończenie zaktuali-
zowany obiekt jest zapisywany z powrotem do bazy danych. Na rysunku 10.2 przedstawiam stronę
edycji profilu.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Aby ułatwić użytkownikom dostęp do tej strony, można dodać bezpośredni link na stronie profilu,
tak jak na listingu 10.9.
Instrukcja warunkowa otaczająca link spowoduje, że pojawi się on tylko w przypadku, gdy użytkownik
będzie oglądać własny profil.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
SelectField to wrapper z pakietu WTForm reprezentujący znacznik HTML <select>, który w tym
formularzu implementuje listę rozwijaną używaną do wyboru roli użytkownika. Instancja klasy
SelectField musi otrzymać elementy listy w atrybucie choices. Wartość podana do atrybutu musi
mieć postać listy krotek, przy czym każda krotka powinna składać się z dwóch wartości: identyfikatora
elementu oraz z tekstu wyświetlanego w kontrolce jako ciąg znaków. Lista choices jest definio-
wana w konstruktorze formularza, a poszczególne wartości są uzyskiwane z modelu Role za pomocą
zapytania, które alfabetycznie sortuje wszystkie role według ich nazw. Identyfikatorem każdej
krotki będzie atrybut id poszczególnych ról. Z uwagi na to, że takie identyfikatory są liczbami całko-
witymi, konstruktorowi klasy SelectField podawany jest argument coerce=int, dzięki czemu
wartości pól są przechowywane jako liczby całkowite, a nie domyślnie stosowane ciągi znaków.
Pola email i username są zbudowane w taki sam sposób jak w formularzach uwierzytelniania, nato-
miast ich walidacja wymaga już pewnej ostrożności. Warunek sprawdzania poprawności zastoso-
wany dla obu tych pól musi najpierw skontrolować, czy dokonano zmiany w polu, i tylko w przy-
padku, gdy zostanie wykryta jakakolwiek zmiana, powinien upewnić się, że nowa wartość nie powiela
nazwy innego użytkownika. Natomiast jeśli te pola nie zostały zmienione, to walidacja nie powinna
zgłaszać błędów. Zaimplementowanie tej logiki wymaga, aby konstruktor formularza przyjmował
w argumencie obiekt użytkownika i zapisywał go w zmiennej obiektu, która później będzie używana
w metodach sprawdzania poprawności.
Na listingu 10.11 została pokazana definicja trasy dla edytora profilu administratora.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Ta trasa ma w dużej mierze tę samą strukturę co prostsza dla zwykłych użytkowników, ale zawiera
dekorator admin_required, utworzony wcześniej w rozdziale 9. Dzięki zastosowaniu tego dekoratora
system automatycznie zwróci błąd 403 dla wszystkich użytkowników, którzy nie są administrato-
rami, a którzy będą próbowali skorzystać z tej trasy.
Identyfikator użytkownika jest podawany jako dynamiczny argument w adresie URL, więc można
użyć komfortowej funkcji get_or_404() z pakietu Flask-SQLAlchemy. Trzeba jednak zdawać sobie
sprawę z tego, że jeśli identyfikator będzie niepoprawny, to żądanie zwróci błąd 404. Warto przyjrzeć
się też liście wyboru związanej z rolą użytkownika. Podczas definiowania wartości początkowej
dla tego pola wartość zmiennej role_id jest przypisywana do zmiennej field.role.data. Jest to moż-
liwe, ponieważ lista krotek podawanych w atrybucie choices używa identyfikatorów numerycznych do
opisywania poszczególnych opcji. Po przesłaniu formularza identyfikator jest pobierany z atrybutu
data tego pola, a następnie jest używany w zapytaniu w celu ponownego załadowania wybranego
obiektu roli. Argument coerce=int użyty w formularzu w deklaracji pola typu SelectField gwarantuje,
że atrybut data tego pola będzie zawsze konwertowany na liczbę całkowitą.
Na stronie profilu użytkownika dodawany jest nowy przycisk, pozwalający przejść do administra-
cyjnej edycji profilu, tak jak pokazano to na listingu 10.12.
Ten przycisk jest renderowany w innym stylu Bootstrapa, tak aby zwrócić na niego uwagę. Zabezpie-
czająca go instrukcja warunkowa powoduje, że przycisk ten będzie się pojawiał na stronach profili
tylko w przypadku, gdy zalogowany użytkownik będzie miał rolę administratora.
Awatary użytkownika
Wygląd stron profilu użytkowników można poprawić, wyświetlając zdjęcia awatarów użytkowników.
W tym podrozdziale dowiesz się, jak dodawać awatary użytkowników dostarczane przez lidera usług
awatarów — Gravatar (https://gravatar.com/). Gravatar kojarzy zdjęcia awatarów z adresami e-mail.
Użytkownicy tworzą konto na portalu https://gravatar.com, a następnie przesyłają swoje zdjęcia.
Usługa ujawnia awatar użytkownika przez specjalnie spreparowany adres URL zawierający skrót MD5
adresu e-mail użytkownika, który można obliczyć w następujący sposób:
(venv) $ python
>>> import hashlib
>>> hashlib.md5('jan@przyklad.pl'.encode('utf-8')).hexdigest()
'c91c9f0f2da21e7c7580cd84e383582f'
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Adresy URL awatara są następnie generowane przez dodanie skrótu MD5 do adresu https://secure.
gravatar.com/avatar/. Na przykład w pasku adresu przeglądarki możesz wpisać https://secure.
gravatar.com/avatar/c91c9f0f2da21e7c7580cd84e383582f, aby uzyskać obraz awatara dla adresu
e-mail jan@przyklad.pl lub domyślny obraz awatara, jeśli dla tego adresu zarejestrowano jeszcze awa-
tara. Po zbudowaniu podstawowego adresu URL awatara można użyć kilku argumentów ciągu zapy-
tania do skonfigurowania właściwości obrazu awatara, tak jak opisano to w tabeli 10.1.
Na przykład dodanie zapytania ?d=identicon do adresu URL awatara przygotowanego dla adresu
jan@przyklad.pl wygeneruje innego domyślnego awatara, opartego na wzorze geometrycznym.
Wszystkie te opcje generowania adresów URL awatarów można dodać do modelu User. Implementa-
cja takiego rozwiązania została pokazana na listingu 10.13.
Adres URL awatara jest generowany z podstawowego adresu URL, skrótu MD5 adresu e-mail
użytkownika oraz z argumentów przyjmujących wartości domyślne. Należy przy tym pamiętać,
że jednym z wymagań usługi Gravatar jest to, że adres e-mail, z którego uzyskuje się skrót MD5,
musi być znormalizowany, tak aby zawierał wyłącznie małe litery alfabetu. W związku z tym w tej
metodzie znalazła się też odpowiednia konwersja. Dzięki tej implementacji łatwo jest wygenerować
adresy URL awatarów w powłoce Pythona:
(venv) $ flask shell
>>> u = User(email='john@example.com')
>>> u.gravatar()
'https://secure.gravatar.com/avatar/c91c9f0f2da21e7c7580cd84e383582f?s=100&d=identicon&r=g'
>>> u.gravatar(size=256)
'https://secure.gravatar.com/avatar/c91c9f0f2da21e7c7580cd84e383582f?s=256&d=identicon&r=g'
Metodę gravatar() można również wywoływać z szablonów Jinja2. Na listingu 10.14 pokazuję, w jaki
sposób można umieścić na stronie profilu awatar o wymiarze 256 pikseli.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 10.14. app/templates/user.html: Dodanie awatara do strony profilu
...
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
<div class="profile-header">
...
</div>
...
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Generowanie awatarów wymaga wygenerowania skrótu MD5, co jest operacją obciążającą procesor.
Jeżeli na danej stronie trzeba wygenerować dużą liczbę awatarów, wówczas prace obliczeniowe
mogą sumarycznie zająć naprawdę dużo czasu. Skrót MD5 dla adresu e-mail użytkownika pozostanie
niezmieniony, dopóki użytkownik nie zdecyduje się zmienić swojego adresu, a zatem raz wyliczoną
wartość można buforować w modelu User. Kod z listingu 10.15 pokazuje zmiany w modelu User,
służące do przechowywania skrótów MD5 w bazie danych.
Listing 10.15. app/models.py: Generowanie adresu URL Gravatar połączone z buforowaniem skrótów MD5
class User(UserMixin, db.Model):
# ...
avatar_hash = db.Column(db.String(32))
def gravatar_hash(self):
return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
Dodano tutaj nową metodę gravatar_hash(), która zajmuje się obliczaniem wartości skrótu dla
usługi Gravatar. Pozwala to uniknąć powielania logiki związanej z tym powtarzalnym zadaniem.
Podczas inicjowania modelu skrót jest zapisywany w nowej kolumnie o nazwie avatar_hash. Jeśli
użytkownik zaktualizuje swój adres e-mail, skrót zostanie ponownie obliczony. Metoda gravatar()
używa przechowywanego skrótu, o ile jest on dostępny. Jeśli nie ma jeszcze przygotowanego
skrótu, to zostanie wygenerowana jego wartość.
W następnym rozdziale zajmiemy się tworzeniem mechanizmu bloga, który będzie napędzał całą
tę aplikację.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
148 Rozdział 10. Profile użytkowników
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 11.
Posty na blogu
Ten rozdział poświęcony jest implementowaniu głównej funkcji aplikacji Flasky, która ma umoż-
liwiać użytkownikom czytanie i pisanie postów na blogu. Tutaj poznasz kilka nowych technik
ponownego wykorzystania szablonów, stronicowania długich list elementów oraz pracy z tekstem
sformatowanym.
Wpis na blogu jest opisywany przez swoją treść, znacznik czasu i relację jeden-do-wielu z modelem
User. Pole body jest zdefiniowane za pomocą typu db.Text, tak aby nie wprowadzać ograniczenia
długości tekstu.
Formularz, który zostanie wyświetlony na stronie głównej aplikacji, pozwala użytkownikom napisać
post na blogu. Formularz ten jest bardzo prosty; zawiera tylko pole tekstowe, w którym można napisać
wpis na blogu, oraz przycisk przesyłania. Definicja formularza została pokazana na listingu 11.2.
Obsługą tego formularza zajmuje się funkcja widoku index(), która przekazuje do szablonu listę
starych postów. Kod tej funkcji przedstawiam na listingu 11.3.
149
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 11.3. app/main/views.py: Trasa strony głównej z postem na blogu
@main.route('/', methods=['GET', 'POST'])
def index():
form = PostForm()
if current_user.can(Permission.WRITE_ARTICLES) and form.validate_on_submit():
post = Post(body=form.body.data,
author=current_user._get_current_object())
db.session.add(post)
db.session.commit()
return redirect(url_for('.index'))
posts = Post.query.order_by(Post.timestamp.desc()).all()
return render_template('index.html', form=form, posts=posts)
Ta funkcja widoku przekazuje do szablonu cały formularz i pełną listę postów na blogu. Lista postów
jest uporządkowana według znacznika czasu, w porządku malejącym. Formularz posta jest obsłu-
giwany w standardowy sposób — po otrzymaniu poprawnego zgłoszenia z danymi przygotowy-
wana jest nowa instancja klasy Post. Zanim pozwolimy użytkownikowi napisać nowy post, spraw-
dzamy najpierw, czy ma do tego odpowiednie uprawnienia.
Zwróć uwagę na to, że atrybut author obiektu nowego posta otrzymuje wartość zwróconą przez
wyrażenie current_user._get_current_object(). Zmienna current_user z Flask-Login, podobnie jak
wszystkie zmienne kontekstowe, jest implementowana jako obiekt proxy dla danego wątku. Obiekt ten
zachowuje się jak obiekt User, ale tak naprawdę jest tylko cienkim opakowaniem, które w środku prze-
chowuje rzeczywisty obiekt użytkownika. Baza danych potrzebuje jednak rzeczywistego obiektu
użytkownika, który można uzyskać przez wywołanie funkcji _get_current_object() w obiekcie proxy.
W szablonie index.html formularz jest renderowany poniżej tekstu powitania, a za nim pojawiają
się posty opublikowane już na blogu. Lista postów jest pierwszą próbą przygotowania osi czasu
dla wszystkich postów na blogu. Wszystkie posty zapisane w bazie danych są tutaj wypisywane w ko-
lejności chronologicznej od najnowszych do najstarszych. Zmiany w kodzie szablonu przedstawiam
na listingu 11.4.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
</a>
</div>
<div class="post-body">{{ post.body }}</div>
</li>
{% endfor %}
</ul>
...
Zauważ, że metoda User.can() używana jest tutaj do ukrywania formularza dla użytkowników,
którzy w swojej roli nie mają uprawnień WRITE. Lista postów została zaimplementowana jako nie-
uporządkowana lista HTML, a zastosowane dla niej klasy CSS zapewniają ładniejsze formatowanie.
Po lewej stronie umieszczany jest mały awatar autora, który razem z nazwą autora renderowany
jest jako link do strony jego profilu. Używane tutaj style CSS są przechowywane w pliku styles.css
znajdującym się w katalogu static. Możesz przejrzeć ten plik w repozytorium na GitHubie. Na ry-
sunku 11.1 prezentuję stronę główną z formularzem nowego posta i listą wcześniejszych postów.
Rysunek 11.1. Strona główna bloga wraz z formularzem wpisu i listą postów
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Wpisy na blogach na stronach profilu
Stronę profilu użytkownika można ulepszyć, wyświetlając na blogu listę postów jego autorstwa.
Na listingu 11.5 pokazano zmiany w funkcji widoku umożliwiające pobranie listy postów użytkownika.
Lista postów autorstwa danego użytkownika jest uzyskiwana z relacji User.posts. Taka relacja
działa jak obiekt zapytania, więc można używać na niej takich filtrów jak order_by(). Jej działanie nie
różni się od znanego już ze zwykłego obiektu zapytania.
W szablonie user.html powinna znaleźć się struktura znacznika <ul> wyświetlającego listę postów
na blogu, którą znamy już z pliku index.html. Wiemy jednak, że utrzymywanie dwóch identycznych
kopii fragmentu kodu HTML nie jest najlepszym pomysłem. W takich przypadkach bardzo przy-
daje się dyrektywa include. Fragment kodu HTML generującego listę postów można przenieść do
osobnego pliku, który może być dołączany zarówno do pliku index.html, jak i do pliku user.html.
Na listingu 11.6 pokazuję, jak taka zmiana będzie wyglądać w pliku user.html.
Na zakończenie tej reorganizacji strukturę znacznika <ul> pochodzącą z pliku index.html przenosimy
do nowego szablonu o nazwie _posts.html i zastępujemy ją dyrektywą include, taką jak pokazana
powyżej. Zauważ, że użycie znaku podkreślenia na początku nazwy szablonu _posts.html nie jest
wymagane. Jest to tylko konwencja pozwalająca rozróżnić szablony pełne i częściowe.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tworzenie fałszywych danych w postach na blogu
Aby pracować z wieloma stronami postów na blogu, konieczne jest przygotowanie testowej bazy
danych zawierającej dużą liczbę postów. Ręczne dodawanie nowych wpisów do bazy danych jest
żmudne i czasochłonne. W tym przypadku znacznie lepszym wyjściem będzie zautomatyzowanie
tego procesu. Istnieje kilka pakietów Pythona, których można użyć do wygenerowania fałszywych
informacji. Przykładem może być tu dość kompletny pakiet Faker, który można zainstalować za
pomocą narzędzia pip:
(venv) $ pip install faker
Muszę tu zaznaczyć, że pakiet Faker nie jest zależnością naszej aplikacji, ponieważ jest on potrzebny
wyłącznie podczas programowania. Aby oddzielić zależności produkcyjne od zależności progra-
mistycznych, plik requirements.txt można zastąpić podkatalogiem requirements, w którym będą
przechowywane różne zestawy zależności. W nowym podkatalogu plik dev.txt może zawierać listę
zależności niezbędnych w trakcie tworzenia oprogramowania, natomiast w pliku prod.txt możemy
umieścić listę zależności potrzebnych w środowisku produkcyjnym. Wiele z tych zależności będzie wy-
stępowało na obu listach, dlatego zostaną one umieszczone w pliku common.txt, a następnie w plikach
dev.txt i prod.txt skorzystamy z przedrostka -r, aby dołączyć do nich plik wspólnych zależności.
Na listingu 11.7 przedstawiam zawartość pliku dev.txt.
Z kolei na listingu 11.8 przedstawiam nowy moduł naszej aplikacji, który zawiera dwie funkcje
generujące fałszywych użytkowników i ich posty.
def users(count=100):
fake = Faker()
i = 0
while i < count:
u = User(email=fake.email(),
username=fake.user_name(),
password='password',
confirmed=True,
name=fake.name(),
location=fake.city(),
about_me=fake.text(),
member_since=fake.past_date())
db.session.add(u)
try:
db.session.commit()
i += 1
except IntegrityError:
db.session.rollback()
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
def posts(count=100):
fake = Faker()
user_count = User.query.count()
for i in range(count):
u = User.query.offset(randint(0, user_count - 1)).first()
p = Post(body=fake.text(),
timestamp=fake.past_date(),
author=u)
db.session.add(p)
db.session.commit()
Wartości dla atrybutów fałszywych obiektów są tworzone przez generatory losowych informacji będą-
ce częścią pakietu Faker. Mogą one generować prawdziwie wyglądające nazwiska, adresy e-mail,
teksty oraz wiele innych atrybutów.
Adresy e-mail i nazwiska użytkowników muszą być unikatowe. Skoro jednak Faker generuje je w cał-
kowicie losowy sposób, to istnieje ryzyko powstawania duplikatów. W mało prawdopodobnym
przypadku wygenerowania takiego duplikatu zatwierdzenie sesji bazy danych zgłosi wyjątek Integrity
Error. Wyjątek ten jest obsługiwany przez wycofanie sesji w celu anulowania duplikatu użytkow-
nika. Pętla będzie działać do momentu wygenerowania żądanej liczby unikatowych użytkowników.
Podczas generowania losowych postów do każdego z nich musimy przypisać losowo wybranego
użytkownika. W tym celu używam funkcji filtra zapytań — offset(). Filtr ten odrzuca wyniki zapyta-
nia w liczbie podanej jako argument. Wystarczy zatem podać tej funkcji losowo wybraną liczbę,
a następnie wywołać funkcję first(), aby w ten sposób uzyskać losowo wybranego użytkownika.
Nowe funkcje sprawiają, że utworzenie dużej liczby fałszywych użytkowników i ich postów z poziomu
powłoki Pythona jest niezwykle proste:
(venv) $ flask shell
>>> from app import fake
>>> fake.users(100)
>>> fake.posts(100)
Jeśli teraz uruchomisz aplikację, to na stronie głównej bloga zobaczysz długą listę losowych postów
wielu różnych użytkowników.
Renderowanie na stronach
Na listingu 11.9 przedstawiam zmiany wprowadzone w trasie strony głównej w celu uzyskania stroni-
cowania postów.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
return render_template('index.html', form=form, posts=posts,
pagination=pagination)
Numer strony do wyświetlenia jest uzyskiwany z sekcji zapytania zapisanej w żądaniu, która jest do-
stępna jako zmienna request.args. Gdy numer strony nie jest podany, to używana jest domyślna,
pierwsza strona. Argument type=int sprawia, że jeśli wartości pierwszego argumentu nie uda się
przekonwertować na liczbę całkowitą, to zwracana będzie wartość domyślna.
Aby załadować jedną stronę danych, trzeba zastąpić ostatnie wywołanie metody all() dla obiektu
zapytania wywołaniem metody paginate() z pakietu Flask-SQLAlchemy. Metoda paginate() w swoim
jedynym wymaganym argumencie przyjmuje numer strony do wyświetlenia. Można podać jej
jeszcze opcjonalny argument per_page, aby wskazać liczbę elementów do umieszczenia na każdej
stronie. Jeśli ten argument nie zostanie podany, to wartością domyślną będzie 20 elementów na stronę.
Kolejnemu opcjonalnemu argumentowi o nazwie error_out można przypisać wartość True (jest
to ustawienie domyślne), aby wywołać błąd 404, gdy żądana będzie strona spoza prawidłowego
zakresu. Jeśli parametr error_out ma wartość False, to strony spoza prawidłowego zakresu będą
zwracane z pustą listą elementów. Wartość argumentu per_page jest odczytywana ze specjalnej
zmiennej konfiguracyjnej o nazwie FLASKY_POSTS_PER_PAGE, którą trzeba dodać do pliku config.py.
Pozwoli to na konfigurowanie rozmiarów wyświetlanych stron.
Dzięki tym zmianom na liście postów z głównej strony bloga pojawi się ograniczona liczba elementów.
Aby zobaczyć drugą stronę z postami, trzeba w pasku adresu przeglądarki dodać do adresu URL
zapytanie ?Page=2.
Atrybut Opis
items Elementy na aktualnej stronie.
query Zapytanie źródłowe, które zostało podzielone na strony.
page Aktualny numer strony.
prev_num Numer poprzedniej strony.
next_num Numer następnej strony.
has_next Ma wartość True, jeśli następna strona jest dostępna.
has_prev Ma wartość True, jeśli poprzednia strona jest dostępna.
pages Łączna liczba stron dla zapytania.
per_page Liczba elementów na stronie.
total Łączna liczba elementów zwróconych przez zapytanie.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obiekt stronicowania udostępnia również kilka metod wymienionych w tabeli 11.2.
Metoda Opis
iter_pages(left_edge=2, Iterator, który zwraca sekwencję numerów stron do wyświetlenia w widżecie
left_current=2, stronicowania. Lista będzie miała left_edge stron na lewym końcu, left_current
stron po lewej stronie aktualnej strony, right_current stron po prawej stronie
right_current=5, aktualnej strony oraz right_edge stron na prawym końcu. Na przykład dla 50. strony
right_edge=2) spośród 100 iterator skonfigurowany z wartościami domyślnymi zwróci taką listę: 1, 2,
None, 48, 49, 50, 51, 52, 53, 54, 55, None, 99, 100. A wartość None
umieszczona na liście oznacza lukę w sekwencji stron.
prev() Obiekt stronicowania dla poprzedniej strony.
next() Obiekt stronicowania dla następnej strony.
Wykorzystując ten potężny obiekt i klasy CSS Bootstrap wspomagające stronicowanie, dość łatwo
będzie można teraz zbudować w szablonie stopkę stronicowania. Implementacja pokazana na li-
stingu 11.10 została przygotowana jako makro Jinja2, którego można używać w wielu miejscach.
Makro tworzy bootstrapowy element stronicowania, który jest listą nieuporządkowaną z odpowiednio
przygotowanymi stylami. Na liście umieszczane są następujące linki stron:
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Link „poprzednia strona”, który otrzymuje klasę CSS disabled, jeśli aktualnie wyświetlana
jest pierwsza strona.
Linki do wszystkich stron zwróconych przez iterator obiektu stronicowania iter_pages().
Strony te są renderowane jako łącza z jawnym numerem strony podanym jako argument do
funkcji url_for(). Aktualnie wyświetlana strona jest wyróżniana za pomocą klasy CSS active.
Luki w sekwencji stron są renderowane za pomocą znaku wielokropka.
Link „następna strona” zostanie wyłączony, jeśli aktualnie wyświetlana jest ostatnia strona.
Makra Jinja2 zawsze otrzymują argumenty słów kluczowych bez konieczności umieszczania **kwargs
na liście argumentów. Makro stronicowania przekazuje wszystkie otrzymane argumenty słów
kluczowych do funkcji url_for(), która generuje linki do poszczególnych stron. Tego rozwiązania
można użyć w przypadku tras, które mają elementy dynamiczne, na przykład w przypadku stron
profili.
Makro pagination_widget można dodać na dole szablonu _posts.html dołączanego do plików index.html
i user.html. Na listingu 11.11 można zobaczyć, w jaki sposób jest ono używane na stronie głównej
aplikacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie
git checkout 11d, aby pobrać tę wersję aplikacji.
Aby zamienić kontrolkę pola tekstowego ze strony głównej na edytor tekstu sformatowanego
Markdown, w polu body formularza PostForm należy umieścić obiekt klasy PageDownField, tak jak
pokazano to na listingu 11.13.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 11.13. app/main/forms.py: Formularz posta z włączoną opcją Markdown
from flask_pagedown.fields import PageDownField
class PostForm(FlaskForm):
body = PageDownField("Co ciekawego powiesz?", validators=[Required()])
submit = SubmitField('Wyślij')
Podgląd Markdown jest generowany za pomocą bibliotek PageDown, więc je również należy dodać
do szablonu. Rozszerzenie Flask-PageDown upraszcza nam to zadanie, udostępniając makro szablonu,
które dołącza wymagane pliki z CDN, jak pokazano na listingu 11.14.
Dzięki tym zmianom tekst wpisany w polu tekstowym z zastosowaniem formatowania metodą
Markdown zostanie natychmiast zrenderowany jako kod HTML w znajdującym się poniżej obszarze
podglądu. Na rysunku 11.3 możemy zobaczyć formularz wpisu na blogu już ze sformatowanym
tekstem.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obsługa tekstu sformatowanego na serwerze
Przy przesyłaniu formularza tylko tekst Markdown jest wysyłany wraz z żądaniem typu POST.
Podgląd HTML, który był wyświetlany na stronie, jest odrzucany. Wysłanie razem z formularzem
wygenerowanego podglądu HTML można uznać za zagrożenie dla bezpieczeństwa, ponieważ
atakujący może łatwo zbudować sekwencję kodu HTML, która nie będzie pasować do źródła
Markdown. Unikamy tego ryzyka, przesyłając tylko tekst źródłowy Markdown, który na serwerze
jest ponownie konwertowany na format HTML za pomocą rozszerzenia Markdown — konwertera
składni Markdown na HTML. Powstały w ten sposób kod HTML jest oczyszczany za pomocą pakietu
Bleach, aby upewnić się, że będzie w nim używana tylko ograniczona lista dozwolonych znaczni-
ków HTML.
Konwersję postów z formatu Markdown na HTML można by wykonać w szablonie posts.html, ale
nie byłoby to zbyt efektywne, ponieważ w takim rozwiązaniu posty musiałyby być konwertowane
za każdym razem, gdy są wyświetlane na stronie. Aby uniknąć powtarzania tej operacji, konwersję
można wykonać tylko raz, zaraz po przesłaniu treści nowego posta, i przechowywać w bazie danych
uzyskany kod HTML. Szablon ma bezpośredni dostęp do kodu HTML renderowanego posta, ponie-
waż zostaje on zapisany w nowym polu, specjalnie dodanym do modelu Post. Oryginalne źródło
Markdown jest również przechowywane w bazie danych na wypadek konieczności edycji treści
tego posta. Na listingu 11.15 można zobaczyć zmiany wprowadzone w modelu Post.
class Post(db.Model):
# ...
body_html = db.Column(db.Text)
# ...
@staticmethod
def on_changed_body(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
'h1', 'h2', 'h3', 'p']
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, strip=True))
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
białej liście. Ostatni krok konwersji odbywa się w funkcji linkify(), udostępnianej przez rozsze-
rzenie Bleach, która konwertuje dowolne adresy URL napisane zwykłym tekstem na odpowiednie
linki <a>. Ten ostatni krok jest konieczny, ponieważ automatyczne generowanie linków nie jest
oficjalnie zawarte w specyfikacji Markdown, a jest to bardzo wygodna funkcja. Po stronie klienta
tę funkcję realizuje opcjonalne rozszerzenie PageDown, dlatego funkcja linkify() przeprowadza tę
operację na serwerze.
Ostatnia zmiana polega na wykorzystaniu w szablonie pola post.body lub post.body_html (o ile
będzie dostępne) w sposób, jaki pokazano na listingu 11.16.
Przyrostek | safe użyty podczas renderowania treści HTML ma poinformować pakiet Jinja2, aby
nie oczyszczał elementów HTML. Jinja2 domyślnie oczyszcza zawartość wszystkich zmiennych
szablonu, traktując tę operacje jako środek bezpieczeństwa. W tym przypadku kod HTML wygenero-
wany przez Markdown został przygotowany przez serwer, tak więc można go bezpośrednio i bez-
piecznie renderować na wyświetlanej stronie.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W przypadku niektórych typów aplikacji lepszym rozwiązaniem może być tworze-
nie stałych linków korzystających z czytelnych adresów URL zamiast z identyfikatorów
numerycznych. Alternatywą dla identyfikatorów numerycznych jest przypisanie
każdemu postowi na blogu specjalnego opisu (ang. slug), który będzie unikatowym
ciągiem znaków zbudowanym na bazie tytułu lub pierwszych słów tego wpisu.
Pamiętaj, proszę, że szablon post.html otrzymuje listę z jednym elementem, który jest postem przezna-
czonym do wyświetlenia. Wysłanie listy jest kwestią wygody, ponieważ umożliwia to użycie szablonu
_posts.html, z którego korzystają też szablony index.html i user.html.
Stałe linki są dodawane u dołu każdego posta w ogólnym szablonie _posts.html, tak jak pokazano
to na listingu 11.18.
Na listingu 11.19 prezentuję nowy szablon post.html, który zajmuje się renderowaniem stron związa-
nych ze stałymi linkami.
{% block page_content %}
{% include '_posts.html' %}
{% endblock %}
Edytor postów
Ostatnią funkcją związaną z publikowanymi postami jest edytor postów, który pozwala użytkow-
nikom redagować własne posty. Edytor postów jest dostępny na osobnej stronie i również wykorzy-
stuje rozszerzenie Flask-PageDown. Oznacza to, że za polem tekstowym, w którym można edytować
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
tekst, używając składni Markdown, wyświetlany będzie podgląd wprowadzonego do tej pory tekstu.
Na listingu 11.20 prezentuję szablon edit_post.html.
{% block page_content %}
<div class="page-header">
<h1>Edytuj post</h1>
</div>
<div>
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
Kod tej funkcji widoku został przygotowany tak, żeby tylko autorowi posta pozwolić na jego edycję.
Wyjątek stanowią tu administratorzy, którzy mogą edytować posty wszystkich użytkowników. Jeśli
użytkownik spróbuje edytować wpis przygotowany przez innego użytkownika, funkcja widoku
odpowie kodem 403. Stosowana tutaj klasa formularzy internetowych PostForm jest tą samą klasą,
której używamy już na stronie głównej.
Na zakończenie naszych prac musimy jeszcze pod każdym postem umieścić link do edytora postów,
najlepiej zaraz obok stałego linka, tak jak pokazano to na listingu 11.22.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
<li class="post">
...
<div class="post-content">
...
<div class="post-footer">
...
{% if current_user == post.author %}
<a href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-primary">Edytuj</span>
</a>
{% elif current_user.is_administrator() %}
<a href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-danger">Edytuj [Admin]</span>
</a>
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ul>
Ta zmiana dodaje link Edytuj do wszystkich tych postów, które zostały przygotowane przez aktualnego
użytkownika. W przypadku administratorów link jest dodawany do wszystkich postów. Link
administratora jest też inaczej wyświetlany. Chodzi o to, żeby od razu było jasne, że jest to funkcja
administracyjna. Na rysunku 11.4 można zobaczyć wygląd linków Edytuj i Permalink w przeglą-
darce internetowej.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 12.
Obserwatorzy
165
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Zastanówmy się nad klasycznym przykładem relacji wiele-do-wielu: baza danych uczniów i zajęć,
na które będą uczęszczać. Oczywiście nie można dodać klucza obcego do klasy w tabeli uczniów
students, ponieważ jeden uczeń uczęszcza na wiele zajęć — jeden klucz obcy tu nie wystarczy.
Podobnie nie można dodać klucza obcego dla ucznia w tabeli klas classes, ponieważ w klasach
jest więcej niż tylko jeden uczeń. Obie strony potrzebują listy kluczy obcych.
Rozwiązaniem tego problemu może być dodanie do bazy danych trzeciej tabeli, zwanej tabelą
powiązań (ang. association table). Teraz relację wiele-do-wielu można rozłożyć na dwie relacje typu
jeden-do-wielu, z każdej z dwóch pierwotnych tabel, i umieścić je w tabeli powiązań. Na rysunku 12.1
można zobaczyć, w jaki sposób reprezentowana jest relacja typu wiele-do-wielu wiążąca uczniów
z ich zajęciami.
Tabela powiązań w tym przykładzie nazywa się registrations (rejestracje). Każdy wiersz w tej tabeli
reprezentuje niezależną rejestrację ucznia na zajęciach.
Obsługa zapytania dotyczącego relacji wiele-do-wielu jest procesem dwuetapowym. Aby uzyskać
listę zajęć, na które zapisał się dany uczeń, zaczynamy od relacji jeden-do-wielu między uczniami
i rejestracjami, uzyskując w ten sposób listę rejestracji dla wybranego ucznia. Następnie przez relację
jeden-do-wielu między klasami i rejestracjami przechodzimy w kierunku relacji wiele-do-jednego,
tak aby uzyskać wszystkie klasy związane z rejestracjami danego ucznia. Podobnie, aby znaleźć
wszystkich uczniów w klasie, zaczynamy od zajęć i uzyskujemy listę rejestracji, a następnie łączymy
uczniów z tymi rejestracjami.
Przechodzenie przez dwie relacje w celu uzyskania wyników zapytania wydaje się trudnym zadaniem,
ale w przypadku prostej relacji, takiej jak w poprzednim przykładzie, większość pracy wykonuje za nas
SQLAlchemy. Poniżej można zobaczyć kod reprezentujący relację wiele-do-wielu z rysunku 12.1:
registrations = db.Table('registrations',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))
)
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
classes = db.relationship('Class',
secondary=registrations,
backref=db.backref('students', lazy='dynamic'),
lazy='dynamic')
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
class Class(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
Relacja jest zdefiniowana za pomocą tego samego wywołania db.relationship(), które jest używane
w relacjach jeden-do-wielu, ale w przypadku relacji wiele-do-wielu dodatkowemu argumentowi
secondary musimy przypisać tabelę powiązań. Relację można zdefiniować w dowolnej z tych dwóch
klas, przy czym argument backref umożliwia zdefiniowanie tej samej relacji od drugiej strony.
Tabela powiązań jest definiowana jako prosta tabela, a nie model, ponieważ SQLAlchemy samodziel-
nie zajmuje się zarządzaniem tą tabelą.
W relacji classes używana jest semantyka listy, dzięki której praca z relacjami wiele-do-wielu
staje się niezwykle łatwa. Przy założeniu, że mamy ucznia s i klasę c, poniższy kod pozwoli zarejestro-
wać tego ucznia na zajęcia:
>>> s.classes.append(c)
>>> db.session.add(s)
Bardzo prosto można też przygotować zapytanie zwracające listę zajęć, na które zarejestrował się
uczeń s, albo listę uczniów zarejestrowanych na zajęcia c:
>>> s.classes.all()
>>> c.students.all()
Relacja students dostępna w modelu Class jest relacją zdefiniowaną przez argument db.backref().
Zauważ, że w tej relacji argument backref został rozszerzony o kolejny atrybut lazy='dynamic',
dzięki czemu obie strony zwracają zapytanie, które może przyjmować dodatkowe filtry.
Jeśli uczeń zdecyduje się kiedyś porzucić zajęcia c, to będziemy mogli w następujący sposób zaktuali-
zować bazę danych:
>>> s.classes.remove(c)
Relacje samoreferencyjne
Relacja wiele-do-wielu może służyć do modelowania użytkowników obserwujących innych użytkow-
ników, ale i tu pojawia się pewien problem. W przykładzie z uczniami i zajęciami istniały dwie bardzo
jasno zdefiniowane encje połączone tabelą powiązań. Gdy jednak chcemy pozwolić użytkownikom
na obserwowanie innych użytkowników, to mamy do dyspozycji wyłącznie użytkowników — nie
ma tu drugiej encji.
Relacja, w której po obu stronach znajduje się ta sama tablica, jest nazywana relacją samoreferencyjną
(ang. selfreferential). W tym przypadku po lewej stronie relacji znajdują się encje użytkowników,
których można nazwać „obserwatorami”. Po prawej stronie również znajdują się encje użytkowników,
ale tym razem są to użytkownicy „obserwowani”. Pod względem koncepcyjnym relacje samorefe-
rencyjne nie różnią się niczym od zwykłych relacji, ale troszkę trudniej je sobie wyobrazić. Na ry-
sunku 12.2 przedstawiam schemat bazy danych dla relacji samoreferencyjnej, która reprezentuje
użytkowników obserwujących innych użytkowników.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 12.2. Obserwatorzy, relacja wiele-do-wielu
W tym przypadku tabela powiązań nosi nazwę follows. Każdy wiersz w tej tabeli reprezentuje
użytkownika obserwującego innego użytkownika. Relacja jeden-do-wielu przedstawiona po lewej
stronie kojarzy użytkowników z listą wierszy w tabeli follows, co sprawia, że ci użytkownicy są obser-
watorami. Relacja jeden-do-wielu przedstawiona po prawej stronie również kojarzy użytkowników
z listą tabeli follows, ale tym razem są oni użytkownikami obserwowanymi.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Niestety SQLAlchemy nie może samodzielnie używać tabeli powiązań, ponieważ wtedy aplikacja
nie będzie miała dostępu do niestandardowych pól tej tabeli. W związku z tym relacja wiele-do-wielu
musi zostać rozłożona na dwie prostsze relacje jeden-do-wielu dla lewej i prawej strony, które dodat-
kowo muszą zostać zdefiniowane jako relacje standardowe. To wszystko zostało pokazane na li-
stingu 12.2.
Listing 12.2. app/models.py: Relacja wiele-do-wielu zaimplementowana jako dwie relacje jeden-do-wielu
class User(UserMixin, db.Model):
# ...
followed = db.relationship('Follow',
foreign_keys=[Follow.follower_id],
backref=db.backref('follower', lazy='joined'),
lazy='dynamic',
cascade='all, delete-orphan')
followers = db.relationship('Follow',
foreign_keys=[Follow.followed_id],
backref=db.backref('followed', lazy='joined'),
lazy='dynamic',
cascade='all, delete-orphan')
Tutaj relacje followed i followers zostały zdefiniowane jako niezależne relacje typu jeden-do-wielu.
Należy pamiętać, że konieczne tu będzie wyeliminowanie wszelkich niejednoznaczności między
kluczami obcymi. Można to osiągnąć za pomocą opcjonalnego argumentu foreign_keys, który
pozwala na określenie klucza obcego używanego w każdej z tych relacji. Argumenty db.backref()
użyte w tych relacjach nie opisują siebie wzajemnie, ale tworzą wsteczną referencję do modelu Follow.
Argument lazy dla referencji wstecznych otrzymał wartość joined. Ten „leniwy” tryb sprawia, że
powiązany obiekt jest ładowany natychmiast w ramach zapytania złączającego. Na przykład jeśli
użytkownik śledzi 100 innych użytkowników, to wywołanie funkcji user.followed.all() zwróci
listę 100 instancji klasy Follow, w których właściwości follower i followed będą miały już przypi-
sanych odpowiednich użytkowników. Tryb lazy='join' pozwala na takie przygotowanie danych
w ramach pojedynczego zapytania do bazy danych. Jeżeli parametr lazy miałby wartość domyślną
select, wówczas użytkownicy z pól follower i followed byliby ładowani „leniwie”, czyli przy
pierwszej próbie dostępu, a każdy z tych atrybutów wymagałby użycia osobnego zapytania. Oznacza-
łoby to, że uzyskanie pełnej listy obserwowanych użytkowników wymagałoby użycia 100 dodatkowych
zapytań do bazy danych.
W obu tych relacjach zupełnie inne wymagania będzie miał argument lazy użyty po stronie modelu
User. Te argumenty znajdują się po stronie „jeden” i zwracają dane ze strony „wiele”. W tym przypadku
używany jest tryb dynamic, dzięki czemu atrybuty relacji zwracają obiekty zapytania, a nie bezpo-
średnio obiekty danych. Umożliwia to dodanie do zapytania kolejnych filtrów, jeszcze przed jego
wykonaniem.
Argument cascade konfiguruje sposób, w jaki operacje wykonywane na obiekcie nadrzędnym będą
oddziaływać na obiekty powiązane. Przykładem opcji kaskady jest reguła mówiąca, że dodawanie
obiektu do sesji bazy danych powoduje, że wszelkie obiekty, powiązane z nim relacjami, powinny zostać
automatycznie dodane do tej samej sesji. Domyślne opcje kaskady sprawdzają się w większości sytu-
acji, ale istnieje pewien przypadek, w którym nie działają one dobrze dla relacji typu wiele-do-wielu.
Chodzi o to, że domyślne zachowanie kaskady podczas usuwania obiektów polega na przypisaniu
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
wartości zerowej do klucza obcego wszelkich obiektów, które są powiązane z obiektem usuwanym.
Natomiast w przypadku tabeli powiązań poprawnym działaniem jest usunięcie wierszy wskazujących
na usunięty właśnie rekord, ponieważ dopiero w ten sposób ostatecznie usuwane jest powiązanie.
I tak właśnie działa opcja delete-orphan.
Wartość podana atrybutowi cascade jest listą opcji rozdzielanych przecinkami. Jest
to nieco mylące, ale opcja o nazwie all oznacza wszystkie opcje kaskady oprócz
delete-orphan. Używając wartości all, delete-orphan, uzyskujemy domyślne
opcje kaskady uzupełnione o opcję usuwania osieroconych elementów.
Aplikacja musi teraz obsługiwać dwie relacje jeden-do-wielu, aby w odpowiedni sposób zaimplemen-
tować funkcjonowanie relacji wiele-do-wielu. Dobrym pomysłem będzie utworzenie w modelu
User metod pomocniczych dla wszystkich operacji związanych z tą relacją, ponieważ są to operacje,
które trzeba często powtarzać. Cztery nowe metody sterujące tą relacją pokazano na listingu 12.3.
Metoda follow()wstawia instancję klasy Follow do tabeli powiązań, która łączy użytkownika obser-
wującego z obserwowanym. Daje to aplikacji możliwość zdefiniowania wartości dodatkowego pola.
Dwóch łączonych ze sobą użytkowników zostaje przekazanych do konstruktora nowej instancji
klasy Follow, a następnie powstały obiekt dodawany jest do sesji bazy danych. Zauważ, że nie trzeba tu
przypisywać wartości do pola timestamp, ponieważ otrzymało już ono domyślną wartość, czyli aktualną
datę i godzinę. Metoda unfollow() używa relacji followed, aby zlokalizować instancję klasy Follow
łączącej aktualnego użytkownika z użytkownikiem obserwowanym, który musi zostać odłączony.
Obiekt typu Follow jest po prostu usuwany, aby w ten sposób usunąć połączenie między tymi
dwoma użytkownikami. Metody is_following() i is_followed_by() przeszukują, odpowiednio,
lewą i prawą relację jeden-do-wielu dla danego użytkownika i jeżeli znajdą użytkownika, to zwracają
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
wartość True. Obie metody przed wykonaniem zapytania sprawdzają, czy dany użytkownik otrzymał
już swój numer identyfikacyjny, aby w ten sposób uniknąć błędów, które mogą się pojawić, jeśli obiekt
użytkownika nie został jeszcze zapisany w bazie danych.
Zmiany w bazie danych potrzebne do realizacji tej funkcji zostały już zakończone. W repozytorium
kodu źródłowego na GitHubie możesz znaleźć testy jednostkowe, który kontrolują tę nową relację
w bazie danych.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 12.3. Strona profilu użytkownika obserwującego
Przedstawiona tu funkcja widoku ładuje żądanego użytkownika, po czym sprawdza, czy jest to po-
prawny użytkownik i czy nie jest już obserwowany przez zalogowanego użytkownika, a następnie
wywołuje funkcję pomocniczą follow() w modelu użytkownika, aby połączyć ze sobą obu użytkowni-
ków. Trasa /unfollow/<nazwa-użytkownika> została zaimplementowana w podobny sposób.
Trasa /followers/<nazwa-użytkownika> jest wywoływana, gdy użytkownik kliknie liczbę obserwują-
cych znajdującą się na stronie profilu innego użytkownika. Implementację tego kodu przedsta-
wiam na listingu 12.6.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 12.6. app/mai /views.py: Trasa i funkcja widoku listy obserwujących
@main.route('/followers/<username>')
def followers(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash(Nieprawidłowy użytkownik.')
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
pagination = user.followers.paginate(
page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
error_out=False)
follows = [{'user': item.follower, 'timestamp': item.timestamp}
for item in pagination.items]
return render_template('followers.html', user=user, title="Obserwujący użytkownika",
endpoint='.followers', pagination=pagination,
follows=follows)
W tym przypadku funkcja pobiera i waliduje danego użytkownika, a następnie dzieli relację followers
na strony przy użyciu tych samych technik, których nauczyliśmy się już w rozdziale 11. Ze wzglę-
du na to, że zapytanie o obserwujących zwraca instancje klasy Follow, otrzymana lista jest konwerto-
wana na inną listę, której pozycje zawierają pola user i timestamp, co znacznie upraszcza rende-
rowanie danych.
Szablon renderujący listę obserwujących można napisać w ogólny sposób, dzięki czemu możliwe
będzie zastosowanie go do wyświetlania list użytkowników obserwujących i obserwowanych. Sza-
blon otrzymuje użytkownika, tytuł strony, punkt końcowy używany w linkach stronicowania,
obiekt stronicowania oraz listę wyników.
Punkt końcowy follow_by jest prawie identyczny. Jedyna różnica polega na tym, że lista użytkowni-
ków jest uzyskiwana z relacji user.followed. Oprócz tego odpowiednio dostosowane muszą być
argumenty szablonu.
W szablonie followers.html zaimplementowano dwukolumnową tabelę, która po lewej stronie
przedstawia nazwy użytkowników oraz ich awatary, natomiast po prawej stronie — znaczniki czasu
Flask-Moment. Możesz przejrzeć repozytorium kodu źródłowego na GitHubie, aby szczegółowo
zapoznać się z tą implementacją.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
z nich i w końcu posortowanie ich w ramach jednej listy. Jak można się domyślać, takie rozwiąza-
nie nie skaluje się zbyt dobrze. Nakład pracy nad taką połączoną listą postów będzie wzrastał wraz
ze zwiększaniem się bazy danych, przez co utrudnione będzie efektywne wykonywanie takich
operacji jak podział listy na strony. Jest to problem znany jako problem N+1, ponieważ ten schemat
pracy z bazą danych wymaga wysłania N + 1 zapytań do bazy danych, gdzie N jest liczbą wyników
zwróconych przez pierwsze zapytanie. Kluczem do uzyskania dobrej wydajności odczytu postów
(niezależnie od wielkości bazy danych) jest zrobienie tego wszystkiego w ramach tylko jednego
zapytania.
To zadanie możemy zrealizować za pomocą bazodanowej operacji złączenia (ang. join). Ta opera-
cja przyjmuje przynajmniej dwie tabele i znajduje wszystkie kombinacje wierszy spełniające dany wa-
runek. Uzyskane w ten sposób złączone wiersze są wstawiane do tymczasowej tabeli, która jest
zwracana jako wynik operacji złączenia. Najlepszym sposobem wyjaśnienia działania złączeń jest
zaprezentowanie przykładu.
W tabeli 12.1 przedstawiam przykładową tabelę users z trzema użytkownikami.
id username
1 jan
2 zuza
3 dawid
Natomiast w tabeli 12.2 można zobaczyć powiązaną z nią tabelę posts przechowującą kilka postów.
id author_id treść
1 2 Post przygotowany przez zuzę
2 1 Post przygotowany przez jana
3 3 Post przygotowany przez dawida
4 1 Drugi post przygotowany przez jana
I wreszcie w tabeli 12.3 pokazuję, kto kogo obserwuje. Dokładnie widać w niej, że jan obserwuje dawida,
zuza obserwuje jana i dawida, a dawid nikogo nie obserwuje.
follower_id followed_id
1 3
2 1
2 3
Aby uzyskać listę postów użytkowników obserwowanych przez zuzę, musimy złączyć tabele posts
i follows. Najpierw tabela follows jest filtrowana tak, żeby zachować w niej tylko wiersze, w któ-
rych zuza jest użytkownikiem obserwującym. W tym przykładzie będą to dwa ostatnie wiersze.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Następnie tworzona jest tymczasowa tabela złączenia zbudowana ze wszystkich możliwych kom-
binacji wierszy z tabeli posts i przefiltrowanej tabeli follows, w której pole author_id danego posta
ma taką samą wartość jak pole follows.followed_id. W ten sposób uzyskujemy wszystkie posty
użytkowników obserwowanych przez użytkownika zuza. W tabeli 12.4 można zobaczyć wynik opera-
cji złączenia tabel. Kolumny, które zostały użyte do wykonania złączenia, są w tej tabeli oznaczone
znakiem gwiazdki *.
W powyższej tabeli znajduje się lista postów przygotowanych przez użytkowników obserwowanych
przez użytkownika zuza. Zapytanie Flask-SQLAlchemy, które wykonuje opisaną powyżej operację
złączenia, jest całkiem skomplikowane:
return db.session.query(Post).select_from(Follow).\
filter_by(follower_id=self.id).\
join(Post, Follow.followed_id == Post.author_id)
Wszystkie prezentowane do tej pory zapytania zaczynają się od atrybutu query odpytywanego modelu.
Niestety format nie sprawdza się jednak w przypadku tego zapytania, ponieważ zapytanie musi
zwrócić wiersze posts, a pierwszą operacją, którą należy tu wykonać, jest zastosowanie filtra
wobec tabeli follows. Używam zatem prostszej formy tego zapytania. Aby w pełni zrozumieć całe
zapytanie, musimy przyjrzeć się osobno poszczególnym jego częściom:
db.session.query(Post) definiuje, że będzie to zapytanie zwracające obiekty typu Post.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 12.7. app/models.py: Zbieranie obserwowanych postów
class User(db.Model):
# ...
@property
def followed_posts(self):
return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
.filter(Follow.follower_id == self.id)
Zauważ, że metoda followed_posts() jest zdefiniowana jako właściwość, więc nie potrzebuje już na-
wiasów(). Dzięki temu wszystkie relacje zachowują spójną składnię.
Informacja o wyświetlaniu wszystkich lub tylko obserwowanych postów zapisywana jest w pliku
cookie o nazwie show_followed. Jeżeli w tym pliku znajduje się niepusty ciąg znaków, to oznacza,
że powinny być wyświetlane tylko obserwowane posty. Pliki cookie są przechowywane w obiekcie
żądania jako słownik request.cookies. Ciąg znaków z pliku cookie jest konwertowany na wartość
typu Boolean i na jej podstawie wartości lokalnej zmiennej query jest przypisywane zapytanie, któ-
re pobiera pełną lub odfiltrowaną listę postów. Aby wyświetlić wszystkie posty, wykorzystamy zapyta-
nie najwyższego poziomu Post.query, z kolei ostatnio dodana właściwość User.followed_posts zosta-
nie użyta wtedy, gdy lista będzie musiała być ograniczona tylko do obserwowanych użytkowników.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Zapytanie zapisane w zmiennej lokalnej query jest następnie stronicowane, a otrzymane wyniki
zostaną przekazane do szablonu, tak jak robiliśmy to wcześniej.
Plik cookie show_followed jest ustawiany w dwóch nowych trasach pokazanych na listingu 12.9.
@main.route('/followed')
@login_required
def show_followed():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '1', max_age=30*24*60*60) # 30 dni
return resp
Linki do tych tras umieszczamy w szablonie strony głównej. Po ich wywołaniu plik cookie
show_followed otrzymuje odpowiednią wartość i następuje przekierowanie z powrotem na stronę
główną.
Pliki cookie można ustawić tylko dla obiektu odpowiedzi, tak więc te trasy muszą utworzyć obiekt
odpowiedzi za pomocą funkcji make_response(), nie korzystając z automatyki frameworka Flask.
Funkcja set_cookie() w dwóch pierwszych argumentach przyjmuje nazwę i wartość pliku cookie.
Opcjonalny argument max_age określa liczbę sekund pozostałych do wygaśnięcia tego pliku. Brak
tego argumentu spowoduje, że plik cookie wygaśnie tuż po zamknięciu okna przeglądarki. W tym
przypadku maksymalny czas ważności wynosi 30 dni, dzięki czemu ustawienie zostaje zapamiętane,
nawet jeśli użytkownik przez kilka dni nie wróci do naszej aplikacji.
Zmiany w szablonie dodają u góry strony dwie karty nawigacji, które wywołują trasy /all lub /follow,
aby wprowadzić odpowiednie ustawienia w sesji. Zmiany wprowadzone do szablonu możesz dokład-
nie przejrzeć w repozytorium kodu źródłowego na GitHubie. Spoglądając na rysunek 12.4, możesz się
przekonać, jak teraz będzie wyglądać strona główna.
Jeśli teraz wypróbujesz naszą aplikację i przełączysz się na listę obserwowanych postów, to zauważysz,
że Twoje własne posty nie pojawiają się na tej liście. Jest to oczywiście poprawne, ponieważ użytkownicy
nie są obserwatorami samych siebie.
Mimo że zapytania działają zgodnie z ich przeznaczeniem, większość użytkowników będzie oczekiwać,
że przeglądając posty swoich znajomych, zobaczy wśród nich również własne posty. Najłatwiejszym
sposobem rozwiązania tego problemu jest zarejestrowanie wszystkich użytkowników jako swoich wła-
snych obserwatorów już na etapie ich tworzenia. Ta sztuczka została pokazana na listingu 12.10.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 12.4. Obserwowane posty na stronie głównej
Listing 12.10. app/models.py: Rejestrowanie użytkowników jako własnych obserwatorów, wykonywane podczas
ich tworzenia
class User(UserMixin, db.Model):
# ...
def __init__(self, **kwargs):
# ...
self.follow(self)
Niestety w bazie danych prawdopodobnie znajduje się już kilku użytkowników, którzy zostali
wcześniej utworzeni i nie obserwują samych siebie. Jeśli baza danych jest niewielka i łatwa do zregene-
rowania, można ją usunąć i ponownie utworzyć, ale jeśli nie będzie to możliwe, to najodpowiedniej-
szym rozwiązaniem będzie dodanie funkcji aktualizującej, która poprawi istniejących już użytkowni-
ków. Kod tej sztuczki został pokazany na listingu 12.11.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Teraz można już zaktualizować bazę danych, uruchamiając funkcję z powyższego listingu w sesji
powłoki:
(venv) $ flask shell
>>> User.add_self_follows()
Tworzenie funkcji wprowadzających aktualizacje do bazy danych jest powszechną techniką stosowaną
do aktualizowania działających już aplikacji, ponieważ przeprowadzanie aktualizacji skryptowej
jest mniej podatne na błędy niż ręczne aktualizowanie danych. W rozdziale 17. dowiesz się, jak tę
i inne podobne funkcje można dołączyć do skryptu wdrożeniowego.
Dzięki temu, że wszyscy użytkownicy obserwują samych siebie, aplikacja jest bardziej użyteczna,
ale ta zmiana wprowadza też kilka komplikacji. Powoduje ona, że liczba obserwujących i obserwowa-
nych użytkowników wyświetlanych na stronie profilu użytkownika jest teraz zawyżona o jeden.
Liczby te muszą zostać zmniejszone o jeden, tak aby pokazywały właściwą wartość. Można tego
łatwo dokonać bezpośrednio w szablonie, stosując zapisy {{ user.followers.count() - 1 }} i {{
user.followed.count() - 1 }}. Listy obserwujących i obserwowanych użytkowników również
muszą zostać dostosowane, aby nie pokazywały tego samego użytkownika. To kolejna prosta
zmiana do wprowadzenia w szablonie, choć wymaga użycia instrukcji warunkowej. I w końcu,
linki prowadzące do samych siebie mają też wpływ na testy jednostkowe sprawdzające liczbę obser-
wujących, dlatego testy również muszą zostać dostosowane, tak aby brać pod uwagę aktualną sytuację.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
180 Rozdział 12. Obserwatorzy
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 13.
Komentarze użytkowników
Komentarze dotyczą określonych postów, dlatego zdefiniowano tu relację typu jeden-do-wielu, wy-
chodzącą z tabeli posts. Relacja ta może być wykorzystana do uzyskania listy komentarzy związanych
z konkretnym postem.
Tabela comments ma też relację typu jeden-do-wielu z tabelą users. Taki rodzaj relacji daje dostęp
do wszystkich komentarzy użytkownika, a pośrednio także do liczby komentarzy napisanych
przez użytkownika, czyli informacji, która może nadawać się do wyświetlenia na stronie profilu
użytkownika. Definicja modelu Comment została pokazana na listingu 13.1.
181
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 13.1. app/models.py: Model Comment
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
disabled = db.Column(db.Boolean)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
@staticmethod
def on_changed_body(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
'strong']
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, strip=True))
Atrybuty modelu Comment są prawie takie same jak atrybuty modelu Post. Jednym z dodatków jest
tutaj pole disabled przyjmujące wartości typu boolean, które to pole będzie używane przez mode-
ratorów do usuwania nieodpowiednich lub obraźliwych komentarzy. Podobnie jak same posty,
komentarze również definiują zdarzenie, które jest uruchamiane za każdym razem, gdy zmieni się
pole body, automatyzując w ten sposób konwertowanie tekstu Markdown na HTML. Ten proces
jest taki sam jak w przypadku postów omówionych już w rozdziale 11. Tutaj mamy jednak drobną
różnicę — komentarze są zwykle krótkie, więc lista dozwolonych znaczników HTML, używanych
podczas konwersji z Markdown, jest jeszcze bardziej restrykcyjna. Usunięto znaczniki związane z aka-
pitami, a pozostały tylko znaczniki formatowania znaków.
Aby zakończyć zmiany w bazie danych, w modelach User i Post musimy zdefiniować relacje typu
jeden-do-wielu z tabelą comments, tak jak pokazano na listingu 13.2.
Listing 13.2. app/models.py: Relacje jeden-do-wielu od modeli User i Post do modelu Comments
class User(db.Model):
# ...
comments = db.relationship('Comment', backref='author', lazy='dynamic')
class Post(db.Model):
# ...
comments = db.relationship('Comment', backref='post', lazy='dynamic')
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 13.3. app/main/forms.py: Formularz wprowadzania komentarza
class CommentForm(FlaskForm):
body = StringField('', validators=[DataRequired()])
submit = SubmitField('Wyślij')
Ta funkcja widoku tworzy formularz komentarza i wysyła go do szablonu post.html, gdzie ma on zo-
stać wyświetlony. Logika wstawiania nowego komentarza po przesłaniu formularza jest podobna
do sposobu obsługi postów. Podobnie jak w przypadku tworzenia posta, autor komentarza nie może
być zapisany przez bezpośrednie odczytanie zmiennej current_user, ponieważ jest to obiekt proxy
zmiennej kontekstowej. Wyrażenie current_user._get_current_object() zwraca rzeczywisty
obiekt użytkownika.
Komentarze są sortowane według znacznika czasu w kolejności chronologicznej, co oznacza, że nowe
komentarze będą zawsze dodawane na dole listy. Po wprowadzeniu nowego komentarza przekiero-
wanie kończące żądanie wraca do tego samego adresu URL, natomiast funkcja url_for() nadaje
parametrowi page wartość -1. To specjalny numer strony, który służy do uzyskania ostatniej
strony komentarzy, dzięki czemu wprowadzony przed chwilą komentarz będzie widoczny na
wyświetlanej stronie. Jeżeli po odczytaniu numeru strony z zapytania adresu URL okaże się, że ma ona
wartość -1, to na podstawie liczby komentarzy i rozmiaru strony obliczany jest właściwy numer
strony do wyświetlenia.
Lista komentarzy związanych z postem jest uzyskiwana poprzez relację typu jeden-do-wielu —
post.comments, a następnie jest sortowana według znacznika czasu komentarza i dzielona na
strony przy użyciu tych samych technik, jakich używaliśmy w przypadku listy postów. Na zakoń-
czenie komentarze i obiekt stronicowania są wysyłane do szablonu w celu ich zrenderowania.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Do pliku config.py dodajemy zmienną konfiguracyjną FLASKY_COMMENTS_PER_PAGE, która ma definio-
wać liczbę komentarzy wyświetlanych na stronie.
Procedurę renderowania komentarzy umieszczamy w nowym szablonie _comments.html, który
jest bardzo podobny do szablonu _posts.html, z ta różnicą, że korzysta z innego zestawu klas CSS.
Ten szablon jest dołączany do pliku _posts.html poniżej treści posta, po czym następuje wywołanie
makra stronicowania listy komentarzy. Wszystkie zmiany wprowadzone w szablonach możesz
przejrzeć w repozytorium aplikacji na GitHubie.
Na zakończenie prac nad tą funkcją do postów wyświetlanych na stronie głównej i stronach profilo-
wych należałoby dodać linki do stron z komentarzami. Tę zmianę przedstawiam na listingu 13.5.
Zwróć uwagę na to, że w tekście linka podawana jest liczba komentarzy, którą można łatwo uzy-
skać przy użyciu filtra count(), zastosowanego do relacji jeden-do-wielu pomiędzy tabelami
posts i comments.
Interesująca jest również tutaj struktura linka do strony z komentarzami, który jest zbudowany
jako stały link do posta z dodanym przyrostkiem #comments. Ta ostatnia część nazywana jest frag-
mentem adresu URL i służy do wskazania początkowej pozycji przewinięcia strony. Przeglądarka
wyszukuje element o podanym identyfikatorze i przewija stronę w taki sposób, aby ten element
pojawił się w górnej części okna. W szablonie post.html pozycja początkowa jest umieszczona
w nagłówku Komentarze, który jest zapisany jako znacznik <h4 id="comments">Komentarze</h4>.
Na rysunku 13.2 możesz zobaczyć, jak komentarze pojawiają się na stronie.
Wprowadzono tu także dodatkową zmianę w makrze stronicowania. Linki prowadzące do stron ko-
mentarzy również wymagają dodania fragmentu #comments, tak więc makro zostało uzupełnione
o argument fragmentu, który podawany jest przy wywoływaniu tego makra w szablonie post.html.
Moderowanie komentarzy
W rozdziale 9. zdefiniowano już listę ról użytkowników, każdej z nich przypisując pewne uprawnienia.
Jednym z uprawnień było Permission.MODERATE, które daje użytkownikom możliwość moderowania
komentarzy innych osób.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 13.2. Komentarze do postów na blogu
Nowa funkcja będzie widoczna jako link na pasku nawigacji, który z kolei będzie widoczny tylko dla
użytkowników mających prawo z niego skorzystać. Wszystko realizowane jest w szablonie base.html
przy użyciu instrukcji warunkowej, co można zobaczyć na listingu 13.6.
Strona moderacji wyświetla komentarze do wszystkich postów, przy czym najnowsze komentarze
są wyświetlane jako pierwsze. Pod każdym komentarzem znajduje się przycisk, którego zadaniem
jest przełączanie atrybutu disabled. Trasa /moderate została pokazana na listingu 13.7.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
To bardzo prosta funkcja, która odczytuje jedną stronę komentarzy z bazy danych i przekazuje ją
do szablonu do zrenderowania. Wraz z komentarzami szablon otrzymuje też obiekt stronicowania
oraz numer aktualnej strony.
Szablon moderate.html, pokazany na listingu 13.8, jest również prosty, ponieważ opiera się na szablo-
nie cząstkowym _comments.html, którzy przygotowaliśmy już wcześniej w celu renderowania
komentarzy.
{% block page_content %}
<div class="page-header">
<h1>Moderowanie komentarzy</h1>
</div>
{% set moderate = True %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.moderate') }}
</div>
{% endif %}
{% endblock %}
Ten szablon przekazuje zadanie renderowania komentarzy do szablonu _comments.html, ale zanim
przekaże kontrolę do szablonu cząstkowego, używa dyrektywy set rozszerzenia Jinja2 do zdefiniowa-
nia zmiennej szablonu moderate z przypisaną wartością True. Ta zmienna jest używana następnie
przez szablon _comments.html, w którym decyduje, czy należy wyświetlić funkcję moderowania
komentarzy.
Część szablonu _comments.html, która renderuje treść każdego komentarza, musi zostać zmodyfiko-
wana na dwa sposoby. Po pierwsze, wszelkie komentarze oznaczone jako wyłączone powinny zostać
pominięte dla zwykłych użytkowników (gdy zmienna moderate nie jest zdefiniowana). Po drugie,
dla moderatorów (gdy zmienna moderate jest zdefiniowana i ma wartość True) treść komentarza
musi zostać wyświetlona bez względu na to, czy został on zablokowany, czy też nie, a poniżej tej treści
powinien znajdować się przycisk, za pomocą którego będzie możliwe zablokowanie lub odbloko-
wanie komentarza. Te wszystkie zmiany przedstawiam na listingu 13.9.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
{% endif %}
{% endif %}
</div>
{% if moderate %}
<br>
{% if comment.disabled %}
<a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable',
id=comment.id, page=page) }}">Odblokuj</a>
{% else %}
<a class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable',
id=comment.id, page=page) }}">Zablokuj</a>
{% endif %}
{% endif %}
...
Dzięki tym zmianom użytkownicy zamiast zablokowanych komentarzy zobaczą tylko krótki komuni-
kat. Z kolei moderatorzy zobaczą zarówno komunikat, jak i treść komentarza. Oprócz tego mode-
ratorzy zobaczą również przycisk do blokowania lub odblokowywania znajdujący się pod każdym
komentarzem. Przycisk ten wywołuje jedną z dwóch nowych tras zależnie od tego, czy komentarz
jest zablokowany, czy też nie. Na listingu 13.10 pokazuję sposób definiowania tych tras.
@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_disable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = True
db.session.add(comment)
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int)))
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 13.3. Strona moderowania komentarzy
W tym rozdziale zakończyliśmy prace nad funkcjami społecznościowymi naszej aplikacji. W następ-
nym rozdziale dowiemy się, jak można udostępniać funkcje aplikacji w ramach interfejsu API,
z którego będą mogły skorzystać klienty, takie jak na przykład aplikacje na smartfony.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 14.
Interfejsy programowania aplikacji
W ostatnich latach w aplikacjach internetowych obserwuje się trend do przenoszenia coraz większej
ilości logiki biznesowej na stronę klienta, w wyniku czego powstaje architektura znana jako RIA
(ang. Rich Internet Applications — bogate aplikacje internetowe). W architekturze RIA główną (a cza-
sem jedyną) funkcją serwera jest udostępnianie aplikacji klienckiej usług odczytywania i zapisywa-
nia danych. W tym modelu serwer staje się usługą internetową (ang. web service) lub interfejsem
programowania aplikacji (ang. API — application programming interface).
Istnieje kilka protokołów, za pomocą których aplikacje RIA mogą komunikować się z usługą interne-
tową. Parę lat temu bardzo popularne były protokoły zdalnego wywoływania procedur (RPC), takie
jak XML-RPC lub jego pochodne, albo protokół SOAP (ang. Simplified Object Access Protocol).
Niedawno architektura REST (ang. Representational State Transfer) stała się preferowanym rozwiąza-
niem dla aplikacji internetowych, ponieważ została zbudowana na bazie znanego modelu sieci
WWW.
Dzięki swojej lekkości Flask jest idealną platformą do budowania usług sieciowych w architekturze
REST. W tym rozdziale dowiemy się, jak zaimplementować REST API na bazie frameworka Flask.
189
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jednolity interfejs
Protokół, za pomocą którego klienty uzyskują dostęp do zasobów serwera, musi być spójny,
dobrze zdefiniowany i ustandaryzowany. To najbardziej złożony aspekt architektury REST,
obejmujący użycie unikatowych identyfikatorów zasobów, różnych reprezentacji zasobów,
samoopisujących komunikatów między klientem a serwerem oraz hipermediów.
System warstwowy
Jeśli to konieczne, pomiędzy klientami i serwerami można umieszczać serwery proxy, pamięci
podręczne lub bramy w celu poprawy wydajności, niezawodności i skalowalności aplikacji.
Kod na żądanie
W pewnych warunkach klienty mogą pobrać kod z serwera, aby wykonać go w swoim kontekście.
Zasoby są wszystkim
Koncepcja zasobów (ang. resources) stanowi rdzeń architektury REST. W tym kontekście zasób
jest najważniejszym elementem w domenie aplikacji. Na przykład w aplikacji do blogowania zasobami
będą użytkownicy, posty i komentarze.
Każdy zasób musi mieć swój unikatowy identyfikator, który będzie go jednoznacznie opisywał.
Praca z protokołem HTTP oznacza, że identyfikatorami zasobów są adresy URL. Kontynuując przy-
kład z aplikacją do blogowania — post może być reprezentowany przez adres URL /api/posts/12345,
gdzie 12345 jest identyfikatorem posta, takim jak jego klucz główny z bazy danych. Format lub
treść adresu URL tak naprawdę nie mają znaczenia, liczy się tylko to, że każdy taki adres URL
musi jednoznacznie identyfikować zasób.
Zbiór wszystkich zasobów danej klasy również ma przypisany adres URL. Adres URL zbioru postów
może mieć postać /api/posts/, natomiast adres URL zbioru wszystkich komentarzy może mieć postać
/api/comments/.
Interfejs API może też definiować adresy URL kolekcji, będących logicznymi podzbiorami wszystkich
zasobów danej klasy. Na przykład zbiór wszystkich komentarzy do posta o identyfikatorze 12345
może być reprezentowany przez adres URL /api/posts/12345/comments/. Powszechne jest umieszcza-
nie ukośnika na końcu adresów URL reprezentujących kolekcje zasobów, ponieważ daje to wrażenie
istnienia jeszcze „podkatalogu”.
Metody żądania
Aplikacja kliencka, korzystając z ustalonych adresów URL do zasobów, wysyła żądania do serwe-
ra, używając przy tym metody żądania, aby wybrać wymaganą operację. Na przykład klient wyśle
żądanie typu GET na adres http://www.przyklad.pl/api/posts/, aby uzyskać listę dostępnych postów
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
w interfejsie API aplikacji. Natomiast wstawienie nowego posta wymagać będzie wysłania żądania
typu POST na ten sam adres URL z dołączoną w treści żądania zawartością nowego posta. Aby pobrać
post o numerze 12345, klient wyśle żądanie typu GET na adres http://www.www.przyklad.pl/
api/posts/12345. W tabeli 14.1 przedstawiono listę metod żądań powszechnie używanych w interfej-
sach API typu REST, wraz z opisem ich znaczenia.
Architektura REST nie wymaga implementacji wszystkich metod dla zasobu. Jeśli
klient wywołuje metodę, która nie jest obsługiwana dla danego zasobu, należy
zwrócić odpowiedź ze statusem kodu 405 (Metoda niedozwolona). Flask automatycz-
nie obsługuje ten błąd.
Metody żądania GET, POST, PUT i DELETE nie są jedynymi metodami używanymi we Flasku. Protokół
HTTP opiera się także na innych metodach, takich jak HEAD i OPTIONS, które są automatycznie
wdrażane przez Flaska.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W usługach sieciowych REST powszechnie używane są dwa formaty — JSON (ang. JavaScript
Object Notation) i XML (ang. Extensible Markup Language). W przypadku sieciowych aplikacji
typu RIA format JSON będzie atrakcyjniejszym wariantem, ponieważ jest on znacznie bardziej
zwięzły od XML, a poza tym jest ściśle powiązany z językiem JavaScript (językiem skryptowym
działającym po stronie klienta, używanym przez przeglądarki internetowe). Wracając jeszcze do przy-
kładowego interfejsu API aplikacji do blogowania — zasób posta może mieć następującą repre-
zentację JSON:
{
"self_url": "http://www.przyklad.pl/api/posts/12345",
"title": "Tworzenie REST API w Pythonie",
"author_url": "http://www.przyklad.pl/api/users/2",
"body": "... cały tekst artykułu ...",
"comments_url": "http://www.przyklad.pl/api/posts/12345/comments"
}
Zwróć, proszę, uwagę na to, że w polach url, author_url i comments_url zapisane są w pełni kwalifi-
kowane adresy URL do zasobów. To ważne, ponieważ te adresy pozwalają klientowi uzyskać nowe
zasoby.
W dobrze zaprojektowanym interfejsie REST API klient zna krótką listę adresów URL zasobów
najwyższego poziomu, a następnie odkrywa całą resztę z łączy zawartych w odpowiedziach. Jest to
podobne do znajdowania nowych stron internetowych poprzez klikanie kolejnych łączy na stronach,
które już znamy.
Kontrola wersji
W tradycyjnej aplikacji sieciowej, mocno korzystającej z serwera, to serwer ma pełną kontrolę nad tą
aplikacją. Przy aktualizowaniu aplikacji samo zainstalowanie nowej wersji na serwerze jest wystarcza-
jące, żeby jednocześnie zaktualizować oprogramowanie wszystkich użytkowników, ponieważ na-
wet małe części aplikacji działające w przeglądarce użytkownika są pobierane z tego serwera.
W przypadku aplikacji RIA z usługami internetowymi sytuacja jest bardziej skomplikowana, po-
nieważ często klienty są rozwijane niezależnie od serwera — być może nawet przez różne osoby.
Przyjrzyjmy się przypadkowi aplikacji, w której usługa typu REST jest używana przez różne klienty,
w tym przez przeglądarki internetowe oraz aplikacje dla smartfonów. Klient w przeglądarce interne-
towej może zostać w dowolnym momencie zaktualizowany przez serwer, ale w przypadku aplika-
cji na smartfony jest inaczej. Nie można na nich wymusić aktualizacji. To właściciel smartfona
musi na nią zezwolić. Nawet jeśli użytkownik byłby skłonny dokonać takiej aktualizacji, to nie ma
możliwości zorganizowania jednoczesnej aktualizacji dla wszystkich istniejących instancji aplikacji
na smartfony, tak aby zbiegła się ona z wdrożeniem nowej wersji serwera.
Z tego właśnie powodu usługi sieciowe muszą być bardziej tolerancyjne niż zwykłe aplikacje interne-
towe i muszą być w stanie pracować ze starymi wersjami swoich klientów. Zmiany w usłudze sieciowej
należy wprowadzać z najwyższą ostrożnością, ponieważ zmiany niezgodne z wcześniejszymi wersjami
mogą powodować błędy w istniejących klientach do czasu ich aktualizacji. Powszechną praktyką
jest nadawanie usługom internetowym wersji, która jest dodawana do wszystkich adresów URL
zdefiniowanych dla tych wersji aplikacji serwera. Na przykład pierwsze wydanie serwisu blogowego
może ujawnić zbiór postów pod adresem /api/v1/posts/.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Uwzględnienie w adresie URL wersji usługi internetowej pomaga utrzymać organizację starych i no-
wych funkcji, dzięki czemu serwer może udostępniać nowe funkcje nowym klientom, jednocze-
śnie kontynuując obsługę starych klientów. Aktualizacja usługi blogowania może zmienić sposób
zapisywania postów w formacie JSON, udostępniając je teraz pod adresem /api/v2/posts/ przy jedno-
czesnym zachowaniu starszego formatu dla klientów łączących się z adresem /api/v1/posts/.
Chociaż obsługa wielu wersji serwera może zwiększać obciążenie związane z konserwacją systemu,
istnieją takie sytuacje, w których jest to jedyny sposób, aby umożliwić rozwój nowych aplikacji bez
jednoczesnego powodowania problemów w już istniejących wdrożeniach. Starsze wersje usług
można wycofać, a później usunąć, gdy wszystkie klienty zostaną zmigrowane do nowszej wersji.
Zwróć uwagę, że w nazwie pakietu przygotowanego dla interfejsu API znajduje się numer wersji.
Jeśli w przyszłości konieczne będzie wprowadzenie wersji API niekompatybilnej z wcześniejszą
wersją, można ją będzie dodać jako osobny pakiet z innym numerem wersji. Dzięki temu do aplikacji
będzie można dołączyć oba interfejsy API.
Schemat API umieszcza każdy zasób w osobnym module. Definiowane są również osobne moduły
do obsługi uwierzytelniania, obsługi błędów oraz moduł udostępniający własne dekoratory. Na
listingu 14.2 przedstawiam konstruktor schematu.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 14.2. app/api/__init__.py: Tworzenie schematu API
from flask import Blueprint
Struktura konstruktora pakietu schematu jest zbliżona do znanej nam już z innych schematów.
Konieczne jest tu zaimportowanie wszystkich elementów schematu, aby można było zarejestrować tra-
sy i metody obsługi zdarzeń. To importowanie odbywa się na końcu skryptu, tak aby zapobiec błędom
wynikającym z zależności cyklicznych, ponieważ wiele importowanych modułów musi importować
opisany tutaj schemat api.
Sposób rejestrowania schematu API można zobaczyć na listingu 14.3.
Schemat interfejsu API jest rejestrowany z prefiksem adresu URL, dzięki czemu wszystkie trasy
tego schematu będą stosowały prefiks /api/v1. Dodanie prefiksu podczas rejestracji schematu jest
dobrym pomysłem, ponieważ dzięki temu nie trzeba ręcznie wpisywać numeru wersji w każdej
trasie schematu.
Obsługa błędów
Internetowa usługa typu REST informuje klienta o statusie żądania, wysyłając w odpowiedzi na to
żądanie odpowiedni kod statusu HTTP i umieszczając w treści odpowiedzi niezbędne informacje.
W tabeli 14.2 podaję typowe kody statusu, których klient może oczekiwać od usługi internetowej.
Tabela 14.2. Typowe kody statusu odpowiedzi HTTP zwracane przez interfejsy API
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obsługa statusu kodów 404 i 500 może powodować małe komplikacje, ponieważ te błędy są zwykle
obsługiwane przez framework Flask, który zwraca odpowiednią odpowiedź HTML. Może to zmylić
klienta API, który prawdopodobnie będzie oczekiwać wszystkich odpowiedzi w formacie JSON.
Jednym ze sposobów generowania właściwych odpowiedzi dla wszystkich klientów jest spowodo-
wanie, że procedury obsługi błędów będą dostosowywać swoje odpowiedzi do formatu żądanego przez
klienta. Taką technikę nazywamy negocjacją treści (ang. content negotiation). Na listingu 14.4
pokazuję ulepszoną procedurę obsługi błędów 404, którą stosuje format JSON dla klientów usługi
sieciowej, a dla pozostałych format HTML. Moduł obsługi błędu 500 został napisany w podobny
sposób.
Listing 14.4. app/api/errors.py: Procedura obsługi błędów 404 z negocjacją treści HTTP
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
Nowa wersja procedury obsługi błędów sprawdza w żądaniu nagłówek Accept, który jest dostępny
przez zmienną request.accept_mimetypes, i na tej podstawie ustala, w jakim formacie klient chce
uzyskać odpowiedź. Przeglądarki zasadniczo nie określają żadnych ograniczeń formatów odpowiedzi,
ale klienty używające API zwykle tak robią. Odpowiedź JSON jest generowana tylko dla klientów,
którzy na liście akceptowanych formatów podają format JSON, ale nie HTML.
Pozostałe kody stanu są generowane przez samą usługę internetową, dzięki czemu można je zaimple-
mentować jako funkcje pomocnicze wewnątrz schematu, w module errors.py. Na listingu 14.5
można zobaczyć implementację obsługi błędu 403. Procedury obsługi pozostałych błędów są bardzo
podobne.
Listing 14.5. app/api/errors.py: Moduł obsługi błędów API dla kodu statusu 403
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
Funkcje widoku w schemacie interfejsu API mogą wywoływać funkcje pomocnicze, tak aby w razie
potrzeby generować odpowiedzi na błędy.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Klienty muszą zamieścić w żądaniu wszystkie informacje niezbędne do wykonania tego żądania, a za-
tem każde żądanie musi zawierać dane uwierzytelniające użytkownika.
Obecna funkcja logowania, zaimplementowana za pomocą rozszerzenia Flask-Login, przechowuje
dane w sesji użytkownika, które Flask domyślnie zapisuje w pliku cookie po stronie klienta. Dzięki
temu serwer nie przechowuje żadnych informacji związanych z użytkownikiem, ale prosi klienta
o ich zachowanie. Mogłoby się wydawać, że taka implementacja jest zgodna z wymogiem REST
dotyczącym bezstanowości, ale użycie plików cookie w usługach typu REST to jednak szara strefa.
Po prostu zaimplementowanie plików cookie może być kłopotliwe dla klientów niebędących przeglą-
darkami. Z tego powodu używanie plików cookie w interfejsach API jest uznawane za złą praktykę.
Wymóg REST dotyczący bezstanowości może wydawać się zbyt rygorystyczny, istnieją
jednak ku temu powody. Serwery bezstanowe można bardzo łatwo skalować. Jeśli
serwery przechowują informacje o klientach, to trzeba upewnić się, że dany serwer
zawsze będzie otrzymywał żądania od wybranego klienta, lub trzeba użyć współdzielo-
nej pamięci przeznaczonej na dane klientów. Trzeba zatem poświęcić sporo wysiłków
na rozwiązanie problemów, które nie istnieją, jeżeli używamy serwerów bezstanowych.
Ze względu na to, że architektura REST bazuje na protokole HTTP, preferowaną metodą używaną
do wysyłania danych uwierzytelniających jest uwierzytelnianie HTTP zarówno w wersji Basic, jak
i Digest. W przypadku uwierzytelniania HTTP dane uwierzytelniające użytkownika są dołączane
do nagłówka Authorization każdego wysyłanego żądania.
Protokół uwierzytelniania HTTP jest na tyle prosty, że można go bezpośrednio zaimplementować,
ale rozszerzenie Flask-HTTPAuth tworzy wygodny wrapper, który ukrywa szczegóły protokołu
w dekoratorze podobnym do login_required z rozszerzenia Flask-Login.
Pakiet Flask-HTTPAuth można zainstalować za pomocą polecenia pip:
(venv) $ pip install flask-httpauth
@auth.verify_password
def verify_password(email, password):
if email == '':
return False
user = User.query.filter_by(email = email).first()
if not user:
return False
g.current_user = user
return user.verify_password(password)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Z uwagi na fakt, że ten typ uwierzytelnienia użytkownika będzie używany tylko w schemacie inter-
fejsu API, rozszerzenie Flask-HTTPAuth jest inicjowane w pakiecie schematu, a nie w pakiecie
aplikacji, tak jak inne rozszerzenia.
Adres e-mail i hasło są weryfikowane przy użyciu funkcji istniejącej już w modelu User. Nasza funkcja
wywołania zwrotnego zwraca wartość True, gdy dane uwierzytelniające są poprawne, a w przeciw-
nym razie zwraca wartość False. Rozszerzenie Flask-HTTPAuth wywoła tę funkcję również w przy-
padku takich żądań, które nie będą miały dołączonych danych uwierzytelniających, podając wtedy
w obu argumentach pusty ciąg znaków. W takiej sytuacji, gdy funkcja wykryje, że argument email
jest pustym ciągiem znaków, od razu zwróci wartość False, aby zablokować żądanie. W niektórych
aplikacjach dopuszczalne może być zwrócenie wartości True dla użytkowników anonimowych.
Nasza funkcja uwierzytelniająca zapisuje uwierzytelnionego już użytkownika w zmiennej kontek-
stowej g, tak aby funkcja widoku mogła później uzyskać dostęp do tej informacji.
Gdy dane uwierzytelniające będą nieprawidłowe, serwer zwróci klientowi w odpowiedzi kod statusu 401.
Flask-HTTPAuth domyślnie generuje odpowiedź z tym statusem. Aby zapewnić spójność odpowiedzi
z innymi błędami zwracanymi przez interfejs API, tę odpowiedź można dostosować tak jak pokazano
to na listingu 14.7.
@auth.error_handler
def auth_error():
return unauthorized('Nieprawidłowe dane uwierzytelniania')
Z uwagi na fakt, iż wszystkie trasy w całym schemacie muszą być chronione w ten sam sposób, deko-
rator login_required może zostać dołączony jeden raz do procedury obsługi zdarzenia before_
request, tak jak pokazano to na listingu 14.8.
@api.before_request
@auth.login_required
def before_request():
if not g.current_user.is_anonymous and \
not g.current_user.confirmed:
return forbidden('Konto niepotwierdzone')
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Teraz żądania będą automatycznie uwierzytelniane dla wszystkich tras w schemacie. Jako dodatkowe
zabezpieczenie metoda obsługi zdarzenia before_request odrzuca również uwierzytelnionych
użytkowników, którzy nie potwierdzili swoich kont.
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
Metoda generate_auth_token() zwraca podpisany token, który koduje pole id danego użytkownika.
Podawany jest również czas ważności tokena, wyrażony w sekundach. Metoda verify_auth_token()
pobiera token i jeśli zostanie on uznany za prawidłowy, zwraca obiekt zapisanego w nim użytkownika.
Jest to metoda statyczna, ponieważ użytkownik będzie znany dopiero po odkodowaniu tokena.
Aby umożliwić uwierzytelnianie żądań przychodzących z tokenem, trzeba tak zmodyfikować
funkcję wywołania zwrotnego verify_password, aby przyjmowała ona zarówno tokeny, jak i zwykłe
dane uwierzytelniające. Zaktualizowany kod tej funkcji można zobaczyć na listingu 14.10.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
if password == '':
g.current_user = User.verify_auth_token(email_or_token)
g.token_used = True
return g.current_user is not None
user = User.query.filter_by(email=email_or_token).first()
if not user:
return False
g.current_user = user
g.token_used = False
return user.verify_password(password)
W tej nowej wersji pierwszym argumentem uwierzytelniającym może być adres e-mail lub token
uwierzytelniający. Jeśli to pole będzie puste, to — tak jak poprzednio — zakładamy, że jest to użytkow-
nik anonimowy. Z kolei w przypadku, gdy puste jest pole hasła, przyjmujemy, że pole email_or_token
zawiera token i podejmowana jest odpowiednia kontrola. Jeśli oba pola są niepuste, to przystę-
pujemy do standardowego uwierzytelniania za pomocą e-maila i hasła. Dzięki tej implementacji
uwierzytelnianie wykorzystujące tokeny jest opcjonalne; każdy klient może, ale nie musi, z niego
skorzystać. Aby zapewnić funkcjom widoku możliwość rozróżnienia między tymi dwiema metodami
uwierzytelniania, dodano zmienną g.token_used.
Do schematu interfejsu API trzeba też dodać trasę zwracającą klientowi tokeny uwierzytelniające.
Kod implementujący tę trasę został przedstawiony na listingu 14.11.
Ze względu to, że ta trasa została zdefiniowana w schemacie, mają do niej zastosowanie mechanizmy
uwierzytelniania dodane do procedury obsługi zdarzenia before_request. Aby wymusić na klientach
uwierzytelnianie za pomocą adresu e-mail i hasła, zabraniając stosowania uzyskanego wcześniej
tokena, musimy sprawdzić wartość zmiennej g.token_used. W ten sposób możemy odrzucać żądania
uwierzytelnione przy użyciu tokena. Ma to uniemożliwić użytkownikom omijanie procedury wyga-
śnięcia tokena przez uzyskiwanie nowego tokena przy użyciu starego tokena jako uwierzytelnienia.
Funkcja zwraca token w odpowiedzi JSON z czasem ważności wynoszącym jedną godzinę. Informacja
o tym czasie jest również zapisywana w odpowiedzi JSON.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 14.12. app/models.py: Konwertowanie danych posta do słownika umożliwiającego serializację do
formatu JSON
class Post(db.Model):
# ...
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author_url': url_for('api.get_user', id=self.author_id),
'comments_url': url_for('api.get_post_comments', id=self.id),
'comment_count': self.comments.count()
}
return json_post
Pola url, author_url i comments_url muszą zwracać adresy URL odpowiednich zasobów, więc są one
generowane za pomocą wywołań funkcji url_for() ze wskazaniami innych tras zdefiniowanych
w schemacie interfejsu API.
Ten przykład pokazuje, że w reprezentacji zasobu można umieszczać generowane atrybuty. Pole
comment_count zwraca liczbę komentarzy, które zostały dodane do wybranego posta. Co prawda ta
informacja nie jest atrybutem modelu, jest jednak uwzględniona w reprezentacji zasobu jako małe
udogodnienie dla klienta.
W podobny sposób można zbudować metodę to_json() dla modelu User. Jej implementacja została
pokazana na listingu 14.13.
Zauważ, że w tej metodzie, ze względu na zasady zachowania prywatności, niektóre atrybuty użytkow-
nika, takie jak email i role, są pomijane w odpowiedzi. Przykład ten ponownie pokazuje, że reprezen-
tacja zasobu oferowanego klientom nie musi być identyczna z wewnętrzną definicją odpowiedniego
modelu bazy danych.
Odwrotność serializacji nazywa się deserializacją (ang. deserialization). Deserializacja struktury JSON
do modelu może być kłopotliwa, ponieważ niektóre dane pochodzące od klientów mogą być niepo-
prawne, błędne lub niepotrzebne. Na listingu 14.14 przedstawiam metodę, która tworzy obiekt klasy
Post na podstawie danych JSON.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 14.14. app/models.py: Tworzenie posta z danych JSON
from app.exceptions import ValidationError
class Post(db.Model):
# ...
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('Post nie ma żadnej treści.')
return Post(body=body)
Jak widać, w tej implementacji korzystamy tylko z atrybutu body pobranego ze słownika JSON. Atry-
but body_html jest ignorowany, ponieważ renderowanie składni Markdown po stronie serwera jest
automatycznie uruchamiane przez zdarzenie SQLAlchemy, które jest wywoływane przy każdej
modyfikacji atrybutu body. Nie trzeba też używać atrybutu timestamp, chyba że klientowi zezwolono
na publikowanie postów w przeszłości lub w przyszłości, ale nie jest to funkcja obsługiwana przez
tę aplikację. Pole author_url nie jest używane, ponieważ klient nie ma uprawnień do wybierania
autora posta. Jedyną prawidłową wartością dla tego pola jest adres URL uwierzytelnionego użytkow-
nika. Atrybuty comments_url i comment_count są generowane automatycznie na podstawie relacji
z bazą danych, więc nie ma w nich informacji potrzebnych do utworzenia nowego posta. I w końcu,
pole url jest ignorowane, ponieważ w tej implementacji adresy URL zasobów są definiowane przez
serwer, a nie przez klienta.
Zwróć uwagę na to, jak odbywa się tutaj sprawdzanie błędów. Jeśli brakuje pola body lub jest ono
puste, zgłaszany jest wyjątek ValidationError. Zgłoszenie tego wyjątku jest w tym przypadku
właściwym sposobem radzenia sobie z błędem, ponieważ metoda ta nie ma wystarczającej wiedzy,
aby poprawnie zareagować w tej sytuacji. Wyjątek przekazuje błąd do wywołującego kodu, dzięki
czemu kod na wyższym poziomie będzie mógł obsłużyć dany błąd. Klasa ValidationError została
zaimplementowana jako prosta podklasa klasy ValueError z Pythona. Implementacja została pokazana
na listingu 14.15.
Aplikacja musi teraz obsłużyć wyjątek, przygotowując właściwą odpowiedź dla klienta. Aby uniknąć
konieczności dodawania kodu przechwytującego wyjątki w funkcjach widoku, można zainstalować
globalną procedurę obsługi wyjątków za pomocą dekoratora errorhandler. Metodę obsługującą
wyjątki ValidationError pokazano na listingu 14.16.
Listing 14.16. app/api/error.py: Metoda obsługi błędów API dla wyjątków ValidationError
@api.errorhandler(ValidationError)
def validation_error(e):
return bad_request(e.args[0])
Dekoratora errorhandler używaliśmy już do rejestrowania metod obsługi dla kodów statusu
HTTP, ale w tym przypadku przyjmuje on w argumencie klasę Exception. Funkcja dekoratora zosta-
nie wywołana za każdym razem, gdy zostanie zgłoszony wyjątek dla danej klasy. Zauważ także, że
dekorator jest pobierany ze schematu interfejsu API, więc odpowiednia metoda zostanie wywołana
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
tylko wtedy, gdy wyjątek pojawi się podczas obsługiwania trasy ze schematu. Przy zastosowaniu
tej techniki można pisać bardzo czysty i zwięzły kod funkcji widoku bez konieczności każdorazowego
dodawania metod obsługi błędów. Na przykład tak:
@api.route('/posts/', methods=['POST'])
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json())
@api.route('/posts/<int:id>')
def get_post(id):
post = Post.query.get_or_404(id)
return jsonify(post.to_json())
Pierwsza trasa obsługuje żądanie pobierające kolekcję postów. Funkcja ta korzysta z wyrażenia listowego
(ang. list comprehension) i z jego pomocą generuje wersję JSON dla wszystkich postów. Druga trasa
zwraca pojedynczy post na blogu i odpowiada błędem kodu 404, gdy w bazie danych nie uda się
znaleźć posta o podanym identyfikatorze.
Metoda obsługi żądań typu POST dla zasobów postów zajmuje się dodawaniem nowych postów do
bazy danych. Kod tej trasy został pokazany na listingu 14.18.
Do tej funkcji widoku został dodany dekorator permission_required (jego kod został pokazany
na listingu 14.19), który sprawdza, czy uwierzytelniony użytkownik ma uprawnienia do pisania po-
stów. Dzięki wcześniejszemu zaimplementowaniu metod obsługujących błędy samo utworzenie posta
w bazie danych jest już całkiem proste. Wpis na blogu jest tworzony z danych JSON, a jako autor
posta przyjmowany jest aktualnie uwierzytelniony użytkownik. Po zapisaniu modelu w bazie danych
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
zwracany jest kod statusu 201, a do tej odpowiedzi dodawany jest nagłówek Location z adresem
URL nowo utworzonego zasobu.
Zauważ, proszę, że dla wygody klientów treść odpowiedzi zawiera nowy zasób. Dzięki temu klient
nie będzie musiał wysyłać żądania GET natychmiast po utworzeniu zasobu.
Dekorator permission_required ma na celu uniemożliwienie nieautoryzowanym użytkownikom
tworzenia nowych postów na blogu. Jest on podobny do dekoratora używanego w aplikacji, z tym
że został dostosowany do używania w schemacie interfejsu API. Implementację tego dekoratora
przedstawiam na listingu 14.19.
Obsługa żądań typu PUT dla postów używana jest do edytowania istniejących zasobów. Kod odpo-
wiedniej funkcji został przedstawiony na listingu 14.20.
W tym przypadku kontrola uprawnień jest bardziej złożona. Samo sprawdzanie uprawnienia do pisa-
nia postów odbywa się za pomocą dekoratora. Ale trzeba też zapewnić, żeby użytkownik mógł
edytować tylko te posty, których jest autorem. Oczywiście administrator również powinien mieć taką
możliwość. Kontrola takiego zestawu warunków jest wprowadzana jawnie do funkcji widoku.
Gdyby podobne warunki musiały być sprawdzane w wielu funkcjach widoku, to zbudowanie nowego
dekoratora byłoby dobrym sposobem uniknięcia powtarzania kodu.
Ze względu na to, że aplikacja nie pozwala na usuwanie postów, nie musimy implementować pro-
cedury obsługi żądania DELETE.
Implementacja procedur obsługi zasobów użytkowników i komentarzy wygląda bardzo podobnie.
W tabeli 14.3 przedstawiam zestaw zasobów zaimplementowanych w naszej aplikacji wraz z obsługi-
wanymi metodami HTTP. Jeśli chcesz dowiedzieć się więcej na ten temat, możesz przejrzeć pełną
implementację dostępną w repozytorium tej aplikacji na GitHubie.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 14.3. Zasoby Flasky API
Zwróć uwagę na to, że zaimplementowane tu zasoby stanową tylko część funkcji dostępnych za
pośrednictwem aplikacji internetowej. W razie potrzeby listę obsługiwanych zasobów można roz-
szerzyć. Na przykład można umożliwić pobieranie listy obserwujących osób albo moderowanie
komentarzy lub wprowadzić inne funkcje, których może potrzebować klient interfejsu API.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Pole posts w odpowiedzi JSON zawiera elementy danych, tak jak w poprzednich przykładach, ale
teraz jest to tylko jedna strona wyników, a nie pełny zbiór. Elementy prev_url i next_url zawierają
adresy URL dla poprzedniej i następnej strony lub wartość None, jeśli w żądanym kierunku kolej-
na strona nie jest dostępna. Wartość count to łączna liczba pozycji w kolekcji.
Technikę tę można zastosować we wszystkich trasach zwracających kolekcje.
Zakładając, że nasz serwer działa pod domyślnym adresem http://127.0.0.1:5000, żądanie GET
można wysłać z innego okna terminala, wydając poniższe polecenie:
(venv) $ http --json --auth <e-mail>:<hasło> GET \
> http://127.0.0.1:5000/api/v1/posts
HTTP/1.0 200 OK
Content-Length: 7018
Content-Type: application/json
Date: Sun, 22 Dec 2013 08:11:24 GMT
Server: Werkzeug/0.9.4 Python/2.7.3
{
"posts": [
...
],
"prev_url": null
"next_url": "http://127.0.0.1:5000/api/v1/posts/?page=2",
"count": 150
}
Zwróć uwagę na linki stronicowania zawarte w odpowiedzi. Otrzymaliśmy pierwszą stronę z postami,
dlatego adres poprzedniej strony nie jest zdefiniowany, ale otrzymujemy adres URL umożliwiający
odczytanie następnej strony oraz informację o łącznej liczbie postów.
Poniższe polecenie wysyła żądanie typu POST tworzące nowy post:
(venv) $ http --auth <e-mail>:<hasło> --json POST \
> http://127.0.0.1:5000/api/v1/posts/ \
> "body=Dodaję nowy post z *wiersza poleceń*."
HTTP/1.0 201 CREATED
Content-Length: 360
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Content-Type: application/json
Date: Sun, 22 Dec 2013 08:30:27 GMT
Location: http://127.0.0.1:5000/api/v1/posts/111
Server: Werkzeug/0.9.4 Python/2.7.3
{
"author": "http://127.0.0.1:5000/api/v1/users/1",
"body": " Dodaję nowy post z *wiersza poleceń*.",
"body_html": "<p>Dodaję nowy post z <em>wiersza poleceń</em>.</p>",
"comments": "http://127.0.0.1:5000/api/v1/posts/111/comments",
"comment_count": 0,
"timestamp": "Sun, 22 Dec 2013 08:30:27 GMT",
"url": "http://127.0.0.1:5000/api/v1/posts/111"
}
Aby użyć tokenów uwierzytelniających zamiast nazwy użytkownika i hasła, najpierw trzeba wysłać żą-
danie POST do trasy /api/v1/tokens/:
(venv) $ http --auth <e-mail>:<hasło> --json POST \
> http://127.0.0.1:5000/api/v1/tokens/
HTTP/1.0 200 OK
Content-Length: 162
Content-Type: application/json
Date: Sat, 04 Jan 2014 08:38:47 GMT
Server: Werkzeug/0.9.4 Python/3.3.3
{
"expiration": 3600,
"token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..."
}
Od teraz przez następną godzinę zwrócony token może służyć do wykonywania wywołań interfejsu
API. Trzeba tylko umieszczać go w polu nazwy użytkownika i pozostawić puste hasło:
(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1/posts/
Po wygaśnięciu tokena takie żądania będą wracały z kodem błędu 401 informującym nas, że musimy
uzyskać nowy token.
Gratulacje! Ten rozdział kończy II część tej książki, a wraz z nią faza rozwoju aplikacji Flasky jest
już zakończona. Następnym krokiem będzie oczywiście wdrożenie aplikacji, z czym wiąże się całkiem
nowy zestaw wyzwań, którymi zajmiemy się w części III.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
CZĘŚĆ III
Ostatnie kroki
207
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
208 Część III Ostatnie kroki
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 15.
Testowanie
Istnieją dwa bardzo dobre powody, aby tworzyć testy jednostkowe. Podczas implementowania nowej
funkcjonalności testy jednostkowe służą do potwierdzenia, że nasz nowy kod działa w oczekiwany
sposób. Ten sam wynik można uzyskać, testując ręcznie, ale oczywiście testy automatyczne oszczę-
dzają czas i zmniejszają nakład pracy, ponieważ można je łatwo powtarzać.
Drugim, ważniejszym powodem jest to, że za każdym razem, gdy aplikacja jest modyfikowana,
można wykonać wszystkie zbudowane wokół niej testy jednostkowe, aby upewnić się, że w istniejącym
kodzie nie ma regresji. Innymi słowy, upewniamy się w ten sposób, że wszystkie nowe zmiany nie
wpłynęły na sposób działania starszego kodu.
Testy jednostkowe były częścią aplikacji Flasky już od samego jej początku. Mają na celu spraw-
dzanie działania określonych funkcji aplikacji zaimplementowanych w klasach modeli baz danych.
Te klasy można łatwo przetestować poza kontekstem działającej aplikacji. Przygotowanie testów
jednostkowych dla wszystkich istniejących już funkcji w modelach baz danych nie kosztuje wiele
wysiłku, a jest najlepszym sposobem na zagwarantowanie, że przynajmniej część aplikacji od samego
początku będzie solidnie zbudowana.
W tym rozdziale omówimy sposoby ulepszania i rozbudowywania testów jednostkowych na inne
obszary aplikacji.
209
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Można go używać jako skryptu wiersza poleceń, który będzie uruchamiał dowolną aplikację Pythona
z włączonym pomiarem pokrycia kodu. Zapewnia także wygodny dostęp do skryptów w celu progra-
mowego uruchomienia mechanizmów pomiaru pokrycia kodu. Aby ładnie zintegrować metryki
pokrycia kodu z poleceniem flask test (omówionym już wcześniej w rozdziale 7.), można wykorzy-
stać opcję --coverage. Na listingu 15.1 przedstawiam implementację tej opcji.
COV = None
if os.environ.get('FLASK_COVERAGE'):
import coverage
COV = coverage.coverage(branch=True, include='app/*')
COV.start()
# ...
@app.cli.command()
@click.option('--coverage/--no-coverage', default=False,
help='Uruchom testy z pomiarem pokrycia kodu.')
def test(coverage):
"""Uruchom testy jednostkowe."""
if coverage and not os.environ.get('FLASK_COVERAGE'):
os.environ['FLASK_COVERAGE'] = '1'
os.execvp(sys.executable, [sys.executable] + sys.argv)
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
if COV:
COV.stop()
COV.save()
print('Informacje o pokryciu kodu:')
COV.report()
basedir = os.path.abspath(os.path.dirname(__file__))
covdir = os.path.join(basedir, 'tmp/coverage')
COV.html_report(directory=covdir)
print('Wersja HTML: file://%s/index.html' % covdir)
COV.erase()
Opcja --coverage dodana do polecenia flask_test włącza pomiary pokrycia kodu podczas przepro-
wadzania testów. Dekorator click.option został tu użyty, aby dodać argument typu Boolean do
własnego polecenia test. Przekazuje on wartość podanego znacznika typu Boolean jako argument tej
funkcji.
Jednak zintegrowanie mechanizmów pokrycia kodu ze skryptem flasky.py to naprawdę pomniejszy
problem. Gdy funkcja test otrzyma opcję --coverage, jest już za późno na to, aby włączyć metry-
ki pokrycia, ponieważ w tym momencie cały kod o zasięgu globalnym został już wykonany. Aby uzy-
skać dokładne metryki, skrypt przygotowuje zmienną środowiskową FLASK_COVERAGE, a następnie
przystępuje do rekurencyjnego uruchomienia się ponownie. W drugim uruchomieniu początkowa
część skryptu wykrywa, że zmienna środowiskowa już istnieje, i już od samego początku, nawet
przed wykonaniem w aplikacji instrukcji import, włącza pomiar pokrycia kodu.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Funkcja coverage.coverage() uruchamia pomiar pokrycia kodu. Opcja branch=True umożliwia
włączenie analizy rozgałęzień, dzięki czemu oprócz protokołowania wykonania poszczególnych
wierszy kodu sprawdzane jest także, czy dla każdego warunku wykonano zarówno przypadek True,
jak i przypadek False. Opcja include służy do ograniczenia analizy pokrycia tylko do plików
znajdujących się w pakiecie aplikacji (to jedyny kod, który powinien podlegać takiej analizie). Gdyby-
śmy nie zastosowali opcji include, to wszystkie rozszerzenia zainstalowane w środowisku wirtualnym
oraz kod samych testów zostałyby zawarte w raporcie zasięgu — a to spowodowałoby poważne
zaśmiecenie takiego raportu.
Po wykonaniu wszystkich testów funkcja test() wypisuje w konsoli prosty raport, a na dysku zapisuje
jego ładniejszą wersję w postaci pliku HTML. Wersja HTML prezentuje cały kod źródłowy z koloro-
wymi adnotacjami wyróżniającymi zarówno wiersze objęte testami, jak i te nieobjęte.
Raport pokazuje całkowite pokrycie kodu wynoszące 45%, co nie jest złym wynikiem, ale także nie-
szczególnie dobrym. Klasy modeli, na których jak dotąd koncentrowały się wszystkie testy jednostkowe,
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
składają się łącznie z 236 instrukcji, z czego 79% objętych jest testami. Natomiast pliki views.py
w schematach main i auth oraz trasy w schemacie api_v1 mają bardzo niskie pokrycie, ponieważ
nie są one wywoływane w żadnym z istniejących testów jednostkowych. Oczywiście metryki pokrycia
nie dają nam żadnej informacji o tym, jaka część kodu w projekcie jest wolna od błędów, ponieważ
tutaj dużą rolę odgrywają inne czynniki, takie jak jakość testów.
Uzbrojeni w ten raport możemy łatwo ustalić, gdzie należy przygotować nowe testy, tak aby po-
prawić pokrycie kodu aplikacji. Niestety nie wszystkie części aplikacji można przetestować tak
łatwo jak modele baz danych. W następnych dwóch podrozdziałach będę omawiał bardziej zaawan-
sowane strategie testowania, które można zastosować do testowania funkcji widoków, formularzy
i szablonów.
class FlaskClientTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
Role.insert_roles()
self.client = self.app.test_client(use_cookies=True)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_home_page(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertTrue('Nieznajomy' in response.get_data(as_text=True))
Na listingu 15.4 można zobaczyć bardziej zaawansowany test jednostkowy, który symuluje nowego
użytkownika rejestrującego swoje konto, logującego się, autoryzującego konto za pomocą tokena
potwierdzającego i wreszcie wylogowującego się.
Listing 15.4. tests/test_client.py: Proces pracy nowego użytkownika symulowany za pomocą klienta testowego
class FlaskClientTestCase(unittest.TestCase):
# ...
def test_register_and_login(self):
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
# Zarejestruj nowe konto.
response = self.client.post('/auth/register', data={
'email': 'jan@przyklad.pl',
'username': 'jan',
'password': 'kot',
'password2': 'kot'
})
self.assertEqual(response.status_code, 302)
# Wyloguj.
response = self.client.get('/auth/logout', follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue('Zostałeś wylogowany' in response.get_data(
as_text=True))
Test rozpoczyna się od przesłania formularza na trasę rejestracji. Argument data w wywołaniu
funkcji post() jest słownikiem z polami formularza, które muszą dokładnie pasować do nazw pól
zdefiniowanych w formularzu HTML. Dzięki temu, że w konfiguracji testowej została wyłączona
ochrona CSRF, nie trzeba już wysyłać tokena CSRF wraz z formularzem.
Trasa /auth/register może odpowiadać na dwa sposoby. Jeśli dane rejestracyjne będą prawidłowe,
to przekierowanie wyśle użytkownika na stronę logowania. W przypadku nieprawidłowej rejestracji
odpowiedź ponownie wyświetli stronę z formularzem rejestracji zawierającą odpowiednie komu-
nikaty o błędach. Aby upewnić się, czy rejestracja została poprawnie zaakceptowana, test musi spraw-
dzić, czy kod statusu odpowiedzi będzie wynosił 302, czyli numer kodu przekierowania.
Druga część testu wysyła żądanie logowania do aplikacji przy użyciu właśnie zarejestrowanego
adresu e-mail i hasła. Odbywa się to za pomocą żądania POST na trasie /auth/login. Tym razem w wy-
wołaniu post() podawany jest argument follow_redirects=True, aby klient testowy działał jak
przeglądarka i automatycznie wysyłał żądanie GET dla przekierowanego adresu URL. Dzięki tej opcji
nie będzie już zwracany kod statusu 302; zamiast tego zwracana będzie odpowiedź z przekierowa-
nego adresu URL.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Pomyślna odpowiedź na przesłanie danych logowania wyświetliłaby stronę, która wita użytkownika
jego nazwą oraz informacją, że dalszy dostęp wymaga potwierdzenia konta. Dwie instrukcje assert
potwierdzają, że zwrócona została dokładnie taka strona. Warto tutaj zauważyć, że wyszukiwanie
ciągu „Witaj, jan!” nie działałoby poprawnie, ponieważ ten ciąg znaków jest składany z części
statycznej i dynamicznej, a ze względu na sposób, w jaki przygotowaliśmy szablon Jinja2, w końcowym
kodzie HTML znajduje się dodatkowa spacja pomiędzy tymi dwoma słowami. Aby uniknąć tego
błędu w teście (spowodowanego dodatkową spacją), użyte zostało wyrażenie regularne.
Następnym krokiem jest potwierdzenie konta, co stanowi kolejną małą przeszkodę. Potwierdzający
adres URL jest wysyłany do użytkownika e-mailem podczas rejestracji, więc w teście nie ma łatwego
sposobu na uzyskanie do niego dostępu. Rozwiązanie przedstawione w teście omija token wygenero-
wany w ramach rejestracji i generuje inny bezpośrednio z instancji User. Inną możliwością byłoby
wydobycie tokena przez analizę treści wiadomości e-mail, którą rozszerzenie Flask-Mail zapisuje
podczas pracy w konfiguracji testowej.
Gdy mamy już token, w następnym kroku testu możemy symulować kliknięcia przez użytkownika
adresu URL otrzymanego e-mailem tokena potwierdzającego. Jest to realizowane przez wysłanie
żądania GET na potwierdzający adres URL, który zawiera sam token. Odpowiedzią na to żądanie jest
przekierowanie na stronę główną, ale po raz kolejny używamy tu parametru follow_redirects=True,
więc klient testowy automatycznie wykonuje przekierowanie i zwróci ostateczną stronę. W otrzymanej
odpowiedzi poszukujemy teraz powitania i wyświetlonej w okienku wiadomości, która informuje
użytkownika, że jego konto zostało potwierdzone.
Ostatnim krokiem w tym teście jest wysłanie żądania GET na trasę wylogowania. Test będzie poszuki-
wał w otrzymanej odpowiedzi treści wiadomości pojawiającej się w okienku. W ten sposób można
potwierdzić, że test zakończył się sukcesem.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
def test_no_auth(self):
response = self.client.get(url_for('api.get_posts'),
content_type='application/json')
self.assertEqual(response.status_code, 401)
def test_posts(self):
# Dodaj użytkownika.
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u = User(email='jan@przyklad.pl', password='kot', confirmed=True,
role=r)
db.session.add(u)
db.session.commit()
# Napisz post.
response = self.client.post(
'/api/v1/posts/',
headers=self.get_api_headers('jan@przyklad.pl', 'kot'),
data=json.dumps({'body': 'treść posta wysłanego na *blog*'}))
self.assertEqual(response.status_code, 201)
url = response.headers.get('Location')
self.assertIsNotNone(url)
Metody setUp()i tearDown() służące do testowania interfejsu API są takie same jak w przypadku
zwykłej aplikacji. Różnica polega na tym, że nie musimy tu konfigurować obsługi plików cookie,
ponieważ interfejs API nie będzie ich używał. Metoda get_api_headers()jest metodą pomocniczą
zwracającą nagłówki, które muszą być wysłane z większością żądań API. Należą do nich dane uwie-
rzytelniające i nagłówki związane z typem MIME.
Test test_no_auth()jest prostym testem sprawdzającym, czy żądanie niezawierające danych uwierzy-
telniających zostanie odrzucone z błędem 401. Test test_posts()dodaje użytkownika do bazy danych,
a następnie używa interfejsu API, żeby przygotować nowy post i go odczytać. Każde żądanie wysyłające
dane musi je najpierw zakodować za pomocą funkcji json.dumps(), ponieważ klient testowy Fla-
ska nie zakodowuje ich automatycznie do formatu JSON. Podobnie, treści odpowiedzi są również
zwracane w formacie JSON i muszą zostać zdekodowane przy użyciu funkcji json.loads(). Dopiero
po takim przygotowaniu będzie można skontrolować ich zawartość.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Kompleksowe testy z użyciem Selenium
Klient testowy Flaska nie może w pełni naśladować środowiska uruchomionej aplikacji. Na przykład
nie będą poprawnie funkcjonować aplikacje wykorzystujące kod JavaScript działający w przeglądarce,
ponieważ kod JavaScript zawarty w odpowiedziach nie zostanie wykonany, tylko po prostu zwró-
cony do testu.
Jeżeli testy wymagają użycia pełnego środowiska, nie ma innej możliwości niż wykorzystanie praw-
dziwej przeglądarki internetowej podłączonej do aplikacji działającej na prawdziwym serwerze. Na
szczęście większość przeglądarek internetowych można zautomatyzować. Selenium (http://seleniumhq.org)
to narzędzie do automatyzacji przeglądarek, które obsługuje najpopularniejsze przeglądarki interne-
towe w trzech głównych systemach operacyjnych.
Interfejs Pythona przeznaczony do współpracy z Selenium można zainstalować za pomocą pole-
cenia pip:
(venv) $ pip install selenium
Selenium wymaga, żeby obok przeglądarki zainstalowany był też właściwy dla niej sterownik.
Aplikacja może przygotować złożone środowisko do testowania kilku przeglądarek, ponieważ do-
stępne są sterowniki dla wszystkich najpopularniejszych przeglądarek. Jednak w przypadku naszej
aplikacji do wykonywania automatycznych testów będziemy używać tylko przeglądarki Google
Chrome wraz z odpowiednim sterownikiem ChromeDriver. Jeśli wykorzystujesz komputer z syste-
mem macOS z instalatorem brew, możesz zainstalować sterownik ChromeDriver za pomocą tego
polecenia:
(venv) $ brew install chromedriver
W przypadku systemów Linux i Microsoft Windows, ale też systemu macOS bez instalatora brew,
można pobrać zwykły instalator ChromeDriver ze strony https://sites.google.com/a/chromium.org/
chromedriver/downloads.
Proces testowania za pomocą Selenium wymaga, żeby aplikacja działała na serwerze WWW, który
oczekuje rzeczywistych żądań HTTP. Metoda prezentowana w tym podrozdziale uruchamia aplikację
z serwerem w osobnym wątku, podczas gdy same testy są uruchamiane w głównym wątku. Selenium,
sterowane przez procedury testowe, uruchamia przeglądarkę internetową i łączy ją z aplikacją
w celu wykonania niezbędnych operacji.
Z tą metodą związany jest mały problem. Wynika on z tego, że po zakończeniu wszystkich testów
serwer Flaska musi zostać poprawnie zatrzymany, tak aby działające w tle zadania, takie jak mecha-
nizm mierzący pokrycie kodu, mogły zakończyć swoją pracę. Serwer WWW z pakietu Werkzeug
ma opcję wyłączania, ale jest odizolowany we własnym wątku, dlatego jedynym sposobem, aby popro-
sić serwer o zamknięcie, jest wysłanie zwykłego żądania HTTP. Na listingu 15.6 prezentuję przykła-
dową implementację trasy zamykania serwera.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
shutdown = request.environ.get('werkzeug.server.shutdown')
if not shutdown:
abort(500)
shutdown()
return 'Wyłączanie...'
Trasa zamykania będzie działać tylko wtedy, gdy aplikacja będzie uruchomiona w trybie testowym.
Próba wywołania jej w innych konfiguracjach zwróci odpowiedź z kodem statusu 404. Sama proce-
dura zamykania polega na wywołaniu funkcji zamykania, którą Werkzeug udostępnia w środowisku.
Po wywołaniu tej funkcji i powrocie z żądania serwer WWW będzie już wiedział, że musi się prawi-
dłowo zamknąć.
Na listingu 15.7 pokazuję kod przypadku testowego skonfigurowanego do uruchamiania testów
z wykorzystaniem Selenium.
class SeleniumTestCase(unittest.TestCase):
client = None
@classmethod
def setUpClass(cls):
# Uruchom Chrome.
options = webdriver.ChromeOptions()
options.add_argument('headless')
try:
cls.client = webdriver.Chrome(chrome_options=options)
except:
pass
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
# Uruchom serwer Flaska w wątku.
cls.server_thread = threading.Thread(
target=cls.app.run, kwargs={'debug': 'false',
'use_reloader': False,
'use_debugger': False})
cls.server_thread.start()
@classmethod
def tearDownClass(cls):
if cls.client:
# Zatrzymaj serwer Flaska i przeglądarkę.
cls.client.get('http://localhost:5000/shutdown')
cls.client.quit()
cls.server_thread.join()
def setUp(self):
if not self.client:
self.skipTest('Przeglądarka internetowa jest niedostępna')
def tearDown(self):
pass
Selenium obsługuje także wiele innych przeglądarek internetowych, nie ograniczając się
do Chrome. Jeśli będziesz chciał użyć innej przeglądarki internetowej lub przetesto-
wać dodatkowe przeglądarki, to zapoznaj się, proszę, z dokumentacją Selenium
(http://bit.ly/sel-docs).
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Metoda setUp(), która jest uruchamiana przed każdym testem, będzie pomijała poszczególne testy,
jeśli Selenium nie uda się uruchomić przeglądarki internetowej w metodzie startUpClass(). Na li-
stingu 15.8 można zobaczyć przykładowy test zbudowany z wykorzystaniem Selenium.
def test_admin_home_page(self):
# Przejdź do strony głównej.
self.client.get('http://localhost:5000/')
self.assertTrue(re.search('Witaj,\s+Nieznajomy!',
self.client.page_source))
# Zaloguj się.
self.client.find_element_by_name('email').\
send_keys('jan@przyklad.pl')
self.client.find_element_by_name('password').send_keys('kot')
self.client.find_element_by_name('submit').click()
self.assertTrue(re.search('Witaj,\s+jan!', self.client.page_source))
Test ten loguje się do aplikacji przy użyciu konta administratora utworzonego w metodzie setUpClass(),
a następnie otwiera stronę profilu użytkownika. Zwróć tutaj uwagę na to, jak bardzo różni się
metoda testowania od tej stosowanej przez klienta testowego. Podczas testowania za pomocą Selenium
testy wysyłają polecenia do przeglądarki internetowej i nigdy nie wchodzą w bezpośrednią interakcję
z aplikacją. Polecenia ściśle odpowiadają działaniom, które prawdziwy użytkownik wykonałby za
pomocą myszy lub klawiatury.
Test rozpoczyna się od wywołania metody get() na stronie głównej aplikacji. W przeglądarce odpo-
wiada to wpisaniu adresu URL w pasku adresu. Aby zweryfikować ten krok, w kodzie źródłowym
strony poszukiwane jest powitanie „Witaj, Nieznajomy!”.
W celu przejścia do strony logowania test szuka linka Zaloguj się, używając do tego funkcji
find_element_by_link_text(), a następnie wywołuje metodę click(), aby zasymulować uru-
chomienie prawdziwego kliknięcia w przeglądarce. Selenium udostępnia kilka wygodnych metod
find_element_by...(), które ułatwiają wyszukiwanie elementów na stronach HTML.
Aby zalogować się do aplikacji, test używa metody find_element_by_name() do wyszukania pól
formularza przeznaczonych na adres e-mail i hasło, a następnie zapisuje w nich tekst za pomocą
metody send_keys(). Formularz jest przesyłany przez wywołanie metody click() w przycisku
przesyłania. Po otrzymaniu odpowiedzi test sprawdza spersonalizowane powitanie, aby się upew-
nić, że logowanie się powiodło i przeglądarka wyświetla teraz stronę główną.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W ostatniej części testu wyszukiwany jest i klikany na pasku nawigacyjnym link Profil. Potem
w źródle strony przeszukiwany jest nagłówek z nazwą użytkownika, co pozwala ustalić, czy strona
profilu została załadowana.
Po uruchomieniu testów jednostkowych za pomocą polecenia flask test nie zobaczymy żadnej różnicy
w sposobie ich wykonania. Test jednostkowy test_admin_home_page (pokazany na listingu 15.8) uru-
chomi instancję przeglądarki Chrome bez widocznego okienka i wykona w niej wszystkie działania.
Jeśli chcesz zobaczyć działania wykonywane w oknie przeglądarki, to w metodzie setUpClass() umieść
w komentarzu wiersz options.add_argument('headless'), aby Selenium utworzyło zwykłe okno.
Czy warto?
W tej chwili możesz zadawać sobie pytanie, czy testowanie przy użyciu klienta testowego lub Selenium
jest naprawdę opłacalne i warte takiego nakładu pracy. To ważne pytanie i nie ma na nie prostej
odpowiedzi.
Niezależnie od tego, czy Ci się to podoba, czy też nie, Twoja aplikacja zostanie przetestowana. Jeśli
nie przetestujesz jej sam, to Twoi użytkownicy staną się bezwiednymi testerami. Gdy oni znajdą
różne błędy, to na Ciebie spadnie presja związana z koniecznością ich szybkiego usunięcia. Zawsze
należy przeprowadzać proste i ukierunkowane testy, takie jak te sprawdzające działanie modeli
baz danych i innych części aplikacji, które można uruchomić poza jej kontekstem. Takie testy mają
bardzo niski koszt wdrożenia i zapewniają prawidłowe funkcjonowanie podstawowych elemen-
tów logiki aplikacji.
Czasem konieczne jest też przygotowanie kompleksowe testów typu end-to-end, które można
przeprowadzić za pomocą Selenium lub klienta testowego Flaska. Ze względu na zwiększoną zło-
żoność takich testów należy je stosować tylko w przypadku funkcji, których nie można przetesto-
wać niezależnie od samej aplikacji. Kod aplikacji powinien być zorganizowany w taki sposób, aby
możliwe było przeniesienie logiki biznesowej do modułów niezależnych od kontekstu aplikacji,
co pozwoli na ich łatwiejsze przetestowanie. Kod funkcji widoku powinien być prosty i działać jak
cienka warstwa przyjmująca żądania i wywołująca odpowiednie akcje w innych klasach lub funkcjach
zawierających logikę aplikacji.
Tak, testowanie jest warte poświęconego czasu i pracy. Bardzo ważne jest też zaprojektowanie
wydajnej strategii testowania i napisanie kodu, który będzie mógł z niej skorzystać.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
222 Rozdział 15. Testowanie
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 16.
Wydajność
Nikt nie lubi powolnych aplikacji. Długie oczekiwanie na załadowanie się strony bardzo frustruje
użytkowników, dlatego tak ważne jest, aby jak najwcześniej wykryć i rozwiązać problemy związane
z wydajnością. W tym rozdziale omówimy dwa ważne aspekty wydajności aplikacji internetowych.
@main.after_app_request
def after_request(response):
for query in get_debug_queries():
if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:
current_app.logger.warning(
'Powolne zapytanie: %s\nParametry: %s\nCzas: %fs\nKontekst: %s\n' %
(query.statement, query.parameters, query.duration,
query.context))
return response
Ta funkcja jest dołączona do obsługi zdarzenia after_app_request, które działa w podobny sposób jak
zdarzenie before_app_request, przy czym jest ono wywoływane po zakończeniu pracy funkcji
223
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
widoku, która obsługuje to żądanie. Flask przekazuje obiekt odpowiedzi do procedury obsługi zdarze-
nia after_app_request, na wypadek gdyby konieczne było wprowadzenie zmian w tej odpowiedzi.
W tym przypadku funkcja obsługująca zdarzenie after_app_request nie modyfikuje odpowiedzi,
a jedynie pobiera czasy wykonania zapytań zarejestrowane przez Flask-SQLAlchemy. Następnie
dane powolnych zapytań zapisuje do rejestru aplikacji, który jest skonfigurowany w zmiennej
app.logger. Na koniec funkcja zwraca obiekt odpowiedzi, która zostanie następnie wysłana do
klienta.
Funkcja get_debug_queries() zwraca listę zapytań wykonanych podczas obsługi żądania. Informa-
cje dostępne na temat każdego zapytania przedstawiono w tabeli 16.1.
Nazwa Opis
statement Instrukcja SQL.
parameters Parametry używane z instrukcją SQL.
start_time Czas uruchomienia zapytania.
end_time Czas powrotu zapytania.
duration Czas trwania zapytania w sekundach.
context Ciąg znaków wskazujący lokalizację w kodzie źródłowym, z którego wydano zapytanie.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Konfiguracja protokołowania w dużej mierze zależy od platformy, na której działa aplikacja. Niektóre
przykłady takich konfiguracji przedstawiam w rozdziale 17.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeśli sklonowałeś repozytorium Git aplikacji na GitHub, możesz wywołać git
checkout 16b, aby sprawdzić tę wersję aplikacji.
Gdy aplikacja zostanie uruchomiona za pomocą polecenia flask profile, w konsoli będą wyświetlane
statystyki profilera dla każdego żądania, zawierające 25 najwolniejszych funkcji. Liczbę funkcji
pokazanych w tym raporcie można zmienić za pomocą opcji --length. Jeśli zdefiniowano opcję
--profile-dir, to dane profilowania każdego żądania będą zapisywane w pliku w podanym katalogu.
Pliki danych profilera można wykorzystać do generowania bardziej szczegółowych raportów, na
przykład takich zawierających wykres wywołań (ang. call graph). Więcej informacji na temat profilera
Pythona znajduje się w oficjalnej dokumentacji (http://bit.ly/py-profile).
I w ten sposób przygotowania do wdrożenia aplikacji zostały zakończone. W następnym rozdziale
znajdziesz przegląd sytuacji, których można się spodziewać podczas wdrażania aplikacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 17.
Wdrożenie
Serwer WWW dostarczany w pakiecie z frameworkiem Flask nie jest dostatecznie stabilny, bez-
pieczny ani wystarczająco wydajny, aby działać w środowisku produkcyjnym. W tym rozdziale
omówimy produkcyjne rodzaje wdrożeń aplikacji Flaska.
@manager.command
def deploy():
"""Wykonuje zadania wdrożeniowe."""
# Migracja bazy danych do najnowszej wersji
upgrade()
Wszystkie funkcje wywoływane przez to polecenie zostały już wcześniej przygotowane. Są one wywo-
ływane w ramach jednego polecenia, aby uprościć proces wdrażania aplikacji.
227
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie
git checkout 17a, aby pobrać tę wersję aplikacji.
Wszystkie te funkcje zostały zaprojektowane w taki sposób, żeby nie powodowały problemów w przy-
padku wielokrotnego wywołania. Takie przygotowanie funkcji aktualizacji umożliwia uruchomienie
samego polecenia wdrożenia deploy podczas każdej wykonywanej instalacji lub aktualizacji bez
obawy o skutki uboczne, jakie mogłyby zostać spowodowane przez funkcję uruchomioną w niewła-
ściwym momencie.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Błąd w aplikacji',
credentials=credentials,
secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
Przypomnij teraz sobie, że wszystkie klasy konfiguracji mają statyczną metodę init_app()wywoływaną
przez metodę create_app(), która do tej pory nie była jeszcze używana. W implementacji tej metody
dla klasy ProductionConfig rejestrator protokołu aplikacji został skonfigurowany z dodaną metodą
obsługi protokołu, która będzie wysyłała informacje o błędach na podany adres e-mail.
Poziom ważności dla wiadomości wysyłanych e-mailem został ustalony na logging.ERROR, więc
będą wysyłane tylko informacje o poważnych problemach. Wiadomości zarejestrowane na niższych
poziomach można zapisać do pliku, do protokołu systemowego syslog lub do dowolnego innego
obsługiwanego miejsca docelowego. Oczywiście wymaga to dodania odpowiednich procedur obsługi
protokołu. Metoda protokołowania używana dla tych wiadomości będzie w dużej mierze zależeć
od platformy hostingowej.
Wdrożenie w chmurze
Trend w hostingu aplikacji polega na umieszczaniu ich „w chmurze”, co może oznaczać wiele
różnych rzeczy. Na najbardziej podstawowym poziomie hosting w chmurze może oznaczać, że
aplikacja jest zainstalowana na jednym lub na większej liczbie serwerów wirtualnych, które pod
każdym względem działają i wyglądają jak maszyny fizyczne, ale w rzeczywistości są to maszyny
wirtualne zarządzane przez operatora chmury. Przykładem tego typu serwerów są serwery dostępne za
pośrednictwem usługi EC2 service z Amazon Web Services (AWS). Wdrażanie aplikacji na serwerze
wirtualnym jest bardzo podobne do tradycyjnego wdrażania na serwerze dedykowanym, o czym
przekonasz się w dalszej części tego rozdziału.
Bardziej zaawansowany model wdrażania polega na wykorzystaniu kontenerów (ang. containers).
Kontener izoluje aplikację w obszarze obrazu aplikacji i jej środowiska. Obraz kontenera zawiera
aplikację oraz wszystkie zależności niezbędne do jej uruchomienia. Platforma kontenerowa, taka
jak Docker, może następnie w dowolnym, kompatybilnym systemie zainstalować i wykonać
wstępnie wygenerowany obraz kontenera.
Inna opcja wdrażania, oficjalnie znana jako Platform as a Service (PaaS) — platforma jako usługa
–— uwalnia twórcę aplikacji od żmudnych zadań związanych z instalowaniem i utrzymywaniem
platform sprzętowych i programowych, na których działa jego aplikacja. W modelu PaaS dostawca
usług oferuje w pełni zarządzaną platformę, na której można uruchamiać aplikacje. Programista
aplikacji musi jedynie przesłać jej kod na serwery obsługiwane przez dostawcę. Aplikacja staje się wtedy
automatycznie dostępna, a dzieje się to zwykle w przeciągu kilku sekund. Większość dostawców
usług PaaS oferuje sposoby dynamicznego „skalowania” aplikacji, dodając lub usuwając serwery
w razie potrzeby, zależnie od liczby otrzymywanych żądań.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W pozostałej części tego rozdziału znajduje się wprowadzenie do pracy z platformą Heroku (jednym
z najpopularniejszych dostawców PaaS), kontenerami Dockera, jak również omówienie tradycyjnych
wdrożeń, odpowiednich dla serwerów dedykowanych lub wirtualnych.
Platforma Heroku
Heroku jest jednym z pierwszych dostawców usług PaaS, działającym na rynku od 2007 roku.
Platforma Heroku jest bardzo elastyczna i obsługuje długą listę języków programowania, w tym
język Python. Wdrożenie aplikacji na platformie Heroku wymaga użycia Gita, aby umieścić aplikację
na specjalnym serwerze Git Heroku, który automatycznie zajmie się procedurami instalacji, aktuali-
zacji, konfiguracji i wdrożenia aplikacji.
Heroku używa jednostek obliczeniowych zwanych dyno do mierzenia wykorzystania usług i obli-
czania za nie opłat. Najpopularniejszym typem dyno jest web dyno, która reprezentuje instancję
serwera WWW. Aplikacja może zwiększyć swoją zdolność do obsługi żądań, wdrażając więcej
web dyno, na których będą działały kolejne instancje aplikacji. Innym typem dyno jest worker dyno,
która służy do wykonywania zadań w tle lub innych zadań uzupełniających.
Platforma ta zapewnia dużą liczbę wtyczek i dodatków do obsługi baz danych, poczty e-mail i do wielu
innych usług. W kolejnych punktach opiszę niektóre ze szczegółów związanych z wdrażaniem
aplikacji Flasky na platformie Heroku.
Przygotowanie aplikacji
Żeby zacząć pracę z Heroku, musimy umieścić aplikację w repozytorium Git. Jeśli swoją aplikację
przechowujesz na zdalnym serwerze Git, takim jak GitHub lub Bitbucket, to operacja klonowania
utworzy lokalne repozytorium Git, które idealnie nadaje się do użycia z platformą Heroku. Jeśli aplika-
cja nie jest jeszcze hostowana w repozytorium Git, to musisz takie utworzyć na swoim komputerze.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Pierwszą rzeczą do zrobienia po zainstalowaniu CLI jest uwierzytelnienie przy użyciu konta Heroku za
pomocą polecenia heroku login:
$ heroku login
Enter your Heroku credentials.
Email: <twój-adres-e-mail>
Password: <twoje-hasło>
Ważne jest, żeby przesłać swój klucz publiczny SSH na platformę Heroku, ponieważ
bez niego nie będzie można wywołać polecenia git push. Zwykle polecenie logowania
automatycznie tworzy i przesyła klucz publiczny SSH, ale polecenia heroku keys:add
można użyć do przesłania klucza publicznego niezależnie od polecenia login, co
przydaje się, jeżeli musisz przesłać dodatkowe klucze.
Tworzenie aplikacji
Następnym krokiem jest utworzenie aplikacji. Najpierw trzeba się jeszcze upewnić, że kod aplikacji
jest pod kontrolą Gita. Jeśli korzystałeś już z repozytorium GitHub, na przykład w celu pobrania
kodu z tej książki, to masz już gotowe repozytorium. Jeśli tak nie jest, to musisz je teraz utworzyć.
Aby zarejestrować aplikację w Heroku, uruchom następujące polecenie z głównego katalogu aplikacji:
$ heroku create <nazwa_aplikacji>
Creating <nazwa_aplikacji>... done
https://<nazwa_aplikacji>.herokuapp.com/ | https://git.heroku.com/<nazwa_aplikacji>.git
Nazwy aplikacji Heroku muszą być globalnie unikatowe, dlatego musisz wymyślić nazwę, która nie
jest już używana przez żadną inną aplikację. Jak wynika z danych wyjściowych polecenia create,
po wdrożeniu aplikacja będzie dostępna pod adresem https://<nazwa_aplikacji>.herokuapp.com.
Heroku pozwala też na stosowanie własnych domen dla swoich aplikacji.
W ramach tworzenia aplikacji Heroku tworzy serwer Git dedykowany dla Twojej aplikacji, który
jest dostępny pod adresem https://git.heroku.com/<nazwa_aplikacji>.git. Polecenie create dodaje ten
serwer do lokalnego repozytorium Gita, używając do tego polecenia git remote z nazwą heroku:
$ git remote show heroku
* remote heroku
Fetch URL: https://git.heroku.com/<nazwa_aplikacji>.git
Push URL: https://git.heroku.com/<nazwa_aplikacji>.git
HEAD branch: (unknown)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Przygotowanie bazy danych
Heroku obsługuje również bazy danych Postgres. W wersji bezpłatnej pozwala wykorzystywać
małą bazę danych zawierającą nie więcej niż 10 000 wierszy. Użyj poniższego polecenia, aby dołączyć
do aplikacji bazę danych Postgres:
$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on <nazwa_aplikacji>... free
Database has been created and is available
! This database is empty. If upgrading, you can transfer
! data from another database with pg:copy
Created postgresql-cubic-41298 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
Jak wynika z danych wyjściowych polecenia, gdy aplikacja zostanie uruchomiona na platformie
Heroku, będzie mogła uzyskać lokalizację bazy danych i dane uwierzytelniające ze zmiennej środowi-
skowej DATABASE_URL. W tej zmiennej zapisany jest adres URL w formacie zgodnym z oczekiwaniami
SQLAlchemy. Przypomnij sobie, że skrypt config.py używa wartości ze zmiennej środowiskowej
DATABASE_URL, jeśli jest ona zdefiniowana. Dzięki temu połączenie z bazą danych Postgres będzie
działać w pełni automatycznie.
Konfigurowanie protokołowania
Zgłaszanie błędów krytycznych za pomocą wiadomości e-mail zostało dodane już wcześniej, ale
oprócz tego ważne jest skonfigurowanie protokołowania wiadomości z mniej istotnych kategorii.
Dobrym przykładem tego rodzaju komunikatów są ostrzeżenia o powolnych zapytaniach do bazy
danych, o których mówiliśmy w rozdziale 16.
Heroku uznaje za protokoły wszystkie dane wypisywane do strumieni stdout lub stderr, dlatego
też musimy przygotować odpowiednie procedury, aby kierować informacje we właściwe miejsce.
Protokołowane dane są przechwytywane przez platformę Heroku i udostępniane za pośrednictwem
klienta Heroku po wywołaniu polecenia heroku logs.
Konfigurację protokołowania można dodać do klasy ProductionConfig i zapisać ją w metodzie sta-
tycznej init_app(). Biorąc pod uwagę fakt, że ten typ rejestrowania jest szczególną cechą platfor-
my Heroku, lepszym podejściem będzie zdefiniowanie nowej konfiguracji, przeznaczonej specjalnie
dla tej platformy. W ten sposób klasę ProductionConfig można traktować jako bazową konfigurację
dla różnych typów platform produkcyjnych. Klasa HerokuConfig została pokazana na listingu 17.3.
# Zapis do stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Gdy aplikacja jest wykonywana przez platformę Heroku, musi używać nowo przygotowanej konfigu-
racji. Instancja aplikacji utworzona w pliku flasky.py korzysta ze zmiennej środowiskowej
FLASK_CONFIG, aby dowiedzieć się, jakiej konfiguracji ma użyć. Oznacza to, że ta zmienna musi być
odpowiednio zdefiniowana w środowisku Heroku. Zmienne środowiskowe dla platformy Heroku
są definiowane za pomocą polecenia config:set przekazanego do klienta Heroku:
$ heroku config:set FLASK_CONFIG=heroku
Setting FLASK_CONFIG and restarting <nazwa_aplikacji>... done, v4
FLASK_CONFIG: heroku
Dobrym pomysłem jest przygotowanie trudnego do odgadnięcia ciągu znaków jako tajnego klucza
aplikacji, który będzie służyć do podpisywania sesji użytkownika i tokenów uwierzytelniania.
W klasie bazowej Config zdefiniowany został atrybut SECRET_KEY, którego wartość przepisywana
jest ze zmiennej środowiskowej o tej samej nazwie, jeśli taka istnieje. Podczas pracy nad aplikacją
w systemie programistycznym można nie definiować tej zmiennej i pozwolić klasie Config wykorzy-
stać wartość umieszczoną w jej kodzie. Jednak na platformie produkcyjnej niezwykle ważne jest sto-
sowanie silnego tajnego klucza, który nie będzie nikomu znany, ponieważ wyciek klucza umożliwi
atakującemu fałszowanie zawartości sesji użytkownika lub wygenerowanie prawidłowych tokenów.
Aby zabezpieczyć ten klucz, musimy zdefiniować zmienną środowiskową SECRET_KEY i przypisać
jej unikatowy ciąg znaków, który nie będzie nigdzie przechowywany:
$ heroku config:set SECRET_KEY=d68653675379485599f7876a3b469a57
Setting SECRET_KEY and restarting <nazwa_aplikacji>... done, v4
SECRET_KEY: d68653675379485599f7876a3b469a57
Istnieje wiele sposobów generowania losowych ciągów, tak aby były odpowiednie do użycia jako tajne
klucze. Możesz to zrobić za pomocą Pythona w następujący sposób:
(venv) $ python -c "import uuid; print(uuid.uuid4().hex)"
d68653675379485599f7876a3b469a57
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Plik wymagań Heroku musi zawierać wszystkie typowe wymagania dla produkcyjnej wersji aplikacji,
a także pakiet psycopg2, który umożliwia SQL-Alchemy dostęp do bazy danych Postgres. Plik heroku.txt
z tymi zależnościami można dodać do katalogu requirements, a następnie zaimportować z pliku
requirements.txt najwyższego poziomu, tak jak pokazano to na listingu 17.4.
Kod aktywujący to rozszerzenie jest dodawany do funkcji produkcyjnej aplikacji, tak jak pokazano
na listingu 17.5.
Obsługa protokołu SSL musi być włączona tylko w trybie produkcyjnym i tylko wtedy, gdy platforma
go obsługuje. Aby ułatwić włączanie i wyłączanie protokołu SSL, dodajemy nową zmienną konfigura-
cyjną o nazwie SSL_REDIRECT. Podstawowa klasa Config przypisuje jej wartość False, dzięki czemu
domyślnie przekierowania SSL nie są używane. Z kolei klasa HerokuConfig zmienia tę wartość,
dzięki czemu przekierowania stosowane są tylko w tej konfiguracji. Implementację tej zmiennej
konfiguracyjnej pokazano na listingu 17.6.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
class HerokuConfig(ProductionConfig):
# ...
SSL_REDIRECT = True if os.environ.get('DYNO') else False
Wartość zmiennej SSL_REDIRECT w klasie HerokuConfig tylko wtedy otrzyma wartość True, gdy będzie
istniała zmienna środowiskowa DYNO. Zmienna ta jest definiowana przez platformę Heroku w swoim
środowisku, więc używanie konfiguracji Heroku do lokalnego wykonywania testów nie spowoduje
aktywowania przekierowań SSL.
Dzięki tym zmianom użytkownicy podczas uzyskiwania dostępu do aplikacji na Heroku będą
zmuszeni do korzystania z serwera SSL — ale jest tu jeszcze jeden problem, który należy rozwiązać,
aby zapewnić pełną obsługę tej funkcji. Podczas korzystania z Heroku klienci nie łączą się bezpo-
średnio z aplikacją, ale z odwrotnym serwerem proxy (ang. reverse proxy server). Odwrotny serwer
proxy odbiera żądania z wielu aplikacji i odpowiednio je przekazuje do każdej z nich. W tym typie
instalacji tylko serwer proxy działa w trybie SSL; połączenie SSL zostaje zakończone na serwerze
proxy, a aplikacje odbierają żądania przesłane z tego serwera bez szyfrowania. Niestety tworzy to
pewien problem, gdy aplikacja musi wygenerować bezwzględne adresy URL, ponieważ w aplikacji
Flaska obiekt żądania opisuje przekazane żądanie, które nie jest zaszyfrowane, a nie pierwotne żądanie,
które zostało wysłane przez klienta w zaszyfrowanym połączeniu.
Przykładem tego problemu może być generowanie linków do potwierdzenia konta lub resetowania
hasła, które są wysyłane do użytkowników e-mailem. Gdy wywoływana jest funkcja url_for() z pa-
rametrem _external=True w celu wygenerowania bezwzględnego adresu URL dla tych linków,
Flask użyje dla nich adresu http://, ponieważ nie wie, że istnieje odwrotne proxy, które przyjmuje
z zewnątrz szyfrowane połączenia.
Serwery proxy przekazują informacje opisujące pierwotne żądanie klienta do właściwych serwerów
WWW, używając przy tym niestandardowych nagłówków HTTP. Dzięki temu, przeglądając te na-
główki, można ustalić, czy użytkownik komunikuje się z aplikacją za pośrednictwem protokołu SSL.
Pakiet Werkzeug zapewnia oprogramowanie WSGI, które kontroluje niestandardowe nagłówki
z serwera proxy i odpowiednio aktualizuje obiekt żądania, tak aby na przykład zmienna request.
is_secure odzwierciedlała stan szyfrowania żądania wysłanego przez klienta do serwera proxy, a nie
żądania, które serwer proxy prześle do aplikacji. Na listingu 17.7 pokazuję, jak dodać pakiet
ProxyFix do aplikacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
jego obsłużeniem. Pakiet ProxyFix jest niezbędny nie tylko w przypadku platformy Heroku, ale także
w przypadku wszystkich wdrożeń korzystających z odwrotnego serwera proxy.
Jeśli chcesz uruchomić aplikację lokalnie na serwerze Gunicorn, użyj następującego polecenia:
(venv) $ gunicorn flasky:app
[2017-08-03 23:54:36 -0700] [INFO] Starting gunicorn 19.7.1
[2017-08-03 23:54:36 -0700] [INFO] Listening at: http://127.0.0.1:8000 (68982)
[2017-08-03 23:54:36 -0700] [INFO] Using worker: sync
[2017-08-03 23:54:36 -0700] [INFO] Booting worker with pid: 68985
Argument flasky:app informuje serwer, gdzie znajduje się instancja aplikacji. Nazwa podana przed
dwukropkiem to pakiet lub moduł definiujący instancję, a nazwa po dwukropku to rzeczywista
nazwa instancji aplikacji. Zauważ, że serwer Gunicorn domyślnie używa portu 8000, a nie 5000,
jak robi to Flask. Podobnie jak w programistycznym serwerze WWW Flaska, możesz zakończyć
pracę serwera Gunicorn za pomocą kombinacji klawiszy Ctrl+C.
Aby uruchomić serwer Waitress, użyj polecenia waitress-serve, tak jak poniżej:
(venv) $ waitress-serve --port 8000 flasky:app
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dodawanie pliku Procfile
Platforma Heroku musi wiedzieć, jakiego polecenia ma użyć, aby uruchomić aplikację. To polecenie
jest zapisywane w specjalnym pliku o nazwie Procfile. Musi się on znajdować w katalogu najwyższego
poziomu aplikacji.
Na listingu 17.8 przedstawiam zawartość tego pliku.
Format pliku Procfile jest bardzo prosty: w każdym wierszu podana jest nazwa zadania, za nią dwu-
kropek, a następnie polecenie uruchamiające to zadanie. Nazwa web jest wyjątkowa, ponieważ jest
rozpoznawana przez platformę Heroku jako zadanie uruchamiające serwer WWW. Heroku przypi-
suje temu zadaniu zmienną środowiskową PORT przechowującą numer portu, na którym aplikacja
będzie musiała nasłuchiwać żądań. Serwer Gunicorn domyślnie stosuje się do wartości w zmien-
nej PORT, jeśli tylko istnieje ona w środowisku, więc nie ma potrzeby dołączania jej do polecenia
uruchamiającego.
W przypadku jeśli korzystasz z systemu Microsoft Windows lub chcesz, aby Twoja
aplikacja była w pełni kompatybilna z tą platformą, możesz użyć serwera WWW
Waitress, korzystając z polecenia:
web: waitress-serve --port=$PORT flasky:app
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Pliku .env nie należy dodawać do systemów kontroli wersji, ponieważ zawiera hasła
i inne poufne informacje o koncie.
Przed uruchomieniem aplikacji należy wykonać zadanie wdrożeniowe, aby skonfigurować bazę da-
nych. Takie jednorazowe zadania można wykonać za pomocą polecenia local:run:
(venv) $ heroku local:run flask deploy
[OKAY] Loaded ENV .env File as KEY=VALUE Format
INFO Context impl SQLiteImpl.
INFO Will assume non-transactional DDL.
INFO Running upgrade -> 38c4e85512a9, initial migration
INFO Running upgrade 38c4e85512a9 -> 456a945560f6, login support
INFO Running upgrade 456a945560f6 -> 190163627111, account confirmation
INFO Running upgrade 190163627111 -> 56ed7d33de8d, user roles
INFO Running upgrade 56ed7d33de8d -> d66f086b258, user information
INFO Running upgrade d66f086b258 -> 198b0eebcf9, caching of avatar hashes
INFO Running upgrade 198b0eebcf9 -> 1b966e7f4b9e, post model
INFO Running upgrade 1b966e7f4b9e -> 288cd3dc5a8, rich text posts
INFO Running upgrade 288cd3dc5a8 -> 2356a38169ea, followers
INFO Running upgrade 2356a38169ea -> 51f5ccfba190, comments
Polecenie heroku local odczytuje plik Procfile i wykonuje zdefiniowane w nim zadania:
(venv) $ heroku local
[OKAY] Loaded ENV .env File as KEY=VALUE Format
11:37:49 AM web.1 | [INFO] Starting gunicorn 19.7.1
11:37:49 AM web.1 | [INFO] Listening at: http://0.0.0.0:5000 (91686)
11:37:49 AM web.1 | [INFO] Using worker: sync
11:37:49 AM web.1 | [INFO] Booting worker with pid: 91689
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.6.2
remote: -----> Installing pip
remote: -----> Installing requirements with pip
...
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 49.4M
remote: -----> Launching...
remote: Released v8
remote: https://<appname>.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/<appname>.git
* [new branch] master -> master
Aplikacja jest teraz wdrożona i działa. Na tym etapie nie będzie jeszcze działać poprawnie, ponieważ
nie została jeszcze wykonana instrukcja deploy, inicjująca tabele bazy danych. Odpowiednie polecenie
można uruchomić w następujący sposób:
$ heroku run flask deploy
Running flask deploy on <appname>... up, run.3771 (Free)
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
...
Po utworzeniu i skonfigurowaniu tabel bazy danych aplikację można uruchomić ponownie, tak aby
bez problemów korzystała z zaktualizowanej bazy danych:
$ heroku restart
Restarting dynos on <nazwa_aplikacji>... done
Aplikacja powinna być teraz w pełni wdrożona i dostępna online pod adresem https://
<nazwa_aplikacji>.herokuapp.com.
Podczas testowania może być wygodne ograniczenie się do ostatnich wpisów w pliku dziennika,
co można wykonać w następujący sposób:
$ heroku logs -t
Wdrażanie aktualizacji
Gdy aplikacja Heroku wymaga aktualizacji, należy powtórzyć opisany wyżej proces. Po zatwierdzeniu
wszystkich zmian w repozytorium Git aktualizację można wykonać za pomocą następujących
poleceń:
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
$ heroku maintenance:on
$ git push heroku master
$ heroku run flask deploy
$ heroku restart
$ heroku maintenance:off
Opcja maintenance dostępna w interfejsie CLI platformy Heroku spowoduje przejście aplikacji
w tryb offline na czas aktualizacji i wyświetli statyczną stronę informującą użytkowników, że wywoły-
wana przez nich strona wkrótce powróci. Uniemożliwia to użytkownikom dostęp do aplikacji na
czas procesu aktualizacji.
Instalowanie Dockera
Najpopularniejszą platformą kontenerową jest Docker (https://www.docker.com), która udostępnia
bezpłatną wersję Community Edition (znaną jako Docker CE) i płatną wersję Enterprise (Docker
EE). Platformę tę można zainstalować na trzech głównych systemach operacyjnych dla komputerów
stacjonarnych, a także na serwerach znajdujących się w chmurze. Najprostszym sposobem na opraco-
wanie i przetestowanie aplikacji „kontenerowej” jest zainstalowanie w systemie programistycznym
platformy Docker CE. W sklepie Dockera (https://hub.docker.com/search/?offering=community&type=edition)
dostępne są instalatory dla systemów macOS i Microsoft Windows. Ta strona zawiera także instrukcje
instalacji dla dystrybucji Linux CentOS, Fedora, Debian i Ubuntu.
Po zakończeniu instalacji Dockera CE w naszym systemie w terminalu powinniśmy mieć dostęp
do polecenia docker:
$ docker version
Client:
Version: 17.06.0-ce
API version: 1.30
Go version: go1.8.3
Git commit: 02c1d87
Built: Fri Jun 23 21:31:53 2017
OS/Arch: darwin/amd64
Server:
Version: 17.06.0-ce
API version: 1.30 (minimum version 1.12)
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Go version: go1.8.3
Git commit: 02c1d87
Built: Fri Jun 23 21:51:55 2017
OS/Arch: linux/amd64
Experimental: true
WORKDIR /home/flasky
Polecenia kompilacji, które mogą być używane w pliku Dockerfile, są szczegółowo opisane w jego do-
kumentacji (https://docs.docker.com/engine/reference/builder/). Zasadniczo są to polecenia wdrażania,
instalujące i konfigurujące aplikację w systemie plików kontenera, który jest odizolowany od systemu
operacyjnego.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Polecenie FROM jest wymagane we wszystkich plikach Dockerfile, aby określić podstawowy obraz
kontenera, od którego należy rozpocząć pracę. W większości przypadków będzie to obraz dostępny
publicznie w Docker Hub (https://hub.docker.com/), czyli repozytorium obrazów kontenerów
Dockera. Repozytorium zawiera oficjalne obrazy dla kilku wersji interpretera Pythona. Są to obrazy,
które zawierają podstawowy system operacyjny z zainstalowanym Pythonem. Wszystkie obrazy
są oznaczone nazwą i znacznikiem. Nazwa oficjalnego obrazu Pythona na Docker Hub to po prostu
python. Na stronie Docker Hub można zobaczyć inne znaczniki dostępne dla danego obrazu.
W przypadku obrazu python znaczniki służą do określania żądanej wersji interpretera i platformy.
W naszej aplikacji używany jest interpreter w wersji 3.6, zbudowany na bazie dystrybucji Alpine
Linux (https://alpinelinux.org/). Alpine Linux to platforma powszechnie stosowana w obrazach konte-
nerów ze względu na swój mały rozmiar.
Polecenie ENV definiuje zmienne środowiskowe dla środowiska wykonawczego. To polecenie przyj-
muje dwa argumenty: nazwę zmiennej i jej wartość. Wszystkie zmienne środowiskowe zdefinio-
wane za pomocą tego polecenia będą dostępne, gdy zostanie uruchomiony kontener bazujący na
tym obrazie. Zdefiniowano tutaj zmienną FLASK_APP wymaganą przez polecenie flask, podobnie
jak FLASK_CONFIG — nazwę klasy konfiguracji, której aplikacja używa podczas uruchamiania. Ten ro-
dzaj wdrożenia będzie korzystać z nowej konfiguracji o nazwie docker, zaimplementowanej w klasie
DockerConfig przedstawionej na listingu 17.10. Ta nowa klasa konfiguracji dziedziczy po klasie
ProductionConfig i ustala, że protokół ma być kierowany do strumienia stderr, który Docker
automatycznie przechwytuje i udostępnia za pomocą polecenia docker logs.
# Zapis do stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
config = {
# ...
'docker': DockerConfig,
# ...
}
Polecenie RUN wykonuje instrukcje w kontekście obrazu kontenera. W pierwszym wystąpieniu RUN
w kontenerze tworzony jest użytkownik flasky. Polecenie adduser jest częścią systemu Alpine Linux
i jest dostępne w obrazie podstawowym wybranym poleceniem FROM. Dołączenie argumentu -D do
polecenia adduser pomija interaktywny monit o hasło użytkownika.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Polecenie USER wybiera użytkownika, z którym będzie działał kontener, a także użytkownika dla pozo-
stałych poleceń w pliku Dockerfile. Docker domyślnie używa użytkownika root, ale dobrą praktyką
jest też przejście na zwykłego użytkownika, gdy uprawnienia administratora nie będą potrzebne.
Polecenie WORKDIR definiuje katalog najwyższego poziomu, w którym aplikacja zostanie zainsta-
lowana. W przypadku tej aplikacji używany jest katalog domowy nowo utworzonego użytkownika
flasky. Pozostałe polecenia w pliku Dockerfile zostaną wykonane z tym katalogiem jako katalogiem
bieżącym.
Polecenie COPY kopiuje pliki z lokalnego systemu plików do systemu plików kontenera. Katalogi
requirements, app i migrations są kopiowane w całości, a następnie do katalogu najwyższego poziomu
kopiowane są również pliki flasky.py, config.py i nowe pliki boot.sh (omówię je za chwilę).
Dwa dodatkowe polecenia RUN tworzą środowisko wirtualne i instalują w nim wymagane pakiety.
Dla Dockera przygotowałem dedykowany plik wymagań o nazwie requirements/docker.txt. Ten plik
importuje wszystkie zależności z pliku requirements/common.txt i dodaje serwer Gunicorn, który
będzie używany jako serwer WWW, podobnie jak robiliśmy to przy wdrożeniu na platformie Heroku.
Polecenie EXPOSE określa port, na którym aplikacja działająca w kontenerze zainstaluje swój serwer.
Po uruchomieniu kontenera Docker powiąże ten port z prawdziwym portem na hoście, tak aby
kontener mógł odbierać żądania ze świata zewnętrznego.
Ostatnim poleceniem jest ENTRYPOINT. Określa ono sposób uruchomienia aplikacji podczas urucha-
miania kontenera. Jako skrypt startowy posłuży nam nowy plik o nazwie boot.sh, który został już
skopiowany do naszego kontenera. Na listingu 17.11 została pokazana zawartość tego pliku.
Skrypt rozpoczyna się od aktywacji wirtualnego środowiska venv, które zostało utworzone w ramach
kompilacji. Następnie uruchamia polecenie deploy dla aplikacji, zastosowane już wcześniej w tym
rozdziale, a także używane do wdrożenia na platformie Heroku. Spowoduje to utworzenie nowej
bazy danych, następnie uaktualnienie jej do najnowszej wersji i utworzenie domyślnych ról. Ponieważ
nie została zdefiniowana zmienna środowiskowa DATABASE_URL, baza danych będzie używać silni-
ka SQLite. Następnie uruchamiany jest serwer Gunicorn nasłuchujący na porcie 5000. Docker
przechwytuje wszystkie dane wyjściowe z aplikacji i udostępnia je jako pliki protokołów, dlatego
Gunicorn jest skonfigurowany tak, że własne protokoły dostępu i błędów kieruje na standardowe
wyjście. Uruchomienie serwera Gunicorn za pomocą polecenia exec spowoduje, że serwer przejmie
proces pliku boot.sh. Dzieje się to tak, ponieważ Docker zwraca szczególną uwagę na proces urucha-
miania kontenera i oczekuje, że będzie on głównym procesem przez cały czas swojego funkcjo-
nowania. Po zakończeniu tego procesu kończy się również działanie kontenera.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obraz kontenera dla Flasky można teraz zbudować w następujący sposób:
$ docker build -t flasky:latest .
Sending build context to Docker daemon 51.08MB
Step 1/14 : FROM python:3.6-alpine
---> a6beab4fa70b
...
Successfully built 930e17a89b42
Successfully tagged flasky:latest
Dodanie argumentu -t do polecenia docker build nadaje obrazowi kontenera nazwę i znacznik, które
muszą być oddzielone dwukropkiem. Znacznik latest jest zwykle używany w najnowszej wersji
obrazu kontenera. Kropka na końcu polecenia build ustala, że podczas kompilacji bieżący katalog
będzie katalogiem najwyższego poziomu. To w tym katalogu Docker będzie szukał pliku Dockerfile,
a także udostępni w nim pliki i wszystkie podkatalogi do dodania do obrazu kontenera.
W wyniku poprawnie wykonanego polecenia docker build zbudowany zostanie obraz kontenera,
a następnie będzie on przechowywany w lokalnym repozytorium obrazów. Natomiast polecenie
docker images wyświetli zawartość repozytorium obrazów w systemie:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
flasky latest 930e17a89b42 5 minutes ago 127MB
python 3.6-alpine a6beab4fa70b 3 weeks ago 88.7MB
Na tym listingu znajdują się wyłącznie obraz flasky:latest oraz bazowy obraz interpretera języka
Python 3.6, do którego odwołuje się polecenie FROM z pliku Dockerfile. Ten obraz jest pobierany
i instalowany przez Dockera w ramach procesu kompilacji.
Uruchamianie kontenera
Po zbudowaniu dla aplikacji obrazu kontenera wystarczy go już tylko uruchomić. Polecenie docker
run sprawia, że jest to bardzo proste zadanie:
$ docker run --name flasky -d -p 8000:5000 \
-e SECRET_KEY=57d40f677aff4d8d96df97223c74d217 \
-e MAIL_USERNAME=<twoja—nazwa_użytkownika-gmail> \
-e MAIL_PASSWORD=<twoje-hasło-gmail> flasky:latest
Opcja --name nadaje kontenerowi nazwę. Nazywanie kontenerów jest opcjonalne. Jeśli nie podasz
nazwy, to Docker będzie ją generował przy użyciu losowo wybranych słów.
Opcja -d uruchamia kontener w trybie odłączonym (ang. detached), co oznacza, że kontener będzie
działał w tle w systemie operacyjnym. Kontener, który nie został odłączony, działa jako zadanie
pierwszego planu dołączone do sesji konsoli.
Opcja -p przypisuje port 8000 w systemie hosta do portu 5000 wewnątrz kontenera. Docker pozwala
na elastyczne mapowanie portów kontenerów na dowolny port w systemie hosta. Umożliwia to
uruchamianie dwóch lub więcej instancji tego samego obrazu kontenera na różnych portach hosta,
podczas gdy każda instancja korzysta z własnego, zwirtualizowanego portu 5000.
Opcja -e definiuje zmienne środowiskowe, które będą istnieć w kontekście kontenera, oprócz wszel-
kich zmiennych zdefiniowanych w czasie kompilacji za pomocą komendy ENV w pliku Dockerfile.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Wartość przypisana do zmiennej SECRET_KEY zapewnia, że sesje użytkownika i tokeny są podpisywane
unikatowym i bardzo trudnym do odgadnięcia kluczem. Dla tej zmiennej należy zatem wygene-
rować swój własny unikatowy klucz. Wartości zmiennych MAIL_USERNAME i MAIL_PASSWORD konfigu-
rują wysyłanie wiadomości e-mail za pośrednictwem usługi Gmail. W przypadku wdrożenia pro-
dukcyjnego korzystającego z innego dostawcy usług e-mail należy również zdefiniować zmienne
MAIL_SERVER, MAIL_PORT i MAIL_USE_TLS.
Ostatnim argumentem w poleceniu docker run jest nazwa i znacznik kontenera, który ma zostać uru-
chomiony. Powinno to pasować do nazwy i znacznika podanego w opcji -t polecenia docker build.
Gdy rozpoczyna się działanie kontenera w tle, polecenie docker run wypisuje identyfikator kontenera
w konsoli. Jest to unikatowy 256-bitowy identyfikator zapisany w notacji szesnastkowej. Tego identyfi-
katora można użyć we wszelkich poleceniach wymagających odwołania do kontenera (w praktyce
wystarczy podać tylko kilka pierwszych znaków identyfikatora, aby jednoznacznie zidentyfikować
swój kontener).
W celu potwierdzenia, że kontener jest już uruchomiony, można użyć polecenia docker ps:
$ docker ps
CONTAINER ID IMAGE CREATED STATUS PORTS NAMES
71357ee776ae flasky:latest 4 secs ago Up 8 secs 0.0.0.0:8000->5000/tcp flasky
Ze względu na to, że kontener jest teraz gotowy do pracy, możesz uzyskać dostęp do aplikacji kon-
tenera na porcie 8000 systemu, używając lokalnie adresu http://localhost:8000 lub z dowolnego innego
komputera w sieci za pomocą adresu http://<adres_ip>:8000.
Aby zatrzymać ten kontener, użyj polecenia docker stop:
$ docker stop 71357ee776ae
71357ee776ae
Polecenie stop zatrzymuje kontener, ale nie usuwa go z systemu. Aby go usunąć, użyj polecenia
docker rm:
$ docker rm 71357ee776ae
71357ee776ae
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W tym przykładzie Docker otworzy sesję powłoki za pomocą polecenia sh (powłoka Uniksa) bez
przerywania działania kontenera. Opcja -it łączy sesję terminala, z której wydano polecenie, z nowym
procesem, dzięki czemu powłoka może być obsługiwana interaktywnie. Jeśli kontener zawiera inne,
bardziej zaawansowane powłoki, takie jak na przykład bash lub nawet interpreter Pythona, to ich rów-
nież będzie można użyć.
Częstą strategią podczas rozwiązywania problemów z kontenerami jest tworzenie specjalnego obrazu
zawierającego dodatkowe narzędzia, takie jak debugger, który można później wywołać z sesji powłoki.
Aby się zalogować do innego repozytorium obrazów kontenera niż Docker Hub, wpisz
adres swojego repozytorium jako argument polecenia docker login.
Lokalne obrazy kontenerów mają prostą nazwę. W celu przygotowania się do wypchnięcia obrazu
do Docker Huba nazwa obrazu musi być poprzedzona nazwą konta Docker Hub i ukośnikiem w roli
separatora. Zbudowanemu wcześniej obrazowi flasky:latest można przypisać dodatkową nazwę,
poprawnie sformatowaną na potrzeby przesłania na Docker Hub. Wystarczy użyć polecenia docker tag:
$ docker tag flasky:latest <twoja-nazwa-użytkownika-dockerhub>/flasky:latest
Obraz kontenera jest teraz publicznie dostępny i na jego podstawie każdy może uruchomić kontener
za pomocą polecenia docker run:
$ docker run --name flasky -d -p 8000:5000 \
<twoja-nazwa-użytkownika-dockerhub>/flasky:latest
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Korzystanie z zewnętrznej bazy danych
Niestety wadą sposobu, w jaki Flasky został wdrożony jako kontener Dockera, jest to, że domyślna
baza danych SQLite znajduje się w tym samym kontenerze co aplikacja. Utrudnia to bardzo aktualiza-
cję, ponieważ po zatrzymaniu działającego kontenera baza danych zostaje usunięta razem z nim.
Lepszym rozwiązaniem jest oddzielenie serwera bazy danych od kontenera aplikacji. Dzięki temu ak-
tualizacja aplikacji, przy jednoczesnym zachowaniu bazy danych, staje się bardzo łatwym zadaniem,
ponieważ jedyne co trzeba w takiej sytuacji zrobić, to zastąpić kontener aplikacji jego nową wersją.
Docker promuje modułowe podejście do budowy aplikacji, w której każda usługa jest hostowana
we własnym kontenerze. Dostępne są publiczne obrazy kontenerów dla MySQL, Postgres i dla wielu
jeszcze innych serwerów baz danych. Polecenia docker run można użyć do wdrożenia dowolnego
z nich bezpośrednio w swoim systemie. Używając poniższego polecenia, można wykorzystać w na-
szym systemie serwer bazy danych MySQL 5.7:
$ docker run --name mysql -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_DATABASE=flasky -e MYSQL_USER=flasky \
-e MYSQL_PASSWORD=<hasło-do-bazy-danych> \
mysql/mysql-server:5.7
Polecenie to tworzy kontener o nazwie mysql, który będzie działać w tle. Opcja -e przypisuje do niego
kilka zmiennych środowiskowych, które kontener przyjmuje jako konfigurację. Ta i wiele innych
zmiennych są dokładnie opisane na stronie Docker Hub dla obrazu MySQL. Powyższe polecenie
konfiguruje bazę danych z losowo generowanym hasłem roota (użyj polecenia docker logs mysql
zaraz po uruchomieniu kontenera, aby zobaczyć w protokole wygenerowane hasło) oraz z zupełnie
nową bazą danych o nazwie flasky, do której dostęp może uzyskać specjalnie zdefiniowany użytkow-
nik o nazwie flasky. Musimy zdefiniować bezpieczne hasło dla tego użytkownika, zapisując je jako
wartość zmiennej środowiskowej MYSQL_PASSWORD.
Aby połączyć się z bazą danych MySQL, SQLAlchemy wymaga zainstalowania odpowiedniego pakietu
klienta MySQL, takiego jak pymysql. Pakiet ten można dopisać do pliku wymagań docker.txt.
Jeśli nadal używasz poprzedniego kontenera aplikacji, to zatrzymaj go teraz i usuń za pomocą pole-
cenia docker rm -f. Następnie uruchom nowy kontener już z zaktualizowaną aplikacją:
$ docker run -d -p 8000:5000 --link mysql:dbserver \
-e DATABASE_URL=mysql+pymysql://flasky:<hasło-do-bazy-danych>@dbserver/flasky \
-e MAIL_USERNAME=<twoja-nazwa-użytkowanika-gmail> -e MAIL_PASSWORD=<twoje-hasło-gmail> \
flasky:latest
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
W pokazanym tutaj poleceniu docker run znalazły się dwa nowe elementy. Opcja --link konfiguruje
połączenie między nowym i istniejącym już innym kontenerem. Argument tej opcji składa się
z dwóch nazw oddzielonych znakiem dwukropka: nazwy lub identyfikatora kontenera źródłowego
oraz aliasu tego kontenera w tworzonym kontenerze. W tym przykładzie kontenerem źródłowym
jest mysql, czyli przygotowany już wcześniej kontener bazy danych. Będzie on dostępny w nowym
kontenerze aplikacji Flasky z nazwą hosta dbserver.
Na zakończenie konfiguracji dodawana jest zmienna środowiskowa DATABASE_URL z adresem URL,
który wskazuje bazę danych flasky w kontenerze mysql. Alias dbserver jest używany jako host
bazy danych, ponieważ Docker upewnia się, że ta nazwa ma powiązany adres IP właściwego
kontenera. Wartość zmiennej środowiskowej MYSQL_PASSWORD, zdefiniowanej w kontenerze mysql,
również musi znaleźć się w adresie URL tego kontenera. Wartość DATABASE_URL zastępuje wskazanie
domyślnej bazy danych SQLite, a zatem dzięki tej prostej zmianie kontener zostanie skonfiguro-
wany do łączenia się z bazą danych MySQL.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 17.12. docker-compose.yml: Konfiguracja orkiestracji
version: '3'
services:
flasky:
build: .
ports:
- "8000:5000"
env_file: .env
links:
- mysql:dbserver
restart: always
mysql:
image: "mysql/mysql-server:5.7"
env_file: .env-mysql
restart: always
Plik ten został napisany w formacie YAML. Jest to prosty format pozwalający na zapisywanie struktur
hierarchicznych składających się z par klucz-wartość oraz z list. Klucz version określa, której wer-
sji narzędzi Compose będziemy używać, natomiast klucz services definiuje kontenery aplikacji jako
jej elementy podrzędne. W przypadku aplikacji Flasky są to dwie usługi o nazwie flasky i mysql.
Dla usług budowanych jako część aplikacji, takich jak flasky, podklucze definiują argumenty przeka-
zywane poleceniom docker build i docker run. Klucz build określa katalog kompilacji, w którym
znajduje się plik Dockerfile. Klucz ports definiuje mapowanie portów sieciowych. Natomiast klucz
env_file stanowi wygodny sposób na zdefiniowanie kilku zmiennych środowiskowych, których
będzie potrzebował kontener. Klucz links ustanawia połączenie z kontenerem MySQL, udostępniając
go wraz z nazwą dbserver. Wartość always przypisana kluczowi restart jest prostą metodą nakazania
platformie Docker automatycznego restartu kontenera, jeżeli zostanie on nieoczekiwanie za-
mknięty. Plik .env dla wdrożenia naszej aplikacji powinien zawierać następujące zmienne:
FLASK_APP=flasky.py
FLASK_CONFIG=docker
SECRET_KEY=3128b4588e7f4305b5501025c13ceca5
MAIL_USERNAME=<twoja-nazwa-użytkowanika-gmail>
MAIL_PASSWORD=<twoje-hasło-gmail>
DATABASE_URL=mysql+pymysql://flasky:<hasło-do-bazy-danych>@dbserver/flasky
Usługa mysql ma prostszą strukturę, ponieważ jest to usługa uruchamiana z obrazu bazowego,
która nie wymaga osobnego budowania. Klucz image określa nazwę i znacznik obrazu kontenera,
który ma być używany dla tej usługi. Po wydaniu polecenia docker run Docker pobierze obraz ten
z rejestru obrazów kontenerów. Klucze env_file i restart są podobne do kluczy używanych w konte-
nerze flasky. Zwróć jednak uwagę na to, że zmienne środowiskowe dla kontenera MySQL są prze-
chowywane w osobnym pliku o nazwie .env-mysql. Chociaż łatwiej byłoby dodać do pliku .env
zmienne środowiskowe potrzebne wszystkim kontenerom, to dobrą praktyką jest zapobieganie dostę-
powi jednego kontenera do tajemnic innego. Plik .env-mysql wymaga zdefiniowania następujących
zmiennych środowiskowych:
MYSQL_RANDOM_ROOT_PASSWORD=yes
MYSQL_DATABASE=flasky
MYSQL_USER=flasky
MYSQL_PASSWORD=<hasło-do-bazy-danych>
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Pliki .env i .env-mysql zawierają hasła i inne poufne informacje, więc nigdy nie powinny
być dodawane do systemu kontroli wersji.
while true; do
flask deploy
if [[ "$?" == "0" ]]; then
break
fi
echo Polecenie deploy się nie powiodło, spróbuję ponownie za 5 sekund...
sleep 5
done
Dzięki uruchamianiu polecenia flask deploy w pętli kontener może reagować na awarie wynikające
z braku gotowości usługi bazy danych do przyjmowania żądań.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Opcja --build dodana do polecenia docker-compose up określa, że przed uruchomieniem aplikacji
powinien zostać uruchomiony krok tworzenia kontenera. Spowoduje to zbudowanie obrazu
kontenera flasky. Po utworzeniu tego obrazu zostaną uruchomione kontenery mysql i flasky —
w tej właśnie kolejności. Opcja -d uruchamia kontenery w trybie odłączonym, tak jak w przypadku
pojedynczego kontenera. Po kilku sekundach aplikacja powinna być już uruchomiona i działać w tle,
a my powinniśmy być w stanie połączyć się z nią pod adresem http://localhost:8000.
Narzędzia Compose konsolidują protokoły ze wszystkich kontenerów w jednym strumieniu, dlatego
można je zobaczyć za pomocą polecenia docker-compose logs:
$ docker-compose logs
Aby zaktualizować aplikację do nowej wersji, wystarczy wprowadzić w niej niezbędne zmiany i powtó-
rzyć użyte już wcześniej polecenie docker-compose up. Jeśli coś się zmieni, to narzędzia Compose
przebudują kontener aplikacji, a następnie zastąpią ten starszy kontener nowym.
W celu zatrzymania aplikacji użyj polecenia docker-compose down lub docker-compose rm --stop --force,
jeśli będziesz chciał również usunąć zatrzymane kontenery.
Jeśli chcesz wyświetlić listę obrazów kontenerów przechowywanych w systemie, to zastosuj polecenie
docker images. Jeśli na liście znajdziesz obrazy do usunięcia, możesz zrobić to za pomocą polecenia
docker rmi.
Niektóre kontenery tworzą wirtualne woluminy na komputerze hosta, które są używane do prze-
chowywania danych poza systemem plików kontenera. Na przykład obraz kontenera MySQL
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
umieszcza wszystkie pliki bazy danych w woluminie. Listę wszystkich woluminów istniejących
w systemie możesz wyświetlić za pomocą polecenia docker volume ls. Natomiast w celu usunięcia
nieużywanego woluminu użyj polecenia docker volume rm.
Jeśli wolisz stosować mechanizmy automatycznego czyszczenia, to polecenie docker system prune --
volumes usunie wszelkie nieużywane obrazy lub woluminy oraz wszystkie zatrzymane kontenery,
które pozostają w systemie.
Tradycyjne wdrożenia
Do tej pory widzieliśmy już, jak platformy Heroku i Docker zarządzają wdrożeniami. Aby zakończyć
przegląd strategii wdrażania aplikacji, w tym podrozdziale opiszę tradycyjną opcję hostingu, która
obejmuje zakup lub wynajem serwera fizycznego lub wirtualnego oraz ręczne skonfigurowanie na
nim wszystkich wymaganych składników. Jest to oczywiście najbardziej pracochłonna opcja ze
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
wszystkich, ale może być naprawdę wygodna, gdy z terminala masz dostęp do sprzętu serwera pro-
dukcyjnego. W kolejnych punktach postaram się zarysować obraz pracy związanej z tym podejściem.
Konfiguracja serwera
Istnieje kilka zadań administracyjnych, które należy wykonać na serwerze, aby przygotować go do
hostowania naszej aplikacji:
Zainstaluj serwer bazy danych, taki jak MySQL lub Postgres. Korzystanie z bazy danych SQLite jest
nadal możliwe, ale dla serwera produkcyjnego nie jest zalecane ze względu na liczne ograniczenia
dotyczące modyfikacji istniejących schematów bazy danych.
Zainstaluj agenta poczty (MTA), takiego jak Sendmail lub Postƒix, aby wysyłać wiadomości
e-mail do użytkowników. Korzystanie z Gmaila w aplikacji produkcyjnej nie jest możliwe, ponie-
waż ta usługa ma bardzo restrykcyjne limity, a firma Google w warunkach świadczenia swoich
usług wyraźnie zabrania użytkowania komercyjnego.
Zainstaluj odpowiednio zaawansowany serwer WWW, taki jak Gunicorn lub uWSGI.
Zainstaluj narzędzie do monitorowania procesów, takie jak Supervisor, które umożliwia ponowne
uruchomienie serwera WWW, jeśli ten ulegnie awarii, a także po wyłączeniu zasilania hosta.
Zainstaluj i skonfiguruj certyfikat SSL, aby włączyć protokół HTTPS.
(Opcjonalne, ale wysoce zalecane). Zainstaluj frontendowy odwrotny serwer proxy, taki jak
nginx lub Apache. Serwer ten jest skonfigurowany do bezpośredniej obsługi plików statycznych
i przekazywania żądań aplikacji do jej serwera WWW, który nasłuchuje na prywatnym porcie
hosta lokalnego.
Zabezpiecz serwer. Obejmuje to kilka zadań, których celem jest zmniejszenie podatności na za-
grożenia na serwerze, takich jak instalowanie zapór sieciowych, usuwanie nieużywanego opro-
gramowania i usług.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
aplikacji. Dzięki temu w momencie importowania konfiguracji przez aplikację zmienne te są już
dostępne w środowisku.
Plik .env może definiować zmienną FLASK_CONFIG (która wybiera używaną konfigurację) i DATABASE_URL
(która ustala połączenie z bazą danych), ale też dane uwierzytelnienia serwera e-mail itp. Jak wyjaśnia-
łem już wcześniej, pliku .env nie należy dodawać do systemu kontroli wersji ze względu na wrażliwy
charakter niektórych zawartych w nim elementów.
Jeśli utworzyłeś już plik .env do użytku z platformami Heroku lub Docker, przejrzyj go
i odpowiednio dostosuj, ponieważ dzięki właśnie wprowadzonym zmianom aplikacja
zaimportuje zmienne zdefiniowane w tym pliku dla wszystkich konfiguracji.
Konfigurowanie protokołowania
W przypadku serwerów z systemem Unix protokoły można wysłać do demona syslog. Nową konfigu-
rację przygotowaną specjalnie dla Uniksa można utworzyć jako podklasę klasy ProductionConfig,
tak jak na listingu 17.15.
# Zapis do syslog
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.WARNING)
app.logger.addHandler(syslog_handler)
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie git
checkout 17g, aby pobrać tę wersję aplikacji.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 18.
Dodatkowe zasoby
Właściwie dotarliśmy do końca książki. Gratulacje! Mam nadzieję, że tematy, które w niej omó-
wiłem, dały Ci pewną i solidną podstawę do rozpoczęcia tworzenia własnych aplikacji za pomocą
frameworka Flask. Przykładowe kody z tej książki udostępniam na zasadach open source i udzie-
lam Ci liberalnej licencji, więc możesz wykorzystać tyle mojego kodu, ile tylko zechcesz, aby rozpocząć
swoje pierwsze projekty, nawet jeśli będą one miały charakter komercyjny. W tym krótkim ostatnim
rozdziale chciałbym podać listę dodatkowych wskazówek i zasobów, które mogą być przydatne
podczas dalszej pracy z Flaskiem.
255
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Wyszukiwanie rozszerzeń
Przykłady w tej książce wykorzystują kilka rozszerzeń i pakietów, ale oprócz nich istnieje jeszcze
wiele innych, które również są bardzo przydatne, a nie zostały tutaj omówione. Poniżej znajduje się
krótka lista wybranych pakietów, którym warto się przyjrzeć:
Flask-Babel (http://bit.ly/fl-babel): obsługa internacjonalizacji i lokalizacji.
Marshmallow (https://marshmallow.readthedocs.io/en/latest/): serializacja i deserializacja
obiektów Pythona, przydatna do reprezentacji zasobów API.
Celery (http://bit.ly/celery-doc): kolejka zadań do przetwarzania w tle.
Frozen-Flask (http://bit.ly/flask-frozen): konwersja aplikacji Flaska na statyczną witrynę
internetową.
Flask-DebugToolbar (http://bit.ly/flask-debug): narzędzia do debugowania w przeglądarce.
Flask-Assets (http://bit.ly/fl-assets): scalanie, minimalizowanie i kompilacja zasobów CSS
i JavaScript.
Flask-Session (https://pythonhosted.org/Flask-Session/): alternatywna implementacja sesji
użytkownika przechowująca dane po stronie serwera.
Flask-SocketIO (https://flask-socketio.readthedocs.io/en/latest/): implementacja serwera Socket.IO
obsługująca WebSocket i wykorzystująca technikę long-polling.
Jeśli funkcje, których potrzebujesz do swojego projektu, nie są dostępne w żadnym z rozszerzeń i pa-
kietów wymienionych w tej książce, pierwszym miejscem do poszukiwania dodatkowych rozszerzeń
powinien być oficjalny rejestr rozszerzeń Flaska — Flask Extension Registry (http://bit.ly/fl-exreg).
Istnieją także inne dobre miejsca do wyszukiwania, takie jak Python Package Index (http://
pypi.python.org), GitHub (http://github.com) lub Bitbucket (http://bitbucket.org).
Uzyskiwanie pomocy
Jeśli natrafisz na problem, którego nie będziesz w stanie samodzielnie rozwiązać, to pamiętaj, że ist-
nieje spora społeczność programistów Flaska, którzy chętnie Ci pomogą.
Świetnym miejscem do zadawania pytań na temat Flaska lub powiązanych rozszerzeń jest Stack
Overflow (https://stackoverflow.com). Inni programiści, którzy zobaczą Twoje pytanie i będą wiedzieć,
jak na nie odpowiedzieć, opublikują odpowiedzi, które w zależności od swojej jakości będą oceniane
wyżej lub niżej. Jako osoba zadająca pytanie możesz następnie wybrać najlepszą odpowiedź.
Wszystkie pytania i odpowiedzi pozostają na stronie i pojawiają się w wynikach wyszukiwania. Zada-
jąc pytanie na tej platformie, pomagasz powiększyć zbiór informacji o Flasku.
W serwisie Reddit dostępny jest również kanał poświęcony frameworkowi Flask (http://reddit.com/r/
flask), w którym to kanale można publikować pytania.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
I wreszcie, jeśli używasz IRC, to wiedz, że kanał #pocoo na Freenode jest odwiedzany przez wielu
programistów Flaska o różnych poziomach zaawansowania, którzy mogą pomóc Ci w rozwiązywa-
niu problemów.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
O autorze
Miguel Grinberg ma ponad 25-letnie doświadczenie jako inżynier oprogramowania. Prowadzi
własny blog (http://blog.miguelgrinberg.com), w którym pisze na różne tematy, w tym na temat two-
rzenia stron internetowych, robotyki, fotografii i sporadycznie wstawia recenzje filmów. Mieszka
w Portland w stanie Oregon w Stanach Zjednoczonych.
Kolofon
Zwierzę na okładce książki Flask. Tworzenie aplikacji internetowych w Pythonie to mastif pirenejski
(rasa Canis lupus familiaris). Te gigantyczne hiszpańskie psy pochodzą od starożytnego psa stró-
żującego o nazwie molos, który został wyhodowany przez Greków i Rzymian. Niestety ta rasa nie
dotrwała do naszych czasów. Wiadomo jednak, że ten przodek odegrał rolę w tworzeniu wielu
popularnych dziś ras, takich jak rottweiler, dog niemiecki, nowofundland i cane corso. Mastify pi-
renejskie dopiero od 1977 roku są uznawane za czystą rasę i w dalszym ciągu Pyrenean Mastiff Club of
America (Klub mastifa pirenejskiego w Stanach Zjednoczonych) pracuje nad promocją tych psów
jako zwierząt domowych w Stanach Zjednoczonych.
Po hiszpańskiej wojnie domowej populacja mastifów pirenejskich w ich rodzinnej ojczyźnie
gwałtownie spadła, a rasa przetrwała tylko dzięki oddanej pracy kilku rozproszonych hodowców
w całym kraju. Nowoczesna pula genów dla mastifów pirenejskich wywodzi się właśnie z tej powojen-
nej populacji, co czyni je podatnymi na choroby genetyczne, takie jak dysplazja stawu biodrowego.
Dziś odpowiedzialni hodowcy, przed założeniem hodowli tych psów, upewniają się, że ich psy są prze-
badane pod kątem chorób, i wykonują prześwietlenia w celu wykrycia nieprawidłowości w budo-
wie bioder.
W pełni dorosłe psy rasy mastif pirenejski mogą osiągnąć wagę do 90 kilogramów, więc posiadanie
tego psa wymaga zaangażowania w dobry trening i poświęcenia dużej ilości czasu na świeżym
powietrzu. Pomimo swojej wielkości i historii łowca niedźwiedzi i wilków, pirenejczyk, ma bardzo
spokojny temperament i jest doskonałym psem rodzinnym. Na mastifach pirenejskich można polegać,
będą dbały o dzieci i chroniły dom, a jednocześnie będą potulne wobec innych psów. Dzięki odpo-
wiedniej socjalizacji i silnemu przywództwu mastif pirenejski rozwija się w środowisku domowym,
będąc tym samym doskonałym opiekunem i towarzyszem.
Wiele zwierząt przedstawianych na okładkach książek wydawnictwa O’Reilly jest zagrożonych,
a wszystkie są ważne dla świata. Jeśli chcesz dowiedzieć się więcej, w jaki sposób możesz im pomóc,
przejdź do strony internetowej animals.oreilly.com.
Obrazek na okładce pochodzi z książki Animate Creation J. G. Wooda.
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f