6.2. ToDo

Aplikacja internetowa ToDo w oparciu o framework Realizacja aplikacji internetowej Quiz w oparciu o framework Flask 3.1.x i bazę danych SQLite. Aplikacja umożliwi dodawanie przez zalogowanego użytkownika zadań z określoną datą, ich przeglądanie i oznaczanie jako wykonane.

Zalecamy zapoznanie się z materiałami zawartymi w scenariuszach:

Do pracy potrzebne nam będzie wirtualne środowisko Pythona z zainstalowanym pakietem Flask. Początek pracy jest taki sam, jak w przypadku aplikacji Quiz, tzn.:

  1. przygotowujemy wirtualne środowisko Pythona w katalogu projekty_flask, chyba że zrobiliśmy to wcześniej podczas realizacji aplikacji Quiz;

  2. w katalogu projekty_flask tworzymy katalog aplikacji: todo;

  3. wykonujemy 2. i 3. punkt scenariusza Quiz, tj.: „Projekt i aplikacja” oraz „Strona główna”.

W pliku app.py zmieniamy w konfiguracji aplikacji nazwę serwisu zapisaną w kluczu SITE_NAME na „Projekty Flask”.

6.2.1. Model danych i baza

Jako źródło danych aplikacji wykorzystamy tym razem bazę SQLite3 obsługiwaną za pomocą modułu Pythona sqlite3.

Model danych, tj. w tym przypadku schemat bazy danych, zdefiniujemy w pliku modele.sql, który tworzymy w katalogu aplikacji i wypełniamy kodem SQL:

Plik modele.sql Kod nr
 1-- projekty_flask/todo/modele.sql
 2
 3-- tabela z użytkownikami
 4DROP TABLE IF EXISTS user;
 5CREATE TABLE user (
 6  id INTEGER PRIMARY KEY AUTOINCREMENT, -- unikalny identyfikator
 7  login TEXT UNIQUE NOT NULL, -- nazwa użytkownika
 8  haslo TEXT NOT NULL -- hasło użytkownika
 9);
10-- przykładowy użytkownik
11INSERT INTO user (id, login, haslo)
12VALUES (null, 'adam', 'scrypt:32768:8:1$b6ySf4OhUqADg4os$9fab79b9175c7e1ac341d06b72a3bb3e3a213733c6211bfa7f2b388988065e837df630be38e7eb5729d59db4f5e7d0abd7886e0697125f1a0e8a0eadd6a9eb3a');
13
14-- tabela z zadaniami
15DROP TABLE IF EXISTS zadanie;
16CREATE TABLE zadanie (
17    id INTEGER PRIMARY KEY AUTOINCREMENT, -- unikalny identyfikator zadania
18    user_id INTEGER, -- identyfikator użytkownika
19    zadanie TEXT NOT NULL, -- opis zadania do wykonania
20    zrobione BOOLEAN NOT NULL, -- informacja czy zadania zostało juz wykonane
21    data_pub DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- data dodania zadania
22    FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE -- wskazanie klucza obcego
23);
24
25-- początkowe dane
26INSERT INTO zadanie VALUES (null, 1, 'Wyrzucić śmieci', 0, CURRENT_TIMESTAMP);
27INSERT into zadanie VALUES (null, 1, 'Nakarmić psa', 0, CURRENT_TIMESTAMP);

Wykonanie klauzul SQL spowoduje utworzenie dwóch tabel i wypełnienie ich przykładowymi danymi. Tabele:

  • user – zawierać będzie identyfikator, nazwę i hasło użytkownika,

  • zadanie – zawierać będzie identyfikator zadania, identyfikator użytkownika, treść zadania, oznaczenie wykonania oraz datę dodania.

Funkcje potrzebne do obsługi bazy danych umieścimy w nowym pliku db.py, który zapisujemy w katalogu aplikacji.

