7.2. ToDo
Realizacja aplikacji internetowej ToDo (lista zadań do zrobienia) w oparciu o framework Flask 0.12.x. Aplikacja umożliwia dodawanie z określoną datą, przeglądanie i oznaczanie jako wykonane różnych zadań, które zapisywane będą w bazie danych SQLite.
Początek pracy jest taki sam, jak w przypadku aplikacji Quiz.
Wykonujemy dwa pierwsze punkty „Projekt i aplikacja” oraz „Strona główna”,
tylko katalog aplikacji nazywamy todo
, a kod zapisujemy w pliku todo.py
.
Po wykonaniu wszystkich kroków i uruchomieniu serwera testowego powinniśmy w przeglądarce zobaczyć stronę główną:

7.2.1. Model danych i baza
Jako źródło danych aplikacji wykorzystamy tym razem bazę SQLite3 obsługiwaną za pomocą Pythonowego modułu sqlite3.
Model danych: w katalogu aplikacji tworzymy plik schema.sql
,
który zawiera instrukcje języka SQL
tworzące tabelę z zadaniami i dodające przykładowe dane.
1-- todo/schema.sql
2
3-- tabela z zadaniami
4DROP TABLE IF EXISTS zadania;
5CREATE TABLE zadania (
6 id integer primary key autoincrement, -- unikalny indentyfikator
7 zadanie text not null, -- opis zadania do wykonania
8 zrobione boolean not null, -- informacja czy zadania zostalo juz wykonane
9 data_pub datetime not null -- data dodania zadania
10);
11
12-- pierwsze dane
13INSERT INTO zadania (id, zadanie, zrobione, data_pub)
14VALUES (null, 'Wyrzucić śmieci', 0, datetime(current_timestamp));
15INSERT into zadania (id, zadanie, zrobione, data_pub)
16VALUES (null, 'Nakarmić psa', 0, datetime(current_timestamp));
W terminalu wydajemy teraz następujące polecenia:
~/todo$ sqlite3 db.sqlite < schema.sql
~/todo$ sqlite3 db.sqlite
sqlite> select * from zadania;
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 zadania. Interpreter zamykamy poleceniem .quit
.

7.2.2. Połączenie z bazą
Bazę danych już mamy, teraz pora napisać funkcje umożiwiające łączenie się
z nią z poziomu naszej aplikacji. W pliku todo.py
dodajemy brakujący kod:
1# -*- coding: utf-8 -*-
2# todo/todo.py
3
4from flask import Flask, g
5from flask import render_template
6import os
7import sqlite3
8
9app = Flask(__name__)
10
11app.config.update(dict(
12 SECRET_KEY='bardzosekretnawartosc',
13 DATABASE=os.path.join(app.root_path, 'db.sqlite'),
14 SITE_NAME='Moje zadania'
15))
16
17
18def get_db():
19 """Funkcja tworząca połączenie z bazą danych"""
20 if not g.get('db'): # jeżeli brak połączenia, to je tworzymy
21 con = sqlite3.connect(app.config['DATABASE'])
22 con.row_factory = sqlite3.Row
23 g.db = con # zapisujemy połączenie w kontekście aplikacji
24 return g.db # zwracamy połączenie z bazą
25
26
27@app.teardown_appcontext
28def close_db(error):
29 """Zamykanie połączenia z bazą"""
30 if g.get('db'):
31 g.db.close()
32
33
34@app.route('/')
35def index():
36 # return 'Cześć, tu Python!'
37 return render_template('index.html')
38
39
40if __name__ == '__main__':
41 app.run(debug=True)
Konfiguracja aplikacji przechowywana jest w obiekcie config
, który
jest podklasą słownika i w naszym przypadku zawiera:
SECRET_KEY
– sekretna wartość wykorzystywana do obsługi sesji;DATABSE
– ścieżka do pliku bazy;SITE_NAME
– nazwa aplikacji.
Funkcja get_db()
:
if not g.get('db'):
– sprawdzamy, czy obiektg
aplikacji, służący do przechowywania danych kontekstowych, nie zawiera właściwościdb
, czyli połączenia z bazą;dalsza część kodu tworzy połączenie w zmiennej
con
i zapisuje w kontekście (obiekcieg
) aplikacji.
Funkcja close_db()
:
@app.teardown_appcontext
– dekorator, który rejestruje funkcję zamykającą połączenie z bazą do wykonania po zakończeniu obsługi żądania;g.db.close()
– zamknięcie połączenia z bazą.
7.2.3. Lista zadań
Dodajemy widok, czyli funkcję zadania()
powiązaną z adresem URL /zadania
:
40@app.route('/zadania')
41def zadania():
42 db = get_db()
43 kursor = db.execute('SELECT * FROM zadania ORDER BY data_pub DESC;')
44 zadania = kursor.fetchall()
45 return render_template('zadania_lista.html', zadania=zadania)
db = get_db()
– utworzenie obiektu bazy danych ();db.execute('select...')
– wykonanie podanego zapytania SQL, czyli pobranie wszystkich zadań z bazy;fetchall()
– metoda zwraca pobrane dane w formie listy;
Szablon tworzymy w pliku todo/templates/zadania_lista.html
:
1<!-- todo/templates/zadania_lista.html -->
2<html>
3 <head>
4 <!-- nazwa aplikacji pobrana z ustawień -->
5 <title>{{ config.SITE_NAME }}</title>
6 </head>
7 <body>
8 <h1>{{ config.SITE_NAME }}:</h1>
9
10 <ol>
11 <!-- wypisujemy kolejno wszystkie zadania -->
12 {% for zadanie in zadania %}
13 <li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></li>
14 {% endfor %}
15 </ol>
16
17 </body>
18</html>
{% %}
– tagi używane w szablonach do instrukcji sterujących;{{ }}
– tagi używane do wstawiania wartości zmiennych;{{ config.SITE_NAME }}
– w szablonie mamy dostęp do obiektu ustawieńconfig
;{% for zadanie in zadania %}
– pętla odczytująca zadania z listy przekazanej do szablonu w zmiennejzadania
;
7.2.3.1. Odnośniki
W szablonie index.html
warto wstawić link do strony z listą zadań,
czyli kod:
<p><a href="{{ url_for('zadania') }}">Lista zadań</a></p>
url_for('zadania')
– funkcja dostępna w szablonach, generuje adres powiązany z podaną nazwą funkcji.
Ćwiczenie
Wstaw link do strony głównej w szablonie listy zadań. Po odwiedzeniu strony 127.0.0.1:5000/zadania powinniśmy zobaczyć listę zadań.

