← Strona główna

14 — Aplikacja webowa

Flask — podstawy, routing, formularze, szablony Jinja2, prosty CRUD.

Przetwarzanie i algorytmy

1. Czym jest Flask i jak go uruchomić?

Flask to mikro-framework webowy dla Pythona — lekki, prosty i wystarczający do zadań egzaminacyjnych. Pozwala budować aplikacje webowe w czystym Pythonie.

  • Instalacja: pip install flask
  • Serwer uruchamiany lokalnie — dostępny pod http://127.0.0.1:5000
  • Tryb debug: debug=True — automatyczne przeładowanie po zmianach
  • Routing — przypisanie URL do funkcji Pythona
  • Szablony Jinja2 — HTML z wbudowaną logiką Pythona
  • Pliki statyczne — CSS, JS, obrazy w folderze static/
Struktura projektu Flask app.py — główny plik aplikacji
templates/ — pliki HTML (szablony Jinja2)
static/ — pliki CSS, JS, obrazy
static/css/style.css — arkusz stylów
Minimalny program Flask — Hello World
# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Witaj w Flasku!'

@app.route('/hello/')
def hello(name):
    return f'Cześć, {name}!'

if __name__ == '__main__':
    app.run(debug=True)

# Uruchomienie:
# python app.py
# → http://127.0.0.1:5000/
# → http://127.0.0.1:5000/hello/Kasia

2. Routing — URL do funkcji

Routing to mechanizm łączenia adresów URL z funkcjami Pythona. Dekorator @app.route() rejestruje trasę.

ZapisZnaczenie
@app.route('/')strona główna
@app.route('/about')stały URL
@app.route('/user/<name>')parametr jako string
@app.route('/post/<int:id>')parametr jako int
methods=['GET','POST']obsługa formularzy
GET vs POST GET — dane w URL, używaj do wyświetlania. POST — dane w ciele żądania, używaj do formularzy z danymi (hasła, dane osobowe). Domyślnie Flask obsługuje tylko GET.
Przykład — routing z parametrami
from flask import Flask, redirect, url_for

app = Flask(__name__)

@app.route('/')
def index():
    return '

Strona główna

' # Parametr tekstowy @app.route('/user/') def profil(username): return f'

Profil użytkownika: {username}

' # Parametr liczbowy @app.route('/post/') def wpis(post_id): return f'

Wpis numer: {post_id}

' # Przekierowanie @app.route('/stara-strona') def stara(): return redirect(url_for('index')) # Kilka URL dla jednej funkcji @app.route('/info') @app.route('/o-nas') def o_nas(): return '

Informacje o nas

' if __name__ == '__main__': app.run(debug=True)

3. Szablony Jinja2 — HTML z logiką

Szablony to pliki HTML z wbudowaną logiką Pythona. Przechowujemy je w folderze templates/.

Składnia Jinja2Znaczenie
{{ zmienna }}wyświetl wartość zmiennej
{% if warunek %}instrukcja warunkowa
{% for x in lista %}pętla
{% extends "base.html" %}dziedziczenie szablonu
{% block nazwa %}blok do nadpisania
{% include "plik.html" %}dołączenie pliku
{{ zmienna|upper }}filtr — wielkie litery
render_template() return render_template('index.html', dane=dane) — renderuje szablon i przekazuje zmienne. W szablonie dostępne są jako {{ dane }}.
Przykład — szablony Jinja2
# templates/base.html
"""
<!DOCTYPE html>
<html lang="pl">
<head>
  <title>{% block title %}Moja Aplikacja{% endblock %}</title>
</head>
<body>
  <nav><a href="/">Strona główna</a></nav>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>
"""

# templates/lista.html
"""
{% extends "base.html" %}
{% block title %}Lista uczniów{% endblock %}
{% block content %}
  <h1>Uczniowie</h1>
  {% if uczniowie %}
    <ul>
    {% for u in uczniowie %}
      <li>{{ u.imie }} — {{ u.wynik }} pkt</li>
    {% endfor %}
    </ul>
  {% else %}
    <p>Brak uczniów.</p>
  {% endif %}
{% endblock %}
"""

# app.py
from flask import Flask, render_template
app = Flask(__name__)

@app.route('/lista')
def lista():
    uczniowie = [
        {"imie": "Kasia", "wynik": 95},
        {"imie": "Marek", "wynik": 87},
    ]
    return render_template('lista.html',
                           uczniowie=uczniowie)

4. Formularze — GET i POST

Formularze HTML pozwalają użytkownikowi wysyłać dane do serwera. Flask odbiera je przez obiekt request.

  • from flask import request — obiekt żądania HTTP
  • request.method — "GET" lub "POST"
  • request.form['pole'] — dane z formularza POST
  • request.args['param'] — parametry z URL (GET)
  • request.form.get('pole', domyslna) — bezpieczny odczyt
Wzorzec POST/Redirect/GET Po obsłudze formularza POST — przekieruj przez redirect(url_for(...)). Bez tego odświeżenie strony ponownie wyśle formularz (duplikaty danych).
Przykład — formularz z obsługą
from flask import Flask, render_template
from flask import request, redirect, url_for

app = Flask(__name__)

# Dane w pamięci (na egzaminie wystarczy lista/słownik)
notatki = []

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        tresc = request.form.get('tresc', '').strip()
        if tresc:
            notatki.append(tresc)
        return redirect(url_for('index'))

    return render_template('index.html',
                           notatki=notatki)

# templates/index.html
"""
{% extends "base.html" %}
{% block content %}
<h1>Moje notatki</h1>

<form method="POST">
  <input type="text" name="tresc"
         placeholder="Nowa notatka...">
  <button type="submit">Dodaj</button>
</form>

<ul>
{% for notatka in notatki %}
  <li>{{ notatka }}</li>
{% endfor %}
</ul>
{% endblock %}
"""

5. Prosty CRUD — tworzenie, odczyt, edycja, usuwanie

CRUD (Create, Read, Update, Delete) to cztery podstawowe operacje na danych. Na egzaminie INF.04 wystarczy prosta lista lub słownik w pamięci — bez bazy danych.

  • Create — formularz POST, lista.append()
  • Read — GET, render_template(dane=lista)
  • Update — formularz z id, modyfikacja listy po indeksie
  • Delete — link lub formularz z id, lista.pop(id)
ID w liście Używaj indeksu listy jako ID. Przy usuwaniu pamiętaj że indeksy się przesuwają — usuwaj od końca lub przepisuj listę. Alternatywa: słownik z kluczem będącym licznikiem.
Przykład — pełny CRUD na liście uczniów
from flask import (Flask, render_template,
                   request, redirect, url_for)
app = Flask(__name__)

uczniowie = [
    {"id": 1, "imie": "Kasia", "wynik": 95},
    {"id": 2, "imie": "Marek", "wynik": 87},
]
next_id = 3

@app.route('/')
def index():
    return render_template('crud.html',
                           uczniowie=uczniowie)

@app.route('/dodaj', methods=['POST'])
def dodaj():
    global next_id
    imie  = request.form.get('imie', '').strip()
    wynik = int(request.form.get('wynik', 0))
    if imie:
        uczniowie.append({"id": next_id,
                          "imie": imie,
                          "wynik": wynik})
        next_id += 1
    return redirect(url_for('index'))

@app.route('/usun/')
def usun(uid):
    global uczniowie
    uczniowie = [u for u in uczniowie
                 if u['id'] != uid]
    return redirect(url_for('index'))

@app.route('/edytuj/',
           methods=['GET', 'POST'])
def edytuj(uid):
    u = next((x for x in uczniowie
               if x['id'] == uid), None)
    if not u:
        return redirect(url_for('index'))
    if request.method == 'POST':
        u['imie']  = request.form.get('imie', u['imie'])
        u['wynik'] = int(request.form.get('wynik', u['wynik']))
        return redirect(url_for('index'))
    return render_template('edytuj.html', u=u)

6. Szablony HTML dla aplikacji CRUD

Poniżej kompletne szablony HTML potrzebne do działającego CRUD. Zapisz je w folderze templates/.

  • base.html — szkielet strony z nawigacją
  • crud.html — lista + formularz dodawania
  • edytuj.html — formularz edycji
  • Stylowanie: static/css/style.css
url_for() w szablonach Zamiast wpisywać URL ręcznie, używaj {{ url_for('nazwa_funkcji') }}. Gdy zmienisz routing — linki w szablonach automatycznie się zaktualizują.
Szablony — base.html i crud.html
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}Aplikacja{% endblock %}</title>
  <link rel="stylesheet"
        href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
  <header>
    <a href="{{ url_for('index') }}">Strona główna</a>
  </header>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>

<!-- templates/crud.html -->
{% extends "base.html" %}
{% block title %}Lista uczniów{% endblock %}
{% block content %}
<h1>Uczniowie</h1>

<form action="/dodaj" method="POST">
  <input type="text" name="imie" placeholder="Imię" required>
  <input type="number" name="wynik"
         placeholder="Wynik" min="0" max="100" required>
  <button type="submit">Dodaj</button>
</form>

<table>
  <tr><th>ID</th><th>Imię</th>
      <th>Wynik</th><th>Akcje</th></tr>
  {% for u in uczniowie %}
  <tr>
    <td>{{ u.id }}</td>
    <td>{{ u.imie }}</td>
    <td>{{ u.wynik }}</td>
    <td>
      <a href="/edytuj/{{ u.id }}">Edytuj</a>
      <a href="/usun/{{ u.id }}">Usuń</a>
    </td>
  </tr>
  {% endfor %}
</table>
{% endblock %}

Zadania przykładowe z omówieniem

Zadanie 1 Kalkulator webowy łatwe

Napisz aplikację Flask z formularzem: dwa pola liczbowe i wybór działania (+, -, *, /). Po wysłaniu formularza wyświetl wynik na tej samej stronie. Obsłuż dzielenie przez zero.

Rozwiązanie — app.py
from flask import Flask, render_template, request
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def kalkulator():
    wynik  = None
    blad   = None

    if request.method == 'POST':
        try:
            a      = float(request.form['a'])
            b      = float(request.form['b'])
            dzial  = request.form['dzialanie']

            if dzial == '+': wynik = a + b
            elif dzial == '-': wynik = a - b
            elif dzial == '*': wynik = a * b
            elif dzial == '/':
                if b == 0:
                    blad = "Błąd: dzielenie przez zero!"
                else:
                    wynik = a / b

            if wynik is not None:
                wynik = round(wynik, 6)
        except ValueError:
            blad = "Błąd: podaj prawidłowe liczby."

    return render_template('kalkulator.html',
                           wynik=wynik, blad=blad)

if __name__ == '__main__':
    app.run(debug=True)
Omówienie krok po kroku
  1. methods=['GET', 'POST']
    Jedna funkcja obsługuje oba typy żądań. GET — pierwsza wizyta, wyświetl pusty formularz. POST — formularz wysłany, oblicz wynik.
  2. try/except ValueError
    float(request.form['a']) rzuci ValueError jeśli użytkownik wpisze tekst. Łapiemy wyjątek i pokazujemy przyjazny komunikat zamiast strony błędu.
  3. wynik = None na starcie
    Gdy metoda to GET — wynik jest None. Szablon sprawdza {% if wynik is not none %} i nie wyświetla sekcji wyniku przy pustym formularzu.
  4. round(wynik, 6)
    Zmiennoprzecinkowe operacje mogą dawać wyniki jak 0.30000000000000004. Zaokrąglamy do 6 miejsc dla czytelności.
Zadanie 2 Lista zadań (ToDo) średnie

Napisz aplikację ToDo: dodawanie zadań przez formularz, oznaczanie jako ukończone (checkbox), usuwanie zadań. Każde zadanie ma tekst i status ukończenia. Wyświetl osobno zadania aktywne i ukończone.

Rozwiązanie — app.py
from flask import (Flask, render_template,
                   request, redirect, url_for)
app = Flask(__name__)

zadania = []
next_id = 1

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        global next_id
        tekst = request.form.get('tekst','').strip()
        if tekst:
            zadania.append({
                'id':       next_id,
                'tekst':    tekst,
                'ukonczone': False
            })
            next_id += 1
        return redirect(url_for('index'))

    aktywne   = [z for z in zadania
                 if not z['ukonczone']]
    ukonczone = [z for z in zadania
                 if z['ukonczone']]
    return render_template('todo.html',
                           aktywne=aktywne,
                           ukonczone=ukonczone)

@app.route('/ukoncz/')
def ukoncz(zid):
    for z in zadania:
        if z['id'] == zid:
            z['ukonczone'] = not z['ukonczone']
            break
    return redirect(url_for('index'))

@app.route('/usun/')
def usun(zid):
    global zadania
    zadania = [z for z in zadania
               if z['id'] != zid]
    return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True)
Omówienie krok po kroku
  1. Lista słowników jako baza danych
    Każde zadanie to słownik z trzema polami. Lista zadań to "baza danych w pamięci" — wystarczająca na egzaminie. Po restarcie serwera dane znikają.
  2. Toggle stanu przez not
    z['ukonczone'] = not z['ukonczone'] — przełącza wartość boolowską. Kliknięcie "ukończ" na ukończonym zadaniu przywraca je do aktywnych.
  3. Filtrowanie przez list comprehension
    [z for z in zadania if not z['ukonczone']] — tworzy listę tylko aktywnych zadań. Przekazujemy do szablonu dwie osobne listy.
  4. redirect po POST
    Po dodaniu zadania (POST) przekierowujemy na GET. Bez tego — odświeżenie strony ponownie wyśle formularz i doda duplikat zadania.
Zadanie 3 Aplikacja — dziennik ocen średnie

Zbuduj aplikację webową do zarządzania ocenami uczniów. Strona główna wyświetla listę uczniów ze średnimi. Podstrona ucznia pokazuje jego oceny i umożliwia dodanie nowej. Dane przechowuj w słowniku w pamięci.

Rozwiązanie — app.py
from flask import (Flask, render_template,
                   request, redirect, url_for)
app = Flask(__name__)

dziennik = {
    "Kasia": [5, 4, 5, 3, 4],
    "Marek": [3, 4, 2, 4, 3],
    "Zofia": [5, 5, 4, 5, 5],
}

@app.route('/')
def index():
    dane = []
    for imie, oceny in dziennik.items():
        sr = sum(oceny) / len(oceny) if oceny else 0
        dane.append({"imie": imie,
                     "srednia": round(sr, 2),
                     "ile": len(oceny)})
    dane.sort(key=lambda x: x['srednia'], reverse=True)
    return render_template('dziennik.html', uczniowie=dane)

@app.route('/uczen/',
           methods=['GET', 'POST'])
def uczen(imie):
    if imie not in dziennik:
        return redirect(url_for('index'))

    if request.method == 'POST':
        try:
            ocena = int(request.form['ocena'])
            if 1 <= ocena <= 6:
                dziennik[imie].append(ocena)
        except ValueError:
            pass
        return redirect(url_for('uczen', imie=imie))

    oceny = dziennik[imie]
    sr    = sum(oceny) / len(oceny) if oceny else 0
    return render_template('uczen.html',
                           imie=imie,
                           oceny=oceny,
                           srednia=round(sr, 2))

if __name__ == '__main__':
    app.run(debug=True)
Omówienie krok po kroku
  1. Słownik jako baza danych
    dziennik = {"Kasia": [5,4,5...]} — klucz to imię, wartość to lista ocen. Prosty i wystarczający model danych dla aplikacji egzaminacyjnej.
  2. Budowanie listy danych dla widoku
    W widoku index() budujemy listę słowników z przetworzonymi danymi (imię + średnia + ile ocen). Logika obliczania jest w Pythonie, nie w szablonie.
  3. Parametr URL jako klucz słownika
    /uczen/Kasiaimie="Kasia"dziennik["Kasia"]. Sprawdzamy czy uczeń istnieje przed użyciem — inaczej KeyError.
  4. Walidacja oceny
    try/except ValueError na konwersji, plus sprawdzenie zakresu 1–6. Zła ocena jest po prostu ignorowana — aplikacja nie crashuje.

Zadania do samodzielnego rozwiązania

Każde zadanie to kompletna mini-aplikacja Flask. Pamiętaj o folderze templates/ i uruchamianiu przez python app.py.

1★☆☆

Przelicznik temperatury

Formularz z polem liczbowym i wyborem jednostki (C/F/K). Po wysłaniu wyświetl temperaturę we wszystkich trzech jednostkach.

Wskazówka: jeden route GET+POST, wzory konwersji, render_template z wynikami
2★☆☆

Licznik odwiedzin

Aplikacja która liczy ile razy odwiedzono stronę główną. Licznik przechowaj w zmiennej globalnej. Dodaj przycisk "Resetuj licznik".

Wskazówka: global licznik, osobny route dla resetu z redirect
3★★☆

Książka adresowa

CRUD dla kontaktów (imię, telefon, email). Lista wszystkich, szczegóły jednego, formularz dodawania, usuwanie. Użyj słownika z ID jako kluczem.

Wskazówka: /kontakt/<int:id>, /dodaj, /usun/<int:id>, szablony z Jinja2
4★★☆

Quiz z wynikiem

Aplikacja quizowa — wyświetl pytania jedno po drugim (lub wszystkie naraz), zbierz odpowiedzi, oblicz wynik i wyświetl podsumowanie z procentem poprawnych.

Wskazówka: lista słowników z pytaniami i odpowiedziami, request.form dla checkboxów lub radio
5★★☆

Wyszukiwarka w danych

Masz listę produktów (nazwa, cena, kategoria). Formularz GET z polem wyszukiwania i filtrem kategorii. Wyświetl pasujące wyniki — szukaj w nazwie bez rozróżniania wielkości liter.

Wskazówka: request.args.get('q',''), filtrowanie przez list comprehension, method="GET" w formularzu
6★★★

Aplikacja z plikiem CSV

Aplikacja wczytuje dane uczniów z pliku CSV przy starcie. Pozwala przeglądać, dodawać i usuwać rekordy. Po każdej zmianie zapisuje zaktualizowany plik CSV.

Wskazówka: wczytaj CSV w app.py poza routami, po modyfikacji zapisz przez csv.writer