plik db.py Kod nr
 1import sqlite3
 2from flask import g, current_app
 3
 4def get_db():
 5    """Funkcja tworzy połączenie z bazą"""
 6    if 'db' not in g:
 7        g.db = sqlite3.connect(
 8            current_app.config['DATABASE'],
 9            detect_types = sqlite3.PARSE_DECLTYPES
10        )
11        g.db.row_factory = sqlite3.Row
12    return g.db
13
14def close_db(e=None):
15    """Funkcja zamyka połączenia z bazą"""
16    db = g.pop('db', None)
17    if db is not None:
18        db.close()
19
20def init_app(app):
21    """Funkcja rejestruje funkcję close_db() w aplikacji"""
22    app.teardown_appcontext(close_db)
23
24def query_db(query, args=(), one=False):
25    """Funkcja wykonuje zapytania SQL i zwraca rezultaty"""
26    cur = get_db().execute(query, args)
27    rv = cur.fetchall()
28    cur.close()
29    return (rv[0] if rv else None) if one else rv
30
31def init_db():
32    """Funkcja tworzy bazę i tabele"""
33    from werkzeug.security import generate_password_hash
34    db = get_db()
35    with current_app.open_resource('modele.sql') as f:
36        db.executescript(f.read().decode('utf8'))

Informacja

Podczas działania aplikacji mamy dostęp do tzw. kontekstu, który zawiera ustawienia zapisane w słowniku config oraz dane w obiektach current_app i g.

Zadaniem funkcji get_db() będzie połączenie z bazą danych i zapisanie obiektu db reprezentującego bazę w kontekście aplikacji:

  • if 'db' not in g: – sprawdzamy, czy w obiekcie g nie ma obiektu db;

  • dalsza część kodu tworzy połączenie wywołując metodę sqlite3.connect() i zapisuje je w kontekście aplikacji.

Funkcja close_db() odpowiadać będzie za zamknięcie połączenia. Będzie ona wywoływana po obsłużeniu każdego żądania dzięki kolejnej funkcji init_app(), która rejestruje funkcję close_db() w kontekście aplikacji: app.teardown_appcontext(close_db).

Funkcja query_db() ułatwi wykonywanie zapytań SQL odczytujących dane z bazy. Pierwszy wymagany argument to zapytanie SELECT, drugi opcjonalny to lista wartości wstawianych do klauzuli WHERE. Domyślnie funkcja zwracała będzie wszystkie pobrane rekordy dzięki metodzie fetchall(). Jeżeli jednak w wywołaniu podamy trzeci argument w postaci one=True, zwrócony zostanie tylko pierwszy pobrany rekord.

Funkcja init_db() posłuży do utworzenia pliku bazy danych, a następnie tabel do przechowywania danych.

Pozostaje nam uzupełnienie kodu w pliku app.py:

Plik app.py Kod nr
 1import os
 2from flask import Flask, render_template, current_app
 3from db import init_app, init_db
 4
 5app = Flask(__name__)
 6
 7# konfiguracja aplikacji
 8app.config.update(dict(
 9    SECRET_KEY='bardzosekretnawartosc',
10    SITE_NAME='Projekty Flask',
11    DATABASE=os.path.join(app.root_path, 'db.sqlite')
12))
13
14init_app(app)
15
16# rejestracja blueprintów
17
18
19@app.route('/')
20def index():
21    # return 'Cześć, tu Python i Flask!'
22    return render_template('index.html')
23
24with app.app_context():
25    if not os.path.exists(current_app.config['DATABASE']):
26        init_db()
27    if __name__ == "__main__":
28        app.run(debug=True)

Przede wszystkim uzupełniamy importy. Następnie w słowniku konfiguracji dodajemy klucz DATABASE wskazujący na plik bazy danych db.sqlite.

Następnie umieszczamy wywołanie funkcji init_app(app), dzięki czemu jeżeli na dysku nie będzie pliku bazy danych, zostanie on utworzony, a wraz z nim tabele zdefiniowane w pliku todo.sql.

Po uruchomieniu serwera deweloperskiego i otwarciu adresu http://127.0.0.1:5000 powinniśmy zobaczyć stronę:

../../_images/todo_01.png

– a w katalogu aplikacji powinien zostać utworzony plik bazy danych db.sqlite.

Podpowiedź

Bazę danych można też utworzyć ręcznie za pomocą wiersza poleceń bazy Sqlite3. W terminalu w katalogu aplikacji możemy użyć następujących poleceń:

Terminal nr
~/projekty_flask/todo$ sqlite3 db.sqlite < modele.sql
~/projekty_flask/todo$ sqlite3 db.sqlite
sqlite> select * from zadanie;
sqlite> .quit

Pierwsze polecenie tworzy bazę danych w pliku db.sqlite. Drugie otwiera ją w interpreterze. Trzecie to zapytanie SQL, które pobiera wszystkie dane z tabeli zadanie. Interpreter zamykamy poleceniem .quit.

../../_images/sqlite3_cmd.png

6.2.2. Użytkownicy

Informacja

Jedna aplikacja Flask może składać się z wielu modułów odpowiedzialnych np. za autoryzację użytkowników, system komentarzy, quiz czy listę zadań. Obsługę tych składników warto rozdzielić na osobne moduły, nazywane we Flasku blueprint, co ułatwi konstruowanie i rozszerzanie aplikacji.

Zadania do wykonania powiązane będą z użytkownikami, dlatego na początku zajmiemy się blueprintem, który pozwoli na logowanie, rejestrację i usuwanie użytkownika.

W katalogu aplikacji tworzymy plik users.py i dodajemy do niego poniższy kod:

Plik users.py Kod nr
1import functools
2from flask import (
3    Blueprint, flash, g, redirect, render_template, request, session, url_for
4)
5from werkzeug.security import check_password_hash, generate_password_hash
6from db import get_db, query_db
7
8bp = Blueprint('users', __name__, template_folder='templates', url_prefix='/users')

Blueprint jest instancją klasy (obiektem typu) Blueprint. Do konstruktora przekazujemy:

  • users – nazwę blueprinta,

  • __name__ – nazwa pliku, w którym blueprint jest definiowany,

  • template_folder – podkatalog katalogu aplikacji, w którym należy szukać szablonów,

  • url_prefix – część adresu URL, który blueprint będzie obsługiwał, w tym przypadku będzie to adres http://nazwa_serwera/users/.

6.2.2.1. (Wy)Logowanie

W utworzonym blueprincie zdefiniujemy widok loguj() powiązany z adresem URL http://nazwa_serwera/users/loguj, który obsłuży żądania GET i POST:

Plik users.py Kod nr
10@bp.route('/loguj', methods=['GET', 'POST'])
11def loguj():
12    if request.method == 'POST':
13        login = request.form['login'].strip()
14        haslo = request.form['haslo'].strip()
15
16        error = None
17        user = query_db('SELECT * FROM user WHERE login = ?', [login], one=True)
18
19        if user is None:
20            error = 'Błędny login.'
21        elif not check_password_hash(user['haslo'], haslo):
22            error = 'Błędne hasło.'
23
24        if error is None:
25            session.clear()
26            session['user_id'] = user['id']
27            flash(f'Zalogowano użytkownika {user['login']}!')
28            return redirect(url_for('index'))
29        flash(error)
30
31    return render_template('users/user_loguj.html')

Jeżeli serwer otrzyma żądanie typu GET oraz w przypadku błędów logowania, funkcja zwróci szablon users/loguj.html. Natomiast kiedy serwer otrzyma dane z formularza, wykonamy następujące operacje:

  • request.form[] – odczytanie loginu i hasłu z przesłanego formularza;

  • query_db('SELECT...', lista) – wykonanie zapytania SQL pobierającego dane użytkownika o podanym loginie, znak zapytania zostanie zastąpiony wartością z listy;

  • przygotowanie informacji o błędzie w zmiennej error, jeżeli w bazie nie ma użytkownika o podanym loginie (user is None) lub jeżeli podano błędne hasło (not check_password_hash(user['haslo'], haslo));

  • session['user_id'] = user['id'] – zapisanie identyfikatora użytkownika w sesji (zob. sesja), jeżeli dane logowania są poprawne;

  • redirect(url_for('index')) przekierowanie zalogowanego użytkownika na stronę główną;

  • flash() – zapisanie komunikatów dla użytkownika, które będzie można później odczytać w szablonie.

Po poprawnym zalogowaniu identyfikator użytkownika będzie dostępny w sesji podczas przetwarzania kolejnych żądań. Będzie on odczytywany przez funkcję load_user(), którą umieszczamy w blueprincie:

Plik users.py Kod nr
33@bp.before_app_request
34def load_user():
35    user_id = session.get('user_id')
36    if user_id is None:
37        g.user = None
38    else:
39        g.user = query_db('SELECT * FROM user WHERE id = ?', [user_id], one=True)

Funkcja wykonywana będzie przed każdym żądaniem, ponieważ poprzedzamy ją dekoratorem @bp.before_app_request. Zadaniem funkcji będzie pobranie danych zalogowanego użytkownika z bazy i zapisanie ich w kontekście g.user.

Adres http://nazwa_serwera/users/wyloguj będzie obsługiwany przez widok wyloguj():

Plik users.py Kod nr
41@bp.route('/wyloguj')
42def wyloguj():
43    session.clear()
44    flash(f'Wylogowano użytkownika {g.user['login']}.')
45    return redirect(url_for('index'))

Wylogowanie polega na usunięciu danych użytkownika z sesji, po czym nastąpi przekierowanie na stronę główną.

Na koniec dodamy jeszcze jedną funkcję login_required(), której użyjemy do zabezpieczenia dostępu niektórych widoków:

Plik users.py Kod nr
47def login_required(view):
48    @functools.wraps(view)
49    def wrapped_view(**kwargs):
50        if g.user is None:
51            return redirect(url_for('users.loguj'))
52        return view(**kwargs)
53    return wrapped_view

Dodana funkcja to dekorator, który jeżeli użytkownik nie będzie zalogowany, przekieruje go na stronę logowania, w przeciwnym razie zwróci żądany widok.

6.2.2.2. Rejestracja blueprinta

Użycie blueprinta wymaga zarejestrowania go w aplikacji.

Na początku pliku app.py importujemy moduł users, następnie poniżej komentarza dodajemy kod:

Plik app.py Kod nr
import users

...

# rejestracja blueprintów
app.register_blueprint(users.bp)

6.2.3. Szablony

W rozbudowanych aplikacjach zawierających wiele blueprintów i widoków zwracających szablony, część kodu HTML powtarza się na każdej stronie, co zapewnia spójność wyglądu. Tę wspólną część, aby jej nie powielać, umieścimy w szablonie bazowym. Użyjemy dotychczasowego pliku templates/index.html, w którym umieszczamy poniższy kod:

Plik index.html Kod nr
 1<!-- projekty_flask/todo/templates/index.html -->
 2<html>
 3  <head>
 4    <title>{{ config.SITE_NAME }}</title>
 5  </head>
 6<body>
 7  <h1>{% block h1 %} {{ config.SITE_NAME }} {% endblock %}</h1>
 8
 9  <nav>
10    <ul>
11      <li><a href="{{ url_for('index') }}">Strona główna »</a></li>
12    {% if g.user %}
13      <li><a href="{{ url_for('users.wyloguj') }}">Wyloguj się »</a></li>
14      <li><span>Zalogowany: {{ g.user['login'] }}</span></li>
15    {% else %}
16      <li><a href="{{ url_for('users.loguj') }}">Zaloguj się »</a></li>
17    {% endif %}
18    </ul>
19  </nav>
20
21  {% with komunikaty = get_flashed_messages() %}
22    {% if komunikaty %}
23      {% for komunikat in komunikaty %}
24        <p> {{ komunikat }} </p>
25      {% endfor %}
26    {% endif %}
27  {% endwith %}
28
29  {% block body %} {% endblock %}
30
31</body>
32</html>

W szablonach wykorzystujemy specjalne tagi dwóch rodzajów:

  • {% instrukcja %} – pozwalają używać instrukcji sterujących, np. warunkowych lub pętli, oraz definiować bloki, które będą uzupełniane przez szablony dziedziczące,

  • {{ zmienna }} – służą wyświetlaniu wartości zmiennych lub wywoływaniu metod obiektów przekazanych do szablonu.

