Přeskočit na obsah

Flask/Grinberg/01-23

Z Wikiverzity
Jak používat klasifikační nálepkuTato 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:

  1. na liště klikneme na 24 releases
  2. jsou označené v0.0v0.23 – to jsou odpovídající čísla kapitol
  3. 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:

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

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]

Chapter 3: Web Forms

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

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]

Chapter 4: Database

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)

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

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:

user
id INTEGER primary_key
username VARCHAR (64) unique
email 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:

  1. V souboru /app/models.py jsme si definovali náš databázový model
  2. 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ý:
    1. def upgrade():
    2. def downgrade():
  3. 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:

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

Chapter 5: User Logins

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živatelem
    • True = 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/08 - Followers

Flask/Grinberg/09 - Stránkování

Flask/Grinberg/10 - Podpora e-mailů

Flask/Grinberg/11 - Facelift

Flask/Grinberg/12 - Datum a čas

Flask/Grinberg/13 - I18n and L10n

Flask/Grinberg/14 - Ajax

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)