Flask/Grinberg/01-23
Tato stránka je součástí projektu: | |
Příslušnost: všeobecná |
Flask/Grinberg – na této stránce se budeme učit Flask dle Miguela Grinberga: The Flask Mega-Tutorial Part I: Hello, World!
Tato stránka shlukuje všech 23 kapitol dohromady. Chceme-li si prohlížet jednotlivé kapitoly odděleně, jejich seznam najdeme na stránce: Flask/Grinberg
01 - Nazdárek!
[editovat]Vše je na GitHubu:
Chceme-li si stáhnout zdrojáky pro určitou kapitolu:
- na liště klikneme na 24 releases
- jsou označené v0.0–v0.23 – to jsou odpovídající čísla kapitol
- můžeme si stáhnout příslušné zdrojáky jako .zip anebo .tar.gz
Takže věci pro tuto 01. kapitolu jsou zde:
- https://github.com/miguelgrinberg/microblog/tree/v0.1
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.1
- https://github.com/miguelgrinberg/microblog/compare/v0.0...v0.1
… a tak podobně i v dalších kapitolách
Nicméně Miguel Grinberg doporučuje, aby si každý příslušné příklady naťukal na klávesnici sám, z didaktických důvodů.
U každého balíčku je MIT licence, která umožňuje tyto zdrojáky volně používat i různě modifikovat – za předpokladu, že tato licence bude všude uváděna. Proto ji uvádíme i zde:
The MIT License (MIT) Copyright (c) 2017 Miguel Grinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Installing Python
[editovat]sudo apt install python3
python3
Installing Flask
[editovat]sudo pip3 install flask
Nicméně Ginberg tento přímý způsob instalace do systému nedoporučuje – a to z toho důvodu, že když vyvineme nějakou aplikaci pod jednou verzí a pak upgradujeme flask, že nám pak s tou starou verzí naší aplikace můžeme mít problémy.
Jako řešení navrhuje vyvíjet každou aplikaci v jejím vlastním virtuálním prostředí a teprve v tomto prostředí pak nainstalovat flask a další věci:
python3 python3 -m venv venv
virtualenv venv
source venv/bin/activate
pip3 install flask
Výhoda toho je také ta, že nemusíme mít právo roota, abychom si vše instalovali přímo do operačního systému – vše děláme v našem uživatelském prostoru.
Ovšem nevýhoda toho je, že pro každou aplikaci pak musíme mít nainstalované vše znova, což zabírá místo na disku atd. Proto si to virtuální prostředí prozatím můžeme odpustit. Kdo ale chce, ať postupuje dle Ginbergoav návodu.
A "Hello, World" Flask Application
[editovat]Zjistíme, že na vytvoření jednoduché webové aplikace potřebujeme tři soubory v adresářové stuktuře (lomítko na začátku neoznačuje systémový kořen, ale nějaký náš kořenový adresář, ve kterém budeme aplikaci vyvíjet):
- /mojeaplikace.py
- /app/__init__.py
- /app/routes.py
/app/__init__.py
# app/__init__.py: Flask application instance
from flask import Flask
app = Flask(__name__)
from app import routes # modul routes si vytvoříme hned za malou chvílku; tady ho ale musíme importovat až na konci skriptu, abychom zamezili vzájemným referencím
Tento skript vytváří proměnnou (= aplikační objekt) app jakožto instanci třídy Flask
importovanou z nainstalovaného balíku flask.
Proměnna __name__
je predefinovanou proměnnou, obsahující jméno modulu, ve kterém je použita.
Flask tuto lokaci použije, aby věděl, kde má hledat další soubory k naší aplikaci – šablony atd.
/app/routes.py: Zde si nadefinujeme, co se má zobrazovat na jednotlivých webových stránkách:
from app import app
# @ označuje tzv. dekorátor, modifikující funkci za ním následující – registruje ji jakožto něco
@app.route('/') # tento dekorátor vyvolá následující view-funkci při požadavku kořenového URL '/'
@app.route('/index') # tento dekorátor vyvolá tu samou funkci, když klient bude chtít '/index'
def index(): # to je tzv. view-funkce, mapovaná do jednoho či více URL
return "Nazdáreček, hi hej!"
/mojeaplikace.py – soubor v nejvyšším patře našeho adresáře
from app import app # z balíku app importujeme proměnnou app
(Ginberg zde namísto mojeaplikace.py píše microblog.py, my jsme tu použili obecnější název)
Naše první aplikace je hotová, ale aby flask věděl, kde ji najít, musíme mu nastavit proměnnou:
export FLASK_APP=mojeaplikace.py
Poté už můžeme flask spustit jednoduchým příkazem:
flask run
Flask spustí webový server, který nám naši aplikaci obslouží na portu 5000, takže ji najdeme na URL http://127.0.0.1:5000/ neboli http:localhost:5000/
Při instalaci na produkční web pak aplikace bude čekat na portu 443 (případně 80, pokud nebudeme implementovat šifrování)
Abychom nemuseli při každém sezení znova exportovat FLASK_APP, nainstalujeme si balík:
sudo pip3 install python-dotenv
A pak na vrchol svého adresáře s naší aplikací umístíme soubor: .flaskenv
FLASK_APP=mojeaplikace.py
Ale to asi bude fungovat jen v tom virtuálním prostředí, které jsme se rozhodli nevyužívat.
02 - Šablony
[editovat]- Chapter 2:Templates
- https://github.com/miguelgrinberg/microblog/tree/v0.2
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.2
- https://github.com/miguelgrinberg/microblog/compare/v0.1...v0.2
Webové stránky budeme psát pomocí šablon, ve kterých můžeme používat výrazy Jinja2 ve dvojitých složených závorkách. Šablony mohou být do sebe vnořeny, takže si uděláme např. jednu základní base.html, na základě které pak vytvoříme výchozí stránku index.html:
/app/templates/base.html
[editovat]<html>
<head>
<title>Naše aplikace</title>
</head>
<body>
<div>Aplikace: <a href="/index">Domů</a></div>
<hr>
{% block content %}{% endblock %}
</body>
</html>
/app/templates/index.html
[editovat]{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
03 - Formuláře
[editovat]- https://github.com/miguelgrinberg/microblog/tree/v0.3
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.3
- https://github.com/miguelgrinberg/microblog/compare/v0.2...v0.3
Introduction to Flask-WTF
[editovat]Configuration
[editovat]User Login Form
[editovat]Form Templates
[editovat]Naučíme se vyplňovat formuláře metodou POST v souboru login.html:
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form method="post" novalidate> <!-- validation will be done on the server side -->
{{ form.hidden_tag() }} <!-- protect the form against CSRF attacks, see SECRET_KEY -->
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Přitom dostávám chybu Method Not Allowed – The method is not allowed for the requested URL (chyba 405).
Co je to za chybu a jak jí čelit? Viz:
Rovněž dle w:Stavové kódy HTTP Pochopíme, že chyby se dělí m.j. na:
- 5xx – server error responses – například známá 500 Internal server error
- 4xx – client error responses – například 404 Page Not Found (je to chyba klienta, že chce něco, co není!)
O.K., dočetli jsme se, že odhalit chybu 405 je často dost obtížné – neodhalil jsem.
Někde na nějakém videu jsem si všiml, že v souboru routes.py při definici webové stránky, která používá jinou metodu, než POST, to musí být explicitně uvedeno. Tedy:
@app.route('/login', methods=['GET','POST'])
A vida! To je to, co nakonec pomohlo. Divím se, že Grinberg to tam neměl uvedeno. AHA! Má to tam uvedeno. Jenže kousek dále, než jsem dočetl. Dokonce i s tou chybou, která tak vznikne. Byl jsem zkrátka rychlejší...
Nicméně jsem přitom také odkoukal, že ten chlapík ten problém řeší v jakémsi prostředí:
- POSTMAN
- Postman Tutorial for Beginners with API Testing Example
- w:en: API testing
- Testing with Postman
- Test ONAP API with Postman
- ONAP
Zajímavé věci...
Form Views
[editovat]Receiving Form Data
[editovat]Vybrání dat z formuláře je jednoduché – vrací je metoda:
form.jméno_pole.data
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit(): # receives the form by POST
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect('/index')
return render_template('login.html', title='Sign In', form=form) # sends the form to the client – GET
V tomto oddílu se také naučíme používat funkci flash
, která kumuluje různé hlášky do svého seznamu, ke kterému je pak možno přistoupit (a zároveň jej vyprázdnit) funkcí
get_flashed_messages()
Zprávy si pak můžeme zobrazit např. HTML kódem:
{% with messages = get_flashed_messages() %}
{% if messages %}
<hr>
<i>Hlášky:</i>
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
<hr>
{% endif %}
{% endwith %}
04 - Databáze
[editovat]- https://github.com/miguelgrinberg/microblog/tree/v0.4
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.4
- https://github.com/miguelgrinberg/microblog/compare/v0.3...v0.4
Databases in Flask
[editovat]Databáze není do Flasku přímo integrovaná, což je dobře – můžeme si zde používat, co chceme – zkrátka použijeme vhodnou extensi.
Databázové systémy zhruba je rozdělujeme do dvou skupin:
SQL je častější, tak se zde budeme zaobírat s ní.
Použijeme dvě extense (ta druhá viz níže)
- Flask-SQLAlchemy = wrapper pro SQLAlchemy, což je ORM = Objektově relační mapování
sudo pip3 install flask-sqlalchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app) #inicializace databáze
Database Migrations
[editovat]Extense, která pomáhá řešit migraci databází:
- Flask-Migrate = wrapper pro Alembic = database migration framework pro SQLAlchemy
sudo pip3 install flask-migrate
from flask_migrate import Migrate
migrate = Migrate(app, db)
Flask-SQLAlchemy Configuration
[editovat]Začneme s SQLite:
config.py:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db') # Kde je databáze? Buď řečeno v environmentu anebo 'app.db' v kořenu naší aplikace
SQLALCHEMY_TRACK_MODIFICATIONS = False # zakážeme signál při každé změně databáze
Na začátku je zapotřebí tu databázi inicializovat – takže nejlépe to vše najednou učiníme v našem souboru app/__init__.py, který teď bude vypadat:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app) # objekt reprezentující databázi
migrate = Migrate(app, db) # objekt reprezentující migrační stroj (migration engine)
from app import routes, models # importuji také modul "models", který bude definovat strukturu databáze
Database Models
[editovat]Struktura databáze (schema)
databázový model = kolekce tříd
objekty těchto tříd ⟹ řádky tabulek
Použijeme WWW SQL Designer tool
Tabulka:
id | INTEGER | primary_key |
username | VARCHAR (64) | unique |
VARCHAR (120) | unique | |
password_hash | VARCHAR (128) |
Takovýto návrh přepíšeme do souboru app/models.py:
from app import db # app dostala db při inicializaci __init__.py
class User(db.Model): # třída User je inheritována ze základní třídy db.Model
id = db.Column(db.Integer, primary_key=True) # sloupce jsou instance třídy db.Column
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self): # metoda __repr__ určuje, jak se mají objekty tisknout:
return '<User {}>'.format(self.username)
Creating The Migration Repository
[editovat]Během vývoje aplikace se struktura databáze může měnit. Migrační framework Alembic (chladič, vytvářející křivuli – v alchymii) umožňuje provádět takové změny databázových schemat, aniž by se musela znovu vytvářet celá databáze.
Alembic udržuje svůj migrační repozitář, což je adresář, do kterého se ukládají migrační skripty.
Po každé změně schematu se sem přidává další skript.
Příkazy skriptu jsou příkazy flasku.
flask-migrate nám přidá příkaz flask db
Od dřívějška (Chapter 1) bychom měli mít nastavenu proměnnou prostředí:
export FLASK_APP naše_aplikace.py
a pak můžeme z příkazové řádky shellu zadat příkaz:
flask db init
což nám vytvoří adresář migration:
- /versions
- script.py.mako
- env.py
- README
- alembic.ini
The First Database Migration
[editovat]První databázová migrace, zahrnující mapování tabulky Users do databázového modelu User
– možnosti:
- manuálně
- automaticky: Alembic porovná schema definované v modelu s aktuální databází a spustí migrační skript
Dosud jsme žádnou předchozí databázi neměli, takže z příkazového řádku spustíme (argument -m přidá komentář):
flask db migrate -m "users table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' Generating /.../04a-Database/migrations/versions/c7329ba56057_users_table.py ... done
A vidíme malý zázrak. Krátce zrekapitulujeme:
- V souboru /app/models.py jsme si definovali náš databázový model
- Alembic nám vytvořil soubor
/migrations/versions/c7329ba56057_users_table.py
, což je pythonovský skript, obsahující definice dvou funkcí, jejichž význam je jasný:- def upgrade():
- def downgrade():
- Alembic nám vytvořil SQLite databázový soubor
/app.db
Předpokládám, že máme na linuxu nainstalováno:
sudo apt-get install sqlite3
sudo apt-get install sqlitebrowser
Podíváme se na tu databázi /app.db
grafickým prohlížečem databáze:
sqlitebrowser app.db &
V případě, že není sqlitebrowser nainstalovaný, spustíme:
sqlite3 app.db
a pak můžeme spouštět příkazy jako:
sqlite> .help sqlite> .show sqlite> .databases sqlite> .tables sqlite> .fullschema sqlite> select * from user;
Vidíme ale, že tam zatím máme jen jednu prázdnou tabulku alembic_version
–
protože samotný příkaz
Abychom v té databázi vytvořili naše schema, musíme spustit příkaz:
flask db upgrade
V Browseru pro SQLite si můžu ověřit, že databáze byla aktualizována.
Database Upgrade and Downgrade Workflow
[editovat]Database Relationships
[editovat]Uděláme si další tabulku:
id | INTEGER | primary_key |
body | VARCHAR (140) | |
timestamp | DATETIME | |
user_id | INTEGER | foreign key |
Tato relace se nazývá one-to-many, protože jeden uživatel může napsat více zpráv.
Druhou tabulku vytvoříme v souboru app/models.py podobně, jako tu první:
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # argument 'default' dostane samotnou funkci, nikoli funkční hodnotu
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) # tento klíč je 'id' z tabulky 'user'
def __repr__(self):
return '<Post {}>'.format(self.body)
Do první tabulky user
ještě doplníme řádek, popisující nové pole:
posts = db.relationship('Post', backref='author', lazy='dynamic')
a na začátek souboru nezapomenu doplnit:
from datetime import datetime
Teď provedu novou migraci databáze a po ní upgrade:
flask db migrate -m "posts table"
flask db upgrade
Play Time
[editovat]Hrajeme si s databází v příkazové řádce pythonu.
>>> from app import db
>>> from app.models import User, Post
A dál si zkoušíme různé další příkazy:
u = User(username='john', email='john@example.com')
db.session.add(u)
db.session.commit()
users = User.query.all()
users
for u in users:
print(u.id, u.username)
u = User.query.get(1)
... atd.
users = User.query.all()
for u in users:
db.session.delete(u)
db.session.commit()
Shell Context
[editovat]Nemusím z příkazové řádky shellu volat python3
, ale můžu rovnou zavolat:
flask shell
a pak nemusím znovu importovat db
a ty další věci.
Vytvoříme si ještě shell context, který mi pre-importuje i další věci – instanci databáze a modely. Přidáme to do hlavního souboru microblog.py, takže bude vypadat:
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
Dekorátor app.shell_context_processor
teď registruje tuhle funkci jakožto kontextovou funkci flaskového shellu:
Jakmíle ze shellu operačního systému spustíme příkaz flask shell
, tak invokuje tuto funkci, která vrací slovník (dictionary).
Takže teď spustíme
flask shell
a můžeme klást takové dotazy, jako:
db
User
Post
V případě, že bychom dostali chybové výjimky, znamená to, že ta funkce make_shell_context()
nebyla Flaskem registrována.
Nejčastější chybou je, že v systémovém shellu nemáme exportovanou proměnnou FLASK_APP
(jak jsme popisovali v kapitole Flask/Grinberg/01 - Nazdárek!#A "Hello, World" Flask Application):
export FLASK_APP=mojeaplikace.py
05 - Zalogování
[editovat]- https://github.com/miguelgrinberg/microblog/tree/v0.5
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.5
- https://github.com/miguelgrinberg/microblog/compare/v0.4...v0.5
Password Hashing
[editovat]Vytvoření a verifikace hashe – zkusíme si to nejdříve v pythonovském shellu:
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> hash = generate_password_hash('brekeke')
>>> hash
>>> check_password_hash (hash, 'brekeke')
>>> check_password_hash (hash, 'brekek')
Introduction to Flask-Login
[editovat]sudo pip3 install flask-login
(Naštěstí na pythonanywhere je nainstalován: pythonanywhere.com/batteries_included/)
Tato flasková extense obhospodařuje stav uživatelů, kteří jsou zalogováni, včetně funkce remember me, která umožňuje udržovat uživatele zalogovaného i když si zavře okno browseru.
Stejně jako jiné extense ji inicializujeme v souboru app/__init__.py:
# …
from flask_login import LoginManager
# …
app = Flask(__name__)
# …
login = LoginManager(app)
Preparing The User Model for Flask-Login
[editovat]Extense Flask-Login potřebuje tři vlastnosti (property) a jednu metodu:
is_authenticated
: property je:True
= uživatel se zalogoval se správnými kredenciály (loginname, password)False
= nikoli
is_active
: property je:True
= uživatelský účet (account) je aktivníFalse
= nikoli
is_anonymous
: property je:False
= uživatel je řádným uživatelemTrue
= speciální anonymní uživatel
get_id()
: metoda, která vrací jednoznačný identifikátor uživatele jako string
Tyto čtyři věci se implementují celkem snadno, nicméně Flask-Login poskytuje třídu mixin, která v sobě už zahrnuje generickou implementaci, vyhovující většině běžných požadavků – takže stačí do souboru app/models.py přidat:
# …
from flask_login import UserMixin
class User(UserMixin, db.Model):
# …
User Loader Function
[editovat]user session = prostor, kam Flask-Login ukládá stopu zalogovaných uživatelů (jejich jedinečný identifikátor) – jaké stránky ten uživatel navštíví.
Protože Flask-Login neví nic o (použitých) databázích a modelech, tak si aplikace musí nakonfigurovat funkci user_loader, a to v modulu app/models.py:
from app import login
# …
@login.user_loader
def load_user(id):
return User.query.get(int(id))
Funkci user_loader jsme zaregistrovali dekorátorem @login.user_loader
.
V databázi je uživatel uložený jako numerické ID a my jej chceme konvertovat na jméno uživatele.
Logging Users In
[editovat]Nyní již máme přístup k databázi a tím pádem můžeme zkompletovat naši view-funkci v souboru app/routes.py:
# app/routes.py: Login view function logic
# …
from flask_login import current_user, login_user
from app.models import User
# …
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated: # kdyby zalogovaný user šel znovu na login,
return redirect(url_for('index')) # pošleme ho na index
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first() # filtrem vyhledám usera dle username; first() vrací None jestliže neexistuje
if user is None or not user.check_password(form.password.data): # Když se mu nepovedlo zalogovat …
flash('Invalid username or password')
return redirect(url_for('login')) # … znovu na login
login_user(user, remember=form.remember_me.data) # login_user() je fce z Flask-Login: zaregistruje zalogovaného usera, nastaví current_user
return redirect(url_for('index')) # vrátíme se na index
return render_template('login.html', title='Sign In', form=form)
Logging Users Out
[editovat]Na to má Flask-Login funkci logout_user()
, kterou velmi jednoduše použijeme opět v souboru app/routes.py:
# app/routes.py: Logout view function
# …
from flask_login import logout_user
# …
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
Aby uživatel ten link viděl, tak mu ho přidáme do navigačního řádku hned po tom, co se zaloguje.
Soubor app/templates/base.html:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
Vlastnost (property) is_anonymous
je jedním z atributů, které Flask-Login přidává objektu user skrzevá třídu UserMixin
.
Výraz current_user.is_anonymous
je True
pouze v případě, že uživatel není zalogován.
Requiring Users To Login
[editovat]Některé stránky (protected pages) můžeme ochránit heslem a Flask-Login podporuje možnost, že pro jejich prohlížení pořádá uživatele o přihlášení.
Flask-Login potřebuje vědět, jaká view-funkce zařizuje login, tak mu to řekneme hned na začátku v souboru app/__init__.py:
# …
login = LoginManager(app)
login.login_view = 'login' # 'login' = jméno té funkce, která dělá login_view neboli to samé jméno, které také použijeme jako argument url_for()
Flask-Login ochrání stránky před anonymním přístupem pomocí dekorátoru @login_required
.
Když ten dekorátor umístíme pod jiné dekorátory, tak ta následná view-funkce bude ochráněna a příslušné stránky budou vyžadovat zalogování.
V následujícím příkladu ochráníme již hlavní stránku aplikace v souboru app/routes.py:
#app/routes.py: @login_required decorator
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# …
Po úspěšném zalogování ještě zbývá nasměrovat uživatele na tu stránku, ke které se předtím pokoušel přistoupit. To se udělá tak, že se do URL přidá query string (tj. string s otazníkem), takže v URL se nám pak ukáže:
…/login?next=/puvodni_stranka_na_kterou_chtel_uzivatel_jit
To lomítko ale bude zaencodované %2F, takže ve skutečnosti uvidíme:
…/login?next=%2Fpuvodni_stranka_na_kterou_chtel_uzivatel_jit
app/routes.py pak bude vypadat:
# app/routes.py: Redirect to "next" page
from flask import request # z flasku importujeme request
from werkzeug.urls import url_parse # z werkzeugu importujeme url_parse: podle netloc nám určí, jestli je URL absolutní nebo relativní
@app.route('/login', methods=['GET', 'POST'])
def login():
# …
if form.validate_on_submit(): # atd … už jsme vysvětlovali v sekci 'Logging Users In'
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data) # user se zaloguje
next_page = request.args.get('next') # var. request obsahuje request clienta, request.args je slovník. next_page = další stránka za 'next'
if not next_page or url_parse(next_page).netloc != '': # next_page není anebo obsahuje kompletní URL včetně domain name (= může být pokus o attack!)
next_page = url_for('index') # … jdi normálně na hlavní stránku
return redirect(next_page) # jdi na next_page
# …
Showing The Logged In User in Templates
[editovat]Namísto fake-usera (kapitola 02) už můžeme v šabloně app/templates/index.html použít skutečného uživatele:
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
A naopak vyndáme user template argument (tj. argument user=user) z view-funkce v souboru app/routes.py:
# app/routes.py: Do not pass user to template anymore
@app.route('/')
@app.route('/index')
def index():
# …
return render_template("index.html", title='Home Page', posts=posts)
Teď si můžeme vyzkoušet, jak nám to funguje. Protože ještě nemáme udělanou registraci uživatele, vyzkoušíme si to z flask shell
:
>>> u = User(username='Kychot', email='kychot@example.com')
>>> u.set_password('kyky')
>>> db.session.add(u)
>>> db.session.commit()
Anebo si takový testovací kousek programu, který nám bude vkládat nějaký záznam do databáze, můžeme dočasně strčit na konec souboru models.py
# TEST:
password_hash = generate_password_hash('kyky')
u = User(username='Kychot', email='kychot@example.com', password_hash = password_hash)
db.session.add(u)
db.session.commit()
User Registration
[editovat]Začneme tím, že si vytvoříme třídu RegistrationForm
v souboru app/forms.py:
# app/forms.py: User registration form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User
# …
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()]) # nový validátor Email() ⇐ WTForms
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField( # heslo napsat dvakrát – pro kontrolu
'Repeat Password', validators=[DataRequired(), EqualTo('password')]) # nový validátor EqualTo()
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
Těm validátorům, které použijeme z WTForms, se říká stock, jako že jsou na skladě.
Mimo nich si ale můžeme vytvořit vlastní validátory – zkrátka když si vytvoříme nějakou metodu, kterou nazveme validate_<field_name>
,
tak to WTForms bude brát jako uživatelský validátor vyvolá ho stejně, jako ostatní validátory, které má „skladem”.
Aby se nám ten registrační formulář zobrazil, musíme si pro něj vytvořit registrační šablonu app/templates/register.html:
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Do souboru app/templates/login.html potřebujeme ještě doplnit link, který pošle uživatele na registrační formulář:
<p>Nový uživatel? <a href="{{ url_for('register') }}">Zaregistruj se!</a></p>
A nakonec musíme napsat view-funkci, která bude tu registraci uživatele dělat (tj. nakonec ho přidá do databáze) – dáme ji do našeho souboru app/routes.py:
from app import db
from app.forms import RegistrationForm
# …
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated: # pokud je už uživatel zalogovaný, nebude se registrovat
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit(): # vše v pořádku, uživatele uložíme do databáze
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Gratulujeme, jsi naším registrovaným uživatelem!')
return redirect(url_for('login')) # Nově vytvořený uživatel se může rovnou zalogovat
return render_template('register.html', title='Register', form=form)
Flask/Grinberg/06 - Uživatelská stránka a avatary
Flask/Grinberg/07 - Ošetření chyb
Flask/Grinberg/09 - Stránkování
Flask/Grinberg/10 - Podpora e-mailů
Flask/Grinberg/12 - Datum a čas
Flask/Grinberg/13 - I18n and L10n
Flask/Grinberg/15 - Lepší struktura aplikací
Flask/Grinberg/16 - Fulltextové vyhledávání
Flask/Grinberg/17 - Nasazení na Linuxu
Flask/Grinberg/18 - Nasazení na Heroku
Flask/Grinberg/19 - Nasazení na Docker Containers
Flask/Grinberg/20 - Trocha JavaScriptového čarování
Flask/Grinberg/21 - Notifice uživatelů
Flask/Grinberg/22 - Úlohy na pozadí
Flask/Grinberg/23 - Application Programming Interfaces (APIs)