Zastosowanie tagów w szablonach:

  • {{ config.SITE_NAME }} – wstawienie nazwy serwisu zdefiniowanego w słowniku ustawień aplikacji config;

  • {% block nazwa %} {% endblock %} – zdefiniowanie bloku o podanej nazwie, którego zawartość może zostać nadpisana w szablonach dziedziczących;

  • {% if g.user %} ... {% else %} ... {% endif %} – jeżeli użytkownik jest zalogowany, wstawiamy odnośnik „Wyloguj się” oraz informację, kto jest zalogowany, w przeciwnym razie wstawiamy odnośnik „Zaloguj się”;

  • {{ url_for('index') }} – funkcja url_for() zwraca adres URL powiązany z podanym jako argument widokiem, poprzedzonym ewentualna nazwą blueprintu, w którym został zdefiniowany, np. {{ url_for('users.loguj') }};

  • {% with komunikaty = get_flashed_messages() %} {% endwith %} – odczytanie komunikatów dla użytkownika utworzonych w widokach, jeżeli zostały utworzone ({% if komunikaty %}), odczytujemy je w pętli ({% for komunikat in komunikaty %}) i wstawiamy do szablonu w osobnych akapitach <p> {{ komunikat }} </p>.

Szablony blueprintów mogą być zapisywane w osobnych podkatalogach. W katalogu projekty_flask/todo/templates tworzymy podkatalog o takiej samej nazwie jak nasz blueprint, tj. users, a w nim plik user_loguj.html. W utworzonym szablonie umieszczamy kod:

Plik user_loguj.html Kod nr
 1<!-- projekty_flask/todo/templates/users/loguj.html -->
 2{% extends "index.html" %}
 3{% block h1 %} Logowanie użytkownika {% endblock %}
 4{% block body %}
 5    <form method="POST" action="{{ url_for('users.loguj') }}">
 6      <label for="login">Login:</label>
 7      <input type="text" id="login" name="login" value="" required>
 8      <br>
 9      <label for="haslo">Hasło:</label>
10      <input type="password" id="haslo" name="haslo" value="" required>
11      <br>
12      <button type="submit">Zaloguj</button>
13    </form>
14{% endblock %}

Tag {% extends "index.html" %} wskazuje szablon bazowy, z którego dziedziczymy kod. W tagach {% block nazwa %} {% endblock %} wstawiamy kod charakterystyczny dla bieżącego szablonu. W tym przypadku tworzymy formularz HTML pozwalający na wpisanie loginu i hasła i przesłanie tych danych na adres zdefiniowany w atrybucie action obsługiwany przez widok loguj() z blueprinta users.

Po uruchomieniu serwera deweloperskiego i wejściu na adres http://127.0.0.1:5000/users/loguj powinniśmy zobaczyć stronę logowania:

../../_images/todo_loguj.png

6.2.3.1. Ćwiczenie

Spróbuj zalogować się na konto użytkownika utworzonego podczas dodawania do bazy przykładowych danych. W formularzu podaj login adam i hasło zaq1@WSX. Po zalogowaniu powinieneś zobaczyć stronę główną z odpowiednim komunikatem:

../../_images/todo_adam.png

Po wylogowaniu podobnie:

../../_images/todo_wyloguj.png

6.2.4. Dodawanie i usuwanie kont

W blueprincie users.py umieścimy jeszcze dwa widoki, które umożliwią zarejestrowanie się użytkownika oraz usuwanie konta:

55@bp.route('/dodaj', methods=['GET', 'POST'])
56def dodaj():
57    if request.method == 'POST':
58        login = request.form['login'].strip()
59        haslo = request.form['haslo'].strip()
60        db = get_db()
61        try:
62            db.execute('INSERT INTO user VALUES (?, ?, ?)',
63                       [None, login, generate_password_hash(haslo)])
64            db.commit()
65        except db.IntegrityError:
66            flash(f'Podany login {login} jest już używany.')
67        else:
68            flash(f'Dodano konto {login}')
69            return redirect(url_for('index'))
70
71    return render_template('users/user_dodaj.html')
72
73@bp.route('/usun', methods=['GET', 'POST'])
74@login_required
75def usun():
76    if request.method == 'POST':
77        login = g.user['login']
78        user_id = g.user['id']
79        db = get_db()
80        db.execute('DELETE FROM user WHERE id = ?', [user_id])
81        db.commit()
82        flash(f'Usunięto użytkownika {login}!')
83        return redirect(url_for('index'))
84
85    return render_template('users/user_usun.html')

