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:

Terminal nr
~$ 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:

Kod nr
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ą:

Terminal nr
~/todo$ python todo.py
../../_images/serwer1.jpg

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ą.

../../_images/todo1.png

7.2.2. Widok (strona główna)

W pliku todo.py umieszcamy funkcję index(), domyślny widok naszej strony:

Kod nr
 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!”

../../_images/todo2.png

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.

Kod nr
 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.

Terminal nr
~/todo$ sqlite3 db.sqlite < schema.sql
~/todo$ sqlite3 db.sqlite
~/todo$ select * from zadania;
../../_images/sqlite.png

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:

Kod nr
 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:

Kod nr
from flask import render_template

Następnie modyfikujemy funkcję index():

Kod nr
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:

Plik zadania_lista.html. Kod nr
 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ń.

../../_images/todo4.png

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:

Kod nr
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:

Kod nr
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:

Plik zadania_lista.html. Kod nr
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.

../../_images/todo5.png

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:

Plik style.css. Kod nr
 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... >:

Plik zadania_lista.html. Kod nr
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.

../../_images/todo6.png

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__':):

Kod nr
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:

Plik zadania_lista.html. Kod nr
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.

../../_images/todo7.png

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:


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:2017-09-08 o 19:38 w Sphinx 1.4.5
Autorzy:Patrz plik “Autorzy”