7.2.4. Dodawanie zadań
Po wpisaniu adresu w przeglądarce i naciśnięciu Enter, wysyłamy do serwera żądanie typu GET, które obsługujemy zwracając klientowi odpowiednie dane (listę zadań). Dodawanie zadań wymaga przesłania danych z formularza na serwer – są to żądania typu POST, które modyfikują dane aplikacji.
Na początku pliku todo.py
trzeba, jak zwykle, zaimportować wymagane funkcje:
8from datetime import datetime
9from flask import flash, redirect, url_for, request
Następnie rozbudujemy widok listy zadań:
43@app.route('/zadania', methods=['GET', 'POST'])
44def zadania():
45 error = None
46 if request.method == 'POST':
47 zadanie = request.form['zadanie'].strip()
48 if len(zadanie) > 0:
49 zrobione = '0'
50 data_pub = datetime.now()
51 db = get_db()
52 db.execute('INSERT INTO zadania VALUES (?, ?, ?, ?);',
53 [None, zadanie, zrobione, data_pub])
54 db.commit()
55 flash('Dodano nowe zadanie.')
56 return redirect(url_for('zadania'))
57
58 error = 'Nie możesz dodać pustego zadania!' # komunikat o błędzie
59
60 db = get_db()
61 kursor = db.execute('SELECT * FROM zadania ORDER BY data_pub DESC;')
62 zadania = kursor.fetchall()
63 return render_template('zadania_lista.html', zadania=zadania, error=error)
methods=['GET', 'POST']
– w liście wymieniamy typy obsługiwanych żądań;request.form['zadanie']
– dane przesyłane w żądaniach POST odczytujemy ze słownikaform
;db.execute(...)
– wykonujemy zapytanie, które dodaje nowe zadanie, w miejsce symboli zastępczych(?, ?, ?, ?)
wstawione zostaną dane z listy podanej jako drugi parametr;flash()
– funkcja pozwala przygotować komunikaty dla użytkownika, które można będzie wstawić w szablonie;redirect(url_for('zadanie'))
– przekierowanie użytkownika na adres związany z podanym widokiem – żądanie typu GET.
Warto zauważyć, że do szablonu przekazujemy dodatkową zmienną error
.
W szablonie zadania_lista.html
po znaczniku <h1>
umieszczamy kod:
10 <!-- formularz dodawania zadania -->
11 <form class="add-form" method="POST" action="{{ url_for('zadania') }}">
12 <input name="zadanie" value=""/>
13 <button type="submit">Dodaj zadanie</button>
14 </form>
15
16 <!-- informacje o sukcesie lub błędzie -->
17 <p>
18 {% if error %}
19 <strong class="error">Błąd: {{ error }}</strong>
20 {% endif %}
21
22 {% for message in get_flashed_messages() %}
23 <strong class="success">{{ message }}</strong>
24 {% endfor %}
25 </p>
{% if error %}
– sprawdzamy, czy zmiennaerror
cokolwiek zawiera;{% for message in get_flashed_messages() %}
– pętla odczytująca komunikaty;

