You are on page 1of 260

W

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

Tłumaczenie: Wojciech Moch

ISBN: 978-83-283-6384-7

© 2020 Helion SA

Authorized Polish translation of the English edition of Flask Web Development 2E


ISBN 9781491991732 © 2018 Miguel Grinberg

This translation is published and sold by permission of O’Reilly Media, Inc.,


which owns or controls all rights to publish and sell the same.

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.

Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej


publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym
powoduje naruszenie praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

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ę.

 Poleć książkę na Facebook.com  Księgarnia internetowa


 Kup w wersji papierowej  Lubię to! » Nasza społeczność
 Oceń książkę

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Dla Alicji

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Spis treści

Wstęp ....................................................................................................................... 11

Część I . Wprowadzenie do Flaska


1. Instalacja .................................................................................................................. 21
Tworzenie katalogu aplikacji 22
Wirtualne środowiska 22
Tworzenie wirtualnego środowiska w Pythonie 3 23
Tworzenie wirtualnego środowiska w Pythonie 2 23
Praca z wirtualnymi środowiskami 24
Instalowanie pakietów Pythona za pomocą narzędzia pip 25

2. Podstawowa struktura aplikacji ................................................................................ 27


Inicjalizacja 27
Trasy i funkcje widoku 27
Kompletna aplikacja 29
Roboczy serwer WWW 29
Trasy dynamiczne 31
Tryb debugowania 32
Opcje wiersza polecenia 33
Cykl żądanie – odpowiedź 35
Kontekst aplikacji i żądania 35
Przesyłanie żądania 36
Obiekt żądania 37
Hooki w żądaniach 37
Odpowiedzi 38
Rozszerzenia Flaska 40

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

4. Formularze internetowe ............................................................................................ 57


Konfiguracja 57
Klasy formularzy 58
Renderowanie formularzy HTML 59
Obsługa formularzy w funkcjach widoku 61
Przekierowania i sesje użytkownika 64
Wyświetlanie komunikatów 66

5. Bazy danych .............................................................................................................. 69


Bazy danych SQL 69
Bazy danych NoSQL 70
SQL czy NoSQL? 71
Frameworki baz danych w Pythonie 71
Zarządzanie bazą danych za pomocą Flask-SQLAlchemy 73
Definicja modelu 74
Relacje 75
Operacje na bazach danych 77
Tworzenie tabel 77
Wstawianie wierszy 78
Modyfikowanie wierszy 79
Usuwanie wierszy 79
Zapytanie o wiersze 79
Wykorzystanie bazy danych w funkcjach widoku 81
Integracja z powłoką Pythona 82
Migrowanie baz danych za pomocą pakietu Flask-Migrate 83
Tworzenie repozytorium migracji 83
Tworzenie skryptu migracji 84
Aktualizacja bazy danych 85
Dodawanie kolejnych migracji 86

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

7. Struktura dużej aplikacji ............................................................................................ 93


Struktura projektu 93
Opcje konfiguracji 94
Pakiet aplikacji 96
Korzystanie z fabryki aplikacji 96
Implementacja funkcji aplikacji w projekcie 97
Skrypt aplikacji 100
Plik wymagań 100
Testy jednostkowe 101
Konfiguracja bazy danych 103
Uruchamianie aplikacji 103

Część II. Przykład: Aplikacja do blogowania społecznościowego


8. Uwierzytelnianie użytkownika .................................................................................107
Rozszerzenia uwierzytelnienia dla Flaska 107
Bezpieczeństwo hasła 107
Haszowanie haseł za pomocą pakietu Werkzeug 108
Tworzenie schematu uwierzytelnienia 110
Uwierzytelnianie użytkownika za pomocą Flask-Login 112
Przygotowywanie modelu User na potrzeby logowania 112
Ochrona tras 113
Dodawanie formularza logowania 114
Logowanie użytkowników 115
Wylogowywanie użytkowników 117
Jak działa Flask-Login? 117
Testowanie 118
Rejestrowanie nowego użytkownika 119
Tworzenie formularza rejestracji użytkownika 119
Rejestracja nowych użytkowników 121
Potwierdzenie konta 122
Generowanie tokenów potwierdzających za pomocą pakietu itsdangerous 122
Wysyłanie wiadomości e-mail z potwierdzeniem 124
Zarządzanie kontem 127

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

10. Profile użytkowników ............................................................................................. 137


Informacje o profilu 137
Strona profilu użytkownika 138
Edytor profilu 140
Edytor profilu z poziomu użytkownika 140
Edytor profilu z poziomu administratora 142
Awatary użytkownika 144

11. Posty na blogu ........................................................................................................ 149


Przesyłanie i wyświetlanie postów na blogu 149
Wpisy na blogach na stronach profilu 152
Stronicowanie długich list postów na blogu 152
Tworzenie fałszywych danych w postach na blogu 153
Renderowanie na stronach 154
Dodawanie widżetu stronicowania 155
Posty z formatowaniem przy użyciu pakietów Markdown i Flask-PageDown 158
Korzystanie z pakietu Flask-PageDown 158
Obsługa tekstu sformatowanego na serwerze 160
Stałe linki do postów na blogu 161
Edytor postów 162

12. Obserwatorzy .......................................................................................................... 165


I znowu relacje w bazach danych 165
Relacje typu wiele-do-wielu 165
Relacje samoreferencyjne 167
Zaawansowane relacje wiele-do-wielu 168
Obserwujący na stronie profilu 171
Uzyskiwanie śledzonych postów za pomocą operacji Join 173
Wyświetlanie obserwowanych postów na stronie głównej 176

13. Komentarze użytkowników ..................................................................................... 181


Zapisywanie komentarzy w bazie danych 181
Przesyłanie i wyświetlanie komentarzy 182
Moderowanie komentarzy 184

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

Część III. Ostatnie kroki


15. Testowanie ..............................................................................................................209
Uzyskiwanie raportów pokrycia kodu 209
Klient testowy Flaska 212
Testowanie aplikacji internetowych 212
Testowanie usług internetowych 215
Kompleksowe testy z użyciem Selenium 217
Czy warto? 221

16. Wydajność ...............................................................................................................223


Niska wydajność bazy danych 223
Profilowanie kodu źródłowego 225

17. Wdrożenie ...............................................................................................................227


Etapy prac wdrożenia 227
Protokołowanie błędów na produkcji 228
Wdrożenie w chmurze 229
Platforma Heroku 230
Przygotowanie aplikacji 230
Testowanie z wykorzystaniem Heroku Local 237
Wdrażanie za pomocą polecenia git push 238
Wdrażanie aktualizacji 239

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

18. Dodatkowe zasoby .................................................................................................. 255


Korzystanie ze zintegrowanego środowiska programistycznego (IDE) 255
Wyszukiwanie rozszerzeń 256
Uzyskiwanie pomocy 256
Angażowanie się w społeczność Flaska 257

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/).

Organizacja tej książki


Swoją książkę podzieliłem na trzy części.
W części I, „Wprowadzenie do Flaska”, zajmiemy się podstawami tworzenia aplikacji WWW za po-
mocą frameworka Flask oraz wybranych rozszerzeń:
 Rozdział 1. zostanie poświęcony instalowaniu i konfigurowaniu frameworka Flask.
 W rozdziale 2. od razu zaczniemy tworzyć małą aplikację.
 Rozdział 3. będzie wprowadzeniem do używania szablonów w aplikacji.
 W rozdziale 4. przyjrzymy się formularzom na stronach WWW.
 Rozdział 5. zostanie poświęcony bazom danych.
 W rozdziale 6. pomówimy o obsłudze wiadomości e-mail.
 W rozdziale 7. przedstawię strukturę aplikacji, która dobrze nadaje się do tworzenia średnich
i wielkich programów.

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.

Jak pracować z przykładowym kodem?


Kod przykładów prezentowanych w tej książce można pobrać z repozytorium https://github.com/
miguelgrinberg/flasky1.
Historia zmian w tym repozytorium została specjalnie przygotowana tak, żeby jej porządek dopaso-
wany był do koncepcji prezentowanych w tej książce. Zalecaną metodą pracy z kodem jest pobieranie
kolejnych zmian, zaczynając od najstarszych, i przesuwanie się naprzód na liście zmian wraz z postę-
pami dokonywanymi w książce. GitHub pozwala też na pobieranie poszczególnych zmian repozyto-
rium w postaci plików ZIP lub TAR.
Jeżeli zdecydujesz się na użycie Gita do pracy z kodem źródłowym, to musisz zainstalować w swoim
systemie klienta Git, który można pobrać ze strony http://git-scm.com. Za pomocą poniższego
polecenia można użyć Gita do pobrania przykładowego kodu z tej książki:
$ git clone https://github.com/miguelgrinberg/flasky.git

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.

Jak pracować z przykładowym kodem?  13

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

To polecenie spowoduje wycofanie wszystkich zmian wprowadzonych w plikach projektu, dlatego


przed jego wydaniem dobrze jest zapisać sobie osobno wszystko to, czego nie chce się stracić.
Oprócz pobierania z repozytorium wybranych wersji plików źródłowych tworzonej aplikacji w pew-
nych momentach konieczne będzie też wykonanie dodatkowych zadań konfiguracyjnych. Na przykład
w pewnym momencie będzie trzeba zainstalować nowe pakiety Pythona albo wprowadzić aktualizacje
do bazy danych. O tym wszystkim będę informował we właściwym czasie.
Od czasu do czasu dobrze jest też odświeżyć sobie lokalne repozytorium, synchronizując je z tym do-
stępnym na GitHubie, gdzie mogą się pojawiać różne poprawki albo usprawnienia. W tym celu
należy posłużyć się poleceniami:
$ git fetch --all
$ git fetch --tags
$ git reset --hard origin/master

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.

Używanie przykładowego kodu


Ta książka ma za zadanie pomóc Ci w wykonaniu Twojej pracy. W związku z tym wszystkie przykłady
prezentowane w niej możesz swobodnie wykorzystać w swoich programach i dokumentacji. Nie
musisz prosić mnie o pozwolenie, chyba że chcesz skorzystać ze znaczącej części przykładowej
aplikacji. Na przykład nie wymaga pozwolenia wykorzystanie kilku wycinków kodu z tej książki
w ramach tworzonej przez Ciebie aplikacji. Ale już sprzedawanie i rozpowszechnianie płyt CD zawie-
rających przykłady z książek wydawnictwa będzie wymagało specjalnego pozwolenia. Odpowia-
danie na pytanie cytatem z tej książki wraz z odpowiednim wycinkiem kodu nie wymaga żadnego
pozwolenia. Ale już umieszczenie znacznej części przykładowego kodu z tej książki w dokumentacji
Twojego produktu będzie takiego pozwolenia wymagało.
Będziemy wdzięczni za wskazanie nas jako autorów, choć nie będziemy tego wymagać. Zazwyczaj
wskazanie autorstwa składa się z takich informacji jak tytuł książki, jej autor, wydawca oraz numer
ISBN. Na przykład: Flask. Tworzenie aplikacji internetowych w Pythonie, książka Miguela Grinberga
(Helion), ISBN: 978-83-283-6383-0.
Jeżeli sądzisz, że Twój sposób wykorzystania przykładów z tej książki nie mieści się w przedstawionych
wyżej zasadach dozwolonego użytku, skontaktuj się z nami pod adresem: permissions@oreilly.com.

Konwencje używane w tej książce


W tej książce używam poniższych konwencji typograficznych:
Kursywa
Oznaczać będzie nowe pojęcia, adresy URL, adresy e-mail, nazwy plików oraz ich rozszerzenia.
Stała szerokość
Używana będzie do podawania zawartości wiersza poleceń oraz listingów programów, a także
wewnątrz akapitów do wyróżniania poleceń oraz takich elementów programów jak zmienne, na-
zwy funkcji, baz danych, typów danych, zmiennych środowiskowych, poleceń i słów kluczowych.
Pogrubiona stała szerokość
Będzie używana do prezentowania treści, które użytkownik powinien samodzielnie wpisać.

Konwencje używane w tej książce  15

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.

Ta ikonka będzie oznaczała wskazówkę lub sugestię.

Ta ikonka będzie oznaczała ogólną uwagę.

Ta ikonka będzie oznaczała ostrzeżenie.

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.

Podziękowania do wydania drugiego  17

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.

Tworzenie katalogu aplikacji


Na początek musisz utworzyć katalog, w którym znajdzie się kod wszystkich naszych przykładów.
Te można pobrać z mojego repozytorium GitHub. Jak już wspominałem w podrozdziale „Jak praco-
wać z przykładowym kodem”, najwygodniej będzie pobrać całość kodu bezpośrednio z GitHuba
za pomocą klienta Git. Poniższe polecenia pozwalają pobrać kod z GitHuba i zainicjować aplikację do
wersji 1a, od której będziemy zaczynać naszą pracę:
$ git clone https://github.com/miguelgrinberg/flasky.git
$ cd flasky
$ git checkout 1a

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ń.

Tworzenie wirtualnego środowiska w Pythonie 3


Tworzenie wirtualnych środowisk to zadanie, którego realizacja różni się w interpreterach Pythona 2
i Pythona 2. W Pythonie 3 wirtualne środowiska są obsługiwane bezpośrednio przez pakiet venv,
który jest częścią standardowej biblioteki Pythona.

Jeżeli używasz standardowego interpretera Pythona dołączanego do Linuksa Ubuntu,


to pakiet venv nie będzie standardowo zainstalowany. Musisz zatem samodzielnie
dodać do systemu pakiet python3-venv, stosując poniższe polecenie:
$ sudo apt-get install python3-venv

Polecenie tworzące wirtualne środowisko ma następującą postać:


$ python3 -m venv nazwa-wirtualnego-środowiska

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.

Tworzenie wirtualnego środowiska w Pythonie 2


W Pythonie 2 nie znajdziemy pakietu venv. W tej wersji interpretera Pythona wirtualne środowiska
tworzone są za pomocą zewnętrznego narzędzia o nazwie virtualenv.
Upewnij się, że aktualnym katalogiem jest katalog flasky, a następnie użyj jednego z poniższych pole-
ceń, odpowiedniego dla Twojego systemu operacyjnego. Jeżeli używasz systemu Linux lub macOS,
to użyj polecenia:
$ sudo pip install virtualenv

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

Tworzenie wirtualnego środowiska w Pythonie 2  23

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.

Praca z wirtualnymi środowiskami


Przed rozpoczęciem używania wirtualnego środowiska trzeba je najpierw „zaktywować”. W kompute-
rach z systemem Linux lub macOS, wirtualne środowisko aktywowane jest za pomocą tego polecenia:
$ source venv/bin/activate

Jeżeli używasz system Microsoft Windows, to musisz skorzystać z następującego polecenia:


$ venv\Scripts\activate

Po zaktywowaniu wirtualnego środowiska lokalizacja związanego z nim interpretera Pythona doda-


wana jest do zmiennej środowiskowej PATH, definiującej miejsca, w których komputer poszukuje
plików wykonywalnych. Dodatkowo modyfikowany jest znak zachęty wiersza poleceń, aby cały czas
przypominać o tym, jakie środowisko jest aktualnie aktywne. W wierszu poleceń pojawia się wtedy
nazwa aktywnego środowiska:
(venv) $

Po zaktywowaniu wybranego wirtualnego środowiska i wpisaniu w wierszu poleceń polecenia python


komputer uruchomi interpreter związany z aktywnym wirtualnym środowiskiem, a nie interpreter
ogólnosystemowy. Jeżeli używasz kilku okien wiersza poleceń, to w każdym z nich musisz zakty-
wować środowisko wirtualne.

Co prawda aktywowanie wirtualnego środowiska jest najwygodniejszą formą pracy,


to jednak możesz z niego korzystać również bez aktywowania. Na przykład konsolę
Pythona dla wirtualnego środowiska venv można uruchomić za pomocą polecenia
venv/bin/python (w systemach Linux lub macOS) albo venv\Scripts\python
(w systemach Windows).

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.

Instalowanie pakietów Pythona za pomocą narzędzia pip  25

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__.

Argument __name__, przekazywany do konstruktora klasy Flask, jest źródłem zamie-


szania wśród nowych programistów frameworka Flask. Flask używa tego argumentu
do wyznaczenia lokalizacji aplikacji, co z kolei pozwala zlokalizować inne pliki aplikacji,
takie jak obrazy lub szablony.

Troszkę później poznasz bardziej złożone sposoby inicjalizowania aplikacji, ale w przypadku prostych
aplikacji to naprawdę wszystko, czego potrzeba.

Trasy i funkcje widoku


Klienty, takie jak przeglądarki internetowe, wysyłają żądania do serwera WWW, który z kolei
przesyła je do instancji aplikacji Flaska. Instancja aplikacji Flaska musi wiedzieć, jaki kod musi
uruchomić w celu obsłużenia adresu URL podanego w żądaniu, dlatego wstępnie definiowane jest
odwzorowanie adresów URL na funkcje Pythona. Powiązanie adresu URL z funkcją, która go obsłu-
guje, nazywane jest trasą (ang. route).

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>'

Dekoratory są standardowym elementem języka Python. Zazwyczaj są używane do


rejestrowania funkcji jako funkcji obsługi zdarzeń. Takie funkcje są wywoływane
w momencie wystąpienia określonego zdarzenia.

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.

Osadzanie ciągów znaków z kodem HTML w plikach źródłowych Pythona powoduje,


że utrzymanie takiego kodu jest bardzo trudne. Przykłady w tym rozdziale służą jedynie
do zaprezentowania koncepcji odpowiedzi. W rozdziale 3. poznasz lepszy sposób
generowania odpowiedzi HTML.

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)

28  Rozdział 2. Podstawowa struktura aplikacji

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.

Listing 2.1. hello.py: Kompletna aplikacja Flaska


from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Witaj, świecie!</h1>'

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.

Roboczy serwer WWW


W aplikacjach Flaska dostępny jest też roboczy serwer WWW, który można uruchomić za pomocą in-
strukcji flask run. Instrukcja ta szuka w zmiennej środowiskowej FLASK_APP nazwy skryptu
Pythona zawierającej instancję aplikacji.
Aby uruchomić aplikację hello.py z poprzedniego podrozdziału, proszę się najpierw upewnić, że
utworzone wcześniej wirtualne środowisko jest aktywne i ma zainstalowany w sobie Flask. W przy-
padku użytkowników systemów Linux i macOS proszę uruchomić serwer WWW w następujący
sposób:
(venv) $ export FLASK_APP=hello.py
(venv) $ flask run
* Serving Flask app "hello"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Natomiast w przypadku użytkowników systemu Microsoft Windows jedyną różnicą jest sposób usta-
wienia zmiennej środowiskowej FLASK_APP:

Roboczy serwer WWW  29

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ą.

Rysunek 2.1. Aplikacja Flaska hello.py

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.

Serwer internetowy dostarczony przez Flaska jest przeznaczony wyłącznie do tworzenia


oprogramowania i do testowania. O produkcyjnych serwerach WWW dowiesz się
więcej w rozdziale 17.

Roboczy serwer WWW dołączany do pakietu Flask można również uruchomić


programowo, wywołując metodę app.run(). Starsze wersje pakietu Flask nie miały
instrukcji flask, dlatego wymagały uruchomienia serwera w ramach głównego
skryptu aplikacji. Skrypt ten musiał zawierać na końcu poniższy fragment kodu:
if __name__ == '__main__':
app.run()

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.

30  Rozdział 2. Podstawowa struktura aplikacji

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.

Listing 2.2. hello.py: Aplikacja Flaska z dynamiczną trasą


from flask import Flask
app = Flask(__name__)

@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.

Rysunek 2.2. Trasa dynamiczna

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.

Rysunek 2.3. Debugger Flaska

32  Rozdział 2. Podstawowa struktura aplikacji

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ą.

Jeśli serwer zostanie uruchomiony za pomocą wywołania metody app.run(), to


zmienne środowiskowe FLASK_APP i FLASK_DEBUG nie będą używane. Aby programowo
włączyć tryb debugowania, użyj instrukcji app.run(debug=True).

Nigdy nie włączaj trybu debugowania na serwerze produkcyjnym. Przede wszystkim


dlatego, że debugger pozwala klientowi zażądać zdalnego wykonania kodu, przez
co serwer produkcyjny będzie bardziej podatny na ataki. Na szczęście można tu zasto-
sować prosty środek ochrony i w przypadku próby uruchomienia debuggera wymagać
podania numeru PIN wyświetlonego w konsoli po wprowadzeniu polecenia flask run.

Opcje wiersza polecenia


Instrukcja flask obsługuje wiele opcji. Aby przekonać się, jakie opcje są dostępne, możesz uruchomić
instrukcję flask --help lub po prostu samo polecenie flask bez żadnych argumentów:
(venv) $ flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...

A general utility script for Flask applications.

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.

Opcje wiersza polecenia  33

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]

Run a local development server.

This server is for development purposes only. It does not provide the
stability, security, or performance of production WSGI servers.

The reloader and debugger are enabled by default if FLASK_ENV=development


or FLASK_DEBUG=1.

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.

34  Rozdział 2. Podstawowa struktura aplikacji

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.

Cykl żądanie – odpowiedź


Teraz, kiedy już pobawiłeś się podstawową aplikacją Flaska, możesz chcieć dowiedzieć się nieco więcej
o tym, jaka magia napędza framework Flask. W poniższych punktach opisano niektóre elementy
projektu tego frameworka.

Kontekst aplikacji i żądania


Gdy Flask otrzyma żądanie od klienta, musi udostępnić kilka obiektów funkcji widoku, która będzie
obsługiwać to żądanie. Dobrym przykładem jest obiekt żądania (ang. request object), który zawiera
dane żądania HTTP wysłanego przez klienta.
Oczywistym sposobem, w jaki Flask mógłby przyznać funkcji widoku dostęp do obiektu żądania,
jest wysłanie tego obiektu jako argumentu, ale wymagałoby to, aby każda funkcja widoku w aplikacji
miała dodatkowy argument. Sprawy trochę się komplikują, jeśli weźmiemy pod uwagę, że obiekt
żądania nie jest jedynym obiektem wymaganym przez funkcję widoku do obsługi danego żądania.
Aby uniknąć zaśmiecania funkcji widoku wieloma argumentami, które nie zawsze są nam potrzebne,
framework Flask używa kontekstów (ang. contexts), aby tymczasowo udostępniać globalnie wybrane
obiekty. Dzięki kontekstom można tworzyć takie funkcje widoku jak poniższa:
from flask import request

@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.

Wątek to najmniejsza sekwencja instrukcji, którymi można zarządzać niezależnie.


Proces często ma wiele aktywnych wątków, czasami współużytkujących pewne zasoby,
takie jak pamięć lub uchwyty plików. Wielowątkowe serwery WWW uruchamiają
pulę wątków i wybierają pojedyncze wątki z puli, które mają obsłużyć poszczególne,
przychodzące żądania.

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.

Cykl żądanie – odpowiedź  35

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 2.1. Zmienne globalne w kontekstach Flaska

Nazwa zmiennej Kontekst Opis


current_app Kontekst aplikacji Instancja aktywnej aplikacji.
g Kontekst aplikacji Obiekt, którego aplikacja może użyć jako pamięci tymczasowej podczas
obsługi żądania. Ta zmienna jest resetowana przy każdym żądaniu.
request Kontekst żądania Obiekt żądania, które hermetyzuje zawartość żądania HTTP wysłanego
przez klienta.
session Kontekst żądania Sesja użytkownika, czyli słownik, którego aplikacja może używać do
przechowywania wartości „zapamiętywanych” między żądaniami.

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

36  Rozdział 2. Podstawowa struktura aplikacji

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.

Cykl żądanie – odpowiedź  37

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 2.2. Metody żądania obiektu Flask

Atrybut lub metoda Opis


form Słownik ze wszystkimi polami formularza przesłanymi wraz z żądaniem.
args Słownik ze wszystkimi argumentami przekazanymi w zapytaniu w adresie URL.
values Słownik, który łączy wartości z pól form i args.
cookies Słownik ze wszystkimi ciasteczkami zawartymi w żądaniu.
headers Słownik ze wszystkimi nagłówkami HTTP zawartymi w żądaniu.
files Słownik zawierający wszystkie przesłane pliki dołączone do żądania.
get_data() Zwraca buforowane dane z treści żądania.
get_json() Zwraca słownik Pythona ze sparsowanymi danymi w formacie JSON zawartymi w treści żądania.
blueprint Nazwa schematu (ang. blueprint) Flaska, który obsługuje żądanie. Temat schematów przedstawiam
w rozdziale 7.
endpoint Nazwa punktu końcowego Flaska, który obsługuje żądanie. Flask używa nazwy funkcji widoku jako
nazwy punktu końcowego trasy.
method Metoda żądania HTTP, taka jak GET lub POST.
scheme Schemat URL (http lub https).
is_secure() Zwraca wartość True, jeśli żądanie nadeszło przez połączenie zabezpieczone (HTTPS).
host Host podany w żądaniu, w tym również numer portu, o ile został podany przez klienta.
path Ścieżka pobrana z adresu URL.
query_string Zapytanie pobrane z adresu URL jako nieprzetworzona wartość binarna.
full_path Ścieżka i zapytanie pobrane z adresu URL.
url Pełny adres URL żądany przez klienta.
base_url Taki sam jak adres url, ale bez komponentu zapytania.
remote_addr Adres IP klienta.
environ Bazowy słownik środowiska WSGI właściwy dla żądania.

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.

Kolejne przykłady hooków w żądaniach zostaną zaprezentowane w następnych rozdziałach.


Nie martw się zatem, jeśli nie widzisz jeszcze sensu ich stosowania.

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.

38  Rozdział 2. Podstawowa struktura aplikacji

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.

Tabela 2.3. Obiekt odpowiedzi

Atrybut lub metoda Opis


status_code Liczbowy kod stanu HTTP.
headers Obiekt podobny do słownika ze wszystkimi nagłówkami, które zostaną wysłane z odpowiedzią.
set_cookie() Dodaje plik cookie do odpowiedzi.
delete_cookie() Usuwa plik cookie.
content_length Długość treści odpowiedzi.
content_type Typ mediów treści odpowiedzi.
set_data() Wstawia ciąg znaków lub sekwencję bajtów jako treść odpowiedzi.
get_data() Pobiera treść 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.

Cykl żądanie – odpowiedź  39

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ł.

40  Rozdział 2. Podstawowa struktura aplikacji

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.

Mechanizm szablonów Jinja2


W najprostszej formie szablon Jinja2 to plik zawierający tekst odpowiedzi. Przykład takiego szablonu
przedstawiam na listingu 3.1. Jego zawartość jest identyczna z odpowiedzią funkcji widoku index()
z listingu 2.1.

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ź.