Po wysłaniu żądania GET na adres http://nazwa_serwera/users/dodaj widok dodaj() zwróci szablon user_dodaj.html zawierający formularz. Po wypełnieniu i wysłaniu formularza, czyli w przypadku żądania typu POST odczytujemy login oraz hasło i próbujemy dodać do bazy nowego użytkownika wykonując klauzulę SQL INSERT INTO user .... Warto zwrócić uwagę, że hasło jest szyfrowane za pomocą funkcji skrótu generate_password_hash().

Jeżeli podany login istnieje w bazie, zwrócony zostanie wyjątek db.IntegrityError, tj. błąd integralności, wtedy przygotowujemy komunikat dla użytkownika i ponownie zawracamy szablon z formularzem. Pod udanym dodaniu konta użytkownik zostanie przekierowany na stronę główną z komunikatem potwierdzającym dodanie konta.

Zalogowany użytkownik będzie mógł usunąć konto po wejściu na stronę o adresie http://nazwa_serwera/users/usun. Widok usun() obsługujący ten adres zabezpieczamy dodanym wcześniej dekoratorem login_required. Jeżeli użytkownik potwierdzi chęć usunięcia konta oraz wszystkich jego zadań przesyłając formularz z szablonu user_usun.html, wykonamy klauzulę SQL DELETE FROM users ..., która usunie jego konto z bazy oraz kaskadowo wszystkie powiązane zadania. Na koniec przekierujemy użytkownika na stronę główną.

Pozostaje dodanie szablonów zwracanych przez omówione widoki. W katalogu projekty_flask/todo/templates/users:

  • dodajemy plik user_dodaj.html:

    Plik user_dodaj.html Kod nr
     1<!-- projekty_flask/todo/templates/users/dodaj.html -->
     2{% extends "index.html" %}
     3{% block h1 %} Dodawanie konta {% endblock %}
     4{% block body %}
     5    <form method="POST" action="{{ url_for('users.dodaj') }}">
     6      <label for="login">Login:</label>
     7      <input type="text" id="login" name="login" required>
     8      <br>
     9      <label for="haslo">Hasło:</label>
    10      <input type="password" id="haslo" name="haslo" required>
    11      <br>
    12      <button type="submit">Dodaj</button>
    13    </form>
    14{% endblock %}
    

– oraz plik user_usun.html:

 1<!-- projekty_flask/todo/templates/users/usun.html -->
 2{% extends "index.html" %}
 3{% block h1 %} Usuwanie użytkownika {% endblock %}
 4{% block body %}
 5    <form method="POST" action="{{ url_for('users.usun') }}">
 6      <p>Czy na pewno chcesz usunąć swoje konto oraz wszystkie zadania?</p>
 7      <br>
 8      <button type="submit">Usuń</button>
 9    </form>
10{% endblock %}

6.2.4.1. Ćwiczenie

  1. Dodaj do szablonu bazowego we właściwych sekcjach odnośniki pozwalające na dodawanie i usuwanie konta przez użytkownika.

  2. Dodaj koto użytkownika ewa, a następnie je usuń.

../../_images/todo_user_dodaj.png
../../_images/todo_ewa.png
../../_images/todo_user_usun.png

6.2.5. Lista zadań

Obsługę zadań umieścimy w osobnym blueprincie. W katalogu aplikacji projekty_flask/todo tworzymy plik todo.py i wypełniamy kodem:

Plik todo.py Kod nr
 1from flask import (
 2    Blueprint, flash, g, render_template, request, redirect, url_for
 3)
 4from db import get_db, query_db
 5from users import login_required
 6
 7bp = Blueprint('todo', __name__, template_folder='templates', url_prefix='/todo')
 8
 9@bp.route('/')
10@login_required
11def index():
12    sql = 'SELECT * FROM zadanie WHERE user_id=? ORDER BY data_pub DESC'
13    zadania = query_db(sql, [g.user['id']])
14    return render_template('todo/index.html', zadania=zadania)

Widok index() wywoływany będzie po wejściu na adres http://nazwa_serwera/todo. Jego zadaniem jest pobranie z bazy wszystkich zadań zalogowanego użytkownika i przekazanie ich do szablonu w zmiennej zadania.

Szablon tworzymy w pliku projekty_flask/todo/templates/todo/index.html:

Plik todo/index.html Kod nr
 1<!-- projekty_flask/todo/templates/todo/index.html -->
 2{% extends "index.html" %}
 3{% block h1 %} Lista zadań {% endblock %}
 4{% block body %}
 5    <ol>
 6      <!-- wypisujemy kolejno wszystkie zadania -->
 7      {% for zadanie in zadania %}
 8        <li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></li>
 9      {% endfor %}
10    </ol>
11{% endblock %}

W pętli {% for zadanie in zadania %} odczytujemy zadania z listy przekazanej do szablonu, wypisujemy treść zadania i datę dodania.

6.2.5.1. Ćwiczenie

  1. W pliku app.py zaimportuj moduł todo i zarejestruj blueprint todo.bp w aplikacji.

  2. Dodaj do szablonu bazowego odnośnik do listy zadań.

  3. Zaloguj się podając login adam i hasło zaq1@WSX.

  4. Wejdź na stronę z listą zadań.

../../_images/todo_adam_zadania.png

6.2.6. Dodawanie zadań

W pliku todo.py dopisujemy widoki:

Plik todo.py Kod nr
16@bp.route('/dodaj', methods=['GET', 'POST'])
17@login_required
18def dodaj():
19    """Dodawanie nowego zadania"""
20    error = None
21    if request.method == 'POST':
22        zadanie = request.form['zadanie'].strip()
23        if len(zadanie):
24            db = get_db()
25            db.execute('INSERT INTO zadanie (user_id, zadanie, zrobione) VALUES (?, ?, ?)',
26                       [g.user['id'], zadanie, 0])
27            db.commit()
28            flash('Dodano nowe zadanie.')
29            return redirect(url_for('todo.index'))
30        else:
31            flash('Nie możesz dodać pustego zadania!')  # komunikat o błędzie
32
33    return render_template('todo/zadanie_dodaj.html')
34
35@bp.route('/zrobione', methods=['POST'])
36def zrobione():
37    """Zmiana statusu zadania na wykonane."""
38    zadanie_id = request.form['id']
39    db = get_db()
40    db.execute('UPDATE zadanie SET zrobione=1 WHERE id=? AND user_id=?',
41               [zadanie_id, g.user['id']])
42    db.commit()
43    flash('Zmieniono status zadania.')
44    return redirect(url_for('todo.index'))

Widok dodaj() w odpowiedzi na żądanie typu GET zwróci szablon zadanie_dodaj.html, który będzie zawierał formularz pozwalający na wpisanie treści zadania.

Po przesłaniu formularza na serwer odczytamy treść zadania i sprawdzimy, czy zawiera jakieś znaki. Jeżeli tak, wykonamy zapytanie SQL INSERT INTO ..., które do tabeli zadanie doda nowy rekord zawierający identyfikator użytkownika, treść zadania oraz wartość 0 oznaczającą, że zadanie nie jest wykonane.

Warto zwrócić uwagę, że nie podajemy daty publikacji, ponieważ zostanie ona utworzona automatycznie przez bazę danych dzięki zdefiniowaniu wartości domyślnej pola data_pub w modelu danych: DEFAULT CURRENT_TIMESTAMP.

Widok zrobione() obsługiwał będzie tylko żądania typu POST. Po otrzymaniu identyfikatora zadania przesłanego z formularza wykonamy zapytanie SQL UPDATE, które oznaczy zadanie wskazane w klauzuli WHERE jako zrobione: SET zrobione=1.

W szablonie projekty_flask/todo/templates/todo/zadanie_dodaj.html umieszczamy kod HTML formularza pozwalającego dodać zadanie:

Plik zadanie_dodaj.html. Kod nr
1<!-- projekty_flask/todo/templates/todo/zadanie_dodaj.html -->
2{% extends "index.html" %}
3{% block h1 %} Dodawanie zadań {% endblock %}
4{% block body %}
5    <form method="POST" action="{{ url_for('todo.dodaj') }}">
6      <input name="zadanie" value=""> <br>
7      <button type="submit">Dodaj zadanie</button>
8    </form>
9{% endblock %}

Szablon projekty_flask/todo/templates/todo/index.html uzupełniamy, tzn. kod w pętli {% for zadanie in zadania %} zmieniamy na:

Plik todo/index.html. Kod nr
 1      {% for zadanie in zadania %}
 2        <li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
 3
 4        <!-- formularz zmiany statusu zadania -->
 5        {% if not zadanie.zrobione %}
 6          <form method="POST" action="{{ url_for('todo.zrobione') }}">
 7            <!-- wysyłamy tylko id zadania -->
 8            <input type="hidden" name="id" value="{{ zadanie.id }}"/>
 9            <button type="submit">Zrobione</button>
10          </form>
11        {% else %}
12          <!-- tu wstawisz kod formularza do usuwania zadania -->
13        {% endif %}
14
15        </li>
16      {% endfor %}
  • {% if not zadanie.zrobione %} – jeżeli zadanie nie jest wykonane, dodajemy formularz zmiany statusu zadania zawierający ukryte pole z identyfikatorem zadania. Jeżeli użytkownik kliknie przycisk Zrobione, do serwera zostanie wysłane żądanie POST z identyfikatorem zadania, które zostanie obsłużone przez widok zrobione().

6.2.6.1. Ćwiczenie

  1. Do szablonu wyświetlającego listę zadań dodaj na końcu odnośnik umożliwiający dodawanie zadań.

  2. Dodaj konto dla użytkownika o loginie ewa i zaloguj się na nie.

  3. Dodaj dwa zadania i oznacz jedno z nich jako wykonane.

../../_images/todo_dodaj_zadanie.png
../../_images/todo_zadania_ewy.png

6.2.7. Style CSS

O wyglądzie aplikacji decydują arkusze stylów CSS. Tego typu zasoby, podobnie jak np. obrazy, nie zmieniają się zbyt często, dlatego umieszczamy je w specjalnym podkatalogu static w folderze aplikacji.

Tworzymy więc podkatalog projekty_flask/todo/static, a w nim plik style.css. W pliku umieszczamy przykładowe definicje:

Plik style.css Kod nr
 1/* todo/static/style.css */
 2body { margin-top: 20px; background-color: lightgreen; }
 3h1, p { margin-left: 20px; }
 4ol { text-align: left; }
 5form { display: inline-block; margin: 5px; }
 6input {
 7  width: 100%;
 8  padding: 12px 20px;
 9  margin: 8px 0;
10  box-sizing: border-box
11  border-color: blue;
12  border-radius: 5px;
13}
14input:focus {
15  background-color: lightgray;
16}
17ul {
18  list-style-type: none;
19  margin: 0;
20  padding: 0;
21  overflow: hidden;
22  background-color: #333333;
23}
24ul li { float: left; }
25ul li a, ul li span{
26  display: block;
27  color: white;
28  text-align: center;
29  padding: 16px;
30  text-decoration: none;
31}
32ul li a:hover { background-color: #111111; }
33li { margin-bottom: 5px; }
34button {
35  padding: 8px 16px;
36  cursor: pointer;
37  color: blue;
38  font-size: 14px/1.5em;
39  background: white;
40  border: 1px solid grey;
41}
42.error { color: red; }
43.success { color: green; }
44.done { text-decoration: line-through; }

Arkusz CSS dołączamy do szablonu bazowego projekty_flask/todo/templates/index.html w sekcji head:

Plik index.html. Kod nr
1<!-- projekty_flask/todo/templates/index.html -->
2<html>
3  <head>
4    <title>{{ config.SITE_NAME }}</title>
5    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
6  </head>

6.2.7.1. Ćwiczenie

Uruchom aplikację lub odśwież stroną z listą zadań zalogowanego użytkownika po dołączeniu stylów CSS.

../../_images/todo_zadania_ewy_css.png

6.2.8. Zadania dodatkowe

Dodaj możliwość usuwania zadań, czyli:

  • w szablonie listy zadań dodaj formularz podobny do formularza oznaczania zadań jako wykonane, z którego identyfikator zadania wysyłany jest do widoku usun,

  • w blueprincie todo.py dodaj wymagający zalogowania się widok usun() obsługujący adres URL /usun, który przy użyciu klauzuli SQL DELETE FROM usuwa z bazy zadanie o odczytanym z formularza identyfikatorze należące do zalogowanego użytkownika,

  • usuń pierwsze zadanie użytkownika ewa.

../../_images/todo_zadanie_usunieto.png

6.2.9. Materiały


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:

2026-04-19 o 17:42 w Sphinx 7.3.7

Autorzy:

Robert Bednarz