7.2.5. Style CSS
O wyglądzie aplikacji decydują arkusze stylów CSS. Umieszczamy je w podkatalogu static
folderu aplikacji. Tworzymy więc plik ~/todo/static/style.css
z przykładowymi definicjami:
1/* todo/static/style.css */
2
3body { margin-top: 20px; background-color: lightgreen; }
4h1, p { margin-left: 20px; }
5.add-form { margin-left: 20px; }
6ol { text-align: left; }
7em { font-size: 11px; margin-left: 10px; }
8form { display: inline-block; margin-bottom: 0;}
9input[name="zadanie"] { width: 300px; }
10input[name="zadanie"]:focus {
11 border-color: blue;
12 border-radius: 5px;
13}
14li { margin-bottom: 5px; }
15button {
16 padding: 3px 5px;
17 cursor: pointer;
18 color: blue;
19 font-size: 12px/1.5em;
20 background: white;
21 border: 1px solid grey;
22}
23.error { color: red; }
24.success { color: green; }
25.done { text-decoration: line-through; }
Arkusz CSS dołączamy do pliku zadania_lista.html
w sekcji head
:
3 <head>
4 <!-- nazwa aplikacji pobrana z ustawień -->
5 <title>{{ config.SITE_NAME }}</title>
6 <!-- dołączamy arkusz CSS -->
7 <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
8 </head>
Ćwiczenie
Dołącz arkusz stylów CSS również do szablonu index.html
. Odśwież aplikację w przeglądarce.

7.2.6. Zadania wykonane
Do każdego zadania dodamy formularz, którego wysłanie będzie oznaczało,
że wykonaliśmy dane zadanie, czyli zmienimy atrybut zrobione
wpisu
z 0 (niewykonane) na 1 (wykonane). Odpowiednie żądanie typu POST
obsłuży nowy widok w pliku todo.py
, który wstawiamy
przed kodem uruchamiającym aplikację (if __name__ == '__main__':
):
65@app.route('/zrobione', methods=['POST'])
66def zrobione():
67 """Zmiana statusu zadania na wykonane."""
68 zadanie_id = request.form['id']
69 db = get_db()
70 db.execute('UPDATE zadania SET zrobione=1 WHERE id=?', [zadanie_id])
71 db.commit()
72 flash('Zmieniono status zadania.')
73 return redirect(url_for('zadania'))
zadanie_id = request.form['id']
– odczytujemy przesłany identyfikator zadania;db.execute('UPDATE zadania SET zrobione=1 WHERE id=?', [zadanie_id])
– wykonujemy zapytanie aktualizujące staus zadania.
W szablonie zadania_lista.html
modyfikujemy fragment wyświetlający
listę zadań i dodajemy formularz:
29 <ol>
30 <!-- wypisujemy kolejno wszystkie zdania -->
31 {% for zadanie in zadania %}
32 <li>
33 <!-- wyróżnienie zadań zakończonych -->
34 {% if zadanie.zrobione %}
35 <span class="done">{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></span>
36 {% else %}
37 {{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
38 {% endif %}
39
40 <!-- formularz zmiany statusu zadania -->
41 {% if not zadanie.zrobione %}
42 <form method="POST" action="{{ url_for('zrobione') }}">
43 <!-- wysyłamy jedynie informacje o id zadania -->
44 <input type="hidden" name="id" value="{{ zadanie.id }}"/>
45 <button type="submit">Wykonane</button>
46 </form>
47 {% endif %}
48 </li>
49 {% endfor %}
50 </ol>
Możemy dodawać zadania oraz zmieniać ich status.

7.2.7. Zadania dodatkowe
Dodaj możliwość usuwania zadań.
Dodaj mechanizm logowania użytkownika tak, aby użytkownik mógł dodawać i edytować tylko swoją listę zadań.
7.2.8. Materiały
Źródła:
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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: