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

../../_images/todo_01.png

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.

plik schema.pl 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));

W terminalu wydajemy teraz następujące polecenia:

Terminal nr
~/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.

../../_images/sqlite.png

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:

Plik todo.py 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
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 obiekt g aplikacji, służący do przechowywania danych kontekstowych, nie zawiera właściwości db, czyli połączenia z bazą;
  • dalsza część kodu tworzy połączenie w zmiennej con i zapisuje w kontekście (obiekcie g) 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:

Plik todo.py Kod nr
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:

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

7.2.3.1. Odnośniki

W szablonie index.html warto wstawić link do strony z listą zadań, czyli kod:

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

../../_images/todo_03_zadania.png

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:

Kod nr
8
9
from datetime import datetime
from flask import flash, redirect, url_for, request

Następnie rozbudujemy widok listy zadań:

Kod nr
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łownika form;
  • 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:

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('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 zmienna error cokolwiek zawiera;
  • {% for message in get_flashed_messages() %} – pętla odczytująca komunikaty;
../../_images/todo_04_dodawanie.png

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:

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

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

../../_images/todo_05_css.png

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

Plik todo.py Kod nr
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:

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

../../_images/todo_06_zrobione.png

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:


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:2022-05-22 o 19:52 w Sphinx 1.5.3
Autorzy:Patrz plik “Autorzy”