7.2. ToDo¶
Realizacja prostej listy ToDo (lista zadań do zrobienia) jako aplikacji internetowej, z wykorzystaniem Pythona i frameworka Flask w wersji 0.10.1. 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.
7.2.1. Projekt i aplikacja¶
W katalogu użytkownika tworzymy nowy katalog dla aplikacji todo
,
a w nim plik główny todo.py
:
~$ mkdir todo; cd todo; touch todo.py
Utworzymy szkielet aplikacji Flask, co pozwoli na uruchomienie testowego serwera www,
umożliwiającego wygodne rozwijanie kodu. W pliku todo.py
wpisujemy:
1 2 3 4 5 6 7 8 9 | # -*- coding: utf-8 -*-
# todo/todo.py
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
app.run(debug=True)
|
Serwer uruchamiamy komendą:
~/todo$ python todo.py
Domyślnie serwer uruchamia się pod adresem http://127.0.0.1:5000. Po wpisaniu go do przeglądarki internetowej otrzymamy kod odpowiedzi HTTP 404, tj. błąd “nie znaleziono”, co wynika z faktu, że nasza aplikacja nie ma jeszcze zdefiniowanego żadnego widoku dla tego adresu.
Odpowiedź aplikacji, tzw. widok, to funkcja obsługująca wywołania powiązanego z nim adresu. Widok (funkcja) zwraca najczęściej użytkownikowi wyrenderowaną z szablonu stronę internetową.
7.2.2. Widok (strona główna)¶
W pliku todo.py
umieszcamy funkcję index()
, domyślny widok naszej strony:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # -*- coding: utf-8 -*-
# todo/todo.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Cześć, tu Python!'
if __name__ == '__main__':
app.run(debug=True)
|
Widok index()
za pomocą dekoratora @app.route('/')
związaliśmy z adresem głównym (/).
Po odświeżeniu adresu 127.0.0.1:5000 zamiast błędu powinniśmy zobaczyć napis: “Cześć, tu Python!”
7.2.3. Model bazy danych¶
W katalogu aplikacji tworzymy plik schema.sql
, który zawiera opis
struktury tabeli z zadaniami. Do tabeli wprowadzimy 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));
|
Tworzymy bazę danych w pliku db.sqlite
, łączymy się z nią i
próbujemy wyświetlić dane, które powinny były zostać zapisane w tabeli zadania:
Pracę z bazą kończymy poleceniem .quit
.
~/todo$ sqlite3 db.sqlite < schema.sql
~/todo$ sqlite3 db.sqlite
~/todo$ select * from zadania;
7.2.4. Połączenie z bazą danych¶
Bazę danych już mamy, teraz pora napisać funkcje umożiwiające łączenie się
z nią z poziomu naszej aplikacji. W pliku todo.py
dodajemy:
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 | # -*- coding: utf-8 -*-
# todo/todo.py
from flask import Flask, g
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 hasattr(g, '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_request
def close_db(error):
"""Zamykanie połączenia z bazą"""
if hasattr(g, 'db'):
g.db.close()
@app.route('/')
def index():
return 'Cześć, tu Python!'
if __name__ == '__main__':
app.run(debug=True)
|
Na początku uzpełniliśmy importy. Następnie w konfiguracji aplikacji dodaliśmy
klucz zabezpieczający sesję, ustawiliśmy ścieżkę do pliku bazy danych
w katalogu aplikacji (stąd użycie funkcji app.root_path
) oraz nazwę aplikacji.
Utworzyliśmy również dwie funkcje odpowiedzialne za nawiązywanie
(get_db
) i kończenie (close_db
) połączenia z bazą danych.
7.2.5. Lista zadań¶
Wyświetlanie danych umożliwia wbudowany we Flask system szablonów,
czyli mechanizm renderowania kodu HTML i żądanych danych.
Na początku pliku todo.py
dopisujemy wymagany import:
from flask import render_template
Następnie modyfikujemy funkcję index()
:
36 37 38 39 40 41 42 | @app.route('/')
def index():
# return 'Cześć, tu Python!'
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)
|
W widoku index()
tworzymy obiekt bazy danych (db = get_db()
)
i wykonujemy zapytanie (db.execute('select...')
), by pobrać z bazy
wszystkie zadania. Metoda fetchall()
zwraca nam pobrane dane w formie listy.
Na koniec wywołujemy funkcję render_template()
, przekazując jej
nazwę szablonu oraz pobrane zadania. Wyrenderowany szablon zwracamy do użytkownika.
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 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <!-- todo/templates/zadania_lista.html -->
<html>
<head>
<!-- nazwa aplikacji pobrana z ustawień -->
<title>{{ config.SITE_NAME }}</title>
</head>
<body>
<h1>{{ config.SITE_NAME }}:</h1>
<!-- formularz dodawania zadania -->
<form class="add-form" method="POST" action="{{ url_for('index') }}">
<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>
<ol>
<!-- wypisujemy kolejno wszystkie zadania -->
{% for zadanie in zadania %}
<li>
{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
</li>
{% endfor %}
</ol>
</body>
</html>
|
Wewnątrz szablonu przeglądamy wszystkie wpisy (zadania) i umieszczamy
je na liście HTML. Do szablonu automatycznie przekazywany jest obiekt
config
(ustawienia aplikacji), z którego pobieramy tytuł strony (SITE_NAME).
Po odwiedzeniu strony 127.0.0.1:5000 powinniśmy zobaczyć listę zadań.
7.2.6. Dodawanie zadań¶
Wpisując adres w polu adresu przeglądarki, 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:
from datetime import datetime
from flask import flash, redirect, url_for, request
Następnie do widoku strony głównej dopisujemy kod obsługujący zapisywanie danych:
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | @app.route('/', methods=['GET', 'POST'])
def index():
"""Główny widok strony. Obsługuje wyświetlanie i dodawanie zadań."""
error = None
if request.method == 'POST':
if len(request.form['zadanie']) > 0:
zadanie = request.form['zadanie']
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('index'))
error = u'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)
|
W dekoratorze dodaliśmy obsługę żądań POST, w widoku index()
natomiast
instrukcję warunkową (if
), która je wykrywa.
Dlej sprawdzamy, czy przesłane pole formularza jest puste. Jeśli tak, ustawiamy zmienną error
.
Jeśli nie, przygotowujemy dane, łączymy się z bazą, zapisujemy nowe zadanie
i tworzymy koumnikat potwierdzający.
Na koniec przekierowujemy użytkownika do widoku głównego (redirect(url_for('index'))
),
ale tym razem z żądaniem GET, którego obsługa jest taka jak poprzednio,
czyli zwracamy listę zadań.
Warto zauważyć, że do szablonu możemy przekazywać wiele danych, w naszym przypadku
zmienną error
zawierającą komunikat błędu. Lepszym sposobem zwracania
informacji użytkownikowi jest wykorzystanie dedykowanej funkcji flash()
.
Do szablonu zadania_lista.html
po znaczniku <h1>
wstawiamy formularz
oraz kod wyświetlający komunikaty:
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('index') }}">
<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>
|
Warto zwrócić uwagę na wykorzystanie wbudowanej funkcji url_for
,
która zamienia nazwę widoku (w tym wypadku index
) na powiązany z nim
adres URL (w tym wypadku /
). W ten sposób łączymy formularz
z widokiem (funkcją), który obsługuje dany adres.
7.2.7. Wygląd aplikacji¶
Wygląd aplikacji możemy zdefiniować w arkuszu stylów CSS, który umieścimy
w podkatalogu static
aplikacji. Tworzymy 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: 0;
cursor: pointer;
font-size: 11px;
background: white;
border: none;
color: blue;
}
.error { color: red; }
.success { color: green; }
.done { text-decoration: line-through; }
|
Arkusz CSS podpinamy do pliku zadania_lista.html
, dodając w sekcji head znacznik <link... >
:
3 4 5 6 7 8 | <head>
<!-- nazwa aplikacji pobrana z ustawień -->
<title>{{ config.SITE_NAME }}</title>
<!-- ładujemy arkusz CSS -->
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
|
Dzięki temu nasza aplikacja nabierze nieco lepszego wyglądu.
7.2.8. 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 po widoku
głównym i przed kodem uruchamiającym aplikację (if __name__ == '__main__':
):
64 65 66 67 68 69 70 71 | @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()
return redirect(url_for('index'))
|
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 51 52 53 54 55 | <ol>
<!-- wypisujemy kolejno wszystkie zdania -->
{% for zadanie in zadania %}
<li>
<!-- wyróżnienie zadań zakończonych -->
{% if zadanie.zrobione %}
<span class="done">
{% endif %}
{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
<!-- wyróżnienie zadań zakończonych -->
{% if zadanie.zrobione %}
</span>
{% 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>
|
Aplikację można uznać za skończoną. Możemy dodawać zadania oraz zmieniać ich status.
7.2.8.1. 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ń. Wprowadź osobne listy zadań dla każdego użytkownika.
7.2.9. 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: | 2017-09-08 o 19:38 w Sphinx 1.4.5 |
---|---|
Autorzy: | Patrz plik “Autorzy” |