Workshop
Flask, Python
Matúš Revický
Ústav informatiky, PF UPJŠ
06.10.2020
Trvanie 135 min
PrezentáciaPrerekvizity
Inštalácia Python-u
Na to, aby sme začali vyvíjať aplikáciu s použitím Frameworku Flask je najprv potrebné nainštalovať najnovšiu verziu pythonu (v čase písania 3.8.5). V prípade Windowsu si môžete stiahnuť inštalačný program z https://www.python.org/downloads/. Ak to bude možne, tak treba počas inštalácie zaškrtnúť políčko pri add Python3.x to Path (ak sa táto možnosť nenachádza nevadí, python sa dá pridať do system variables aj manuálne)
Inštalácia VS code
Pracovať môžete v ľubovoľnom IDE alebo iba textovom editore. Môžete si stiahnuť inštalačný program z https://code.visualstudio.com/. Inštaláciou sa stačí preklikať.
Príprava venv
Na riešenie problému udržiavania rôznych verzií balíkov pre rôzne aplikácie používa Python koncept virtuálnych prostredí. Virtuálne prostredie je úplnou kópiou Python interpretera. Riešením úplnej slobody inštalácie akýchkoľvek verzií vašich balíkov pre každú aplikáciu je teda použitie iného virtuálneho prostredia pre každú aplikáciu. Virtuálne prostredia majú ďalšiu výhodu v tom, že ich vlastní používateľ, ktorý ich vytvára, takže nevyžadujú admin účet.
Je potrebné vytvoriť adresár, kde budeme projekt vyvíjať.
mkdir micrometeo
cd micrometeo
Aktuálna verzia pythonu sa zistí takto
python -V
Ak používate verziu Pythonu 3.4.x a vyššie, je v nej zahrnutá podpora virtuálneho prostredia, takže všetko, čo musíte urobiť, aby ste ju vytvorili, je toto (v prípade problémov https://phoenixnap.com/kb/how-to-install-python-3-windows):
python -m venv venv
Vo Windowse sa dostaneme do venv nasledovným príkazom
venv\Scripts\activate
Ak sme sa úspešne dostali do venv v konzole uvidíme
(venv) PS C:\Users\user\micrometeo>
Inštalácia Flask vo venv
(venv)$ pip install flask
"Hello, World" Flask
Flask neobsahuje nástroj, ktorý vygeneruje štruktúru projektu za nás. Na "Hello world" aplikáciu potrebujeme vytvoriť nasledujúce súbory
app/__init__.py
from flask import Flask
app = Flask(__name__)
from app import routes
app/routes.py
from app import app
@app.route('/')
@app.route('/index')
def index():
return "Hello, World!"
micrometeo.py
from app import app
Na to, aby flask vedel, čo má spustiť treba ešte nastaviť premennú prostredia (environment variable) FLASK_APP=micrometeo.py;
Aby sme nemuseli stále pred štartom ručne nastavovať premennú prostredia
(venv) $ export FLASK_APP=microblog.py
doinštalujeme ešte jeden balíček
(venv) $ pip install python-dotenv
.flaskenv
FLASK_APP=micrometeo.py
Aplikáciu spustíme nasledovným príkazom.
(venv) $ flask run
Templates
Príklad ako by vyzerala aplikácia bez použitia templates
app/routes.py
@app.route('/index')
def index():
user = {'username': 'SPS1'}
return '''
<html>
<head>
<title>Home Page - Micrometeo</title>
</head>
<body>
<h1>Hello, ''' + user['username'] + '''!</h1>
</body>
</html>'''
Preto je výhodné použiť templaty, ktoré zabezpečia, že prezentačná logika bude oddelená od business logiky a aplikácia bude jednoduchšia na údržbu. Pre templaty vytvoríme nový priečinok
(venv) $ mkdir app/templates
app/templates/index.html
<html>
<head>
<title>{{ title }} - Micrometeo</title>
</head>
<body>
<h1>Hello, {{ user.username }}!</h1>
</body>
</html>
View funkcia sa teda môže zjednoduchšiť
app/routes.py
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'SPS1'}
return render_template('index.html', title='Home', user=user)
Templates majú aj podporu pre if, for
Templates dedičnosť
app/templates/base.html
<html>
<head>
{% if title %}
<title>{{ title }} - Micrometeo</title>
{% else %}
<title>Welcome to Micrometeo</title>
{% endif %}
</head>
<body>
<div>Micrometeo: <a href="/index">Home</a></div>
<hr>
{% block content %}{% endblock %}
</body>
</html>
app/templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ user.username }}!</h1>
{% endblock %}
Úloha 1.
Vyskúšať spraviť pole nejakých objektov a vypísať jeho obsah s použitím
{% for xx in xxx %} {% endfor %}
index.html
Web formuláre
Flask-WTF
(venv) $ pip install flask-wtf
Konfigurácia
Rozšírenia Flasku je potrebné konfigurovať. Existuje viacero spôsobov ako aplikáciu
konfigurovať. Najzákladnejším riešením je definovať svoje premenné ako kľúče v app.config.
Napr. app.config['SECRET_KEY'] = 'you-will-never-guess'
To by však znamenalo, že konfigurácia je priamo v aplikácií a my by sme ju chceli mať oddelene.
config.py
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
Flask a niektoré jeho rozšírenia používajú SECRET_KEY ako kryptografický kľúč, ktorý je užitočný na generovanie podpisov alebo tokenov. Zabraňuje Cross-Site Request Forgery
app/__init__.py
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
Login formulár
Rozšírenie Flask-WTF používa na reprezentáciu webových formulárov triedy Python. Trieda formulára jednoducho definuje polia formulára ako premenné triedy.
app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
Atribút novalidate sa používa na to, aby informoval webový prehľadávač, aby nevalidoval polia v tomto formulári, ale nechal to na Flask. form.hidden_tag () generuje skryté pole, ktoré obsahuje token, ktorý sa používa na ochranu formulára pred útokmi CSRF.
app/templates/login.html
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<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 %}
form.validate_on_submit () vykonáva všetku prácu so spracovaním formulárov. flash() je užitočný spôsob, ako zobraziť správu používateľovi.
app/routes.py
from flask import render_template, flash, redirect, url_for
from app import app
from app.forms import LoginForm
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'SPS1'}
return render_template('index.html', title='Home', user=user)
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
app/templates/base.html
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
Úloha 2.
Pridať validáciu do login.html (inšpirácia https://flask.palletsprojects.com/en/1.1.x/patterns/wtforms/) (inak povedané vypísať errory pre jednotlivé polia formulára)
Databázy
Flask natívne nepodporuje databázy. Programátor má slobodu zvoliť si databázu, ktorá najlepšie vyhovuje jeho aplikácii, namiesto toho, aby bol nútení prispôsobiť sa jednej. V tejto aplikácií si ukážeme prácu s Object Relational Mapper-om SQLAlchemy. Slúži pre viaceré datábazy medzi nimi aj MySQL, PostgreSQL and SQLite. Výhoda spočiva v tom, že počas vývoja sa môže použiť SQlite databáza, ktorá nevyžaduje žiaden server. V prípade ak sa už aplikácia ide nasadiť na produkciu tak sa dá bez zmien v aplikácii použiť robustnejší PostgreSQL
Príkaz na inštaláciu Flask-SQLAlchemy
(venv) $ pip install flask-sqlalchemy
Migrácia databázy
Pri relačných databázach sa pri zmene štruktúry modelu musia upraviť všetky existujúce dáta. Na toto je možné použiť nástroj Flask-Migrate. Je to framework na migráciu pre SQLAlchemy.
Príkaz na inštaláciu Flask-Migrate
(venv) $ pip install flask-migrate
Flask-SQLAlchemy konfigurácia
config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
app/__init__.py
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)
migrate = Migrate(app, db)
from app import routes, models
Databázové modely
Keďže sa v tejto aplikácií používame SQLAlchemy tak na to, aby sme získali v databáze tabuľku "users" nám stačí vytvoriť v pythone nasledujúci objekt
app/models.py
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
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):
return ''.format(self.username)
Objekty sa vytvárajú v nasledujúcom štýle:
u = User(username='name', email='name@example.com')
Vytvorenie Migračného repozitára
(venv) $ flask db init
(venv) $ flask db migrate -m "users table"
(venv) $ flask db upgrade
V prípade ak sa používa SQLite databáza, tak príkaz Upgrade v prípade ak neexistuje databáza ju aj vytvorí.
app/models.py
from datetime import datetime
from app import app, db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
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):
return ''.format(self.username)
class City(db.Model):
id = db.Column(db.Integer, primary_key=True)
city = db.Column(db.String(140), index=True)
country_code = db.Column(db.String(5), index=True)
lon = db.Column(db.Float)
lat = db.Column(db.Float)
users = db.relationship('User', backref='city', secondary='user_city', lazy='dynamic')
def __repr__(self):
return ''.format(self.city)
user_city = db.Table('user_city',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('city_id', db.Integer, db.ForeignKey('city.id'), primary_key=True)
)
V modeli boli zmeny, teda je potrebné aktualizovať databázu
(venv) $ flask db migrate -m "cities"
(venv) $ flask db upgrade
Prihlasovanie užívateľov
V databáze už máme tabuľku user, ktorá obsahuje stĺpec password_hash
app/models.py
from datetime import datetime
from app import app, db
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
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))
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
cities = db.relationship('City', backref='user', secondary='user_city', lazy='dynamic')
def __repr__(self):
return ''.format(self.username)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class City(db.Model):
id = db.Column(db.Integer, primary_key=True)
city = db.Column(db.String(140), index=True)
country_code = db.Column(db.String(5), index=True)
lon = db.Column(db.Float)
lat = db.Column(db.Float)
users = db.relationship('User', backref='city', secondary='user_city', lazy='dynamic')
def __repr__(self):
return ''.format(self.city)
user_city = db.Table('user_city',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('city_id', db.Integer, db.ForeignKey('city.id'), primary_key=True)
)
Pre jednoduchšiu prácu s prihlásením použijeme flask-login
(venv) $ pip install flask-login
app/__init__.py
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app)
from app import routes, models
Príprava user modelu pre Flask-login
Rozšírenie Flask-Login pracuje s user modelom a očakáva, že user model implementuje určité
vlastnosti a metódy. Flask-Login poskytuje triedu mixin nazvanú UserMixin
is_authenticated, is_active, is_anonymous, get_id()
app/models.py
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
Flask-Login sleduje prihláseného používateľa ukladaním jeho jedinečného identifikátora do "Flask user session", čo je úložný priestor pridelený každému používateľovi, ktorý sa pripojí k aplikácii. Zakaždým, keď prihlásený používateľ prejde na novú stránku, Flask-Login získa z relácie ID používateľa a potom ho načíta do pamäte.
Stačí použiť dekorátor @login.user_loader
app/models.py
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
app/routes.py
# ...
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:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
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)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
app/routes.py
# ...
from flask_login import logout_user
# ...
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
app/templates/base.html
# ...
<div>
Micrometeo:
<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>
# ...
Vyžadovanie prihlásenia používateľov
Aby bola táto funkcionalita implementovaná, musí Flask-Login vedieť, čo je view funkcia,
ktorá spracováva prihlásenie. Toto je možné pridať v aplikácii __ init__.py
# ...
login = LoginManager(app)
login.login_view = 'login'
Flask-Login chráni view funkciu pred anonymnými používateľmi pomocou dekorátora s názvom
@login_required
app/routes.py
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
Do login funkcie je ešte potrebné doplniť funkcionalitu, ktorá zabezpeči, že po tom ako bol neprihlásený uživateľ požiadaný o prihlásenie tak sa po prihlásení automaticky presmeruje na požadovanú stránku
app/routes.py
from flask import request
from werkzeug.urls import url_parse
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
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)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
Zobrazenie prihláseného používateľa v templates
app/templates/index.html
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% endblock %}
V app/routes.py sa môže odobrať parameter "user"
app/routes.py
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
return render_template("index.html", title='Home Page')
Registrácia
V tomto bode treba vytvoriť registračný formulár, registračný template, pridať odkaz do navigačného menu a vytvoriť funkciu, ktorá spracuje registráciu.
Validátor Email () z WTForms vyžaduje inštaláciu externej závislosti.
(venv) $ pip install email-validator
app/forms.py
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()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
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.')
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 %}
app/templates/login.html
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ 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.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
{% endblock %}
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:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
Úloha 3.
Umožniť uživateľovi editovať profil (Vytvoriť formulár forms.py class EditProfileForm(FlaskForm), def edit_profile() v routes.py, edit_profile.html )
Odporúčaný postup riešenia:
app/forms.py
### doplnte nový formulár (podobne ako loginform), about_me typu TextAreaField (nezabudnut na import TextAreaField, wtforms.validators Length)
app/models.py
### doplnte about_me stlpec (typu string), netreba zabudnúť na migráciu a upgrade databázy po úprave modelu
app/routes.py
### importujte formulár,
@app.route('/user/<username>')
@login_required
def user(username):
### treba získať usera z databázy, podobne ako v login
return render_template('user.html', user=user)
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.username = ...ziskat username z formulara
current_user.about_me = ...ziskat about_me z formulara
db.session.commit()
flash('Your changes have been saved.')
return redirect(url_for('edit_profile'))
elif request.method == 'GET':
form.username.data = ...ziskat username aktualneho uzivatela
form.about_me.data = ...ziskat about_me aktualneho uzivatela
return render_template('edit_profile.html', title='Edit Profile',form=form)
app/templates/base.html
...
<a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a>
...
app/templates/edit_profile.html
{% extends "base.html" %}
{% block content %}
<h1>Edit Profile</h1>
<form action="" method="post">
### doplniť možnosť editácie username a about_me (veľmi podobné ako login.html)
</form>
{% endblock %}
app/templates/user.html
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td>
<h1>User: {{ user.username }}</h1>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% endif %}
</td>
</tr>
</table>
<hr>
{% endblock %}