Listing 3.2. Szablon Jinja2: templates/user.html


<h1>Witaj, {{ name }}!</h1>

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.

Listing 3.3. hello.py: Renderowanie szablonu


from flask import Flask, render_template

# ...

@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.

Tabela 3.1. Filtry zmiennych w Jinja2

Nazwa filtra Opis


safe Renderuje wartość bez interpretowania znaków modyfikacji.
capitalize Zmienia pierwszy znak wartości na wielką literę, a resztę na małe.
lower Zmienia tekst na małe litery.
upper Zmienia tekst na wielkie litery.
title Pierwsza litera każdego słowa zapisana jest wielką literą.
trim Usuwa z wartości początkowe i końcowe białe znaki.
striptags Przed renderowaniem usuwa z wartości wszystkie znaczniki HTML.

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 '&lt;h1&gt;Cześć&lt;/h1&gt;', 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.

Pełną listę filtrów można uzyskać z oficjalnej dokumentacji Jinja2 (http://bit.ly/jinja-filters).

Mechanizm szablonów Jinja2  43

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ą.

Integracja Bootstrapa z Flask-Bootstrap


Bootstrap (http://getbootstrap.com) to otwartoźródłowy framework dla przeglądarek, wywodzący
się z Twittera, udostępniający komponenty interfejsu użytkownika, które wspomagają tworzenie
czystych i atrakcyjnych stron internetowych, kompatybilnych ze wszystkimi nowoczesnymi przeglą-
darkami używanymi na platformach stacjonarnych i mobilnych.
Bootstrap jest frameworkiem działającym po stronie klienta, tak więc serwer nie ma z nim bezpośred-
niego kontaktu. Wszystko, co serwer musi zrobić, to udzielić odpowiedzi HTML, które korzystają
z kaskadowych arkuszy stylów (CSS) i plików JavaScript frameworka Bootstrap, i utworzyć niezbędne
elementy interfejsu użytkownika za pomocą kodu HTML, CSS i JavaScript. Idealnym miejscem
do przygotowania tego wszystkiego są właśnie szablony.
Najprostszą metodą integracji Bootstrapa z własną aplikacją jest wprowadzenie do szablonów HTML
wszystkich niezbędnych zmian, zgodnie z zaleceniami podanymi w dokumentacji Bootstrapa. Jest to
jednak obszar, w którym użycie rozszerzenia Flaska znacznie upraszcza prace integracyjne i jednocze-
śnie pozwala na schludne organizowanie zmian.

Integracja Bootstrapa z Flask-Bootstrap  45

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.

Listing 3.4. hello.py: Inicjalizacja rozszerzenia Flask-Bootstrap


from flask_bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)

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.

Listing 3.5. templates/user.html: Szablon korzystający z rozszerzenia Flask-Bootstrap


{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% 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.

Rysunek 3.1. Szablony Bootstrapa

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, to możesz uruchomić po-


lecenie git checkout 3b, aby pobrać tę wersję aplikacji. W środowisku wirtual-
nym musi być również zainstalowany pakiet Flask-Bootstrap. Oficjalna dokumen-
tacja Bootstrapa (http://getbootstrap.com/docs/) jest świetnym materiałem do nauki,
pełnym przykładów gotowych do wykorzystania sposobem kopiuj/wklej.

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:

Integracja Bootstrapa z Flask-Bootstrap  47

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 3.2. Bloki szablonu bazowego

Nazwa bloku Opis


doc Cały dokument HTML.
html_attribs Atrybuty wewnątrz znacznika <html>.
html Zawartość znacznika <html>.
head Zawartość znacznika <head>.
title Zawartość znacznika <title.>
metas Lista znaczników <meta>.
styles Definicje stylów CSS.
body_attribs Atrybuty wewnątrz znacznika <body>.
body Zawartość znacznika <body>.
navbar Pasek nawigacji zdefiniowany przez użytkownika.
content Treść strony określona przez użytkownika.
scripts Deklaracje JavaScript znajdujące się na końcu dokumentu.

{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}

Niestandardowe strony błędów


Po wpisaniu w pasku adresu przeglądarki nieprawidłowej trasy pojawi się strona błędu o kodzie 404.
W porównaniu ze stronami zbudowanymi na Bootstrapie domyślna strona tego błędu wydaje się
teraz zbyt prosta i nieatrakcyjna, a poza tym nie jest graficznie podobna do stron generowanych
przez aplikację.
Flask pozwala aplikacji na definiowanie niestandardowych stron błędów, które mogą bazować na sza-
blonach, podobnie jak strony zwykłych tras. Dwa najczęściej występujące kody błędów to 404, uru-
chamiany, gdy klient żąda nieznanej strony lub trasy, oraz kod błędu 500, uruchamiany w sytuacji,
gdy w aplikacji pojawi się nieobsłużony wyjątek. Na listingu 3.6 można zobaczyć, jak zdefiniować
własną obsługę tych dwóch błędów za pomocą dekoratora app.errorhandler.

Listing 3.6. hello.py: Niestandardowe strony błędów


@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404

@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.

Listing 3.7. templates/base.html: Szablon bazowy aplikacji z paskiem nawigacji


{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% 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.

Niestandardowe strony błędów  49

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 title %}Flasky – Nie znaleziono strony{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Nie znaleziono</h1>
</div>
{% endblock %}

Wygląd tak przygotowanej strony błędu można zobaczyć na rysunku 3.2.

Rysunek 3.2. Strona niestandardowego kodu błędu 404

Szablon templates/user.html można teraz uprościć, dziedzicząc go po szablonie bazowym, tak jak
pokazano na listingu 3.9.

Listing 3.9. templates/user.html: Uproszczony szablon strony wykorzystujący dziedziczenie szablonów


{% extends "base.html" %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Witaj, {{ name }}!</h1>
</div>
{% endblock %}

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 3c, aby pobrać tę wersję aplikacji.

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/.

Względne adresy URL sprawdzają się przy generowaniu odnośników łączących


różne trasy w aplikacji. Bezwzględne adresy URL są niezbędne tylko w przypadku
tych odnośników, które będą używane poza przeglądarką internetową, na przykład
będą wysyłane e-mailem.

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.

Listing 3.10. templates/base.html: Definicja favikonki


{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"
type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}"
type="image/x-icon">
{% endblock %}

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, to możesz uruchomić po-


lecenie git checkout 3d, aby pobrać tę wersję aplikacji.

Lokalizowanie dat i czasu za pomocą pakietu Flask-Moment


Obsługa dat i czasu w aplikacji internetowej nie jest niczym trywialnym, szczególnie gdy użytkownicy
pracują w różnych częściach świata.
Serwer potrzebuje jednolitej definicji czasu, która jest niezależna od lokalizacji każdego użytkownika.
Dlatego też zwykle używany jest czas uniwersalny (UTC). Jednak wyświetlanie użytkownikom
czasu uniwersalnego może być mylące, ponieważ zawsze oczekują oni daty i czasu właściwego dla
ich lokalizacji i sformatowanego zgodnie z obyczajami danego regionu.
Eleganckim rozwiązaniem, które pozwala serwerowi pracować wyłącznie w UTC, jest wysyłanie
danych o czasie UTC do przeglądarki internetowej, gdzie są one konwertowane na czas lokalny i
renderowane za pomocą funkcji JavaScriptu. Przeglądarki internetowe znacznie lepiej wykonają
to zadanie, ponieważ mają dostęp do strefy czasowej i ustawień regionalnych na komputerze
użytkownika.
Istnieje doskonała, napisana w języku JavaScript, biblioteka otwartoźródłowa o nazwie Moment.js
(http://momentjs.com), która renderuje w przeglądarce datę i czas. Flask-Moment to rozszerzenie,
które bardzo ułatwia integrację pliku Moment.js z szablonami Jinja2. Flask-Moment można zainstalo-
wać za pomocą narzędzia pip:
(venv) $ pip install flask-moment

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.

Listing 3.11. hello.py: Inicjowanie pakietu Flask-Moment


from flask_moment import Moment
moment = Moment(app)

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.

Listing 3.12. templates/base.html: Importowanie biblioteki Moment.js


{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}

Pakiet Flask-Moment udostępnia szablonom obiekt moment, umożliwiający pracę ze znacznikami


czasu. Na listingu 3.13 możesz zobaczyć sposób przekazania zmiennej current_time do szablonu
w celu renderowania jej zawartości.

Listing 3.13. hello.py: Używanie zmiennej typu datetime


from datetime import datetime

@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())

Na listingu 3.14 przedstawiam sposób renderowania zmiennej current_time w szablonie.

Listing 3.14. templates/index.html: Renderowanie informacji o czasie za pomocą pakietu Flask-Moment


<p>Lokalna data i czas: {{ moment(current_time).format('LLL') }}.</p>
<p>To było {{ moment(current_time).fromNow(refresh=True) }}</p>

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

Lokalizowanie dat i czasu za pomocą pakietu Flask-Moment  53

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.

Rysunek 3.3. Strona zawierająca dwa znaczniki czasu z pakietu Flask-Moment

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 3e, aby pobrać tę wersję aplikacji.

Pakiet Flask-Moment udostępnia metody format(), fromNow(), fromTime(), calendar(), valueOf()


oraz unix() z biblioteki Moment.js. Zapoznaj się z dokumentacją tej biblioteki (http://momentjs.com/
docs/#/displaying/), aby poznać wszystkie oferowane przez nią opcje formatowania.

Pakiet Flask-Moment zakłada, że znaczniki czasu obsługiwane przez aplikację po


stronie serwera są „naiwnymi” obiektami typu datetime, przechowującymi czas
UTC. Informacje na temat naiwnych i świadomych obiektów daty i godziny znajdują
się w dokumentacji pakietu datetime (https://docs.python.org/3.6/library/datetime.html)
w bibliotece standardowej.

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.

Lokalizowanie dat i czasu za pomocą pakietu Flask-Moment  55

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
56  Rozdział 3. Szablony

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 4.
Formularze internetowe

Szablony, z którymi pracowaliśmy w rozdziale 3., są jednokierunkowe — w tym sensie, że umożli-


wiają przepływ informacji z serwera do użytkownika. Jednak w przypadku większości aplikacji
istnieje również potrzeba tworzenia informacji zwrotnej, która przepływa w przeciwnym kierunku.
To użytkownik dostarcza nam danych, które serwer przyjmuje i następnie przetwarza.
Język HTML umożliwia tworzenie formularzy internetowych (https://en.wikipedia.org/wiki/
Form_(HTML)), w których to użytkownicy mogą wprowadzać różne informacje. Dane formularza są
następnie przesyłane przez przeglądarkę do serwera, zwykle w formie żądania POST. Wprowadzony
już w rozdziale 2. obiekt żądania przechowuje wszystkie informacje wysłane przez klienta w żądaniu.
W szczególności w przypadku żądań typu POST, przenoszących dane z formularza, taki obiekt zapew-
nia dostęp do informacji wprowadzonych przez użytkownika za pośrednictwem pola request.form.
Mimo że mechanizmy obsługi formularzy internetowych realizowane w obiekcie żądania są w wielu
miejscach całkowicie wystarczające, to jednak istnieje wiele zadań, które na dłuższą metę mogą
stać się żmudne i powtarzalne. Dwa dobre przykłady takich prac to generowanie kodu HTML dla
formularzy i sprawdzanie poprawności danych przesłanych za ich pośrednictwem.
Rozszerzenie Flask-WTF (http://pythonhosted.org/Flask-WTF/) sprawia, że praca z formularzami
staje się znacznie przyjemniejsza. To rozszerzenie jest wrapperem integrującym niezależny od frame-
worków pakiet WTForms (http://wtforms.simplecodes.com/) z Flaskiem
Rozszerzenie Flask-WTF oraz jego zależności można zainstalować za pomocą narzędzia pip:
(venv) $ pip install flask-wtf

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.

Dla zwiększenia bezpieczeństwa tajny klucz powinien być przechowywany w zmiennej


środowiskowej i nie powinien być zapisywany w kodzie źródłowym aplikacji. Tę tech-
nikę opisałem szerzej w rozdziale 7.

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.

Listing 4.2. hello.py: Definicja klasy formularza


from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

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.

58  Rozdział 4. Formularze internetowe

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

Typ pola Opis


BooleanField Pole wyboru z wartościami True i False.
DateField Pole tekstowe przyjmujące wartość typu datetime.date w danym formacie.
DateTimeField Pole tekstowe przyjmujące wartość typu datetime.datetime w danym formacie.
DecimalField Pole tekstowe przyjmujące wartość typu decimal.Decimal.
FileField Pole przesyłania pliku.
HiddenField Ukryte pole tekstowe.
MultipleFileField Pole przesyłania wielu plików.
FieldList Lista pól danego typu.
FloatField Pole tekstowe przyjmujące wartość liczby zmiennoprzecinkowej.
FormField Formularz osadzony jako pole w formularzu.
IntegerField Pole tekstowe przyjmujące wartość liczy całkowitej.
PasswordField Pole tekstowe hasła.
RadioField Lista przycisków opcji.
SelectField Rozwijana lista opcji.
SelectMultipleField Rozwijana lista opcji z wielokrotnym wyborem.
SubmitField Przycisk przesłania formularza.
StringField Pole tekstowe.
TextAreaField Pole tekstu wielowierszowego.

W tabeli 4.2 znajduje się lista wbudowanych walidatorów z pakietu WTForms.

Renderowanie formularzy HTML


Pola formularza to elementy, które po wywołaniu w ramach szablonu renderują się do kodu HTML.
Przy założeniu, że funkcja widoku przekazuje do szablonu instancję klasy NameForm jako argument
o nazwie form, szablon może wygenerować prosty formularz HTML w następujący sposób:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>

Renderowanie formularzy HTML  59

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

60  Rozdział 4. Formularze internetowe

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.

Listing 4.3. templates/index.html: Użycie pakietów Flask-WTF i Flask-Bootstrap do renderowania formularza


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% 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().

Obsługa formularzy w funkcjach widoku


W nowej wersji pliku hello.py funkcja widoku index() będzie miała dwa zadania. Najpierw wyrende-
ruje formularz, a następnie będzie musiała obsłużyć wprowadzone przez użytkownika dane z formula-
rza. Na listingu 4.4 przedstawiam zaktualizowaną funkcję widoku index().

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.

Obsługa formularzy w funkcjach widoku  61

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.

62  Rozdział 4. Formularze internetowe

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 4.1. Formularz internetowy Flask-WTF

Rysunek 4.2. Wygląd formularza po przesłaniu danych

Obsługa formularzy w funkcjach widoku  63

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 4.3. Wygląd formularza po błędzie zgłoszonym przez walidator

Przekierowania i sesje użytkownika


Ostatnia wersja pliku hello.py ma mały problem z użytecznością. Jeśli wpiszesz swoje imię i je prze-
ślesz, a następnie klikniesz przycisk odświeżania w przeglądarce, prawdopodobnie pojawi się nie-
jasne ostrzeżenie z prośbą o potwierdzenie zamiaru ponownego przesłania formularza. Dzieje się tak,
ponieważ przeglądarki proszone o odświeżenie strony powtarzają ostatnio wysłane żądanie. Gdy
ostatnim wysłanym żądaniem będzie żądanie POST z danymi formularza, odświeżenie strony spowo-
duje przesłanie duplikatu formularza, co w niemal każdym przypadku nie jest pożądanym działaniem.
Z tego powodu przeglądarka prosi użytkownika o potwierdzenie tej czynności.
Wielu użytkowników nie rozumie jednak tego ostrzeżenia w przeglądarce. W związku z tym za dobrą
praktykę uważa się, aby aplikacje internetowe nigdy nie pozostawiały żądania POST jako ostatniego
żądania wysłanego przez przeglądarkę.
Można to osiągnąć poprzez odpowiadanie na żądania POST przekierowaniem (ang. redirect), a nie zwy-
kłą odpowiedzią. Przekierowanie to specjalny typ odpowiedzi, który zawiera adres URL zamiast
ciągu znaków z kodem HTML. Gdy przeglądarka otrzyma odpowiedź z przekierowaniem, wysyła
żądanie GET dla wskazanego adresu URL i to właśnie będzie strona, która zostanie wyświetlona.
Załadowanie strony może potrwać kilka milisekund dłużej z powodu konieczności wysłania do ser-
wera drugiego żądania, ale poza tym użytkownik nie zobaczy żadnej różnicy. Teraz ostatnim żądaniem
jest GET, więc polecenie odświeżania działa zgodnie z oczekiwaniami. Ta sztuczka jest znana jako
wzorzec projektowy Post/Redirect/Get.
Niestety takie rozwiązanie powoduje powstanie innego problemu. Gdy aplikacja obsługuje żą-
danie POST, ma dostęp do tekstu wprowadzonego przez użytkownika w formularzu (w zmiennej
form.name.data), ale gdy tylko obsługa tego żądania się zakończy, dane z formularza zostaną utracone.

64  Rozdział 4. Formularze internetowe

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.

Domyślnie sesje użytkownika są przechowywane w plikach cookie po stronie klienta,


gdzie są kryptograficznie podpisywane przy użyciu skonfigurowanego tajnego klucza.
Wszelkie manipulacje przy zawartości plików cookie spowodowałyby, że podpis byłby
nieważny, co zaowocowałoby unieważnieniem sesji.

Na listingu 4.5 można zobaczyć nową wersję funkcji widoku index(), która implementuje przekiero-
wania i sesje użytkownika.

Listing 4.5. hello.py: Przekierowania i sesje użytkownika


from flask import Flask, render_template, session, redirect, url_for

@app.route('/', methods=['GET', 'POST'])


def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))

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

Przekierowania i sesje użytkownika  65

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
uniknąć powstania wyjątku w razie podania nieznanego klucza. Dla brakujących kluczy metoda get()
zwraca wartość domyślną None.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić teraz


polecenie git checkout 4b, aby pobrać tę wersję aplikacji.

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().

Listing 4.6. hello.py: Wyświetlanie wyskakującego komunikatu


from flask import Flask, render_template, session, redirect, url_for, flash

@app.route('/', methods=['GET', 'POST'])


def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Wygląda na to, że teraz nazywasz się inaczej!')
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html',
form = form, name = session.get('name'))

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.

66  Rozdział 4. Formularze internetowe

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">&times;</button>
{{ message }}
</div>
{% endfor %}

{% block page_content %}{% endblock %}


</div>
{% endblock %}

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

Rysunek 4.4. Wyskakujący komunikat

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić teraz


polecenie git checkout 4c, aby pobrać tę wersję aplikacji.

Możliwość przyjmowania od użytkownika danych za pośrednictwem formularzy internetowych


jest funkcją niezbędną dla większości aplikacji, podobnie jak możliwość przechowywania tych danych
w pamięci trwałej. Tematem następnego rozdziału będzie używanie baz danych we frameworku Flask.

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.

Bazy danych SQL


Relacyjne bazy danych przechowują dane w tabelach, które odwzorowują różne encje w domenie
aplikacji. Na przykład baza danych aplikacji do zarządzania zamówieniami prawdopodobnie będzie
zawierać tabele klientów customers, produktów products i zamówień orders.
Tabela ma ustaloną liczbę kolumn i zmienną liczbę wierszy. Kolumny definiują atrybuty danych
encji reprezentowanej przez tabelę. Na przykład tabela customers będzie zawierała takie kolumny
jak nazwa (name), adres (address), telefon (phone) i tak dalej. Każdy wiersz w tabeli definiuje rzeczywi-
sty element danych, który może mieć wartości przypisane do niektórych lub do wszystkich kolumn.
Tabele mają specjalną kolumnę zwaną kluczem głównym (ang. primary key), zawierającą unikatowy
identyfikator każdego wiersza przechowywanego w tabeli. Tabele mogą również zawierać kolum-
ny zwane kluczami obcymi (ang. foreign keys), które odwołują się do klucza głównego w wierszach tej
samej lub innej tabeli. Takie powiązania między wierszami są nazywane relacjami (ang. relationships)
i stanowią podstawę modelu relacyjnej bazy danych.
Na rysunku 5.1 przedstawiam schemat prostej bazy danych z dwiema tabelami, które przechowują
użytkowników (tabela users) i ich role (tabela roles). Linia łącząca te dwie tabele reprezentuje
relację między nimi.
Ten graficzny styl reprezentowania struktury bazy danych nazywa się diagramem relacji encji
(ang. entity-relationship diagram). Na tym rysunku ramki reprezentują tabele bazy danych i zawierają
listy atrybutów (czyli kolumn) tabeli. Tabela roles przechowuje listę wszystkich możliwych ról użyt-
kowników, z których każda jest identyfikowana przez unikatową wartość id — klucz główny tabeli.

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.

Bazy danych NoSQL


Bazy danych, które nie są zgodne z modelem relacyjnym, opisanym w poprzednim podrozdziale,
są łącznie nazywane bazami danych NoSQL. Jedna wspólna organizacja baz danych NoSQL używa
kolekcji (ang. collections) zamiast tabel i dokumentów (ang. documents) zamiast rekordów. Bazy da-
nych NoSQL są zaprojektowane w sposób utrudniający tworzenie złączeń, więc większość z nich
w ogóle nie obsługuje tej operacji. W przypadku bazy danych NoSQL, skonstruowanej zgodnie ze
schematem pokazanym na rysunku 5.1, wyświetlenie listy użytkowników wraz z ich powiązaniami
wymaga, aby sama aplikacja wykonała operację złączenia, odczytując pole role_id każdego użyt-
kownika, a następnie przeszukując dla niego tabelę roles.
Na rysunku 5.2 przedstawiam projekt bardziej odpowiedni dla bazy danych NoSQL. Jest to wynik
zastosowania operacji zwanej denormalizacją (ang. denormalization), która zmniejsza liczbę tabel
kosztem duplikacji danych.

70  Rozdział 5. Bazy danych

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ń.

SQL czy NoSQL?


Bazy danych SQL specjalizują się w przechowywaniu danych strukturalnych w wydajnej i zwartej
formie. Takie bazy danych dokładają wszelkich starań, aby zachować spójność, nawet w przypadku
awarii zasilania lub awarii sprzętu. Paradygmat, który pozwala relacyjnym bazom danych osiągnąć ten
wysoki poziom niezawodności, nazywa się ACID (https://pl.wikipedia.org/wiki/ACID), co oznacza
atomowość, spójność, izolację i trwałość (ang. Atomicity, Consistency, Isolation, Durability).
Bazy danych NoSQL rozluźniają niektóre wymagania ACID, w wyniku czego czasami mogą uzyskać
większą wydajność.
Jednak pełna analiza i porównanie typów baz danych nie wchodzi w zakres tej książki. W przypadku
małych i średnich aplikacji świetnie sprawdzają się zarówno bazy danych SQL, jak i NoSQL, prezen-
tując praktycznie jednakową wydajność.

Frameworki baz danych w Pythonie


W Pythonie dostępne są pakiety dla większości istniejących baz danych, zarówno tych o otwartych
źródłach, jak i komercyjnych. Flask nie nakłada żadnych ograniczeń na to, które z tych pakietów
baz danych mogą być używane, tak więc możemy pracować z MySQL, Postgres, SQLite, Redis,
MongoDB, CouchDB lub DynamoDB, zależnie od tego, który jest Ci aktualnie potrzebny.
Gdyby jednak to wszystko nie wystarczyło, istnieje również wiele pakietów budujących warstwę
abstrakcji bazy danych, na przykład SQLAlchemy lub MongoEngine, które pozwalają pracować
na wyższym poziomie ze zwykłymi obiektami Pythona zamiast z elementami bazy danych, takimi jak
tabele, dokumenty lub języki zapytań.

Frameworki baz danych w Pythonie  71

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/).

72  Rozdział 5. Bazy danych

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.

Tabela 5.1. Adresy URL baz danych używane we Flask-SQLAlchemy

Silnik bazy danych URL


MySQL mysql://nazwa_użytkownika:hasło@nazwa_hosta/baza_danych
Postgres postgresql://nazwa_użytkownika:hasło@nazwa_hosta/baza_danych
SQLite (Linux, macOS) sqlite:////bezwzględna/ścieżka/do/bazy_danych
SQLite (Windows) sqlite:///c:/bezwzględna/ścieżka/do/bazy_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.

Listing 5.1. hello.py: Konfiguracja bazy danych


import os
from flask_sqlalchemy import SQLAlchemy

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)

Zarządzanie bazą danych za pomocą Flask-SQLAlchemy  73

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.

Listing 5.2. hello.py: Definicja modeli Role i User


class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)

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.

Flask-SQLAlchemy wymaga, aby wszystkie modele definiowały kolumnę klucza


głównego, która zazwyczaj otrzymuje nazwę id.

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.

74  Rozdział 5. Bazy danych

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 5.2. Najpopularniejsze typy kolumn SQLAlchemy

Nazwa typu Typ PythonA Opis


Integer int Zwykła liczba całkowita, zwykle 32-bitowa.
SmallInteger int Mała liczba całkowita, zwykle 16-bitowa.
BigInteger int lub long Liczba całkowita o nieograniczonej wielkości.
Float float Liczba zmiennoprzecinkowa.
Numeric decimal.Decimal Liczba stałoprzecinkowa.
String str Ciąg znaków o zmiennej długości.
Text str Ciąg znaków o zmiennej długości, zoptymalizowany pod kątem dużej
lub nieograniczonej długości.
Unicode unicode Ciąg znaków Unicode o zmiennej długości.
UnicodeText unicode Ciąg znaków Unicode o zmiennej długości, zoptymalizowany pod kątem
dużej lub nieograniczonej długości.
Boolean bool Wartość logiczna.
Date datetime.date Wartość daty.
Time datetime.time Wartość czasu.
DateTime datetime.datetime Wartość daty i czasu.
Interval datetime.timedelta Przedział czasowy.
Enum str Lista ciągów znaków.
PickleType Any Python object Automatyczna serializacja.
LargeBinary str Zbiór danych binarnych.

Tabela 5.3. Najpopularniejsze opcje kolumn SQLAlchemy

Nazwa opcji Opis


primary_key Jeśli ma wartość True, to kolumna jest kluczem głównym tabeli.
unique Jeśli ma wartość True, to wartości z tej kolumny nie mogą być powielane.
index Jeśli ma wartość True, to tworzony jest indeks dla tej kolumny, aby zwiększyć szybkość obsługi zapytań.
nullable Jeśli ma wartość True, to dana kolumna może nie mieć wartości. Jeśli ma wartość False, to w kolumnie
nie mogą pojawiać się wartości null.
default Definiuje wartość domyślną dla kolumny.

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.

Tabela 5.4. Typowe opcje relacji SQLAlchemy

Nazwa opcji Opis


backref Dodaj referencję wsteczną w drugim modelu w tej relacji.
primaryjoin Jawnie podaj warunki złączenia dwóch modeli. Jest to konieczne tylko w przypadku niejednoznacznych relacji.
lazy Określ sposób ładowania powiązanych elementów. Można tu użyć wartości select (elementy są
ładowane na żądanie przy pierwszym otwarciu), immediate (elementy są ładowane, gdy ładowany jest
obiekt źródłowy), joined (elementy są ładowane natychmiast, ale jako złączenie), subquery
(elementy są ładowane natychmiast, ale jako podzapytanie), noload (elementy nigdy nie są ładowane)
i dynamic (zamiast ładowania elementów podawane jest zapytanie, które może je załadować).
uselist Jeśli ma wartość False, zamiast listy użyj skalara.
order_by Określ sposób porządkowania elementów w relacji.
secondary Określa nazwę tabeli asocjacji, która jest używana w relacjach typu wiele do wielu.
secondaryjoin Określ dodatkowy warunek złączenia dla relacji typu wiele do wielu, gdy SQLAlchemy nie może
samodzielnie go określić.

76  Rozdział 5. Bazy danych

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.

Operacje na bazach danych


Modele są już całkowicie skonfigurowane zgodnie z diagramem przedstawionym na rysunku 5.1
i można zacząć ich używać. Najlepszą metodą nauki korzystania z tak przygotowanych modeli
jest zastosowanie powłoki Pythona. W kolejnych punktach omawiać będę najczęściej używane
operacje na bazach danych, wykonywane w powłoce uruchamianej poleceniem flask shell. Zanim
użyjesz tego polecenia, upewnij się, że zmienna środowiskowa FLASK_APP ma przypisaną wartość
hello.py, zgodnie z instrukcjami z rozdziału 2.

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

Niestety ta metoda ma niepożądany efekt uboczny polegający na zniszczeniu wszystkich danych


znajdujących się w starej bazie danych. Lepsze rozwiązanie problemu aktualizacji baz danych przed-
stawiono pod koniec tego rozdziału.

Operacje na bazach danych  77

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)

Można też użyć bardziej zwięzłego zapisu:


>>> db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, 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

78  Rozdział 5. Bazy danych

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'>]

Operacje na bazach danych  79

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

Tabela 5.5. Typowe zapytania SQLAlchemy

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'>

80  Rozdział 5. Bazy danych

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.

Listing 5.4. hello.py: Dynamiczna relacja w bazie danych


class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
# ...

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

Wykorzystanie bazy danych w funkcjach widoku


Operacje na bazie danych, opisane w poprzednim podrozdziale, mogą być używane bezpośrednio
w funkcjach widoku. Na listingu 5.5 pokazuję nową wersję trasy strony głównej, która zapisuje w bazie
danych imiona wprowadzone przez użytkowników.

Listing 5.5. hello.py: Użycie bazy danych w funkcjach widoku


@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)

Wykorzystanie bazy danych w funkcjach widoku  81

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.

Listing 5.6. templates/index.html: Niestandardowe powitanie w szablonie


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% 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 %}

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 5b, aby pobrać tę wersję aplikacji.

Integracja z powłoką Pythona


Importowanie instancji bazy danych oraz modeli przy każdym uruchomieniu sesji powłoki to bardzo
żmudne zadanie. Można uniknąć potrzeby ciągłego powtarzania tych kroków, konfigurując polecenie
flask shell tak, żeby automatycznie importowało wszystkie niezbędne obiekty.

82  Rozdział 5. Bazy danych

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.

Listing 5.7. hello.py: Dodawanie kontekstu powłoki


@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)

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'>

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 5c, aby pobrać tę wersję aplikacji.

