Workshop

Flask, Python


Matúš Revický

Ústav informatiky, PF UPJŠ


06.10.2020

Trvanie 135 min

Prezentácia

Prerekvizity

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

Niečo na záver

Ukážka aplikácie vytvorenej vo Flasku

Ďakujem za pozornosť