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

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# -*- 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 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@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:

Plik zadania_lista.html. Kod nr
 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 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
8from datetime import datetime
9from flask import flash, redirect, url_for, request

Następnie rozbudujemy widok listy zadań:

Kod nr
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ł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    <!-- 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 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/* 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:

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

../../_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@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:

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

../../_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:

2025-04-12 o 10:21 w Sphinx 7.3.7

Autorzy:

Patrz plik „Autorzy”