Migrowanie baz danych za pomocą pakietu Flask-Migrate


W miarę postępu prac nad aplikacją może się okazać, że modele baz danych muszą się zmienić, a kiedy
to nastąpi, baza danych również będzie musiała zostać zaktualizowana. Pakiet Flask-SQLAlchemy
tylko wtedy tworzy tabele baz danych na podstawie modeli, gdy te jeszcze nie istnieją, dlatego jedynym
sposobem na zaktualizowanie struktury tabel jest zniszczenie starych tabel — oczywiście powoduje
to utratę wszystkich danych znajdujących się w bazie danych.
Lepszym rozwiązaniem będzie tu użycie frameworka migracji bazy danych (ang. database migration).
Tak jak narzędzia kontroli wersji kodu źródłowego śledzą zmiany w plikach źródłowych, tak frame-
work migracji baz danych śledzi zmiany w schemacie bazy, umożliwiając tym samym stosowanie
zmian przyrostowych.
Twórca narzędzia SQLAlchemy napisał framework migracji o nazwie Alembic (http://bit.ly/alembic-doc).
Aplikacje Flaska nie muszą bezpośrednio używać tego narzędzia, ale mogą skorzystać z rozszerzenia
Flask-Migrate (http://bit.ly/fl-migrate) — lekkiego wrappera dla Alembic, który integruje go z polece-
niem flask.

Tworzenie repozytorium migracji


Na początek musisz zainstalować pakiet Flask-Migrate w środowisku wirtualnym:
(venv) $ pip install flask-migrate

Migrowanie baz danych za pomocą pakietu Flask-Migrate  83

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Kod z listingu 5.8 pokazuje, jak inicjowane jest to rozszerzenie.

Listing 5.8. hello.py: Inicjowanie pakietu Flask-Migrate


from flask_migrate import Migrate
# ...
migrate = Migrate(app, db)

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.

Tworzenie skryptu migracji


W Alembic migracja bazy danych jest reprezentowana przez skrypt migracji (ang. migration script).
Skrypt ten ma dwie funkcje o nazwach upgrade() i downgrade(). Funkcja upgrade() wprowadza zmiany
do bazy danych, które są częścią migracji, natomiast funkcja downgrade() je usuwa. Możliwość
dodawania i usuwania zmian oznacza, że Alembic jest w stanie skonfigurować bazę danych do stanu
odpowiadającego dowolnemu punktowi w historii zmian.
Migracje Alembica można tworzyć ręcznie lub automatycznie za pomocą poleceń revision i migrate.
Ręczna migracja tworzy szkielet skryptu migracji z pustymi funkcjami upgrade() i downgrade(),
które muszą zostać zaimplementowane przez programistę korzystającego z dyrektyw udostępnianych
przez obiekt Operations Alembica. Automatyczna migracja próbuje wygenerować kod dla funkcji
upgrade() i downgrade(), szukając różnic między definicjami modelu a bieżącym stanem bazy danych.

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.

84  Rozdział 5. Bazy danych

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 5d, aby pobrać tę wersję aplikacji. Pamiętaj, że nie musisz generować
repozytorium i skryptów migracji dla tej aplikacji, ponieważ są one zawarte w repozyto-
rium GitHuba.

Aktualizacja bazy danych


Jeśli pracujesz z aplikacją, przechodząc wszystkie wcześniejsze etapy, to masz już plik
bazy danych, który został uprzednio utworzony za pomocą funkcji db.create_all().
W tym stanie instrukcja flask db upgrade zakończy się niepowodzeniem, ponie-
waż będzie próbowała utworzyć tabele bazy danych, które już istnieją. Prostym
sposobem rozwiązania tego problemu jest usunięcie pliku bazy danych data.sqlite,
a następnie uruchomienie flask db upgrade w celu wygenerowania nowej bazy
danych za pośrednictwem środowiska migracji. Inną opcją jest pominięcie instrukcji
flask db upgrade i zamiast tego oznaczenie istniejącej już bazy danych jako zaktuali-
zowanej za pomocą polecenia flask db stamp.

Po przejrzeniu i zaakceptowaniu skryptu migracji można go zastosować w bazie danych za pomocą


polecenia flask db upgrade:

Migrowanie baz danych za pomocą pakietu Flask-Migrate  85

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

W przypadku pierwszej migracji będzie to równoważne wywołaniu funkcji db.create_all(), ale


w kolejnych migracjach instrukcja flask db upgrade zastosuje aktualizacje do tabel bez wpływu na
ich zawartość.

Dodawanie kolejnych migracji


Pracując nad własnymi projektami, zapewne zauważysz, że musisz bardzo często wprowadzać
zmiany w modelach baz danych. Zarządzając bazą danych za pomocą środowiska migracji, wszystkie
zmiany musisz zdefiniować w skryptach migracji, ponieważ wszystko, co nie będzie zapisane w takim
skrypcie, nie będzie także powtarzalne. Procedura wprowadzania zmiany w bazie danych jest podobna
do tej, która została wykonana w celu przeprowadzenia pierwszej migracji, a odbywa się ona w nastę-
pujących krokach:
1. Wprowadź niezbędne zmiany w modelach bazy danych.
2. Wygeneruj migrację za pomocą instrukcji flask db migrate.
3. Przejrzyj wygenerowany skrypt migracji i popraw go, jeśli zawiera nieścisłości.
4. Zastosuj zmiany w bazie danych za pomocą instrukcji flask db upgrade.
Podczas pracy nad konkretną funkcją może okazać się konieczne wprowadzenie kilku zmian w mo-
delach baz danych, jeszcze zanim uzyskamy ich ostateczną postać. Jeśli Twoja ostatnia migracja
nie została jeszcze przesłana do systemu kontroli źródeł, to możesz ją rozszerzyć, aby uwzględnić
wprowadzane później zmiany, co pozwoli na ograniczenie liczby bardzo małych skryptów migracji,
które same w sobie nie mają żadnego znaczenia. Procedura rozszerzenia ostatniego skryptu migracji
jest następująca:
1. Usuń ostatnią migrację z bazy danych za pomocą polecenia flask db downgrade (pamiętaj, że
może to spowodować utratę niektórych danych).
2. Usuń ostatni skrypt migracji, który jest teraz osierocony.
3. Wygeneruj nową migrację bazy danych za pomocą polecenia flask db migrate. Będzie ona
teraz zawierać te zmiany w skrypcie migracji, który właśnie usunąłeś, a także wszelkie inne
zmiany wprowadzone w modelach.
4. Przejrzyj i zastosuj skrypt migracji zgodnie z wcześniejszym opisem.

Zapoznaj się z dokumentacją (https://flask-migrate.readthedocs.io) pakietu Flask-Migrate,


aby dowiedzieć się więcej o działaniu innych podinstrukcji związanych z migracjami
baz danych.

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.

86  Rozdział 5. Bazy danych

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.

Obsługa e-mail za pomocą rozszerzenia Flask-Mail


Oczywiście możesz używać pakietu smtplib, pochodzącego ze standardowej biblioteki Pythona,
do wysyłania wiadomości e-mail w aplikacji Flaska, jednak rozszerzenie Flask-Mail opakowuje
pakiet smtplib i ładnie integruje go z Flaskiem. Pakiet Flask-Mail można zainstalować przy użyciu
narzędzia pip:
(venv) $ pip install flask-mail

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.

Tabela 6.1. Klucze konfiguracyjne serwera SMTP w rozszerzeniu Flask-Mail

Klucz Domyślna wartość Opis


MAIL_SERVER localhost Nazwa hosta lub adres IP serwera e-mail.
MAIL_PORT 25 Port serwera e-mail.
MAIL_USE_TLS False Zabezpiecza połączenie za pomocą TLS (ang. Transport Layer Security).
MAIL_USE_SSL False Zabezpiecza połączenie za pomocą SSL (ang. Secure Sockets Layer).
MAIL_USERNAME None Nazwa użytkownika konta pocztowego.
MAIL_PASSWORD None Hasło użytkownika konta pocztowego.

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

Nigdy nie zapisuj danych konta bezpośrednio w swoich skryptach, szczególnie


w przypadku, gdy planujesz udostępnić swoją pracę jako oprogramowanie o otwartych
źródłach. Aby chronić dane konta, należy przygotować skrypt importujący poufne
informacje ze zmiennych środowiskowych.

Ze względów bezpieczeństwa konta GMail są skonfigurowane tak, aby wymagać od


zewnętrznych aplikacji korzystania z uwierzytelniania OAuth2 do łączenia się z serwe-
rem e-mail. Niestety biblioteka smtplib Pythona nie obsługuje tej metody uwierzytel-
niania. Aby konto Gmail akceptowało standardowe uwierzytelnianie SMTP, przejdź do
strony ustawień konta Google (https://myaccount.google.com) i wybierz pozycję Logo-
wanie do Google z lewego paska menu. Na tej stronie znajdź ustawienie Zezwalaj na
mniej bezpieczne aplikacje i upewnij się, że jest ono włączone. Jeśli włączenie tego usta-
wienia na Twoim osobistym koncie Gmail będzie stanowiło problem, to utwórz
konto dodatkowe, tak aby bezpiecznie przetestować wysyłanie wiadomości e-mail.

Na listingu 6.2 pokazano sposób inicjowania rozszerzenia Flask-Mail.

Listing 6.2. hello.py: Inicjowanie Flask-Mail


from flask_mail import Mail
mail = Mail(app)
Musisz jeszcze zdefiniować dwie zmienne środowiskowe, w których znajdzie się nazwa użytkownika
i hasło serwera e-mail. Jeśli korzystasz z systemu Linux lub macOS, możesz ustawić te zmienne w ten
sposób:
(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>
Natomiast użytkownicy systemu Microsoft Windows mogą utworzyć zmienne środowiskowe w ten
sposób:
(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>

Wysyłanie wiadomości e-mail z powłoki Pythona


Aby przetestować przygotowaną konfigurację, możesz uruchomić sesję powłoki i wysłać testową wia-
domość e-mail (zamień adres twoj@przyklad.pl na własny):
(venv) $ flask shell
>>> from flask_mail import Message
>>> from hello import mail

88  Rozdział 6. Wiadomości e-mail

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.

Integrowanie wiadomości e-mail z aplikacją


Dobrym pomysłem będzie wyodrębnienie typowych elementów operacji wysyłania wiadomości
e-mail do osobnej funkcji, która pozwoli nam uniknąć ręcznego tworzenia każdej wiadomości. Dodat-
kową korzyścią będzie to, że ta funkcja może renderować treść wiadomości e-mail na bazie szablonów
Jinja2, tak aby uzyskać największą elastyczność działania. Taka implementacja została pokazana
na listingu 6.3.

Listing 6.3. hello.py: Obsługa wiadomości e-mail


from flask_mail import Message

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@przyklad.pl>'

def send_email(to, subject, template, **kwargs):


msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)

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.

Listing 6.4. hello.py: Przykład wysyłania wiadomości e-mail


# ...
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
# ...
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()

Obsługa e-mail za pomocą rozszerzenia Flask-Mail  89

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić pole-


cenie git checkout 6a, aby pobrać tę wersję aplikacji.

Ta wersja aplikacji, obok opisanych wcześniej zmiennych środowiskowych MAIL_USERNAME


i MAIL_PASSWORD, będzie wymagać dodatkowej zmiennej środowiskowej FLASKY_ADMIN. W przypadku
użytkowników systemów Linux i macOS polecenie przygotowujące tę zmienną będzie wyglądać tak:
(venv) $ export FLASKY_ADMIN=<twój adres e-mail>

W przypadku użytkowników systemu Microsoft Windows odpowiadające temu polecenie jest


następujące:
(venv) $ set FLASKY_ADMIN=<twój adres e-mail>

Po przypisaniu wartości tym zmiennym środowiskowym możesz przetestować aplikację i otrzymywać


wiadomość e-mail za każdym razem, gdy w formularzu wprowadzisz nowe imię.

Asynchroniczne wysyłanie e-maila


Jeśli wysłałeś kilka testowych wiadomości e-mail, prawdopodobnie już zauważyłeś, że funkcja
mail.send() blokuje się na kilka sekund podczas wysyłania wiadomości e-mail, co powoduje, że
w tym czasie przeglądarka nie odpowiada. Aby uniknąć niepotrzebnych opóźnień podczas przetwa-
rzania żądań, funkcję wysyłania wiadomości e-mail można przenieść do wątku w tle. Kod z listingu 6.5
pokazuje tę zmianę.

90  Rozdział 6. Wiadomości e-mail

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 6.5. hello.py: Asynchroniczna obsługa poczty e-mail
from threading import Thread

def send_async_email(app, msg):


with app.app_context():
mail.send(msg)

def send_email(to, subject, template, **kwargs):


msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr

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 sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 6b, aby pobrać tę wersję aplikacji.

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.

Obsługa e-mail za pomocą rozszerzenia Flask-Mail  91

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.

Listing 7.1. Podstawowa struktura aplikacji Flaska z wieloma plikami


|-flasky
|-app/
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/
|-tests/
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt
|-config.py
|-flasky.py

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.

Listing 7.2. config.py: Konfiguracja aplikacji


import os
basedir = os.path.abspath(os.path.dirname(__file__))

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

94  Rozdział 7. Struktura dużej aplikacji

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.

W każdej z trzech konfiguracji zmienna SQLALCHEMY_DATABASE_URI ma przypisaną inną wartość.


Dzięki temu w każdej konfiguracji aplikacja może korzystać z innej bazy danych. Jest to bardzo
ważne, ponieważ nie chcemy, aby przeprowadzanie testów jednostkowych zmieniało bazę danych
używaną podczas codziennego programowania. Każda konfiguracja próbuje zaimportować adres URL
bazy danych ze zmiennej środowiskowej, a gdy nie jest on dostępny, to jako domyślny przyjmuje adres
bazy SQLite. W konfiguracji testowej domyślnie jest to baza danych w pamięci, ponieważ nie ma po-
trzeby przechowywania żadnych danych poza przebiegiem testowym.
Każda konfiguracja programistyczna i produkcyjna ma zestaw opcji konfiguracji serwera pocztowego.
Jako dodatkowy sposób umożliwiający aplikacji dostosowanie własnej konfiguracji klasa Config
i jej podklasy mogą definiować metodę klasy init_app(), która jako argument przyjmuje instancję
aplikacji. Na razie bazowa klasa Config implementuje tylko pustą metodę init_app().

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.

Korzystanie z fabryki aplikacji


Tworzenie aplikacji w wersji jednoplikowej jest bardzo wygodne, ale ma jedną dużą wadę. Taka apli-
kacja jest tworzona w zasięgu globalnym, a zatem nie ma możliwości dynamicznego wprowadza-
nia zmian w konfiguracji: od momentu uruchomienia skryptu instancja aplikacji jest już utworzona,
więc jest już za późno na modyfikowanie konfiguracji. Jest to szczególnie ważne w przypadku testów
jednostkowych, ponieważ czasami, w celu uzyskania lepszego pokrycia, konieczne jest uruchomie-
nie aplikacji przy innych ustawieniach konfiguracji.
Rozwiązaniem tego problemu może być opóźnienie tworzenia aplikacji poprzez przeniesienie jej
do funkcji wytwórczej (ang. factory function), którą można jawnie wywołać ze skryptu. Daje to
skryptowi nie tylko czas na przygotowanie konfiguracji, ale także możliwość tworzenia wielu in-
stancji aplikacji — a to kolejna rzecz, która może się okazać bardzo przydatna podczas testowania.
Funkcja wytwórcza aplikacji, pokazana na listingu 7.3, jest zdefiniowana w konstruktorze pakietu app.

Listing 7.3. app/__init__.py: Konstruktor pakietu aplikacji


from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config

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)

# Dołącz tutaj trasy i niestandardowe strony błędów.

return app

96  Rozdział 7. Struktura dużej aplikacji

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.

Implementacja funkcji aplikacji w projekcie


Konwersja do fabryki aplikacji wprowadza pewne komplikacje dla tras. W aplikacjach jednoskrypto-
wych instancja aplikacji istnieje w zasięgu globalnym, więc trasy można łatwo zdefiniować za pomocą
dekoratora app.route. Ale teraz, gdy aplikacja jest tworzona dynamicznie w czasie wykonywania
skryptu, dekorator app.route zaczyna istnieć dopiero po wywołaniu metody create_app(), niestety
wtedy już jest za późno. Ten sam problem mamy z własnymi stronami obsługi błędów, ponieważ są
one definiowane za pomocą dekoratora app.errorhandler.
Na szczęście Flask oferuje lepsze rozwiązanie przy użyciu schematów (ang. blueprints). Schemat
jest podobny do aplikacji, ponieważ może również definiować trasy i procedury obsługi błędów.
Jednak różnica polega na tym, że gdy są one zdefiniowane w schemacie, są w stanie uśpienia, dopóki
ten nie zostanie zarejestrowany w aplikacji, a wtedy stają się jej częścią. Korzystając ze schematu
zdefiniowanego w zasięgu globalnym, trasy i procedury obsługi błędów aplikacji można zdefiniować
prawie w taki sam sposób jak w aplikacji jednoskryptowej.
Podobnie jak aplikacje, schematy mogą być definiowane w jednym pliku, ale też mogą zostać przygo-
towane w bardziej uporządkowany sposób z wieloma modułami w pakiecie. Aby zapewnić jak naj-
większą elastyczność, utworzymy w pakiecie aplikacji podpakiet przeznaczony do obsługi pierwszego
schematu tej aplikacji. Na listingu 7.4 przedstawiam konstruktor pakietu służący do tworzenia
schematu.

Listing 7.4. app/main/__init__.py: Tworzenie głównego projektu


from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors

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.

Składnia from . import <pewien-moduł> jest używana w Pythonie do zapisywania


importu względnego (ang. relative imports). Znak kropki w tej instrukcji reprezentuje
aktualny pakiet. Wkrótce zobaczymy kolejny bardzo przydatny import względny,
który używa instrukcji from .. import <pewien-moduł>, gdzie znak dwóch kropek
.. reprezentuje element nadrzędny aktualnego pakietu.

Schemat zostaje zarejestrowany w aplikacji w ramach funkcji wytwórczej create_app(), co pokazano


na listingu 7.5.

Listing 7.5. app/__init__.py: Rejestracja schematu main


def create_app(config_name):
# ...

from .main import main as main_blueprint


app.register_blueprint(main_blueprint)

return app

Na listingu 7.6 przedstawiam procedury obsługi błędów.

Listing 7.6. app/main/error.py: Procedury obsługi błędów w projekcie main


from flask import render_template
from . import main

@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.

98  Rozdział 7. Struktura dużej aplikacji

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Na listingu 7.7 przedstawiam trasę aplikacji zaktualizowanej, tak aby mogła się znaleźć w schemacie.

Listing 7.7. app/main/views.py: Trasy aplikacji w schemacie main


from datetime import datetime
from flask import render_template, session, redirect, url_for
from . import main
from .forms import NameForm
from .. import db
from ..models import User

@main.route('/', methods=['GET', 'POST'])


def index():
form = NameForm()
if form.validate_on_submit():
# ...
return redirect(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_time=datetime.utcnow())

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.

Listing 7.8. flasky.py: Skrypt główny


import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate

app = create_app(os.getenv('FLASK_CONFIG') or 'default')


migrate = Migrate(app, db)

@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

Z kolei w systemie Microsoft Windows użyj tych poleceń:


(venv) $ set FLASK_APP=flasky.py
(venv) $ set 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

100  Rozdział 7. Struktura dużej aplikacji

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.

Listing 7.9. tests/test_basics.py: Testy jednostkowe


import unittest
from flask import current_app
from app import create_app, db

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'])

Testy jednostkowe  101

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 7a, aby pobrać przekonwertowaną wersję aplikacji. Aby upewnić się,
że masz zainstalowane wszystkie zależności, uruchom także polecenie pip install
-r requirements.txt.

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.

Listing 7.10. flasky.py: Polecenie uruchamiania testu jednostkowego


@app.cli.command()
def test():
"""Uruchom testy jednostkowe."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)

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

102  Rozdział 7. Struktura dużej aplikacji

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

Konieczność przygotowania zmiennych środowiskowych FLASK_APP i FLASK_DEBUG przy każdym


uruchomieniu nowej sesji wiersza poleceń może być nieco uciążliwe, dlatego dobrze jest tak skonfigu-
rować system, aby te zmienne były tworzone w sposób domyślny. Jeśli używasz powłoki bash, to
możesz dodać je do swojego pliku ~/.bashrc.
Wierzcie mi lub nie, ale właśnie dotarliśmy do końca I części tej książki. Nauczyliście się już podsta-
wowych elementów niezbędnych do zbudowania aplikacji internetowej za pomocą Flaska, ale naj-
prawdopodobniej nie jesteście jeszcze pewni, jak te wszystkie elementy połączyć ze sobą, tworząc
prawdziwą aplikację. Zatem celem II części tej książki będzie przeprowadzenie Cię przez proces
tworzenia kompletnej aplikacji.

Uruchamianie aplikacji  103

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.

Rozszerzenia uwierzytelnienia dla Flaska


Istnieje wiele doskonałych pakietów uwierzytelniania w języku Python, ale żaden z nich nie robi
wszystkiego. Rozwiązanie uwierzytelniania użytkowników przedstawione w tym rozdziale wykorzy-
stuje kilka pakietów i zapewnia spoiwo, które sprawia, że dobrze ze sobą współpracują. Oto lista pa-
kietów, które będą tu używane, wraz z opisami spełnianych przez nie funkcji:
 Flask-Login: zarządzanie sesjami dla zalogowanych użytkowników,
 Werkzeug: stosowanie funkcji skrótów i weryfikacja hasła,
 itsdangerous: kryptograficzne generowanie i weryfikacja tokenów zabezpieczających.

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

Haszowanie haseł za pomocą pakietu Werkzeug


Moduł bezpieczeństwa pakietu Werkzeug wygodnie implementuje bezpieczne haszowanie haseł.
W tym celu udostępniane są tylko dwie funkcje, stosowane, odpowiednio, w fazie rejestracji i weryfikacji:
generate_password_hash(password, method='pbkdf2:sha256', salt_length=8)
Ta funkcja przyjmuje hasło w postaci zwykłego tekstu i zwraca zhaszowane hasło jako ciąg
znaków, który można zapisać w bazie danych użytkowników. Domyślne wartości dla parametrów
method i salt_length są w większości przypadków całkowicie wystarczające.
check_password_hash(hash, password)
Ta funkcja pobiera zhaszowane hasło zapisane wcześniej w bazie danych i hasło wprowadzone
przez użytkownika. Zwracana wartość True wskazuje, że hasło użytkownika jest prawidłowe.
Kod zawarty na listingu 8.1 pokazuje zmiany w modelu User, utworzonym już wcześniej w roz-
dziale 5. w celu uwzględnienia haszowania hasła.

Listing 8.1. app/models.py: Haszowanie hasła w modelu użytkownika


from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
# ...
password_hash = db.Column(db.String(128))

@property

108  Rozdział 8. Uwierzytelnianie użytkownika

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)

def verify_password(self, password):


return check_password_hash(self.password_hash, password)

Funkcja haszowania hasła jest realizowana za pośrednictwem właściwości tylko-do-zapisu o nazwie


password. Gdy tej właściwości jest przypisywana wartość, metoda ustalająca wywoła funkcję generate_
password_hash() i zapisze wynik w polu password_hash. Próba odczytania właściwości password
zwróci błąd, ponieważ nie pozwalamy na odczytanie oryginalnego hasła po jego zhaszowaniu.
Metoda verify_password() pobiera hasło i przekazuje je do funkcji check_password_hash()w celu
porównania z zakodowaną wersją zapisaną w modelu User. Jeśli metoda ta zwróci wartość True,
to oznacza, że hasło jest poprawne.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 8a, aby pobrać tę wersję aplikacji.

Funkcja haszowania hasła jest teraz kompletna i można ją przetestować w powłoce:


(venv) $ flask shell
>>> u = User()
>>> u.password = 'cat'
>>> u.password
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/flask/flasky/app/models.py", line 24, in password
raise AttributeError('Nie można odczytać atrybutu password.')
AttributeError: Nie można odczytać atrybutu password.
>>> u.password_hash
'pbkdf2:sha256:50000$moHwFH1B$ef1574909f9c549285e8547cad181c5e0213cfa44a4aba4349
fa830aa1fd227f'
>>> u.verify_password('cat')
True
>>> u.verify_password('dog')
False
>>> u2 = User()
>>> u2.password = 'cat'
>>> u2.password_hash
'pbkdf2:sha256:50000$Pfz0m0KU$27be930b7f0e0119d38e8d8a62f7f5e75c0a7db61ae16709bc
aa6cfd60c44b74'

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.

Bezpieczeństwo hasła  109

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)

Użyj poniższego polecenia, aby uruchomić nowe testy jednostkowe:


(venv) $ flask test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
test_no_password_getter (test_user_model.UserModelTestCase) ... ok
test_password_salts_are_random (test_user_model.UserModelTestCase) ... ok
test_password_setter (test_user_model.UserModelTestCase) ... ok
test_password_verification (test_user_model.UserModelTestCase) ... ok

.----------------------------------------------------------------------
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.

Tworzenie schematu uwierzytelnienia


W rozdziale 7. schematy zostały wprowadzone jako sposób definiowania tras w zasięgu globalnym po
przeniesieniu tworzenia aplikacji do funkcji wytwórczej. W tym podrozdziale trasy związane z pod-
systemem uwierzytelniania użytkownika zostaną dodane do drugiego schematu o nazwie auth.
Stosowanie różnych schematów dla osobnych podsystemów aplikacji jest świetnym sposobem na
uporządkowanie kodu.
Schemat auth zostanie umieszczony w pakiecie Pythona o tej samej nazwie. Konstruktor pakietu
schematu utworzy obiekt schematu i zaimportuje trasy z modułu views.py. Zostało to pokazane
w kodzie na listingu 8.3.

110  Rozdział 8. Uwierzytelnianie użytkownika

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.3. app/auth/__init__.py: Tworzenie schematu uwierzytelnienia
from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views

Moduł app/auth/views.py, pokazany na listingu 8.4, importuje schemat i za pomocą dekoratora


route definiuje trasy związane z uwierzytelnianiem. Na razie dodajemy jedynie trasę /login, która
renderuje szablon zastępczy o tej samej nazwie.

Listing 8.4. app/auth/views.py: Trasy schematu uwierzytelnienia i funkcje widoku


from flask import render_template
from . import auth

@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.

Listing 8.5. app/__init__.py: Rejestracja schematu uwierzytelnienia


def create_app(config_name):
# ...
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz uruchomić polecenie


git checkout 8b, aby pobrać tę wersję aplikacji.

Tworzenie schematu uwierzytelnienia  111

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

Przygotowywanie modelu User na potrzeby logowania


Flask-Login ściśle współpracuje z obiektami klasy User należącymi do aplikacji. Rozszerzenie
Flask-Login wymaga zaimplementowania kilku właściwości i metod, aby mogło pracować z modelem
User z naszej aplikacji. Wymagane elementy pokazano w tabeli 8.1.

Tabela 8.1. Elementy wymagane przez 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.

Te właściwości i metody można zaimplementować bezpośrednio w klasie modelu, jednak roz-


szerzenie Flask-Login udostępnia prostsze rozwiązanie w postaci klasy UserMixin, która ma domyślne
implementacje tych elementów sprawdzające się w większości przypadków. Zaktualizowany model
User pokazano na listingu 8.6.

Listing 8.6. app/models.py: Aktualizacja modelu User w celu obsługi logowania użytkownika
from flask_login import UserMixin

class User(UserMixin, db.Model):


__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

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.

112  Rozdział 8. Uwierzytelnianie użytkownika

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.

Listing 8.8. app/models.py: Funkcja ładująca dane użytkownika


from . import login_manager

@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

Dekorator login_manager.user_loader służy do zarejestrowania funkcji w rozszerzeniu Flask-Login,


które wywoła ją, gdy będzie musiało pobrać informacje o zalogowanym użytkowniku. Identyfi-
kator użytkownika zostanie przekazany jako ciąg znaków, więc funkcja skonwertuje go na liczbę
całkowitą przed przekazaniem jej do zapytania Flask-SQLAlchemy, które załaduje dane użytkownika.
Wartość zwracana przez funkcję musi być obiektem klasy User lub wartością None, jeżeli identyfikator
użytkownika będzie niepoprawny lub wystąpi inny błąd.

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

Uwierzytelnianie użytkownika za pomocą Flask-Login  113

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.

Dodawanie formularza logowania


Prezentowany użytkownikom formularz logowania składa się z pola tekstowego na adres e-mail,
pola hasła, pola wyboru „zapamiętaj mnie”, a także przycisku przesyłania. Klasę rozszerzenia
Flask-WTF, która definiuje taki formularz, pokazano na listingu 8.9.

Listing 8.9. app/auth/forms.py: Formularz logowania


from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email

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

Klasa PasswordField reprezentuje element <input> o typie type="password". Klasa BooleanField


reprezentuje pole wyboru.
Pole email korzysta z walidatorów Length() i Email() pochodzących z rozszerzenia WTForms,
a dodatkowo z walidatora DataRequired(). W ten sposób zyskujemy pewność, że użytkownik
wprowadzi do tego pola poprawną wartość. Rozszerzenie WTForms wykonuje walidatory w kolej-
ności, w jakiej zostały podane przy definiowaniu pola. W przypadku wystąpienia błędu walidacji zo-
stanie wyświetlony komunikat o błędzie pochodzący z pierwszego walidatora zgłaszającego błąd.
Szablon powiązany ze stroną logowania jest przechowywany w pliku auth/login.html. Musi on jedynie
zrenderować formularz przy użyciu makra wtf.quick_form() z rozszerzenia Flask-Bootstrap. Na ry-
sunku 8.1 pokazuję formularz logowania wyświetlany w przeglądarce internetowej.
Pasek nawigacji w szablonie base.html wykorzystuje instrukcję warunkową Jinja2 do wyświetlania
linków Zaloguj się lub Wyloguj w zależności od stanu zalogowania aktualnego użytkownika. Tę in-
strukcję pokazano na listingu 8.10.

Listing 8.10. app/templates/base.html: Linki nawigacyjne Zaloguj się i Wyloguj


<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Wyloguj</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Zaloguj się</a></li>
{% endif %}
</ul>

114  Rozdział 8. Uwierzytelnianie użytkownika

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.

Listing 8.11. app/auth/views.py: Trasa logowania


from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user
from . import auth
from ..models import User
from .forms import LoginForm

@auth.route('/login', methods=['GET', 'POST'])


def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)

Uwierzytelnianie użytkownika za pomocą Flask-Login  115

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

Zgodnie ze wzorem Prześlij/Przekieruj/Pobierz (ang. Post/Redirect/Get), omówionym wcześniej


w rozdziale 4., żądanie POST, które przesłało dane logowania, kończy się przekierowaniem. Tym razem
mamy jednak dwa możliwe docelowe adresy URL. Jeśli formularz logowania został przedstawiony
użytkownikowi, aby zapobiec nieautoryzowanemu dostępowi do chronionego adresu URL, to rozsze-
rzenie Flask-Login zapisze ten oryginalny adres URL w argumencie next w segmencie zapytania.
Do tej informacji można uzyskać dostęp za pomocą słownika request.args. Jeśli argument next
w segmencie zapytania nie będzie dostępny, to wygenerowane zostanie przekierowanie na stronę
główną. Adres URL w argumencie next jest dodatkowo sprawdzany, aby upewnić się, że jest względ-
nym adresem URL. Ma to uniemożliwić złośliwemu użytkownikowi wykorzystanie tego argumentu
do przekierowania niczego niepodejrzewających użytkowników do innej witryny.
W przypadku gdy adres e-mail lub hasło podane przez użytkownika są nieprawidłowe, wyświetlana
jest wiadomość w okienku i formularz jest ponownie renderowany, tak aby użytkownik mógł ponowić
próbę logowania.

Na serwerze produkcyjnym aplikacja musi zostać udostępniona za pośrednictwem


zabezpieczonego protokołu HTTPS, aby dane logowania i sesje użytkownika były zaw-
sze przesyłane w postaci zaszyfrowanej. Bez takiego zabezpieczenia poufne dane
mogą zostać przechwycone przez hakera podczas ataku.

Musimy jeszcze zaktualizować szablon logowania, aby odpowiednio renderować formularz. Zmiany te
pokazano na listingu 8.12.

116  Rozdział 8. Uwierzytelnianie użytkownika

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 8.12. app/templates/auth/login.html: Szablon formularza logowania
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Login{% endblock %}

{% 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.

Listing 8.13. app/auth/views.py: Trasa wylogowania


from flask_login import logout_user, login_required

@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Zostałeś wylogowany.')
return redirect(url_for('main.index'))

Aby wylogować użytkownika, trzeba wywołać funkcję logout_user() z rozszerzenia Flask-Login.


Zajmie się ona usunięciem i zresetowaniem sesji użytkownika. Wylogowanie kończy się pojawieniem
się wiadomości w okienku, która potwierdza wykonanie tej operacji, i przekierowaniem do strony
głównej.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 8c, aby pobrać tę wersję aplikacji. Ta wersja zawiera migrację bazy
danych, dlatego pamiętaj, żeby po pobraniu kodu wywołać polecenie flask db upgrade.
Aby upewnić się, że masz zainstalowane wszystkie zależności, wywołaj także polecenie
pip install -r requirements.txt.

Jak działa Flask-Login?


Flask-Login jest względnie małym rozszerzeniem, ale z uwagi na wiele ruchomych elementów
biorących udział w operacji uwierzytelnienia użytkownicy Flaska często mają problem ze zrozumie-
niem, jak to rozszerzenie działa. Poniżej znajduje się kolejność operacji wykonywanych, gdy użytkow-
nik loguje się do systemu:
1. Użytkownik przechodzi na stronę http://localhost:5000/auth/login, klikając łącze Zaloguj się.
Procedura obsługi tego adresu URL zwraca formularz logowania zbudowany na podstawie
szablonu.

Uwierzytelnianie użytkownika za pomocą Flask-Login  117

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.

Listing 8.14. app/templates/index.html: Powitanie zalogowanego użytkownika


Witaj,
{% if current_user.is_authenticated %}
{{ current_user.username }}
{% else %}
nieznajomy
{% endif %}!

118  Rozdział 8. Uwierzytelnianie użytkownika

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.

Rysunek 8.2. Wygląd strony głównej po udanym logowaniu

Rejestrowanie nowego użytkownika


Gdy nowi użytkownicy chcą korzystać z naszej aplikacji, muszą się najpierw zarejestrować, co pozwoli
im się później zalogować. Link na stronie logowania wyśle ich na stronę rejestracji, na której będą
mogli podać swój adres e-mail, nazwę użytkownika oraz hasło.

Tworzenie formularza rejestracji użytkownika


Formularz, który będzie używany na stronie rejestracji, poprosi użytkownika o podanie jego adresu
e-mail, nazwy użytkownika i hasła. Taki formularz pokazano na listingu 8.15.

Listing 8.15. app/auth/forms.py: Formularz rejestracyjny użytkownika


from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo

Rejestrowanie nowego użytkownika  119

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

def validate_email(self, field):


if User.query.filter_by(email=field.data).first():
raise ValidationError('Ten e-mail jest już zarejestrowany.')

def validate_username(self, field):


if User.query.filter_by(username=field.data).first():
raise ValidationError('Ta nazwa użytkownika jest już używana.')

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.

120  Rozdział 8. Uwierzytelnianie użytkownika

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 8.3. Formularz rejestracji nowego użytkownika

Listing 8.16. app/templates/auth/login.html: Link do strony rejestracji


<p>
Nowy użytkownik?
<a href="{{ url_for('auth.register') }}">
Kliknij tutaj, aby się zarejestrować
</a>
</p>

Rejestracja nowych użytkowników


Obsługa rejestracji użytkowników nie stanowi większych niespodzianek. Po przesłaniu i zatwier-
dzeniu formularza rejestracyjnego nowy użytkownik jest dodawany do bazy danych przy użyciu
informacji dostarczonych przez użytkownika. Funkcja widoku, która wykonuje to zadanie, została po-
kazana na listingu 8.17.

Listing 8.17. app/auth/views.py: Trasa rejestracji użytkownika


@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data,
username=form.username.data,
password=form.password.data)
db.session.add(user)
db.session.commit()

Rejestrowanie nowego użytkownika  121

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
flash('Możesz się już zalogować.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 8d, aby pobrać tę wersję aplikacji.

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.

Generowanie tokenów potwierdzających za pomocą pakietu itsdangerous


Najprostszym linkiem potwierdzenia konta byłby adres URL w formacie http://www.przyklad.pl/
auth/confirm/<id>, umieszczony w treści e-maila z prośbą o potwierdzenie, gdzie <id> to identy-
fikator przypisany użytkownikowi w bazie danych. Gdy użytkownik kliknie takie łącze, funkcja
widoku obsługująca tę trasę otrzyma jako argument identyfikator użytkownika w celu jego potwier-
dzenia. W ten sposób funkcja może łatwo zaktualizować status użytkownika.
Ale oczywiście nie jest to bezpieczna implementacja, ponieważ każdy użytkownik, który odkryje
format linków potwierdzających, będzie mógł potwierdzić dowolne konta, wysyłając losowe liczby
w adresie URL. Chodzi tu o zastąpienie <id> w adresie URL tokenem zawierającym te same informa-
cje, ale w taki sposób, aby tylko serwer mógł generować prawidłowe potwierdzające adresy URL.
Jeśli przypominasz sobie dyskusję na temat sesji użytkownika prowadzoną w rozdziale 4., to wiesz,
że Flask używa ciasteczek z podpisem kryptograficznym, aby chronić zawartość sesji użytkownika
przed manipulacją. Pliki cookie sesji użytkownika zawierają podpis kryptograficzny generowany
przez pakiet o nazwie itsdangerous. Jeśli zawartość sesji użytkownika zostanie zmieniona, podpis
nie będzie już zgodny z treścią, a wtedy Flask odrzuci sesję i rozpocznie nową. To samo rozwiązanie
można zastosować w tokenach potwierdzających.
Poniżej znajduje się krótka sesja powłoki, która pokazuje, jak pakiet itsdangerous może wygenerować
podpisany token zawierający w sobie identyfikator użytkownika:
(venv) $ flask shell
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in=3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token

122  Rozdział 8. Uwierzytelnianie użytkownika

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.

Obiekt serializatora udostępnia metodę loads(), pozwalającą na zdekodowanie tokena podanego


jej w jedynym argumencie. Funkcja weryfikuje podpis i czas jego wygaśnięcia, a jeśli oba są poprawne,
zwraca oryginalne dane. Natomiast gdy metoda loads() otrzyma niepoprawny token lub poprawny
token, który utracił już swoją ważność, wtedy zgłasza odpowiedni wyjątek.
Możemy teraz rozbudować model User o operacje generowania i weryfikowania tokenów za pomocą
tych funkcji. Zmiany w kodzie pokazano na listingu 8.18.

Listing 8.18. app/models.py: Potwierdzenie konta użytkownika


from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db

class User(UserMixin, db.Model):


# ...
confirmed = db.Column(db.Boolean, default=False)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id}).decode('utf-8')

def confirm(self, token):


s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True

Metoda generate_confirmation_token() generuje token z domyślnym czasem ważności wynoszącym


jedną godzinę. Metoda confirm() weryfikuje ten token i, jeśli jest prawidłowy, przypisuje wartość
True atrybutowi confirmed w modelu użytkownika.

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.

Potwierdzenie konta  123

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.

Wysyłanie wiadomości e-mail z potwierdzeniem


Aktualnie trasa /register, po dodaniu nowego użytkownika do bazy danych, przekierowuje na adres
/index. Jednak jeszcze przed przekierowaniem trasa musi wysłać wiadomość e-mail z prośbą o po-
twierdzenie. Zastosowaną zmianę przedstawiono w kodzie z listingu 8.19.

Listing 8.19. app/auth/views.py: Trasa rejestracji konta z wysyłaniem e-maila potwierdzającego


from ..email import send_email

@auth.route('/register', methods=['GET', 'POST'])


def register():
form = RegistrationForm()
if form.validate_on_submit():
# ...
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Potwierdź swoje konto.',
'auth/email/confirm', user=user, token=token)
flash('Na twój adres e-mail wysłano prośbę o potwierdzenie konta.')
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)

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.

Listing 8.20. app/templates/auth/email/confirm.txt: Tekstowa wersja e-maila z prośbą o potwierdzenie


Drogi {{user.username}},
Witamy we Flasky!
Aby potwierdzić swoje konto, kliknij ten link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Z poważaniem,
Zespół Flasky
Uwaga: Nie odpowiadaj na tę wiadomość.

124  Rozdział 8. Uwierzytelnianie użytkownika

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.

Listing 8.21. app/auth/views.py: Potwierdzenie konta użytkownika


from flask_login import current_user

@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.

Potwierdzenie konta  125

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

Metoda obsługi before_app_request przechwytuje żądanie, gdy spełnione są trzy warunki:


1. Użytkownik jest zalogowany (current_user.is_authenticated ma wartość True).
2. Konto użytkownika nie zostało potwierdzone.
3. Żądany adres URL znajduje się poza schematem uwierzytelnienia i nie dotyczy pliku statycz-
nego. Dostęp do tras uwierzytelniania nie może zostać zablokowany, ponieważ są to trasy,
które pozwolą użytkownikowi potwierdzić konto lub wykonać inne funkcje zarządzania nim.
Jeśli te trzy warunki są spełnione, następuje przekierowanie na nową trasę /auth/unconfirmed, która
wyświetla stronę z informacją o potwierdzeniu konta.

Jeżeli wywołanie zwrotne before_request lub before_app_request zwróci odpowiedź


lub przekierowanie, to Flask wyśle je do klienta bez wywoływania funkcji widoku
powiązanej z tym żądaniem. W efekcie taki mechanizm pozwala przechwycić żądanie,
gdy zachodzi taka potrzeba.

Strona prezentowana niepotwierdzonym użytkownikom (pokazana na rysunku 8.4) po prostu


renderuje szablon zawierający instrukcje, jak mogą potwierdzić swoje konto, i oferujący link pozwala-
jący wygenerować nową wiadomości e-mail z potwierdzeniem na wypadek utraty oryginalnej wiado-
mości. Trasa, która ponownie wysyła wiadomość e-mail z potwierdzeniem, została pokazana
w kodzie z listingu 8.23.

Listing 8.23. app/auth/views.py: Ponowne wysłanie e-maila z potwierdzeniem konta


@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Potwierdź swoje konto.',
'auth/email/confirm', user=current_user, token=token)
flash('Nowa wiadomość z potwierdzeniem została wysłana.')
return redirect(url_for('main.index'))

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.

126  Rozdział 8. Uwierzytelnianie użytkownika

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 8.4. Niepotwierdzona strona konta

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 8e, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, pamiętaj zatem, aby po pobraniu kodu uruchomić polecenie flask
db upgrade.

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.

Zarządzanie kontem  127

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.

128  Rozdział 8. Uwierzytelnianie 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ń.

Reprezentacja ról w bazie danych


W rozdziale 5. przygotowaliśmy już prostą tabelę ról jako sposób na zademonstrowanie relacji jeden-
do-wielu. Teraz na listingu 9.1 przedstawiam ulepszony model roli z pewnymi dodatkami.

Listing 9.1. app/models.py: Bazodanowy model roli


class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')

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.

Tabela 9.1. Uprawnienia w aplikacji

Nazwa zadania Nazwa uprawnienia Wartość uprawnienia


Obserwuj użytkowników FOLLOW 1
Komentuj posty innych osób COMMENT 2
Pisz artykuły WRITE 4
Moderuj komentarze innych osób MODERATE 8
Dostęp administracyjny ADMIN 16

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.

Listing 9.2. app/models.py: Stałe uprawnień


class Permission:
FOLLOW = 1
COMMENT = 2
WRITE = 4
MODERATE = 8
ADMIN = 16

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.

130  Rozdział 9. Role użytkowników

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 9.3. app/models.py: Zarządzanie uprawnieniami w modelu Role
class Role(db.Model):
# ...

def add_permission(self, perm):


if not self.has_permission(perm):
self.permissions += perm

def remove_permission(self, perm):


if self.has_permission(perm):
self.permissions -= perm

def reset_permissions(self):
self.permissions = 0

def has_permission(self, perm):


return self.permissions & perm == perm

Metody add_permission(), remove_permission() i reset_permission() wykorzystują podstawowe


operacje arytmetyczne podczas aktualizowania listy uprawnień. Metoda has_permission() jest najbar-
dziej skomplikowana, ponieważ wykorzystuje bitowy operator koniunkcji & (https://docs.python.org/
3/reference/expressions.html#binary-bitwise-operations) i z jego pomocą sprawdza, czy wartość
pola uprawnień zawiera wybrane uprawnienia. Możesz nieco się pobawić tymi metodami w po-
włoce Pythona:
(venv) $ flask shell
>>> r = Role(name='User')
>>> r.add_permission(Permission.FOLLOW)
>>> r.add_permission(Permission.WRITE)
>>> r.has_permission(Permission.FOLLOW)
True
>>> r.has_permission(Permission.ADMIN)
False
>>> r.reset_permissions()
>>> r.has_permission(Permission.FOLLOW)
False

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.

Tabela 9.2. Role użytkowników

Rola użytkownika Uprawnienia Opis


None None Dostęp do aplikacji w trybie tylko do odczytu. Dotyczy to nieznanych
użytkowników, którzy nie są zalogowani.
User FOLLOW, COMMENT, Podstawowe uprawnienia do pisania postów i komentarzy oraz śledzenia
WRITE innych użytkowników. Jest to ustawienie domyślne dla nowych
użytkowników.
Moderator FOLLOW, COMMENT, Dodaje uprawnienia do moderowania komentarzy innych użytkowników.
WRITE, MODERATE
Administrator FOLLOW, COMMENT, Pełny dostęp, który obejmuje uprawnienia do zmiany ról innych
WRITE, MODERATE,
użytkowników.
ADMIN

Reprezentacja ról w bazie danych  131

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.

Listing 9.4. app/models.py: Tworzenie ról w bazie danych


class Role(db.Model):
# ...
@staticmethod
def insert_roles():
roles = {
'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
'Moderator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE, Permission.MODERATE],
'Administrator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE, Permission.MODERATE,
Permission.ADMIN],
}
default_role = 'User'
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
role.reset_permissions()
for perm in roles[r]:
role.add_permission(perm)
role.default = (role.name == default_role)
db.session.add(role)
db.session.commit()

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

132  Rozdział 9. Role użytkowników

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.

Listing 9.5. app/models.py: Definiowanie domyślnej roli dla użytkowników


class User(UserMixin, db.Model):
# ...
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(name='Administrator').first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
# ...

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.

Listing 9.6. app/models.py: Sprawdzanie, czy użytkownik ma określone uprawnienia


from flask_login import UserMixin, AnonymousUserMixin
class User(UserMixin, db.Model):
# ...

def can(self, perm):


return self.role is not None and self.role.has_permission(perm
)
def is_administrator(self):
return self.can(Permission.ADMIN)

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().

Weryfikacja roli  133

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.

Listing 9.7. app/decorators.py: Własne dekoratory sprawdzające uprawnienia użytkownika


from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission

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)

Te dekoratory zostały przygotowane z wykorzystaniem pakietu functools ze standardowej biblioteki


Pythona. Gdy aktualny użytkownik nie ma niezbędnego uprawnienia, zwracają odpowiedź 403,
kod statusu HTTP “Forbidden” — Zabroniony. W rozdziale 3. przygotowaliśmy już własne strony
błędów dla kodów 404 i 500, dlatego teraz w podobny sposób dodamy stronę dla błędu 403.
Poniżej znajdują się dwa przykłady demonstrujące użycie tych dekoratorów:
from .decorators import admin_required, permission_required

@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

134  Rozdział 9. Role użytkowników

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.

Listing 9.8. app/main/__init__.py: Dodanie klasy Permission do kontekstu szablonu


@main.app_context_processor
def inject_permissions():
return dict(Permission=Permission)

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.

Listing 9.9. tests/test_user_model.py: Testy jednostkowe ról i uprawnień


class UserModelTestCase(unittest.TestCase):
# ...

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

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 9a, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, więc pamiętaj, żeby po pobraniu kodu uruchomić polecenie flask
db upgrade.

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'>]

Weryfikacja roli  135

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.

136  Rozdział 9. Role 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.

Listing 10.1. app/models.py: Pola informacji o użytkowniku


class User(UserMixin, db.Model):
# ...
name = db.Column(db.String(64))
location = db.Column(db.String(64))
about_me = db.Column(db.Text())
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)

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.

Listing 10.3. app/auth/views.py: Pingowanie zalogowanego użytkownika


@auth.before_app_request
def before_request():
if current_user.is_authenticated:
current_user.ping()
if not current_user.confirmed \
and request.endpoint \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))

Strona profilu użytkownika


Utworzenie strony profilu dla każdego użytkownika nie powinno być żadnym wyzwaniem. Na li-
stingu 10.4 prezentuję definicję odpowiedniej trasy.

Listing 10.4. app/main/views.py: Trasa strony profilu


@main.route('/user/<username>')
def user(username):
user = User.query.filter_by(username=username).first_or_404()
return render_template('user.html', user=user)

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.

Listing 10.5. app/templates/user.html: Szablon profilu użytkownika


{% extends "base.html" %}
{% block title %}Flasky - {{ user.username }}{% endblock %}

{% block page_content %}

138  Rozdział 10. Profile użytkowników

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 %}

Ten szablon zawiera kilka interesujących szczegółów implementacji:


 Pola name i location są renderowane w jednym elemencie <p>. Instrukcja warunkowa Jinja2
gwarantuje, że element <p> zostanie utworzony tylko w przypadku, gdy zdefiniowane jest co
najmniej jedno z tych pól.
 Pole location jest renderowane jako łącze do zapytania w Mapach Google, więc kliknięcie go
otwiera mapę wyśrodkowaną na lokalizacji zapisanej w tym polu.
 Jeśli zalogowany użytkownik jest administratorem, to adres e-mail wyświetlanego użytkownika
renderowany jest jako łącze mailto. Przydaje się to w sytuacji, gdy administrator przegląda
stronę profilu innego użytkownika i musi się z nim skontaktować.
 Dwa znaczniki czasu związane z użytkownikiem są renderowane na stronie za pomocą pakietu
Flask-Moment, tak jak to robiliśmy wcześniej w rozdziale 3.
Większość użytkowników będzie chciała mieć łatwy dostęp do własnej strony profilu, dlatego link
do niej można dodać do paska nawigacji. Odpowiednie zmiany w szablonie base.html przedstawiam
na listingu 10.6.

Listing 10.6. app/templates/base.html: Dodawanie linka do strony profilu na pasku nawigacyjnym


{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('main.user', username=current_user.username) }}">
Profil
</a>
</li>
{% endif %}

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.

Strona profilu użytkownika  139

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 10.1. Strona profilu użytkownika

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 10a, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, więc po pobraniu kodu pamiętaj o uruchomieniu polecenia flask db
upgrade.

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.

Edytor profilu z poziomu użytkownika


Na listingu 10.7 przedstawiam formularz edycji profilu przeznaczony dla zwykłych użytkowników.

Listing 10.7. app/main/forms.py: Formularz edytowania profilu


class EditProfileForm(FlaskForm):
name = StringField('Prawdziwe imię', validators=[Length(0, 64)])
location = StringField('Lokalizacja', validators=[Length(0, 64)])
about_me = TextAreaField('O mnie')
submit = SubmitField('Wyślij')

140  Rozdział 10. Profile użytkowników

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.

Listing 10.8. app/main/views.py: Trasa edycji profilu


@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.name = form.name.data
current_user.location = form.location.data
current_user.about_me = form.about_me.data
db.session.add(current_user._get_current_object())
db.session.commit()
flash('Twój profil został zaktualizowany.')
return redirect(url_for('.user', username=current_user.username))
form.name.data = current_user.name
form.location.data = current_user.location
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)

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.

Rysunek 10.2. Edytor profilu

Edytor profilu  141

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.

Listing 10.9. app/templates/user.html: Link do edycji profilu


{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('.edit_profile') }}">
Edytuj profil
</a>
{% endif %}

Instrukcja warunkowa otaczająca link spowoduje, że pojawi się on tylko w przypadku, gdy użytkownik
będzie oglądać własny profil.

Edytor profilu z poziomu administratora


Formularz edycji profilu dla administratorów jest bardziej złożony niż formularz przeznaczony
dla zwykłych użytkowników. Ten formularz pozwala administratorom edytować nie tylko podsta-
wowe trzy pola informacyjne, ale też adres e-mail użytkownika, jego nazwę, status potwierdzenia
oraz rolę. Tę wersję formularza przedstawiam na listingu 10.10.

Listing 10.10. app/main/forms.py: Formularz edycji profilu dla administratorów


class EditProfileAdminForm(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,
'Nazwy użytkownika mogą składać się tylko z liter, '
'cyfr, kropek i znaków podkreślenia.')])
confirmed = BooleanField('Potwierdzony')
role = SelectField('Rola', coerce=int)
name = StringField('Prawdziwe imię', validators=[Length(0, 64)])
location = StringField('Lokalizacja', validators=[Length(0, 64)])
about_me = TextAreaField('O mnie')
submit = SubmitField('Wyślij')

def __init__(self, user, *args, **kwargs):


super(EditProfileAdminForm, self).__init__(*args, **kwargs)
self.role.choices = [(role.id, role.name)
for role in Role.query.order_by(Role.name).all()]
self.user = user

def validate_email(self, field):


if field.data != self.user.email and \
User.query.filter_by(email=field.data).first():
raise ValidationError('Ten adres e-mail już jest zarejestrowany.')

def validate_username(self, field):


if field.data != self.user.username and \
User.query.filter_by(username=field.data).first():
raise ValidationError('Ta nazwa użytkownika jest już używana.')

142  Rozdział 10. Profile użytkowników

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.

Listing 10.11. app/main/views.py: Trasa edycji profilu dla administratorów


from ..decorators import admin_required

@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])


@login_required
@admin_required
def edit_profile_admin(id):
user = User.query.get_or_404(id)
form = EditProfileAdminForm(user=user)
if form.validate_on_submit():
user.email = form.email.data
user.username = form.username.data
user.confirmed = form.confirmed.data
user.role = Role.query.get(form.role.data)
user.name = form.name.data
user.location = form.location.data
user.about_me = form.about_me.data
db.session.add(user)
db.session.commit()
flash('Profil został zaktualizowany.')
return redirect(url_for('.user', username=user.username))
form.email.data = user.email
form.username.data = user.username
form.confirmed.data = user.confirmed
form.role.data = user.role_id
form.name.data = user.name
form.location.data = user.location
form.about_me.data = user.about_me
return render_template('edit_profile.html', form=form, user=user)

Edytor profilu  143

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.

Listing 10.12. app/templates/user.html: Link do edycji profilu dla administratorów


{% if current_user.is_administrator() %}
<a class="btn btn-danger"
href="{{ url_for('.edit_profile_admin', id=user.id) }}">
Edytuj profil [Admin]
</a>
{% endif %}

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 10b, aby pobrać tę wersję aplikacji.

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'

144  Rozdział 10. Profile użytkowników

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.

Tabela 10.1. Argumenty ciągu zapytania usługi Gravatar

Nazwa argumentu Opis


s Rozmiar obrazu w pikselach.
r Ocena obrazu. Dostępne opcje to "g", "pg", "r" i "x".
d Domyślny generator obrazów dla użytkowników, którzy nie mają jeszcze zarejestrowanych
awatarów w usłudze Gravatar. Opcje to "404", aby zwrócić błąd 404, adres URL wskazujący
domyślny obraz lub jeden z następujących generatorów obrazów: "mm", "identicon",
"monsterid", "wavatar", "retro" lub "blank".
fd Wymuś użycie domyślnych awatarów.

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.

Listing 10.13. app/models.py: Generowanie adresu URL dla usługi Gravatar


import hashlib
from flask import request

class User(UserMixin, db.Model):


# ...
def gravatar(self, size=100, default='identicon', rating='g'):
url = 'https://secure.gravatar.com/avatar'
hash = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, size=size, default=default, rating=rating)

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.

Awatary użytkownika  145

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>
...

Klasa CSS profile-thumbnail pomaga w pozycjonowaniu obrazu na stronie. W elemencie <div>


umieszczonym za obrazem znalazły się wszystkie informacje o profilu. Wykorzystuje on klasę stylów
CSS profile-header, żeby poprawić formatowanie. Definicję tej klasy CSS możesz zobaczyć w repo-
zytorium GitHuba dla przykładowej aplikacji.
Szablon bazowy umieszcza małą miniaturę zalogowanego użytkownika na pasku nawigacyjnym,
wykorzystując do tego rozwiązanie podobne do przedstawionych powyżej. Możemy użyć własnych
klas CSS, aby z ich pomocą lepiej sformatować zdjęcia awatarów na stronie. Przygotowane przez mnie
style znajdziesz w repozytorium kodu źródłowego, w pliku styles.css umieszczonym w folderze
plików statycznych aplikacji. Z tego pliku korzysta też szablon base.html. Na rysunku 10.3 przedsta-
wiam stronę profilu użytkownika wraz z awatarem.

Rysunek 10.3. Strona profilu użytkownika wraz z awatarem

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 10c, aby pobrać tę wersję aplikacji.

146  Rozdział 10. Profile użytkowników

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 __init__(self, **kwargs):


# ...
if self.email is not None and self.avatar_hash is None:
self.avatar_hash = self.gravatar_hash()

def change_email(self, token):


# ...
self.email = new_email
self.avatar_hash = self.gravatar_hash()
db.session.add(self)
return True

def gravatar_hash(self):
return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()

def gravatar(self, size=100, default='identicon', rating='g'):


if request.is_secure:
url = 'https://secure.gravatar.com/avatar'
else:
url = 'http://www.gravatar.com/avatar'
hash = self.avatar_hash or self.gravatar_hash()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, size=size, default=default, rating=rating)

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ść.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 10d, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, dlatego pamiętaj, żeby po pobraniu kodu uruchomić polecenie flask
db upgrade.

W następnym rozdziale zajmiemy się tworzeniem mechanizmu bloga, który będzie napędzał całą
tę aplikację.

Awatary użytkownika  147

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.

Przesyłanie i wyświetlanie postów na blogu


Do obsługi postów na blogu niezbędny jest nowy model bazy danych, który będzie ją reprezentować.
Ten model pokazano na listingu 11.1.

Listing 11.1. app/models.py: Model postów


class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))

class User(UserMixin, db.Model):


# ...
posts = db.relationship('Post', backref='author', lazy='dynamic')

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.

Listing 11.2. app/main/forms.py: Formularz posta na blogu


class PostForm(FlaskForm):
body = TextAreaField("Co chcesz dzisiaj napisać?", validators=[DataRequired()])
submit = SubmitField('Wyślij')

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.

Listingu 11.4. app/templates/index.html: Szablon strony głównej bloga wraz z postami


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
...
<div>
{% if current_user.can(Permission.WRITE_ARTICLES) %}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
<ul class="posts">
{% for post in posts %}
<li class="post">
<div class="profile-thumbnail">
<a href="{{ url_for('.user', username=post.author.username) }}">
<img class="img-rounded profile-thumbnail"
src="{{ post.author.gravatar(size=40) }}">
</a>
</div>
<div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
<div class="post-author">
<a href="{{ url_for('.user', username=post.author.username) }}">
{{ post.author.username }}

150  Rozdział 11. Posty na blogu

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11a, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migra-
cję bazy danych, więc pamiętaj, aby po pobraniu kodu uruchomić polecenie
flask_db_upgrade.

Rysunek 11.1. Strona główna bloga wraz z formularzem wpisu i listą postów

Przesyłanie i wyświetlanie postów na blogu  151

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.

Listing 11.5. app/main/views.py: Trasa strony profilu z postami


@main.route('/user/<username>')
def user(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
posts = user.posts.order_by(Post.timestamp.desc()).all()
return render_template('user.html', user=user, posts=posts)

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.

Listing 11.6. app/templates/user.html: Szablon strony profilu wraz z postami


...
<h3>Posty użytkownika {{ user.username }}</h3>
{% include '_posts.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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11b, aby pobrać tę wersję aplikacji.

Stronicowanie długich list postów na blogu


Gdy popularność witryny będzie rosła, zwiększać się będzie liczba postów na blogu, a wtedy okaże
się, że wyświetlanie pełnej listy postów na stronie głównej i stronach profilowych będzie powolne
i niepraktyczne. Wielkie strony potrzebują więcej czasu na ich wygenerowanie, pobieranie i wyświe-
tlenie w przeglądarce, a zatem w miarę powiększania się stron będzie spadać jakość doświadczeń
użytkowników. Rozwiązaniem tego problemu może być stronicowanie (ang. paginate) danych i rende-
rowanie ich we fragmentach.

152  Rozdział 11. Posty na blogu

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.

Listing 11.7. requirements/dev.txt: Plik wymagań programistycznych


-r common.txt
faker==0.7.18

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.

Listing 11.8. app/fake.py: Generowanie fałszywych użytkowników i ich posty


from random import randint
from sqlalchemy.exc import IntegrityError
from faker import Faker
from . import db
from .models import User, Post

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

Stronicowanie długich list postów na blogu  153

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11c, aby pobrać tę wersję aplikacji. Aby upewnić się, że masz zainstalo-
wane wszystkie zależności, uruchom także polecenie pip install -r requirements/
dev.txt.

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.

Listing 11.9. app/main/views.py: Stronicowanie listy postów na blogu


@main.route('/', methods=['GET', 'POST'])
def index():
# ...
page = request.args.get('page', 1, type=int)

154  Rozdział 11. Posty na blogu

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.

Dodawanie widżetu stronicowania


Funkcja paginate() zwraca obiekt klasy Pagination, która jest definiowana w pakiecie Flask-
SQLAlchemy. Obiekt ten zawiera kilka właściwości przydatnych do generowania w szablonie łączy do
stron i dlatego jest przekazywany do szablonu jako jeden z argumentów. W tabeli 11.1 przedstawiam
krótki opis atrybutów obiektu stronicowania.

Tabela 11.1. Atrybuty obiektu stronicowania Flask-SQLAlchemy

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.

Stronicowanie długich list postów na blogu  155

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Obiekt stronicowania udostępnia również kilka metod wymienionych w tabeli 11.2.

Tabela 11.2. Metody obiektu stronicowania Flask-SQLAlchemy

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.

Listing 11.10. app/templates/_macros.html: Makro szablonu stronicowania


{% macro pagination_widget(pagination, endpoint) %}
<ul class="pagination">
<li{% if not pagination.has_prev %} class="disabled"{% endif %}>
<a href="{% if pagination.has_prev %}{{ url_for(endpoint,
page = pagination.page - 1, **kwargs) }}{% else %}#{% endif %}">
&laquo;
</a>
</li>
{% for p in pagination.iter_pages() %}
{% if p %}
{% if p == pagination.page %}
<li class="active">
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="disabled"><a href="#">&hellip;</a></li>
{% endif %}
{% endfor %}
<li{% if not pagination.has_next %} class="disabled"{% endif %}>
<a href="{% if pagination.has_next %}{{ url_for(endpoint,
page = pagination.page + 1, **kwargs) }}{% else %}#{% endif %}">
&raquo;
</a>
</li>
</ul>
{% endmacro %}

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:

156  Rozdział 11. Posty na blogu

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.

Listing 11.11. app/templates/index.html: Stopka stronicowania listy postów zawartych na blogu


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
...
{% include '_posts.html' %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}

Sposób wyświetlania linków do poszczególnych stron prezentuję na rysunku 11.2.

Rysunek 11.2. Stronicowanie postów na blogach

Stronicowanie długich list postów na blogu  157

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie
git checkout 11d, aby pobrać tę wersję aplikacji.

Posty z formatowaniem przy użyciu pakietów Markdown


i Flask-PageDown
Posty w postaci zwykłego tekstu są wystarczające do krótkich wiadomości i aktualizacji statusu,
ale użytkownicy, którzy będą chcieli pisać dłuższe artykuły, niestety szybko zauważą, jak bardzo
ograniczone możliwości formatowania tekstu mają do dyspozycji. W tym podrozdziale pole tekstowe,
w którym będą wpisywane posty, zostanie dostosowane do obsługi składni Markdown (https://
daringfireball.net/projects/markdown/). Oprócz tego dodamy też podgląd ostatecznego wyglądu wpro-
wadzanego tekstu.
Implementacja tej funkcji wymaga kilku nowych pakietów:
 PageDown — konwertera Markdown-na-HTML działającego po stronie klienta, zaimplemento-
wanego w języku JavaScript.
 Flask-PageDown — wrappera pakietu PageDown dla Flaska, który to wrapper integruje PageDown
z formularzami Flask-WTF.
 Markdown — zaimplementowanego w Pythonie konwertera Markdown-na-HTML działającego
po stronie serwera.
 Bleach — specjalnego pakietu czyszczącego HTML zaimplementowanego w Pythonie.
Wszystkie pakiety Pythona można zainstalować za pomocą narzędzia pip:
(venv) $ pip install flask-pagedown markdown bleach

Korzystanie z pakietu Flask-PageDown


Rozszerzenie Flask-PageDown definiuje klasę PageDownField, która ma taki sam interfejs jak klasa
TextAreaField z rozszerzenia WTForms. Przed użyciem tego pola należy zainicjować rozszerzenie,
tak jak pokazano na listingu 11.12.

Listing 11.12. app/__init_.py: Inicjowanie rozszerzenia Flask-PageDown


from flask_pagedown import PageDown
# ...
pagedown = PageDown()
# ...
def create_app(config_name):
# ...
pagedown.init_app(app)
# ...

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.

158  Rozdział 11. Posty na blogu

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.

Listing 11.14. app/templates/index.html: Deklaracja szablonu Flask-PageDown


{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11e, aby pobrać tę wersję aplikacji. Aby upewnić się, że masz zainstalo-
wane wszystkie zależności, uruchom także polecenie pip install -r requirements/
dev.txt.

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.

Rysunek 11.3. Formularz wpisu na blogu z tekstem sformatowanym

Posty z formatowaniem przy użyciu pakietów Markdown i Flask-PageDown  159

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.

Listing 11.15. app/models.py: Obsługa tekstu Markdown w modelu Post


from markdown import markdown
import bleach

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

db.event.listen(Post.body, 'set', Post.on_changed_body)

Funkcja on_changed_body()będzie reagowała na pojawienie się zdarzenia „set” ze SQLAlchemy wy-


woływanego dla elementu body, co oznacza, że będzie ona automatycznie wywoływana za każdym
razem, gdy do pola body zostanie przypisana nowa wartość. Funkcja ta renderuje wersję HTML
na podstawie otrzymanej treści i zapisuje ją w polu body_html. Całkowicie automatyzuje to proces
przekształcania tekstu Markdown w kod HTML.
Sama konwersja wymaga wykonania trzech kroków. Najpierw funkcja markdown() dokonuje
wstępnej konwersji na kod HTML. Wynik jest przekazywany do funkcji clean() wraz z listą dozwolo-
nych znaczników HTML. Następnie funkcja clean() usuwa wszelkie znaczniki, których nie ma na

160  Rozdział 11. Posty na blogu

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.

Listing 11.16. app/templates/_posts.html: Użycie w szablonie treści postów w wersji HTML


...
<div class="post-body">
{% if post.body_html %}
{{ post.body_html | safe }}
{% else %}
{{ post.body }}
{% endif %}
</div>
...

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11f, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera również
migrację bazy danych, więc pamiętaj, żeby po pobraniu kodu uruchomić polecenie
flask db upgrade. Aby upewnić się, że masz zainstalowane wszystkie zależności,
uruchom także polecenie pip install -r requirements/dev.txt.

Stałe linki do postów na blogu


Użytkownicy mogą chcieć udostępniać linki do określonych postów opublikowanych na blogu,
przekazując je swoim znajomym w różnych sieciach społecznościowych. W tym celu każdemu
postowi zostanie przypisana strona z unikatowym adresem URL. Na listingu 11.17 przedstawiam
trasę i funkcję widoku zajmującą się obsługą takich stałych linków.

Listing 11.17. app/main/views.py: Udostępnienie stałych linków do postów


@main.route('/post/<int:id>')
def post(id):
post = Post.query.get_or_404(id)
return render_template('post.html', posts=[post])

Konstrukcja adresów URL przypisywanych do postów wykorzystuje unikatowe pole identyfikatora,


który jest dodawany do nich podczas wstawiania posta do bazy danych.

Stałe linki do postów na blogu  161

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.

Listing 11.18. app/templates/_posts.html: Dodawanie stałych linków do postów


<ul class="posts">
{% for post in posts %}
<li class="post">
...
<div class="post-content">
...
<div class="post-footer">
<a href="{{ url_for('.post', id=post.id) }}">
<span class="label label-default">Permalink</span>
</a>
</div>
</div>
</li>
{% endfor %}
</ul>

Na listingu 11.19 prezentuję nowy szablon post.html, który zajmuje się renderowaniem stron związa-
nych ze stałymi linkami.

Listing 11.19. app/templates/post.html: Szablon stałego linka


{% extends "base.html" %}

{% block title %}Flasky - Post{% endblock %}

{% block page_content %}
{% include '_posts.html' %}
{% endblock %}

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11g, aby pobrać tę wersję aplikacji.

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ć

162  Rozdział 11. Posty na blogu

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.

Listing 11.20. app/templates/edit_post.html: Szablon edycji posta


{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Edytuj post{% endblock %}

{% 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 %}

Na listingu 11.21 przedstawiam trasę obsługującą edytor postów.

Listing 11.21. app/main/views.py: Trasa edycji posta


@main.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit(id):
post = Post.query.get_or_404(id)
if current_user != post.author and \
not current_user.can(Permission.ADMIN):
abort(403)
form = PostForm()
if form.validate_on_submit():
post.body = form.body.data
db.session.add(post)
db.session.commit()
flash('Post został zaktualizowany.')
return redirect(url_for('.post', id=post.id))
form.body.data = post.body
return render_template('edit_post.html', form=form)

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.

Listing 11.22. app/templates/_posts.html: Dodanie linka do edytowania postów


<ul class="posts">
{% for post in posts %}

Edytor postów  163

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.

Rysunek 11.4. Linki Edytuj i Permalink widoczne pod postem

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 11h, aby pobrać tę wersję aplikacji.

164  Rozdział 11. Posty na blogu

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 12.
Obserwatorzy

Społecznościowe aplikacje internetowe pozwalają użytkownikom łączyć się z innymi użytkowni-


kami. Poszczególne aplikacje różnie nazywają takie związki: obserwującymi, przyjaciółmi, kontaktami,
połączeniami lub kumplami, ale bez względu na ich nazwę funkcja jest taka sama i we wszystkich
przypadkach wymaga śledzenia kierunkowych połączeń między parami użytkowników i wyko-
rzystywania ich w zapytaniach do bazy danych.
W tym rozdziale dowiesz się, jak zaimplementować funkcję obserwowania użytkowników w aplikacji
Flasky. Użytkownicy będą mogli „obserwować” innych użytkowników i wybrać opcję filtrowania
listy postów na swojej stronie głównej, tak aby uwzględnić użytkowników obserwowanych.

I znowu relacje w bazach danych


Jak mówiliśmy już w rozdziale 5., bazy danych tworzą powiązania między rekordami za pomocą
relacji (ang. relationships). Najczęstszym rodzajem relacji jest relacja jeden-do-wielu, w której jeden
rekord zostaje połączony z listą powiązanych rekordów. Zaimplementowanie tego rodzaju relacji
wymaga, żeby elementy po stronie „wiele” miały klucz obcy wskazujący na powiązany element
po stronie „jeden”. W aktualnej postaci przykładowej aplikacji zdefiniowane zostały dwie relacje
jeden-do-wielu: jedna łączy role w systemie z listami użytkowników, a druga łączy użytkowników
z przygotowanymi przez nich postami.
Większość innych typów relacji można wyprowadzić z typu jeden-do-wielu. Relacja wiele-do-jednego
jest relacją jeden-do-wielu z punktu widzenia strony „wiele”. Typ relacji jeden-do-jednego to uprosz-
czenie relacji jeden-do-wielu, gdzie strona „wiele” jest ograniczona do co najwyżej jednego elementu.
Jedynym rodzajem relacji, którego nie można zaimplementować jako prostej odmiany modelu
jeden-do-wielu, jest relacja typu wiele-do-wielu, która ma po obu stronach listy elementów. Zależ-
ność tę opisuję szczegółowo w następnym punkcie.

Relacje typu wiele-do-wielu


Relacje jeden-do-wielu, wiele-do-jednego i jeden-do-jednego mają co najmniej jedną stronę z poje-
dynczą encją, więc połączenia między powiązanymi rekordami są implementowane za pomocą
kluczy obcych wskazujących na ten pojedynczy element. Ale jak wdrożyć relację, w której obie
strony są stroną „wiele”?

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.

Rysunek 12.1. Przykład relacji wiele-do-wielu

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

166  Rozdział 12. Obserwatorzy

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.

I znowu relacje w bazach danych  167

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.

Zaawansowane relacje wiele-do-wielu


Relacja typu wiele-do-wielu, skonfigurowana tak jak pokazano w poprzednim przykładzie, umoż-
liwia zapisanie w bazie danych informacji o obserwowaniu użytkowników. Niestety mamy tutaj jedno
ograniczenie. Podczas pracy z relacjami wiele-do-wielu często pojawia się potrzeba przechowy-
wania dodatkowych danych, które dotyczą połączenia między dwiema encjami. W przypadku re-
lacji wiążącej obserwujących i obserwowanych użytkowników przydatne może być zapisanie daty, kie-
dy użytkownik zaczął obserwować innego użytkownika. Umożliwi to wyświetlanie list użytkowników
obserwujących w kolejności chronologicznej. Jedynym miejscem, w którym można przechowy-
wać takie informacje, jest tabela powiązań, ale w implementacji podobnej do przedstawionego
wcześniej przykładu z uczniami i ich zajęciami tabela powiązań będzie tabelą wewnętrzną, która
jest zarządzana przez SQLAlchemy.
Aby mieć możliwość pracy z własnymi danymi umieszczonymi w relacji, tabela powiązań musi
zostać przeniesiona do odpowiedniego modelu, do którego aplikacja będzie miała pełny dostęp.
Na listingu 12.1 przedstawiam nową tabelę powiązań reprezentowaną przez model Follow.

Listing 12.1. app/models.py: Tabela powiązań follows zapisana jako model


class Follow(db.Model):
__tablename__ = 'follows'
follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=True)
followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=True)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)

168  Rozdział 12. Obserwatorzy

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

I znowu relacje w bazach danych  169

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.

Listing 12.3. app/models.py: Metody pomocnicze dla użytkowników obserwujących


class User(db.Model):
# ...
def follow(self, user):
if not self.is_following(user):
f = Follow(follower=self, followed=user)
db.session.add(f)

def unfollow(self, user):


f = self.followed.filter_by(followed_id=user.id).first()
if f:
db.session.delete(f)

def is_following(self, user):


if user.id is None:
return False
return self.followed.filter_by(
followed_id=user.id).first() is not None

def is_followed_by(self, user):


if user.id is None:
return False
return self.followers.filter_by(
follower_id=user.id).first() is not None

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ą

170  Rozdział 12. Obserwatorzy

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 12a, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, więc pamiętaj, aby po pobraniu kodu uruchomić polecenie flask db
upgrade.

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.

Obserwujący na stronie profilu


Jeśli użytkownik oglądający stronę profilową innego użytkownika jeszcze go nie obserwuje, to na
wyświetlanej stronie musi znaleźć się przycisk Obserwuj. Z drugiej strony, jeśli użytkownik jest
już obserwatorem, to na stronie powinien znaleźć się przycisk Przestań obserwować. Miłym dodatkiem
jest również wyświetlenie liczby osób obserwujących i obserwowanych, jak również list osób obser-
wujących i obserwowanych, a w stosownych przypadkach — znaczka Obserwuje cię. Zmiany
w szablonie profilu użytkownika pokazano na listingu 12.4. Natomiast na rysunku 12.3 możesz
zobaczyć, jak wyglądają takie dodatki na stronie profilu użytkownika.

Listing 12.4. app/templates/user.html: Ulepszenia w nagłówku profilu użytkownika obserwującego


{% if current_user.can(Permission.FOLLOW) and user != current_user %}
{% if not current_user.is_following(user) %}
<a href="{{ url_for('.follow', username=user.username) }}"
class="btn btn-primary">Obserwuj</a>
{% else %}
<a href="{{ url_for('.unfollow', username=user.username) }}"
class="btn btn-default">Przestań obserwować</a>
{% endif %}
{% endif %}
<a href="{{ url_for('.followers', username=user.username) }}">
Obserwujących: <span class="badge">{{ user.followers.count() }}</span>
</a>
<a href="{{ url_for('.followed_by', username=user.username) }}">
Obserwowanych: <span class="badge">{{ user.followed.count() }}</span>
</a>
{% if current_user.is_authenticated and user != current_user and
user.is_following(current_user) %}
| <span class="label label-default">Obserwuje cię</span>
{% endif %}

W zmianach wprowadzonych do szablonu zdefiniowano cztery nowe punkty końcowe. Trasa


/follow/<nazwa-użytkownika> jest wywoływana w przypadku, gdy użytkownik kliknie przycisk
Obserwuj, znajdujący się na stronie profilu innego użytkownika. Implementację tej trasy przedsta-
wiam na listingu 12.5.

Obserwujący na stronie profilu  171

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 12.3. Strona profilu użytkownika obserwującego

Listing 12.5. app/main/views.py: Trasa i funkcja widoku obserwowania użytkownika


@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Nieprawidłowy użytkownik.')
return redirect(url_for('.index'))
if current_user.is_following(user):
flash('Już obserwujesz tego użytkownika. ')
return redirect(url_for('.user', username=username))
current_user.follow(user)
db.session.commit()
flash('Zaczynasz obserwować użytkownika %s.' % username)
return redirect(url_for('.user', username=username))

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.

172  Rozdział 12. Obserwatorzy

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ą.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 12b, aby pobrać tę wersję aplikacji.

Uzyskiwanie śledzonych postów za pomocą operacji Join


Strona główna aplikacji pokazuje obecnie wszystkie posty zapisane w bazie danych, ułożone chronolo-
gicznie w porządku malejącym. Skoro ukończyliśmy już funkcję obserwowania użytkowników,
miłym dodatkiem byłoby umożliwienie użytkownikom przeglądania postów pochodzących wyłącznie
od obserwowanych przez nich użytkowników.
Oczywistym sposobem na załadowanie wszystkich postów autorstwa obserwowanych użytkowni-
ków jest po pierwsze, pobranie listy tych użytkowników, po drugie, pobranie postów od każdego

Uzyskiwanie śledzonych postów za pomocą operacji Join  173

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.

Tabela 12.1. Tabela users

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.

Tabela 12.2. Tabela posts

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.

Tabela 12.3. Tabela follows

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.

174  Rozdział 12. Obserwatorzy

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 *.

Tabela 12.4. Złączona tabela

id author_id* Treść follower_id followed_id*


2 1 Post przygotowany przez jana 2 1
3 3 Post przygotowany przez dawida 2 3
4 1 Drugi post przygotowany przez jana 2 1

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.

 select_from(Follow) mówi, że zapytanie zaczyna się od modelu Follow.

 filter_by(follower_id=self.id) filtruje tabelę follows według użytkownika obserwującego.

 join(Post, Follow.followed_id == Post.author_id) złącza wyniki funkcji filter_by()z obiek-


tami klasy Post.
Nasze zapytanie można też uprościć, zamieniając kolejnością filtr i złączenie:
return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
.filter(Follow.follower_id == self.id)

Zastosowanie najpierw operacji złączenia będzie oznaczało, że zapytanie można uruchomić


z atrybutu Post.query, dzięki czemu trzeba zastosować tylko dwa filtry — join()i filter(). Może
się wydawać, że wykonanie najpierw złączenia, a później filtrowania wymagać będzie większego na-
kładu pracy, ale w rzeczywistości te dwa zapytania są sobie równoważne. SQLAlchemy najpierw zbiera
wszystkie filtry, a następnie generuje najefektywniejsze zapytanie. Instrukcje SQL dla tych dwóch
zapytań są prawie identyczne, co można potwierdzić, wypisując obiekt zapytania przekonwertowany
na ciąg znaków (na przykład wywołaniem print(str(query))). Ostateczna wersja tego zapytania
jest dodawana do modelu Post, jak pokazano to na listingu 12.7.

Uzyskiwanie śledzonych postów za pomocą operacji Join  175

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ę.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 12c, aby pobrać tę wersję aplikacji.

Niestety złączenia są niezwykle trudne do szybkiego ogarnięcia. Zrozumienie wszystkich elementów


może wymagać poeksperymentowania z przykładowym kodem w powłoce.

Wyświetlanie obserwowanych postów na stronie głównej


Strona główna może teraz pozwolić użytkownikom wybrać, czy będą oni mogli przeglądać posty
wszystkich użytkowników, czy tylko tych obserwowanych przez siebie. Kod z listingu 12.8 pokazuje,
jak ten wybór jest realizowany.

Listing 12.8. app/main/views.py: Wyświetlanie wszystkich lub tylko obserwowanych postów


@main.route('/', methods = ['GET', 'POST'])
def index():
# ...
show_followed = False
if current_user.is_authenticated:
show_followed = bool(request.cookies.get('show_followed', ''))
if show_followed:
query = current_user.followed_posts
else:
query = Post.query
pagination = 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,
show_followed=show_followed, pagination=pagination)

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.

176  Rozdział 12. Obserwatorzy

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.

Listing 12.9. app/main/views.py: Wybór wszystkich lub tylko obserwowanych postów


@main.route('/all')
@login_required
def show_all():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '', max_age=30*24*60*60) # 30 dni
return resp

@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 sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecnie


git checkout 12d, aby pobrać tę wersję aplikacji.

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.

Wyświetlanie obserwowanych postów na stronie głównej  177

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.

Listing 12.11. app/models.py: Definiowanie użytkowników jako obserwujących samych siebie


class User(UserMixin, db.Model):
# ...
@staticmethod
def add_self_follows():
for user in User.query.all():
if not user.is_following(user):
user.follow(user)
db.session.add(user)
db.session.commit()
# ...

178  Rozdział 12. Obserwatorzy

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ę.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 12e, aby pobrać tę wersję aplikacji.

W następnym rozdziale zajmiemy się implementowaniem podsystemu komentarzy użytkowników —


jest to kolejna bardzo ważna funkcja aplikacji społecznościowej.

Wyświetlanie obserwowanych postów na stronie głównej  179

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
180  Rozdział 12. Obserwatorzy

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
ROZDZIAŁ 13.
Komentarze użytkowników

Kluczem do sukcesu platform społecznościowych jest pozwolenie na interakcję między użytkow-


nikami. W tym rozdziale dowiesz się, jak zaimplementować komentarze użytkowników. Przedstawio-
ne tu techniki są na tyle ogólne, że można je bezpośrednio zastosować w dużej liczbie aplikacji
ukierunkowanych społecznościowo.

Zapisywanie komentarzy w bazie danych


Komentarze nie różnią się bardzo od postów na blogu. Oba mają treść, autora i znacznik czasu,
a w tej konkretnej implementacji oba będą napisane ze składnią Markdown. Na rysunku 13.1 pokazuję
schemat tabeli comments i jej relacji z innymi tabelami w bazie danych.

Rysunek 13.1. Diagram tabel bazy danych zawierającej komentarze do postó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))

db.event.listen(Comment.body, 'set', Comment.on_changed_body)

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

Przesyłanie i wyświetlanie komentarzy


W naszej aplikacji komentarze są wyświetlane na stronach poszczególnych postów, którym w roz-
dziale 11. przygotowaliśmy już stałe linki. Na tych stronach znajduje się również formularz wpro-
wadzania tekstu komentarza. Kod przedstawiony na listingu 13.3 pokazuje formularz, który będzie
używany do wprowadzania komentarzy — to niezwykle prosty formularz, który ma tylko pole
tekstowe i przycisk wysyłania.

182  Rozdział 13. Komentarze użytkowników

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 13.3. app/main/forms.py: Formularz wprowadzania komentarza
class CommentForm(FlaskForm):
body = StringField('', validators=[DataRequired()])
submit = SubmitField('Wyślij')

Na listingu 13.4 przedstawiam zaktualizowaną trasę /post/<int:id> uzupełnioną o obsługę komentarzy.

Listing 13.4. app/main/views.py: Obsługa komentarzy do postów


@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
post = Post.query.get_or_404(id)
form = CommentForm()
if form.validate_on_submit():
comment = Comment(body=form.body.data,
post=post,
author=current_user._get_current_object())
db.session.add(comment)
db.session.commit()
flash('Twój komentarz został opublikowany.')
return redirect(url_for('.post', id=post.id, page=-1))
page = request.args.get('page', 1, type=int)
if page == -1:
page = (post.comments.count() - 1) // \
current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
return render_template('post.html', posts=[post], form=form,
comments=comments, pagination=pagination)

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.

Przesyłanie i wyświetlanie komentarzy  183

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.

Listing 13.5. _app/templates/_posts.html: Link do komentarzy pod postami


<a href="{{ url_for('.post', id=post.id) }}#comments">
<span class="label label-primary">
{{ post.comments.count() }} Komentarze
</span>
</a>

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 13a, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, pamiętaj zatem, aby po pobraniu kodu uruchomić polecenie flask
db upgrade.

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.

184  Rozdział 13. Komentarze użytkowników

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.

Listing 13.6. app/templates/base.html: Link „Moderuj komentarze” na pasku nawigacyjnym


...
{% if current_user.can(Permission.MODERATE) %}
<li><a href="{{ url_for('main.moderate') }}">Moderuj komentarze</a></li>
{% endif %}
...

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.

Listing 13.7. app/main /views.py: Trasa moderowania komentarzy


@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def moderate():
page = request.args.get('page', 1, type=int)
pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
return render_template('moderate.html', comments=comments,
pagination=pagination, page=page)

Moderowanie komentarzy  185

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.

Listing 13.8. app/templates/moderate.html: Szablon moderowania komentarza


{% extends "base.html" %}
{% import "_macros.html" as macros %}

{% block title %}Flasky – Moderowanie komentarzy{% endblock %}

{% 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.

Listing 13.9. app/templates/_comments.html: Renderowanie treści komentarzy


...
<div class="comment-body">
{% if comment.disabled %}
<p></p><i>Ten komentarz został zablokowany przez moderatora.</i></p>
{% endif %}
{% if moderate or not comment.disabled %}
{% if comment.body_html %}
{{ comment.body_html | safe }}
{% else %}
{{ comment.body }}

186  Rozdział 13. Komentarze użytkowników

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.

Listing 13.10. app/main/views.py: Trasy moderowania komentarzy


@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_enable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = False
db.session.add(comment)
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int)))

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

Trasy blokowania i odblokowywania komentarzy ładują obiekt komentarza, przypisują odpowiednią


wartość do pola disabled i zapisują obiekt do bazy danych. Na koniec przekierowują na stronę
moderowania komentarzy (pokazaną na rysunku 13.3), a jeśli w zapytaniu URL podano argument
page, dołączają go do przekierowania. W szablonie _comments.html przyciski są renderowane z przy-
gotowanym argumentem page, dzięki czemu przekierowanie prowadzi użytkownika z powrotem
na tę samą stronę.

Moderowanie komentarzy  187

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Rysunek 13.3. Strona moderowania komentarzy

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 13b, aby pobrać tę wersję aplikacji.

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.

188  Rozdział 13. Komentarze użytkowników

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.

Wprowadzenie do architektury REST


Rozprawa doktorska Roya Fieldinga opisuje styl architektury REST dla usług internetowych pod ką-
tem sześciu charakterystycznych cech:
Klient – serwer
Musi być wyraźny rozdział między klientami a serwerami.
Bezstanowość
Żądanie klienta musi zawierać wszystkie informacje niezbędne do jego realizacji. Serwer nie
może przechowywać żadnej informacji o stanie klienta, która byłaby zachowywana pomiędzy
kolejnymi żądaniami.
Pamięć podręczna
Odpowiedzi z serwera można oznaczyć jako buforowalne lub niebuforowalne, aby klienty
(lub pośrednicy między klientami a serwerami) mogli używać pamięci podręcznej do celów
optymalizacji.

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”.

Pamiętaj jednak, że Flask specjalnie traktuje trasy, które są zakończone ukośnikiem.


Jeśli klient zażąda adresu URL bez końcowego ukośnika, a w aplikacji istnieje trasa
z ukośnikiem pasująca do tego adresu, to Flask automatycznie odpowie przekiero-
waniem na URL z ukośnikiem końcowym. W odwrotnym przypadku przekierowania
nie są generowane.

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

190  Rozdział 14. Interfejsy programowania aplikacji

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.

Tabela 14.1. Metody żądania HTTP w interfejsach API typu REST

Metoda Cel Opis Kod statusu odpowiedzi


żądania HTTP
GET Indywidualny adres URL Uzyskaj zasób. 200
zasobu
GET Adres URL kolekcji zasobów Uzyskaj kolekcję zasobów (lub 200
jedną stronę z kolekcji, jeśli
serwer implementuje
stronicowanie).
POST Adres URL kolekcji zasobów Utwórz nowy zasób i dodaj go 201
do kolekcji. Serwer wybiera
adres URL nowego zasobu
i zwraca go w odpowiedzi,
w nagłówku Location.
PUT Indywidualny adres URL Zmodyfikuj istniejący zasób. 200 lub 204
zasobu Alternatywnie można również
użyć tej metody do utworzenia
nowego zasobu, gdy klient
może samodzielnie wybrać
adres URL zasobu.
DELETE Indywidualny adres URL Usuń zasób. 200 lub 204
zasobu
DELETE Adres URL kolekcji zasobów Usuń wszystkie zasoby 200 lub 204
z kolekcji.

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.

Treści żądań i odpowiedzi


Zasoby są przesyłane pomiędzy klientem a serwerem w treściach żądań i odpowiedzi, ale architektura
REST nie określa formatu używanego do kodowania tych zasobów. Format, w jakim został zakodo-
wany zasób, podawany jest w nagłówku Content-Type umieszczanym w żądaniach i odpowiedziach.
Pomiędzy klientem a serwerem można używać standardowych mechanizmów negocjowania treści
w protokole HTTP w celu uzgodnienia formatu obsługiwanego przez obie strony.

Wprowadzenie do architektury REST  191

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/.

192  Rozdział 14. Interfejsy programowania aplikacji

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.

Flask i usługi sieciowe typu REST


Flask ułatwia tworzenie usług sieciowych typu REST. Znany dekorator route(), wraz z jego opcjonal-
nym argumentem methods, może służyć do deklarowania tras, które będą obsługiwały adresy URL
zasobów ujawnione przez usługę. Praca z formatem JSON jest również prosta, ponieważ dane JSON
zawarte w żądaniu można uzyskać w formie słownika, wywołując polecenie request.get_json().
Z kolei odpowiedź, która musi stosować format JSON, można w Pythonie łatwo wygenerować ze
słownika za pomocą funkcji pomocniczej jsonify() udostępnianej przez framework Flask.
W następnych punktach omówię sposoby umożliwiające rozszerzenie aplikacji Flasky o usługę sie-
ciową REST, która będzie dawała klientom dostęp do postów i powiązanych z nimi zasobów.

Tworzenie schematu interfejsu API


Trasy powiązane z interfejsem REST API tworzą samodzielny podzbiór aplikacji, dlatego umieszczenie
ich w osobnym schemacie jest najlepszym sposobem na ich dobrą organizację. Ogólna struktura
schematu API w aplikacji została pokazana na listingu 14.1.

Listing 14.1. Struktura schematu API


|-flasky
|-app/
|-api
|-__init__.py
|-users.py
|-posts.py
|-comments.py
|-authentication.py
|-errors.py
|-decorators.py

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.

Flask i usługi sieciowe typu REST  193

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Listing 14.2. app/api/__init__.py: Tworzenie schematu API
from flask import Blueprint

api = Blueprint('api', __name__)

from . import authentication, posts, users, comments, errors

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.

Listing 14.3. app/init.py: Rejestracja schematu API


def create_app(config_name):
# ...
from .api import api as api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/api/v1')
# ...

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

Kod statusu Nazwa Opis


HTTP
200 OK Żądanie zostało pomyślnie zakończone.
201 Utworzony Żądanie zostało pomyślnie zakończone, w wyniku czego utworzono nowy zasób.
202 Przyjęty Żądanie zostało przyjęte, ale nadal działa i będzie działać asynchronicznie.
204 Brak zawartości Żądanie zostało pomyślnie zakończone i nie ma danych do zwrócenia
w odpowiedzi.
400 Błędne żądanie Żądanie jest nieprawidłowe lub niespójne.
401 Nieautoryzowany Żądanie nie zawiera informacji uwierzytelniających lub podane dane
uwierzytelniające są nieprawidłowe.
403 Zabroniony Dane uwierzytelniające wysłane z żądaniem są niewystarczające dla żądania.
404 Nie znaleziono Nie znaleziono zasobu, do którego odwołuje się adres URL.
405 Metoda niedozwolona Żądana metoda nie jest obsługiwana dla danego zasobu.
500 Wewnętrzny błąd serwera Wystąpił nieoczekiwany błąd podczas przetwarzania żądania.

194  Rozdział 14. Interfejsy programowania aplikacji

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.

Uwierzytelnianie użytkownika za pomocą Flask-HTTPAuth


Usługi sieciowe, podobnie jak zwykłe aplikacje internetowe, muszą chronić swoje informacje i unie-
możliwiać dostęp do nich osobom nieupoważnionym. Z tego powodu aplikacje RIA muszą poprosić
użytkowników o podanie danych uwierzytelniających, a następnie przekazać je do serwera w celu
weryfikacji.
Wspominałem już wcześniej, że jedną z cech usług typu REST jest to, że są one bezstanowe (ang.
stateless), co oznacza, że serwer nie może „zapamiętać” niczego o kliencie między żądaniami.

Flask i usługi sieciowe typu REST  195

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

Aby zainicjować rozszerzenie, przygotowując je do stosowania uwierzytelniania HTTP Basic, należy


utworzyć obiekt klasy HTTPBasicAuth. Podobnie jak Flask-Login, rozszerzenie Flask-HTTPAuth nie
tworzy żadnych założeń dotyczących procedury stosowanej przy weryfikacji danych uwierzytelniają-
cych użytkownika, dlatego też informacje te są przekazywane do funkcji wywołania zwrotnego.
Na listingu 14.6 można zobaczyć, w jaki sposób inicjowane jest rozszerzenie i jak można dodać do
niego funkcję wywołania zwrotnego zajmującą się weryfikacją danych uwierzytelniających.

Listing 14.6. app/api/authentication.py: Inicjowanie rozszerzenia Flask-HTTPAuth


from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

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

196  Rozdział 14. Interfejsy programowania aplikacji

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.

Jako że dane uwierzytelniające użytkownika są wymieniane przy każdym żądaniu,


niezwykle ważne jest to, aby trasy interfejsu API były udostępniane za pośrednictwem
bezpiecznego protokołu HTTPS, tak aby wszystkie żądania i odpowiedzi były szyfro-
wane podczas przesyłania.

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.

Listing 14.7. _app/api/authentication.py: Metoda obsługi błędów dla rozszerzenia Flask-HTTPAuth


from .errors import unauthorized

@auth.error_handler
def auth_error():
return unauthorized('Nieprawidłowe dane uwierzytelniania')

Dekorator auth.login_required używany jest do zabezpieczenia wybranych tras:


@api.route('/posts/')
@auth.login_required
def get_posts():
pass

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.

Listing 14.8. app/api/authentication.py: Obsługa zdarzenia before_request z uwierzytelnieniem


from .errors import forbidden

@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')

Flask i usługi sieciowe typu REST  197

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.

Uwierzytelnianie za pomocą tokenów


Klienty muszą wysyłać dane uwierzytelniające przy każdym żądaniu. Aby uniknąć konieczności cią-
głego przesyłania poufnych informacji, takich jak hasło, można zastosować rozwiązanie uwierzytel-
niania za pomocą tokenów.
W uwierzytelnianiu wykorzystującym tokeny klient żąda tokena dostępowego, wysyłając odpowiednie
żądanie zawierające dane uwierzytelniające. Token może być następnie użyty do uwierzytelnienia
żądań zamiast pierwotnych danych uwierzytelniających. Ze względów bezpieczeństwa wydawane
tokeny mają określony czas ważności, po którym wygasają. Po wygaśnięciu tokena klient musi się
ponownie uwierzytelnić, aby uzyskać nowy token. Ryzyko dostania się tokena w niepowołane ręce jest
ograniczone ze względu na krótki czas ważności. Na listingu 14.9 przedstawiam dwie nowe metody
dodane do modelu User, obsługujące operacje generowania i weryfikowania tokenów uwierzytelniają-
cych przy użyciu pakietu itsdangerous.

Listing 14.9. app/models.py: Obsługa uwierzytelniania wykorzystującego tokeny


class User(db.Model):
# ...
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id}).decode('utf-8')

@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.

Listing 14.10. app/api/authentication.py: Ulepszona weryfikacja uwierzytelnienia z obsługą tokenów


@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
return False

198  Rozdział 14. Interfejsy programowania aplikacji

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.

Listing 14.11. app/api/authentication.py: Generowanie tokena uwierzytelniającego


@api.route('/tokens/', methods=['POST'])
def get_token():
if g.current_user.is_anonymous or g.token_used:
return unauthorized('Niepoprawne dane uwierzytelniające')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})

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.

Serializacja zasobów do i z formatu JSON


Podczas tworzenia usługi internetowej często zachodzi potrzeba konwertowania wewnętrznych
zasobów aplikacji do i z formatu JSON, który jest formatem używanym do transportu danych w żąda-
niach i odpowiedziach HTTP. Proces konwersji wewnętrznej reprezentacji na format transportowy,
taki jak JSON, nazywa się serializacją (ang. serialization). Na listingu 14.12 możesz zobaczyć nową
metodę to_json(), którą dodałem do klasy Post.

Flask i usługi sieciowe typu REST  199

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.

Listing 14.13. app/models.py: Konwertowanie obiektu użytkownika na słownik serializacji JSON


class User(UserMixin, db.Model):
# ...
def to_json(self):
json_user = {
'url': url_for('api.get_user', id=self.id),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts_url': url_for('api.get_user_posts', id=self.id),
'followed_posts_url': url_for('api.get_user_followed_posts',
id=self.id),
'post_count': self.posts.count()
}
return json_user

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.

200  Rozdział 14. Interfejsy programowania aplikacji

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.

Listing 14.15. app/exceptions.py: Wyjątek ValidationError


class ValidationError(ValueError):
pass

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

Flask i usługi sieciowe typu REST  201

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

Implementacja punktów końcowych dla zasobów


Pozostaje wdrożyć trasy obsługujące różne zasoby. Żądania GET są zazwyczaj najłatwiejsze, ponieważ
zwracają tylko informacje i nie wymagają wprowadzania żadnych zmian. Na listingu 14.17 możesz
zobaczyć dwie metody obsługi żądań GET dotyczących postów.

Listing 14.17. app/api/posts.py: Metody obsługi żądań GET dla postów


@api.route('/posts/')
def get_posts():
posts = Post.query.all()
return jsonify({ 'posts': [post.to_json() for post in posts] })

@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.

Listing 14.18. app/api/posts.py: Metoda obsługi żądań POST dla postów


@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE)
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()), 201, \
{'Location': url_for('api.get_post', id=post.id)}

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

202  Rozdział 14. Interfejsy programowania aplikacji

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.

Listing 14.19. app/api/decorators.py: Dekorator permission_required


def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.current_user.can(permission):
return forbidden('Niewystarczające uprawnienia')
return f(*args, **kwargs)
return decorated_function
return decorator

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.

Listing 14.20. app/api/posts.py: Metoda obsługi żądań PUT dla postów


@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and \
not g.current_user.can(Permission.ADMIN):
return forbidden('Niewystarczające uprawnienia')
post.body = request.json.get('body', post.body)
db.session.add(post)
db.session.commit()
return jsonify(post.to_json())

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.

Flask i usługi sieciowe typu REST  203

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Tabela 14.3. Zasoby Flasky API

URL zasobu Metoda Opis


/users/<int:id> GET Zwraca użytkownika.
/users/<int:id>/posts/ GET Zwraca wszystkie posty napisane przez użytkownika.
/users/<int:id>/timeline/ GET Zwraca wszystkie posty obserwowane przez użytkownika.
/posts/ GET Zwraca wszystkie posty.
/posts/ POST Tworzy nowy post.
/posts/<int:id> GET Zwraca jeden post.
/posts/<int:id> PUT Modyfikuje treść posta.
/posts/<int:id>/comments/ GET Zwraca komentarze do posta.
/posts/<int:id>/comments/ POST Dodaje komentarz do posta.
/comments/ GET Zwraca wszystkie komentarze.
/comments/<int:id> GET Zwraca jeden komentarz.

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.

Podział dużych kolekcji zasobów na strony


W przypadku bardzo dużych kolekcji żądania GET zwracające zbiór zasobów mogą okazać się bardzo
kosztowne i kłopotliwe w obsłudze. Podobnie jak aplikacje internetowe, usługi sieciowe również
mogą stronicować zwracane kolekcje.
Na listingu 14.21 przedstawiam przykładową implementację stronicowania listy postów.

Listing 14.21. app/api/posts.py: Stronicowanie listy postów


@api.route('/posts/')
def get_posts():
page = request.args.get('page', 1, type=int)
pagination = Post.query.paginate(
page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_posts', page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_posts', page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev_url': prev,
'next_url': next,
'count': pagination.total
})

204  Rozdział 14. Interfejsy programowania aplikacji

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.

Jeśli sklonowałeś repozytorium Git aplikacji na GitHubie, możesz wywołać polecenie


git checkout 14a, aby pobrać tę wersję aplikacji. Aby upewnić się, że masz zainstalo-
wane wszystkie zależności, uruchom także polecenie pip install -r requirements/
dev.txt.

Testowanie usług internetowych za pomocą HTTPie


Aby przetestować usługę internetową, należy użyć klienta HTTP. Dwoma najczęściej używanymi
klientami do testowania sieciowych usług Pythona z poziomu wiersza poleceń są cURL i HTTPie.
Oczywiście oba są użytecznymi narzędziami, ale to drugie ma znacznie bardziej zwięzłą i czytelną
składnię wiersza poleceń, która została specjalnie dostosowana do wykonywania żądań API. Narzędzie
HTTPie można zainstalować za pomocą polecenia pip:
(venv) $ pip install httpie

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

Flask i usługi sieciowe typu REST  205

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.

206  Rozdział 14. Interfejsy programowania aplikacji

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.

Uzyskiwanie raportów pokrycia kodu


Przygotowanie zestawu testów jest bardzo ważne, ale równie ważne jest to, aby wiedzieć, czy te
wszystkie testy są dobre czy złe. Narzędzia mierzące pokrycie kodu podają nam, jaka część aplikacji
jest sprawdzana przez testy jednostkowe. Tworzą one szczegółowy raport wskazujący na to, które
części kodu tej aplikacji nie zostały przetestowane. Te informacje są bardzo cenne, ponieważ można je
wykorzystać do odpowiedniego ukierunkowania prac nad tworzeniem nowych testów na obszary,
które najbardziej ich potrzebują.
Python ma doskonałe narzędzie do mierzenia pokrycia kodu, o nazwie coverage. Możesz zainstalować
je za pomocą polecenia pip:
(venv) $ pip install coverage

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.

Listing 15.1. flasky.py: Metryki pokrycia kodu


import os
import sys
import click

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.

210  Rozdział 15. Testowanie

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 15a, aby pobrać tę wersję aplikacji. Aby upewnić się, że masz zainstalo-
wane wszystkie zależności, uruchom także polecenie pip install -r requirements/
dev.txt.

Oto przykład raportu w formie tekstowej:


(venv) $ flask test --coverage
...
.----------------------------------------------------------------------
Ran 23 tests in 6.337s
OK
Informacje o pokryciu kodu:
Name Stmts Miss Branch BrPart Cover
----------------------------------------------------------------
app/__init__.py 32 0 0 0 100%
app/api_v1/__init__.py 3 0 0 0 100%
app/api_v1/authentication.py 29 18 10 0 28%
app/api_v1/comments.py 40 30 12 0 19%
app/api_v1/decorators.py 11 3 2 0 62%
app/api_v1/errors.py 17 10 0 0 41%
app/api_v1/posts.py 36 24 8 0 27%
app/api_v1/users.py 30 24 12 0 14%
app/auth/__init__.py 3 0 0 0 100%
app/auth/forms.py 45 8 8 0 70%
app/auth/views.py 116 91 42 0 16%
app/decorators.py 14 3 2 0 69%
app/email.py 15 9 0 0 40%
app/exceptions.py 2 0 0 0 100%
app/main/__init__.py 6 1 0 0 83%
app/main/errors.py 20 15 6 0 19%
app/main/forms.py 39 7 6 0 71%
app/main/views.py 178 140 34 0 18%
app/models.py 236 42 42 6 79%
----------------------------------------------------------------
TOTAL 872 425 184 6 45%
Wersja HTML: file:///home/flask/flasky/tmp/coverage/index.html

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,

Uzyskiwanie raportów pokrycia kodu  211

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.

Klient testowy Flaska


Niektóre części kodu aplikacji w dużym stopniu wykorzystują środowisko utworzone przez działa-
jącą już aplikację. Na przykład w ramach testów nie można po prostu wywołać kodu funkcji widoku,
ponieważ ta funkcja może wymagać dostępu do zmiennych kontekstowych Flaska (takich jak
request lub session), oczekiwać danych z formularza dostarczonego w żądaniu POST, a dodatkowo
może wymagać, żeby aktualny użytkownik był zalogowany. Krótko mówiąc, funkcje widoku można
uruchamiać tylko w kontekście żądania i działającej już aplikacji.
Flask jest wyposażony w klienta testowego (ang. test client), który próbuje przynajmniej częściowo
rozwiązać ten problem. Klient testowy replikuje środowisko powstające, gdy aplikacja działa na
serwerze WWW, umożliwiając w ten sposób testom działanie w roli klientów i wysyłanie żądań.
Funkcje widoku uruchamiane w kliencie testowym nie zauważają większych różnic w stosunku do
normalnego środowiska aplikacji. Przychodzące żądania są odbierane i kierowane do odpowiednich
funkcji widoku, które z kolei generują i zwracają odpowiedzi. Po uruchomieniu funkcji widoku
jej odpowiedź jest przekazywana do testu, który może sprawdzić jej poprawność.

Testowanie aplikacji internetowych


Na listingu 15.2 pokazano strukturę testów jednostkowych, które korzystają z klienta testowego.

Listing 15.2. tests/test_client.py: Struktura testów używających klienta testowego Flaska


import unittest
from app import create_app, db
from app.models import User, Role

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)

212  Rozdział 15. Testowanie

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

W porównaniu z plikiem tests/test_basics.py tutaj dodawana jest zmienna instancji self.client,


która przechowuje obiekt klienta testowego. Obiekt ten udostępnia metody wysyłające żądania do
aplikacji. Gdy klient testowy zostanie utworzony z włączoną opcją use_cookies, będzie przyjmował
i wysyłał pliki cookie dokładnie tak samo, jak robią to przeglądarki. Można zatem korzystać z funkcji
używających plików cookie do przechowywania kontekstu między żądaniami. W szczególności
takie rozwiązanie umożliwia nam korzystanie z sesji użytkownika, które są przechowywane w pli-
kach cookie.
Test test_home_page() jest prostym przykładem tego, co może zrobić klient testowy. W tym teście
wysyłane jest żądanie pobrania strony z głównego adresu URL aplikacji. Wartość zwracana przez
metodę get() klienta testowego jest obiektem odpowiedzi Flaska zawierającym informacje zwrócone
przez wywoływaną funkcję widoku. Aby sprawdzić, czy test się powiódł, musimy skontrolować kod
statusu odpowiedzi, a następnie przeszukać treść odpowiedzi (otrzymaną z funkcji response.get_
data()) pod kątem słowa 'Nieznajomy', które jest częścią powitania: „Witaj, Nieznajomy!”, pokazy-
wanego anonimowym użytkownikom. Zauważ, że funkcja get_data()domyślnie zwraca treść odpo-
wiedzi jako tablicę bajtów. Po podaniu jej argumentu as_text=True zwracana odpowiedź jest
przekształcana w ciąg znaków, z którym znacznie łatwiej się pracuje.
Klient testowy może również wysyłać żądania POST zawierające dane formularza, używając do tego
metody post(). Niestety takie przesyłanie formularzy powoduje małe komplikacje. Jak wspominałem
już w rozdziale 4., wszystkie formularze generowane przez rozszerzenie Flask-WTF mają ukryte
pole z tokenem CSRF, który należy przesłać wraz z formularzem. Aby móc wysłać token CSRF,
test musiałby zażądać strony wyświetlającej formularz, przeanalizować kod HTML otrzymany
w odpowiedzi i wydobyć z niego token, aby następnie wysłać go z danymi formularza. W tej sytuacji
lepiej będzie wyłączyć ochronę CSRF w konfiguracji testowej, aby uniknąć kłopotów związanych
z tokenami CSRF w testach. Pokazano to na listingu 15.3.

Listing 15.3. config.py: Wyłączanie ochrony CSRF w konfiguracji testowej


class TestingConfig(Config):
# ...
WTF_CSRF_ENABLED = False

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

Klient testowy Flaska  213

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)

# Zaloguj się przy użyciu nowego konta.


response = self.client.post('/auth/login', data={
'email': 'jan@przyklad.pl',
'password': 'kot'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(re.search(Witaj,\s+jan!',
response.get_data(as_text=True)))
self.assertTrue(
'Nie potwierdziłeś jeszcze swojego konta' in response.get_data(
as_text=True))

# Wyślij token potwierdzający.


user = User.query.filter_by(email='jan@przyklad.pl').first()
token = user.generate_confirmation_token()
response = self.client.get('/auth/confirm/{}'.format(token),
follow_redirects=True)
user.confirm(token)
self.assertEqual(response.status_code, 200)
self.assertTrue(
'Potwierdziłeś swoje konto' in response.get_data(
as_text=True))

# 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.

214  Rozdział 15. Testowanie

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 15b, aby pobrać tę wersję aplikacji.

Testowanie usług internetowych


Klienta testowego można też użyć do testowania sieciowych usług REST. Na listingu 15.5 prezentuję
przykładową klasę testów jednostkowych zawierającą dwa testy.

Listing 15.5. tests/test_api.py: Testy API REST z klientem testowym


class APITestCase(unittest.TestCase):
# ...
def get_api_headers(self, username, password):
return {
'Authorization':
'Basic ' + b64encode(
(username + ':' + password).encode('utf-8')).decode('utf-8'),
'Accept': 'application/json',
'Content-Type': 'application/json'
}

Klient testowy Flaska  215

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)

# Dostać nowy post.


response = self.client.get(
url,
headers=self.get_api_headers('jan@przyklad.pl', 'kot'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual('http://localhost' + json_response['url'], url)
self.assertEqual(json_response['body'], 'treść posta wysłanego na *blog*')
self.assertEqual(json_response['body_html'],
'<p>treść posta wysłanego na <em>blog</em></p>')

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ść.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 15c, aby pobrać tę wersję aplikacji.

216  Rozdział 15. Testowanie

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.

Listing 15.6. _app/main/views.py: Trasa zamykania serwera


@main.route('/shutdown')
def server_shutdown():
if not current_app.testing:
abort(404)

Kompleksowe testy z użyciem Selenium  217

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.

Listing 15.7. tests/test_selenium.py: Framework do testów z użyciem Selenium


from selenium import webdriver

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

# Pomiń te testy, jeśli nie można uruchomić przeglądarki.


if cls.client:
# create the application
cls.app = create_app('testing')
cls.app_context = cls.app.app_context()
cls.app_context.push()

# Wyłącz protokołowanie, aby oczyścić protokoły testów jednostkowych.


import logging
logger = logging.getLogger('werkzeug')
logger.setLevel("ERROR")

# Utwórz bazę danych i wypełnij ją zmyślonymi danymi.


db.create_all()
Role.insert_roles()
fake.users(10)
fake.posts(10)

# Dodaj użytkownika — administratora.


admin_role = Role.query.filter_by(permissions=0xff).first()
admin = User(email='john@example.com',
username='john', password='cat',
role=admin_role, confirmed=True)
db.session.add(admin)
db.session.commit()

218  Rozdział 15. Testowanie

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

# Usuń bazę danych.


db.drop_all()
db.session.remove()

# Usuń kontekst aplikacji.


cls.app_context.pop()

def setUp(self):
if not self.client:
self.skipTest('Przeglądarka internetowa jest niedostępna')

def tearDown(self):
pass

Metody setUpClass()i tearDownClass() są wywoływane przed wykonaniem testów z danej klasy i po


ich wykonaniu. Konfiguracja środowiska testowego obejmuje uruchomienie instancji Chrome za
pośrednictwem interfejsu API webdriver Selenium oraz utworzenie aplikacji i bazy danych z kilkoma
początkowymi fałszywymi danymi, które będą wykorzystywane w testach. Aplikacja jest uruchamiana
w osobnym wątku za pomocą wywołania metody app.run(). Na koniec aplikacja otrzymuje żądanie
zamknięcia /shutdown, które powoduje zakończenie wątku działającego w tle. Przeglądarka jest na-
stępnie zamykana, a testowa baza danych usuwana.

Przed wprowadzeniem interfejsu wiersza polecenia Flaska wykorzystującego narzędzie


Click trzeba było uruchamiać serwer WWW, wywołując z głównego skryptu apli-
kacji metodę app.run() lub użyć zewnętrznego rozszerzenia takiego jak Flask-Script.
Co prawda uruchamianie serwera metodą app.run() zostało zastąpione poleceniem
flask run, ale metoda app.run() jest nadal dostępna i jak widać w naszym przykła-
dzie, może być przydatna w scenariuszach wymagających stosowania złożonych testów
jednostkowych.

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

Kompleksowe testy z użyciem Selenium  219

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.

Listing 15.8. tests/test_selenium.py: Przykład testu jednostkowego Selenium


class SeleniumTestCase(unittest.TestCase):
# ...

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

# Przejdź do strony logowania.


self.client.find_element_by_link_text('Zaloguj się').click()
self.assertIn('<h1>Login</h1>', 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))

# Przejdź do strony profilu użytkownika.


self.client.find_element_by_link_text('Profil').click()
self.assertIn('<h1>jan</h1>', 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ą.

220  Rozdział 15. Testowanie

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 15d, aby pobrać tę wersję aplikacji. Ta aktualizacja zawiera migrację
bazy danych, więc pamiętaj, aby po pobraniu kodu uruchomić polecenie flask db
upgrade. Aby upewnić się, że masz zainstalowane wszystkie zależności, uruchom także
polecenie pip install -r requirements/dev.txt.

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ć.

Czy warto?  221

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.

Niska wydajność bazy danych


Jeżeli wydajność aplikacji z czasem zaczyna się powoli zmniejszać, może to być spowodowane
powolnymi zapytaniami do bazy danych, których działanie pogarsza się wraz ze wzrostem wielkości
bazy danych. Optymalizacja zapytań do bazy danych może polegać na prostym dodaniu większej
liczby indeksów, ale może też wymagać wprowadzenia pamięci podręcznej między aplikacją a bazą
danych. Instrukcja explain, dostępna w większości języków zapytań do bazy danych, prezentuje
kolejne kroki wykonywane przez bazę danych podczas wykonywania zapytania. W ten sposób można
często ujawnić nieefektywne elementy w projekcie bazy danych lub indeksów.
Jednak zanim zaczniesz optymalizować zapytania, musisz najpierw ustalić, które z nich wymagają
optymalizacji. Podczas obsługi typowego żądania może zostać wysłanych kilka zapytań do bazy da-
nych, dlatego też często trudno jest ustalić, które z tych zapytań są najwolniejsze. Pakiet Flask-
SQLAlchemy ma opcję rejestrowania statystyk dotyczących zapytań do bazy danych wydanych
podczas obsługi żądania. Na listingu 16.1 można zobaczyć, jak używać tej funkcji do protokołowania
zapytań, które będą wolniejsze niż ustalony pułap.

Listing 16.1. app/main/views.py: Raportowanie powolnych zapytań bazy danych


from flask_sqlalchemy import get_debug_queries

@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.

Tabela 16.1. Statystyki zapytań zarejestrowane przez Flask-SQLAlchemy

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.

Funkcja obsługi zdarzenia after_app_request przechodzi przez tę listę i protokołuje wszelkie


zapytania, które trwały dłużej niż zadany pułap zdefiniowany w zmiennej konfiguracyjnej
FLASKY_SLOW_DB_QUERY_TIME. W naszej aplikacji takie zapisy są protokołowane jako ostrzeżenia,
ale w niektórych przypadkach lepszym rozwiązaniem może być traktowanie informacji o powolnych
zapytaniach do bazy danych jako błędów.
Funkcja get_debug_queries() domyślnie działa tylko w trybie debugowania. Niestety problemy z wy-
dajnością bazy danych bardzo rzadko pojawiają się podczas tworzenia aplikacji, ponieważ wtedy
używane są bazy danych o znacznie mniejszych rozmiarach. Z tego powodu o wiele przydatniejsze
będzie włączenie tej funkcji na etapie produkcyjnym. Na listingu 16.2 pokazano zmiany dokonane
w konfiguracji dla trybu produkcyjnego, niezbędne do włączenia funkcji monitorowania wydajności
zapytań do bazy danych.

Listing 16.2. config.py: Konfiguracja raportowania powolnych zapytań


class Config:
# ...
SQLALCHEMY_RECORD_QUERIES = True
FLASKY_SLOW_DB_QUERY_TIME = 0.5
# ...

Opcja SQLALCHEMY_RECORD_QUERIES informuje Flask-SQLAlchemy, aby włączyć rejestrowanie statystyk


zapytań. Próg powolnego zapytania ustawiony jest na pół sekundy. Obie zmienne konfiguracyjne
zostały zdefiniowane w bazowej klasie Config, więc zostaną włączone dla wszystkich konfiguracji.
Za każdym razem, gdy zostanie wykryte powolne zapytanie, do protokołu aplikacji Flaska zostanie
dodany nowy wpis. Oczywiście wymaga to wcześniejszego skonfigurowania rejestratora protokołu.

224  Rozdział 16. Wydajność

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 16a, aby pobrać tę wersję aplikacji.

Profilowanie kodu źródłowego


Kolejnym możliwym źródłem problemów związanych z wydajnością może być wysokie zużycie
procesora, spowodowane przez funkcje wykonujące intensywne obliczenia. Do znajdowania najwol-
niejszych części aplikacji świetnie nadają się profilery kodu źródłowego. Profiler obserwuje działającą
aplikację, rejestrując wywoływane funkcje oraz czas działania każdej z nich. Następnie generuje
szczegółowy raport wykazujący najwolniejsze z tych funkcji.

Profilowanie jest zwykle wykonywane tylko w środowisku programistycznym.


Profilowanie kodu źródłowego powoduje, że aplikacja działa znacznie wolniej niż
zwykle, ponieważ musi w czasie rzeczywistym obserwować i robić notatki o wszystkim,
co się w niej dzieje. W systemie produkcyjnym profilowanie nie jest zalecane, chyba
że zostanie użyty lekki profiler zaprojektowany specjalnie do pracy w środowisku
produkcyjnym.

Internetowy serwer programistyczny Flaska, pochodzący z pakietu Werkzeug, umożliwia opcjonalnie


włączenie profilera Pythona, aby rejestrować wszystkie żądania. Na listingu 16.3 można zobaczyć
nową opcję wiersza polecenia aplikacji, która uruchamia serwer WWW z profilerem.

Listing 16.3. flasky.py: Uruchomienie aplikacji z profilerem żądań


@app.cli.command()
@click.option('--length', default=25,
help='Liczba funkcji uwzględniana w raporcie profilera.')
@click.option('--profile-dir', default=None,
help='Katalog, w którym profiler zapisuje dane.')
def profile(length, profile_dir):
"""Uruchamia aplikację z profilerem kodu."""
from werkzeug.contrib.profiler import ProfilerMiddleware
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],
profile_dir=profile_dir)
app.run(debug=False)

To polecenie dołącza do atrybutu wsgi_app aplikacji instancję klasę ProfilerMiddleware z pakietu


Werkzeug. Oprogramowanie WSGI jest wywoływane za każdym razem, gdy serwer WWW wysyła
żądanie do aplikacji. Może ono wpływać na sposób obsługi żądania, a w tym przypadku przechwytuje
jedynie dane do profilowania. Zauważ, że aplikacja jest następnie uruchamiana programowo przy
użyciu metody app.run().

Profilowanie kodu źródłowego  225

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.

226  Rozdział 16. Wydajność

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.

Etapy prac wdrożenia


Niezależnie od zastosowanej metody hostingu istnieje także szereg zadań, które należy wykonać
podczas instalowania aplikacji na serwerze produkcyjnym. Należą do nich zadania związane z tworze-
niem lub aktualizacją tabel w bazie danych.
Konieczność ręcznego uruchamiania tych zadań przy każdej instalacji lub aktualizacji aplikacji
sprawia, że cała operacja jest czasochłonna i podatna na błędy. Aby tego uniknąć, możemy dodać
do pliku flasky.py polecenie, które wykona te wszystkie wymagane zadania za nas.
Na listingu 17.1 przedstawiam odpowiedni dla Flasky sposób implementacji polecenia deploy, które
zajmuje się wdrożeniem aplikacji.

Listing 17.1. flasky.py: Polecenie deploy


from flask_migrate import upgrade
from app.models import Role, User

@manager.command
def deploy():
"""Wykonuje zadania wdrożeniowe."""
# Migracja bazy danych do najnowszej wersji
upgrade()

# Tworzenie lub aktualizowanie ról użytkowników


Role.insert_roles()

# Sprawdzanie, czy wszyscy użytkownicy obserwują samych siebie


User.add_self_follows()

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.

Protokołowanie błędów na produkcji


Gdy aplikacja działa w trybie debugowania, interaktywny debugger pakietu Werkzeug zaczyna
działać za każdym razem, gdy wystąpi jakiś błąd. W przeglądarce wyświetlany jest wtedy stos wywołań
(ang. stack trace) błędu, a interaktywny debugger umożliwia przeglądanie kodu źródłowego, a nawet
sprawdzanie wartości wyrażeń w kontekście każdej ramki stosu.
Debugger jest doskonałym narzędziem do wyszukiwania problemów z aplikacjami podczas progra-
mowania, ale oczywiście nie można go użyć w środowisku produkcyjnym. Błędy występujące na pro-
dukcji są wyciszane i zamiast tego użytkownik otrzymuje dyskretną stronę błędu 500. Na szczęście
stosy wywołań związane z tymi błędami nie są tracone, ponieważ Flask zapisuje je w pliku protokołu.
Podczas uruchamiania aplikacji framework Flask tworzy instancję klasy logging.Logger i dołącza
ją do instancji aplikacji w zmiennej app.logger. W trybie debugowania rejestrator wypisuje wszystkie
informacje w konsoli, ale w trybie produkcyjnym nie ma żadnych domyślnie skonfigurowanych
procedur obsługi. Jeśli nie dodamy do aplikacji procedur obsługi protokołów, to takie zapisy nie
będą nigdzie przechowywane. Zmiany w kodzie z listingu 17.2 konfigurują procedurę obsługi
protokołowania, tak aby ta wysyłała wszystkie pojawiające się błędy na adres e-mail administratora
zdefiniowany w zmiennej konfiguracyjnej FLASKY_ADMIN.

Listing 17.2. config.py: Wysyłanie wiadomości e-mail z informacjami o błędach w aplikacji


class ProductionConfig(Config):
# ...
@classmethod
def init_app(cls, app):
Config.init_app(app)
# E-mail z błędami do administratorów
import logging
from logging.handlers import SMTPHandler
credentials = None
secure = None
if getattr(cls, 'MAIL_USERNAME', None) is not None:
credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
if getattr(cls, 'MAIL_USE_TLS', None):
secure = ()
mail_handler = SMTPHandler(
mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
fromaddr=cls.FLASKY_MAIL_SENDER,
toaddrs=[cls.FLASKY_ADMIN],

228  Rozdział 17. Wdrożenie

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 17b, aby pobrać tę wersję aplikacji.

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ń.

Wdrożenie w chmurze  229

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.

Jeśli planujesz hostować swoją aplikację na platformie Heroku, dobrym pomysłem


będzie używanie Gita od samego początku prac nad aplikacją. Na GitHubie znajdziesz
wszystkie informacje na temat instalacji i konfiguracji dla trzech głównych systemów
operacyjnych (http://help.github.com).

Tworzenie konta Heroku


Aby uzyskać dostęp do usługi, musisz utworzyć konto na platformie Heroku (https://heroku.com).
Heroku ma też darmową wersję swojej usługi, która pozwala na hostowanie kilku prostych aplikacji,
jest zatem świetną platformą do eksperymentowania.

Instalowanie interfejsu Heroku CLI


Do prawidłowej pracy z usługą Heroku należy zainstalować interfejs Heroku CLI (https://devcenter.
heroku.com/articles/heroku-cli). Jest to klient wiersza poleceń, który zarządza interakcjami z usługą.
Heroku zapewnia instalatory dla trzech głównych systemów operacyjnych.

230  Rozdział 17. Wdrożenie

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)

Polecenie flask do prawidłowego działania wymaga istnienia zmiennej środowiskowej FLASK_APP.


Aby się upewnić, czy wszystkie polecenia będą prawidłowo wykonane w środowisku Heroku,
trzeba zarejestrować tę zmienną środowiskową, tak aby była zawsze zdefiniowana, gdy Heroku
będzie wykonywać polecenia związane z aplikacją. Można to zrobić za pomocą polecenia config:
$ heroku config:set FLASK_APP=flasky.py
Setting FLASK_APP and restarting <nazwa_aplikacji>... done, v4
FLASK_APP: flasky.py

Platforma Heroku  231

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.

Listing 17.3. config.py: Konfiguracja Heroku


class HerokuConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)

# Zapis do stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)

232  Rozdział 17. Wdrożenie

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

Konfigurowanie poczty e-mail


Platforma Heroku nie zapewnia serwera SMTP, dlatego trzeba skonfigurować serwer zewnętrzny.
Istnieje kilka zewnętrznych dodatków, które integrują obsługę wysyłania wiadomości e-mail z Heroku,
ale do celów testowania i oceny wystarczy użyć domyślnej konfiguracji Gmaila, odziedziczonej
z bazowej klasy Config.
Ze względu na to, że osadzanie danych uwierzytelniających bezpośrednio w skrypcie może stanowić
zagrożenie dla bezpieczeństwa, nazwa użytkownika i hasło dostępu do serwera SMTP Gmaila są
dostarczane jako zmienne środowiskowe (dobrze by było, żeby zamiast osobistego konta e-mail utwo-
rzyć dodatkowy adres, który będzie używany tylko do testowania aplikacji):
$ heroku config:set MAIL_USERNAME=<twoja-nazwa-gmail>
$ heroku config:set MAIL_PASSWORD=<twoje-hasło-gmail>

Dodanie pliku wymagań najwyższego poziomu


Heroku instaluje zależności pakietu z pliku requirements.txt, przechowywanego w katalogu najwyższego
poziomu aplikacji. Wszystkie zależności w tym pliku zostaną zaimportowane w ramach wdrożenia
do środowiska wirtualnego zarządzanego przez Heroku.

Platforma Heroku  233

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.

Listing 17.4. requirements.txt: Plik wymagań Heroku


-r requirements/heroku.txt

Włączanie bezpiecznego HTTP z Flask-SSLify


Gdy użytkownik loguje się do aplikacji, podając nazwę użytkownika i hasło w formie sieciowej,
istnieje pewne ryzyko, że dane te zostaną przechwycone przez złośliwą stronę trzecią (co zostało już
wcześniej kilkukrotnie omówione). Podczas programowania nie stanowi to problemu, ale ryzyko
to należy wyeliminować podczas wdrażania aplikacji na serwerze produkcyjnym. Aby zapobiec
podczas wysyłki ujawnieniu danych uwierzytelniających użytkownika, konieczne jest użycie bezpiecz-
nego protokołu HTTP, który szyfruje całą komunikację między klientami a serwerem za pomocą
kryptografii klucza publicznego.
Heroku udostępnia wszystkie aplikacje, które są dostępne w domenie herokuapp.com, zarówno
pod adresem http://, jak i pod adresem https://, bez wymaganej konfiguracji. Ponieważ aplikacja działa
w domenie Heroku, będzie używać własnego certyfikatu SSL Heroku. Jedynym niezbędnym
działaniem, aby w pełni zabezpieczyć aplikację, jest przechwycenie wszelkich żądań wysłanych do
interfejsu http:// i przekierowanie ich na https://, co dokładnie robi rozszerzenie Flask-SSLify.
Jak zwykle, Flask-SSLify jest instalowany za pomocą polecenia pip:
(venv) $ pip install flask-sslify

Kod aktywujący to rozszerzenie jest dodawany do funkcji produkcyjnej aplikacji, tak jak pokazano
na listingu 17.5.

Listing 17.5. app/__init__.py: Przekierowanie wszystkich żądań do protokołu HTTPS


def create_app(config_name):
# ...
if app.config['SSL_REDIRECT']:
from flask_sslify import SSLify
sslify = SSLify(app)
# ...

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.

Listing 17.6. config.py: Konfiguracja użycia SSL


class Config:
# ...
SSL_REDIRECT = False

234  Rozdział 17. Wdrożenie

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.

Listing 17.7. config.py: Dodanie obsługi serwerów proxy


class HerokuConfig(ProductionConfig):
# ...
@classmethod
def init_app(cls, app):
# ...

# Obsługa nagłówków odwrotnego serwera proxy


from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

Pakiet pośredniczący jest dodawany w metodzie inicjalizującej konfigurację Heroku. Oprogramowa-


nie WSGI, takie jak ProxyFix, jest dodawane jako wrapper dla aplikacji WSGI. Po odebraniu żądania
pakiet ten ma możliwość skontrolowania środowiska i dokonania zmian w żądaniu jeszcze przed

Platforma Heroku  235

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.

Uruchamianie produkcyjnego serwera WWW


Platforma Heroku oczekuje, że aplikacje uruchomią własny produkcyjny serwer WWW i skonfigu-
rują go w taki sposób, aby odpowiednio nasłuchiwał żądań na porcie o numerze zdefiniowanym
wcześniej w zmiennej środowiskowej PORT.
W tej sytuacji programistyczny serwer WWW dostarczany wraz z Flaskiem nie będzie się dobrze
sprawdzał, ponieważ nie został zaprojektowany do działania w środowisku produkcyjnym. Gunicorn
(http://gunicorn.org) i uWSGI (http://bit.ly/uwsgi-proj) to dwa serwery WWW przygotowane do dzia-
łania w środowisku produkcyjnym, które dobrze współpracują z aplikacjami Flaska.
Dobrym pomysłem może być także zainstalowanie wybranego serwera WWW w lokalnym środowi-
sku wirtualnym, aby można go było przetestować w warunkach podobnych do tych istniejących
w środowisku Heroku. Na przykład serwer Gunicorn można zainstalować w następujący sposób:
(venv) $ pip install gunicorn

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.

Serwer WWW Gunicorn nie działa w systemie Microsoft Windows. Natomiast


drugi zalecany serwer WWW — uWSGI, co prawda działa w systemie Windows,
ale jego instalacja jest mocno utrudniona. Jeśli wdrożenie na platformie Heroku
chcesz przetestować w systemie Windows, możesz użyć serwera Waitress (https://
docs.pylonsproject.org/projects/waitress) — kolejnego czystego serwera WWW z Pythona,
który jest pod wieloma względami podobny do Gunicorna, ale ma tę zaletę, że
w pełni współdziała z systemem Windows. Serwer Waitress można zainstalować za
pomocą polecenia pip:
(venv) $ pip install waitress

Aby uruchomić serwer Waitress, użyj polecenia waitress-serve, tak jak poniżej:
(venv) $ waitress-serve --port 8000 flasky:app

236  Rozdział 17. Wdrożenie

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.

Listing 17.8. Procfile: Heroku Procfile


web: gunicorn flasky:app

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

Aplikacje mogą używać pliku Procfile do deklarowania dodatkowych zadań o nazwach


innych niż web. Każde zadanie zawarte w tym pliku zostanie uruchomione na osob-
nym dyno.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 17c, aby pobrać tę wersję aplikacji. Jeśli korzystasz z systemu Micro-
soft Windows, użyj polecenia git checkout 17c-waitress, aby pobrać wersję
aplikacji skonfigurowaną do użycia z serwerem Waitress zamiast Gunicorn.

Testowanie z wykorzystaniem Heroku Local


Interfejs CLI platformy Heroku zawiera polecenie local używane do lokalnego uruchamiania
aplikacji w sposób bardzo podobny do tego używanego na serwerach Heroku. Różnica polega na
tym, że podczas lokalnego uruchamiania aplikacji nie będą dostępne zmienne środowiskowe, takie jak
na przykład FLASK_APP. Polecenie heroku local będzie wyszukiwało zmienne środowiskowe kon-
figurujące aplikację w pliku o nazwie .env (znajdującym się w katalogu najwyższego poziomu aplika-
cji). Na przykład ten plik może zawierać następujące zmienne:
FLASK_APP=flasky.py
FLASK_CONFIG=heroku
MAIL_USERNAME=<twoja—nazwa_użytkownika-gmail>
MAIL_PASSWORD=<twoje-hasło-gmail>

Platforma Heroku  237

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

Dane protokołów wszystkich zadań uruchomionych za pomocą tego polecenia są skonsolidowane


w jednym strumieniu, który jest wypisywany w konsoli, przy czym każdy wiersz poprzedzony jest
znacznikiem czasu i nazwą zadania.
Polecenie heroku local umożliwia także symulację użycia wielu dyno do skalowania aplikacji.
Poniższe polecenie uruchamia trzy robocze serwery WWW, z których każdy nasłuchuje na innym
porcie:
(venv) $ heroku local web=3

Wdrażanie za pomocą polecenia git push


Ostatnim krokiem w tym procesie jest przesłanie aplikacji na serwery Heroku. Upewnij się, że wszyst-
kie zmiany są już umieszczone w lokalnym repozytorium Git, a następnie użyj polecenia git
push heroku master, aby przesłać aplikację na platformę Heroku:
$ git push heroku master
Counting objects: 502, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (426/426), done.
Writing objects: 100% (502/502), 108.03 KiB | 0 bytes/s, done.
Total 502 (delta 303), reused 146 (delta 61)
remote: Compressing source files... done.
remote: Building source:

238  Rozdział 17. Wdrożenie

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.

Przeglądanie dzienników aplikacji


Dane protokołów wygenerowane przez aplikację są przechwytywane przez platformę Heroku. Aby
wyświetlić zawartość takiego dziennika, użyj polecenia logs:
$ heroku logs

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

Platforma Heroku  239

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.

Kontenery na platformie Docker


Znasz już platformę Heroku, która stosuje wysokopoziomowe rozwiązania wdrażania aplikacji.
W tym podrozdziale dowiesz się, jak pracować z kontenerami (ang. containers), a zwłaszcza z platformą
Docker. Platforma ta nie jest tak zautomatyzowana jak platformy PaaS, ale zapewnia większą ela-
styczność i nie jest powiązana z żadnym konkretnym dostawcą chmury.
Kontenery to specjalny rodzaj maszyny wirtualnej działającej na jądrze systemu operacyjnego hosta,
w przeciwieństwie do standardowych maszyn wirtualnych, które mają własne zwirtualizowane
jądro i sprzęt. Ze względu na fakt, że wirtualizacja zatrzymuje się w jądrze, kontenery są znacznie
lżejsze i wydajniejsze niż maszyny wirtualne, ale wymagają mechanizmów obsługi wbudowanych
w system operacyjny. Jądro Linuksa wyposażone jest w pełną obsługę kontenerów.

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)

240  Rozdział 17. Wdrożenie

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
Go version: go1.8.3
Git commit: 02c1d87
Built: Fri Jun 23 21:51:55 2017
OS/Arch: linux/amd64
Experimental: true

Docker dla Windows wymaga włączonej funkcji Microsoft Hyper-V. Instalator


zwykle włączy ją dla Ciebie, natomiast jeśli Docker nie będzie po instalacji działać
poprawnie, to pierwszą rzeczą do sprawdzenia będzie stan hiperwizora Hyper-V.
Należy tu pamiętać, że włączenie funkcji Hyper-V na komputerze z systemem
Windows uniemożliwi działanie innych hiperwizorów (takich jak VirtualBox firmy
Oracle). Jeśli Twój system nie obsługuje wirtualizacji Hyper-V lub potrzebujesz
jeszcze innego rozwiązania, które nie będzie powodowało, że inne technologie
wirtualizacji będą niepoprawnie działać, to możesz zainstalować Docker Toolbox
(https://docs.docker.com/toolbox/overview/). Jest to starszy produkt Dockera dla syste-
mu Windows bazujący na VirtualBox.

Budowanie obrazu kontenera


Pierwszym zadaniem podczas pracy z kontenerami jest zbudowanie obrazu kontenera dla aplikacji.
Obraz jest migawką systemu plików kontenera, używaną jako szablon podczas uruchamiania nowych
kontenerów. Docker oczekuje, że instrukcje dotyczące tworzenia obrazu zostaną dostarczone w pliku
o nazwie Dockerfile. Na listingu 17.9 został pokazany plik Dockerfile, który tworzy aplikację opisaną
w tej książce.

Listing 17.9. Dockerfile: Skrypt tworzący obraz kontenera


FROM python:3.6-alpine

ENV FLASK_APP flasky.py


ENV FLASK_CONFIG docker

RUN adduser -D flasky


USER flasky

WORKDIR /home/flasky

COPY requirements requirements


RUN python -m venv venv
RUN venv/bin/pip install -r requirements/docker.txt

COPY app app


COPY migrations migrations
COPY flasky.py config.py boot.sh ./

# Konfiguracja środowiska wykonawczego


EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

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.

Kontenery na platformie Docker  241

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.

Wersje Dockera dla systemów macOS i Windows mogą uruchamiać kontenery


zbudowane dla systemu Linux.

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.

Listing 17.10. config.py: Konfiguracja Dockera


class DockerConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)

# 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.

242  Rozdział 17. Wdrożenie

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.

Listing 17.11. boot.sh: Skrypt uruchamiania kontenera


#!/bin/sh
source venv/bin/activate
flask deploy
exec gunicorn -b 0.0.0.0:5000 --access-logfile - --error-logfile - flasky:app

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.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 17d, aby pobrać tę wersję aplikacji.

Kontenery na platformie Docker  243

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.

244  Rozdział 17. Wdrożenie

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

Te dwie operacje można połączyć w jedną za pomocą polecenia docker rm -f:


$ docker rm -f 71357ee776ae
71357ee776ae

Sprawdzanie działającego kontenera


Kiedy kontener zachowuje się nieprawidłowo, konieczne może być jego debugowanie. Najbardziej
oczywistym mechanizmem debugowania jest dodanie do aplikacji instrukcji protokołu, a następnie
monitorowanie działającego już kontenera za pomocą polecenia docker logs.
Jednak w niektórych sytuacjach wygodniej jest otworzyć sesję powłoki w działającym kontenerze,
aby można go było dokładniej skontrolować. Umożliwia nam to polecenie docker exec:
$ docker exec -it 71357ee776ae sh

Kontenery na platformie Docker  245

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.

Przekazywanie obrazu kontenera do rejestru zewnętrznego


Lokalne przechowywanie obrazu kontenera jest wygodne podczas tworzenia i testowania aplikacji,
ale gdy jesteśmy już gotowi, aby udostępnić obraz innym osobom, musimy wypchnąć go na serwer
zewnętrznego rejestru (ang. external registry).
Rejestr Docker Hub to repozytorium obrazów Dockera. To bardzo wygodna usługa, w której można
przechowywać swoje obrazy. Bezpłatne konto Docker Hub pozwala przechowywać nieograniczo-
ną liczbę publicznych obrazów kontenerów, lecz tylko jeden obraz prywatny. Płatne konta zwiększają
liczbę prywatnych obrazów, które można przechowywać na serwerze. Aby utworzyć konto Docker
Hub, przejdź na stronę internetową dostępną pod adresem https://hub.docker.com.
Po utworzeniu konta Docker Hub możesz się na nie zalogować z wiersza poleceń za pomocą polecenia
docker login:
$ docker login
Login with your Docker ID to push and pull images from Docker Hub.
Username: <twoja-nazwa-użytkownika-dockerhub>
Password: <twoje-hasło-dockerhub>
Login Succeeded

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

Aby przesłać obraz do Docker Huba, użyj polecenia docker push:


$ docker push <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

246  Rozdział 17. Wdrożenie

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 sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 17e, aby pobrać tę wersję aplikacji.

Zmiana wprowadzona do pliku requirements/docker.txt wymaga ponownego przygotowania obrazu


kontenera:
$ docker build -t flasky:latest .

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

Kontenery na platformie Docker  247

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.

Repozytorium Docker Hub to prawdziwa kopalnia bardzo przydatnych aplikacji


i usług, które są spakowane i gotowe do użycia w środowisku Docker (samodzielnie
lub jako obrazy bazowe dla własnych kontenerów). Zaglądając tam, przekonasz się,
że najróżniejsze projekty (w tym bazy danych, serwery WWW, języki programowania,
systemy operacyjne i inne) udostępniają swoje oficjalne obrazy.

Orkiestracja kontenerów za pomocą Docker Compose


Aplikacje w kontenerach składają się zwykle z kilku działających kontenerów. W poprzednim punkcie
mogliśmy już zobaczyć, że główna aplikacja i serwer bazy danych działają w niezależnych konte-
nerach. Gdy złożoność rozbudowywanej aplikacji będzie stale rosła, z pewnością będzie potrzebować
większej liczby kontenerów. Niektóre z aplikacji będą wymagać dodatkowych usług, takich jak kolejki
komunikatów lub pamięci podręczne. Inne aplikacje mogą korzystać z architektury mikroserwisów
i mieć rozproszoną strukturę z kilkoma mniejszymi podaplikacjami, z których każda będzie działać we
własnym kontenerze. Aplikacje, które muszą obsługiwać duże obciążenia lub muszą być odporne na
uszkodzenia, muszą pozwalać na skalowanie, co oznacza uruchamianie kilku instancji aplikacji za
modułem równoważenia obciążenia.
Wraz ze wzrostem liczby kontenerów składających się na aplikację zadanie zarządzania wszystkimi
tymi kontenerami i koordynowanie ich będzie coraz trudniejsze, jeżeli ograniczymy się do samych na-
rzędzi Dockera. W tym zadaniu może nam pomóc środowisko orkiestracji (ang. orchestration)
kontenerów, zbudowane na platformie Docker.
Instalując platformę Docker, otrzymujemy też specjalny zestaw narzędzi o nazwie Compose do orkie-
stracji. W przypadku używania tego zestawu narzędzi kontenery będące częścią aplikacji opisywane są
w pliku konfiguracyjnym, zazwyczaj nazywającym się docker-compose.yml. Polecenie docker-compose
pozwala nam uruchomić wszystkie kontenery powiązane z aplikacją za pomocą tylko jednego
polecenia.
Na listingu 17.12 został przedstawiony plik docker-compose.yml, reprezentujący kontener z aplikacją
Flasky powiązany z usługą MySQL.

248  Rozdział 17. Wdrożenie

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>

Kontenery na platformie Docker  249

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.

Pełny opis pliku docker-compose.yml znajduje się na stronie internetowej platformy


Docker (https://docs.docker.com/compose/compose-file/).

Typowym problemem związanym z orkiestracją jest to, że kontenery są uruchamiane w niewłaściwej


kolejności lub we właściwej kolejności, ale bez dawania kontenerom usług podstawowych wystarczają-
cej ilości czasu potrzebnego na ich uruchomienie i zainicjowanie, zanim uruchomione zostaną
korzystające z nich kontenery wyższego poziomu. W przypadku aplikacji Flasky kontener mysql musi
być uruchomiony jako pierwszy, aby baza danych była gotowa do pracy podczas uruchamiania konte-
nera flasky. Dopiero wtedy aplikacja może połączyć się z bazą danych, zastosować migracje bazy da-
nych i wreszcie uruchomić serwer WWW.
Narzędzia Compose uruchomią kontenery mysql i flasky we właściwej kolejności, ponieważ zależno-
ści między nimi wykryją w kluczu links zawartym w kontenerze flasky. Ale narzędzia te nie będą
czekać na uruchomienie MySQL, które może potrwać nawet kilka sekund. Podczas projektowania
systemów rozproszonych dobrą praktyką jest stosowanie wielokrotnych prób ustanowienia połączeń
z usługami zewnętrznymi. Na listingu 17.13 pokazuję bardziej niezawodną postać skryptu boot.sh,
który uruchamia kontener flasky. W tym przypadku polecenie flask deploy jest ponawiane tak długo,
aż aktualizacja bazy danych zakończy się powodzeniem.

Listing 17.13. boot.sh: Oczekiwanie na uruchomienie bazy danych


# !/bin/sh
source venv/bin/activate

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

exec gunicorn -b :5000 --access-logfile - --error-logfile - flasky:app

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ń.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie


git checkout 17f, aby pobrać tę wersję aplikacji. Upewnij się także, czy zostały
utworzone pliki zmiennych środowiskowych .env i .env-mysql i czy zawierają one
wartości odpowiednie dla Twojego środowiska.

Po zakończeniu konfiguracji narzędzi Compose aplikację można uruchomić za pomocą polecenia


docker-compose up:
$ docker-compose up -d --build

250  Rozdział 17. Wdrożenie

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

Jeśli chcesz stale monitorować strumień protokołu, użyj polecenia:


$ docker-compose logs -f

Z kolei polecenie docker-compose ps wyświetla podsumowanie wszystkich uruchomionych kontene-


rów aplikacji oraz ich stanu:
$ docker-compose ps
Name Command State Ports
------------------------------------------------------------------------
flasky_flasky_1 ./boot.sh Up 0.0.0.0:8000->5000/tcp
flasky_mysql_1 /entrypoint.sh mysqld Up 3306/tcp, 33060/tcp

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.

Sprzątanie starych kontenerów i obrazów


Podczas pracy z kontenerami system nieustannie gromadzi stare kontenery lub obrazy, które nie
są już potrzebne. Dobrym pomysłem będzie tutaj rutynowe sprawdzanie i czyszczenie tych plików,
aby nie zajmowały miejsca w systemie.
Aby wyświetlić listę kontenerów w systemie, użyj następującego polecenia:
$ docker ps -a

Spowoduje to wyświetlenie działających już kontenerów i kontenerów, które zostały zatrzymane,


ale nadal są w systemie. Aby usunąć dowolne kontenery z tej listy, użyj polecenia docker rm -f
i podaj nazwy lub identyfikatory do usunięcia:
$ docker rm -f <nazwa-lub-id> <nazwa-lub-id> ...

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

Kontenery na platformie Docker  251

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.

Korzystanie z platformy Docker podczas produkcji


Wiele osób uważa platformę Docker za platformę programistyczną i testową. Oczywiście techniki
przedstawione w poprzednich punktach można wykorzystać do wdrażania aplikacji na serwerach
produkcyjnych z systemem Docker. Istnieją tu jednak pewne ograniczenia i problemy związane z bez-
pieczeństwem, które należy wziąć pod uwagę:
Monitorowanie i alarmowanie
Co się stanie, jeśli aplikacja kontenerowa ulegnie awarii? Docker może ponownie uruchomić
kontener, który niespodziewanie zaprzestanie działania, jednak nie będzie on monitorował
naszych kontenerów ani nie będzie wysyłał alertów, gdy będą się one zachowywać nieprawidłowo.
Protokołowanie
Docker utrzymuje osobny strumień protokołu dla każdego kontenera. Narzędzia Compose
poprawiają nieco sytuację, udostępniając skonsolidowany strumień protokołów, jednak nie
jest on przechowywany długoterminowo i nie pozwala na wyszukiwanie i filtrowanie.
Zarządzanie tajnymi danymi
Konfigurowanie haseł i innych danych uwierzytelniających za pomocą zmiennych środowi-
skowych jest dość niepewne, ponieważ Docker pozwala wypisać swoje zmienne środowiskowe za
pomocą polecenia docker inspect oraz za pośrednictwem interfejsu API.
Niezawodność i skalowanie
Aby zwiększyć poziom tolerancji błędów lub dostosować aplikację do rosnącego obciążenia,
konieczne jest uruchomienie kilku jej instancji na kilku hostach i umieszczenie ich za jednym
lub kilkoma modułami równomiernie rozkładającymi obciążenie.
Ograniczenia te są na ogół eliminowane przez bardziej złożone środowiska orkiestracji, zbudowane na
bazie Dockera lub innych środowisk kontenerowych. Środowiska, takie jak Docker Swarm (teraz
jest już częścią platformy Docker), Apache Mesos i Kubernetes, są dobrym wyborem do budowania
solidnych wdrożeń wykorzystujących kontenery.

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

252  Rozdział 17. Wdrożenie

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.

Zamiast wykonywać te wszystkie zadania ręcznie, utwórz skrypt wdrożeniowy przy


użyciu środowiska automatyzacji takiego jak Ansible, Chef lub Puppet.

Importowanie zmiennych środowiskowych


Podobnie jak w przypadku platform Heroku i Docker aplikacja działająca na autonomicznym serwe-
rze korzysta z pewnych ustawień, takich jak adres URL bazy danych, dane uwierzytelniania serwera
e-mail oraz kilka innych podobnych rzeczy, które są definiowane w zmiennych środowiskowych.
Ze względu na to, że nie mamy platformy Heroku lub Docker, które przygotowałyby dla nas te
zmienne przed uruchomieniem aplikacji, procedura ich konfigurowania będzie zależała od plat-
formy i użytych narzędzi. Aby ułatwić sobie konfigurację zmiennych środowiskowych i ujednolicić ją
na różnych platformach, można zastosować krótki blok kodu z listingu 17.14. Importuje on do danego
środowiska plik .env (podobny do tego używanego z poleceniami heroku local i docker-compose),
używając w tym celu pakietu Pythona o nazwie python-dotenv, który należy zainstalować za pomocą
polecenia pip. Ta operacja odbywa się to w pliku flasky.py jeszcze przed utworzeniem instancji

Tradycyjne wdrożenia  253

f68e0958e7bb3db1cc579c6f6e0fa0e6
f
aplikacji. Dzięki temu w momencie importowania konfiguracji przez aplikację zmienne te są już
dostępne w środowisku.

Listing 17.14. flasky.py: Importowanie zmiennych środowiskowych z pliku .env


import os
from dotenv import load_dotenv

dotenv_path = os.path.join(os.path.dirname(__file__), '.env')


if os.path.exists(dotenv_path):
load_dotenv(dotenv_path)

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.

Listing 17.15. config.py: Przykładowa konfiguracja Uniksa


class UnixConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)

# Zapis do syslog
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.WARNING)
app.logger.addHandler(syslog_handler)

Po użyciu tej konfiguracji protokoły aplikacji są zapisywane w skonfigurowanym pliku komunikatów


syslog. Zwykle są to pliki /var/log/messages lub /var/log/syslog, zależnie od dystrybucji Linuksa.
Usługę syslog można skonfigurować tak, żeby zapisywała osobne pliki dla protokołu aplikacji lub
wysyłała zapisy protokołów na inny komputer.

Jeśli sklonowałeś repozytorium Git aplikacji z GitHuba, możesz wywołać polecenie git
checkout 17g, aby pobrać tę wersję aplikacji.

254  Rozdział 17. Wdrożenie

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.

Korzystanie ze zintegrowanego środowiska


programistycznego (IDE)
Tworzenie aplikacji Flaska w zintegrowanym środowisku programistycznym (IDE) zwykle jest
bardzo wygodne, ponieważ takie funkcje jak uzupełnianie kodu i interaktywny debugger mogą
znacznie przyspieszyć proces programowania. Oto niektóre ze środowisk IDE dobrze współpra-
cujących z Flaskiem:
PyCharm (http://bit.ly/py-charm)
Środowisko IDE tworzone przez firmę JetBrains, dostępne w wersjach Community (bezpłatna)
i Professional (płatna). Obie wersje są kompatybilne z aplikacjami Flaska. Można z nich korzystać
w systemach Linux, macOS i Windows.
Visual Studio Code (https://code.visualstudio.com)
Otwartoźródłowe środowisko IDE firmy Microsoft. W tym przypadku, aby mieć dostęp do funk-
cji uzupełniania kodu i debugowania w aplikacjach Flaska, wymagana jest dodatkowa instala-
cja zewnętrznej wtyczki Pythona. Dostępne jest dla systemów Linux, macOS i Windows.
PyDev (http://pydev.org)
Otwartoźródłowe środowisko IDE zbudowane na bazie Eclipse. Dostępne jest dla systemów
Linux, macOS i Windows.

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.

256  Rozdział 18. Dodatkowe zasoby

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.

Angażowanie się w społeczność Flaska


Flask nie byłby tak niesamowity, gdyby nie praca wykonana przez społeczność programistów. Teraz
i Ty stajesz się jej częścią, będziesz czerpać korzyści z pracy wielu wolontariuszy. Spróbuj zatem
znaleźć sposób na oddanie im czegoś w zamian. Poniżej przedstawiam kilka pomysłów, które pomogą
Ci zacząć pracę na rzecz społeczności:
 Przejrzyj dokumentację Flaska lub swojego ulubionego powiązanego projektu i prześlij poprawki
lub ulepszenia.
 Przetłumacz dokumentację na nowy język.
 Odpowiedz na pytania na stronach z pytaniami i odpowiedziami, takimi jak Stack Overflow
(https://stackoverflow.com).
 Rozmawiaj o swojej pracy na spotkaniach z innymi programistami lub na konferencjach
grup użytkowników.
 Wnieś poprawki lub ulepszenia do używanych już pakietów.
 Napisz nowe rozszerzenia Flaska i udostępnij je na zasadach otwartych źródeł.
 Udostępniaj swoje aplikacje jako otwarte źródła.
Mam nadzieję, że zdecydujesz się na jeden z tych sposobów lub na inne, które będą dla Ciebie ważne,
i tym samym zostaniesz kolejnym członkiem społeczności Flaska. Jeśli to zrobisz, bardzo Ci dziękuję!

Angażowanie się w społeczność Flaska  257

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

You might also like