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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | -- todo/schema.sql
-- tabela z zadaniami
DROP TABLE IF EXISTS zadania;
CREATE TABLE zadania (
id integer primary key autoincrement, -- unikalny indentyfikator
zadanie text not null, -- opis zadania do wykonania
zrobione boolean not null, -- informacja czy zadania zostalo juz wykonane
data_pub datetime not null -- data dodania zadania
);
-- pierwsze dane
INSERT INTO zadania (id, zadanie, zrobione, data_pub)
VALUES (null, 'Wyrzucić śmieci', 0, datetime(current_timestamp));
INSERT into zadania (id, zadanie, zrobione, data_pub)
VALUES (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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | # -*- coding: utf-8 -*-
# todo/todo.py
from flask import Flask, g
from flask import render_template
import os
import sqlite3
app = Flask(__name__)
app.config.update(dict(
SECRET_KEY='bardzosekretnawartosc',
DATABASE=os.path.join(app.root_path, 'db.sqlite'),
SITE_NAME='Moje zadania'
))
def get_db():
"""Funkcja tworząca połączenie z bazą danych"""
if not g.get('db'): # jeżeli brak połączenia, to je tworzymy
con = sqlite3.connect(app.config['DATABASE'])
con.row_factory = sqlite3.Row
g.db = con # zapisujemy połączenie w kontekście aplikacji
return g.db # zwracamy połączenie z bazą
@app.teardown_appcontext
def close_db(error):
"""Zamykanie połączenia z bazą"""
if g.get('db'):
g.db.close()
@app.route('/')
def index():
# return 'Cześć, tu Python!'
return render_template('index.html')
if __name__ == '__main__':
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 41 42 43 44 45 | @app.route('/zadania')
def zadania():
db = get_db()
kursor = db.execute('SELECT * FROM zadania ORDER BY data_pub DESC;')
zadania = kursor.fetchall()
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!-- todo/templates/zadania_lista.html -->
<html>
<head>
<!-- nazwa aplikacji pobrana z ustawień -->
<title>{{ config.SITE_NAME }}</title>
</head>
<body>
<h1>{{ config.SITE_NAME }}:</h1>
<ol>
<!-- wypisujemy kolejno wszystkie zadania -->
{% for zadanie in zadania %}
<li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></li>
{% endfor %}
</ol>
</body>
</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:
8 9 | from datetime import datetime
from flask import flash, redirect, url_for, request
|
Następnie rozbudujemy widok listy zadań:
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | @app.route('/zadania', methods=['GET', 'POST'])
def zadania():
error = None
if request.method == 'POST':
zadanie = request.form['zadanie'].strip()
if len(zadanie) > 0:
zrobione = '0'
data_pub = datetime.now()
db = get_db()
db.execute('INSERT INTO zadania VALUES (?, ?, ?, ?);',
[None, zadanie, zrobione, data_pub])
db.commit()
flash('Dodano nowe zadanie.')
return redirect(url_for('zadania'))
error = 'Nie możesz dodać pustego zadania!' # komunikat o błędzie
db = get_db()
kursor = db.execute('SELECT * FROM zadania ORDER BY data_pub DESC;')
zadania = kursor.fetchall()
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 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <!-- formularz dodawania zadania -->
<form class="add-form" method="POST" action="{{ url_for('zadania') }}">
<input name="zadanie" value=""/>
<button type="submit">Dodaj zadanie</button>
</form>
<!-- informacje o sukcesie lub błędzie -->
<p>
{% if error %}
<strong class="error">Błąd: {{ error }}</strong>
{% endif %}
{% for message in get_flashed_messages() %}
<strong class="success">{{ message }}</strong>
{% endfor %}
</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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* todo/static/style.css */
body { margin-top: 20px; background-color: lightgreen; }
h1, p { margin-left: 20px; }
.add-form { margin-left: 20px; }
ol { text-align: left; }
em { font-size: 11px; margin-left: 10px; }
form { display: inline-block; margin-bottom: 0;}
input[name="zadanie"] { width: 300px; }
input[name="zadanie"]:focus {
border-color: blue;
border-radius: 5px;
}
li { margin-bottom: 5px; }
button {
padding: 3px 5px;
cursor: pointer;
color: blue;
font-size: 12px/1.5em;
background: white;
border: 1px solid grey;
}
.error { color: red; }
.success { color: green; }
.done { text-decoration: line-through; }
|
Arkusz CSS dołączamy do pliku zadania_lista.html
w sekcji head
:
3 4 5 6 7 8 | <head>
<!-- nazwa aplikacji pobrana z ustawień -->
<title>{{ config.SITE_NAME }}</title>
<!-- dołączamy arkusz CSS -->
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</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 66 67 68 69 70 71 72 73 | @app.route('/zrobione', methods=['POST'])
def zrobione():
"""Zmiana statusu zadania na wykonane."""
zadanie_id = request.form['id']
db = get_db()
db.execute('UPDATE zadania SET zrobione=1 WHERE id=?', [zadanie_id])
db.commit()
flash('Zmieniono status zadania.')
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <ol>
<!-- wypisujemy kolejno wszystkie zdania -->
{% for zadanie in zadania %}
<li>
<!-- wyróżnienie zadań zakończonych -->
{% if zadanie.zrobione %}
<span class="done">{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></span>
{% else %}
{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
{% endif %}
<!-- formularz zmiany statusu zadania -->
{% if not zadanie.zrobione %}
<form method="POST" action="{{ url_for('zrobione') }}">
<!-- wysyłamy jedynie informacje o id zadania -->
<input type="hidden" name="id" value="{{ zadanie.id }}"/>
<button type="submit">Wykonane</button>
</form>
{% endif %}
</li>
{% endfor %}
</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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |