6.2. ToDo
Aplikacja internetowa ToDo w oparciu o framework Realizacja aplikacji internetowej Quiz w oparciu o framework Flask 3.1.x i bazę danych SQLite. Aplikacja umożliwi dodawanie przez zalogowanego użytkownika zadań z określoną datą, ich przeglądanie i oznaczanie jako wykonane.
Zalecamy zapoznanie się z materiałami zawartymi w scenariuszach:
Do pracy potrzebne nam będzie wirtualne środowisko Pythona z zainstalowanym pakietem Flask. Początek pracy jest taki sam, jak w przypadku aplikacji Quiz, tzn.:
przygotowujemy wirtualne środowisko Pythona w katalogu
projekty_flask, chyba że zrobiliśmy to wcześniej podczas realizacji aplikacji Quiz;w katalogu
projekty_flasktworzymy katalog aplikacji:todo;wykonujemy 2. i 3. punkt scenariusza Quiz, tj.: „Projekt i aplikacja” oraz „Strona główna”.
W pliku app.py zmieniamy w konfiguracji aplikacji nazwę serwisu
zapisaną w kluczu SITE_NAME na „Projekty Flask”.
6.2.1. Model danych i baza
Jako źródło danych aplikacji wykorzystamy tym razem bazę SQLite3 obsługiwaną za pomocą modułu Pythona sqlite3.
Model danych, tj. w tym przypadku schemat bazy danych, zdefiniujemy w pliku modele.sql,
który tworzymy w katalogu aplikacji i wypełniamy kodem SQL:
1-- projekty_flask/todo/modele.sql
2
3-- tabela z użytkownikami
4DROP TABLE IF EXISTS user;
5CREATE TABLE user (
6 id INTEGER PRIMARY KEY AUTOINCREMENT, -- unikalny identyfikator
7 login TEXT UNIQUE NOT NULL, -- nazwa użytkownika
8 haslo TEXT NOT NULL -- hasło użytkownika
9);
10-- przykładowy użytkownik
11INSERT INTO user (id, login, haslo)
12VALUES (null, 'adam', 'scrypt:32768:8:1$b6ySf4OhUqADg4os$9fab79b9175c7e1ac341d06b72a3bb3e3a213733c6211bfa7f2b388988065e837df630be38e7eb5729d59db4f5e7d0abd7886e0697125f1a0e8a0eadd6a9eb3a');
13
14-- tabela z zadaniami
15DROP TABLE IF EXISTS zadanie;
16CREATE TABLE zadanie (
17 id INTEGER PRIMARY KEY AUTOINCREMENT, -- unikalny identyfikator zadania
18 user_id INTEGER, -- identyfikator użytkownika
19 zadanie TEXT NOT NULL, -- opis zadania do wykonania
20 zrobione BOOLEAN NOT NULL, -- informacja czy zadania zostało juz wykonane
21 data_pub DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- data dodania zadania
22 FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE -- wskazanie klucza obcego
23);
24
25-- początkowe dane
26INSERT INTO zadanie VALUES (null, 1, 'Wyrzucić śmieci', 0, CURRENT_TIMESTAMP);
27INSERT into zadanie VALUES (null, 1, 'Nakarmić psa', 0, CURRENT_TIMESTAMP);
Wykonanie klauzul SQL spowoduje utworzenie dwóch tabel i wypełnienie ich przykładowymi danymi. Tabele:
user– zawierać będzie identyfikator, nazwę i hasło użytkownika,zadanie– zawierać będzie identyfikator zadania, identyfikator użytkownika, treść zadania, oznaczenie wykonania oraz datę dodania.
Funkcje potrzebne do obsługi bazy danych umieścimy w nowym pliku db.py, który zapisujemy
w katalogu aplikacji.
1import sqlite3
2from flask import g, current_app
3
4def get_db():
5 """Funkcja tworzy połączenie z bazą"""
6 if 'db' not in g:
7 g.db = sqlite3.connect(
8 current_app.config['DATABASE'],
9 detect_types = sqlite3.PARSE_DECLTYPES
10 )
11 g.db.row_factory = sqlite3.Row
12 return g.db
13
14def close_db(e=None):
15 """Funkcja zamyka połączenia z bazą"""
16 db = g.pop('db', None)
17 if db is not None:
18 db.close()
19
20def init_app(app):
21 """Funkcja rejestruje funkcję close_db() w aplikacji"""
22 app.teardown_appcontext(close_db)
23
24def query_db(query, args=(), one=False):
25 """Funkcja wykonuje zapytania SQL i zwraca rezultaty"""
26 cur = get_db().execute(query, args)
27 rv = cur.fetchall()
28 cur.close()
29 return (rv[0] if rv else None) if one else rv
30
31def init_db():
32 """Funkcja tworzy bazę i tabele"""
33 from werkzeug.security import generate_password_hash
34 db = get_db()
35 with current_app.open_resource('modele.sql') as f:
36 db.executescript(f.read().decode('utf8'))
Informacja
Podczas działania aplikacji mamy dostęp do tzw. kontekstu, który zawiera
ustawienia zapisane w słowniku config oraz dane w obiektach current_app i g.
Zadaniem funkcji get_db() będzie połączenie z bazą danych i zapisanie obiektu
db reprezentującego bazę w kontekście aplikacji:
if 'db' not in g:– sprawdzamy, czy w obiekciegnie ma obiektudb;dalsza część kodu tworzy połączenie wywołując metodę
sqlite3.connect()i zapisuje je w kontekście aplikacji.
Funkcja close_db() odpowiadać będzie za zamknięcie połączenia. Będzie ona wywoływana
po obsłużeniu każdego żądania dzięki kolejnej funkcji init_app(), która
rejestruje funkcję close_db() w kontekście aplikacji: app.teardown_appcontext(close_db).
Funkcja query_db() ułatwi wykonywanie zapytań SQL odczytujących dane z bazy.
Pierwszy wymagany argument to zapytanie SELECT, drugi opcjonalny to lista wartości
wstawianych do klauzuli WHERE. Domyślnie funkcja zwracała będzie wszystkie
pobrane rekordy dzięki metodzie fetchall(). Jeżeli jednak w wywołaniu podamy trzeci argument
w postaci one=True, zwrócony zostanie tylko pierwszy pobrany rekord.
Funkcja init_db() posłuży do utworzenia pliku bazy danych, a następnie tabel
do przechowywania danych.
Pozostaje nam uzupełnienie kodu w pliku app.py:
1import os
2from flask import Flask, render_template, current_app
3from db import init_app, init_db
4
5app = Flask(__name__)
6
7# konfiguracja aplikacji
8app.config.update(dict(
9 SECRET_KEY='bardzosekretnawartosc',
10 SITE_NAME='Projekty Flask',
11 DATABASE=os.path.join(app.root_path, 'db.sqlite')
12))
13
14init_app(app)
15
16# rejestracja blueprintów
17
18
19@app.route('/')
20def index():
21 # return 'Cześć, tu Python i Flask!'
22 return render_template('index.html')
23
24with app.app_context():
25 if not os.path.exists(current_app.config['DATABASE']):
26 init_db()
27 if __name__ == "__main__":
28 app.run(debug=True)
Przede wszystkim uzupełniamy importy. Następnie w słowniku konfiguracji dodajemy
klucz DATABASE wskazujący na plik bazy danych db.sqlite.
Następnie umieszczamy wywołanie funkcji init_app(app), dzięki czemu jeżeli
na dysku nie będzie pliku bazy danych, zostanie on utworzony, a wraz z nim
tabele zdefiniowane w pliku todo.sql.
Po uruchomieniu serwera deweloperskiego i otwarciu adresu http://127.0.0.1:5000
powinniśmy zobaczyć stronę:
– a w katalogu aplikacji powinien zostać utworzony plik bazy danych db.sqlite.
Podpowiedź
Bazę danych można też utworzyć ręcznie za pomocą wiersza poleceń bazy Sqlite3. W terminalu w katalogu aplikacji możemy użyć następujących poleceń:
~/projekty_flask/todo$ sqlite3 db.sqlite < modele.sql
~/projekty_flask/todo$ sqlite3 db.sqlite
sqlite> select * from zadanie;
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 zadanie. Interpreter zamykamy poleceniem .quit.
6.2.2. Użytkownicy
Informacja
Jedna aplikacja Flask może składać się z wielu modułów odpowiedzialnych np. za
autoryzację użytkowników, system komentarzy, quiz czy listę zadań.
Obsługę tych składników warto rozdzielić na osobne moduły, nazywane we Flasku
blueprint, co ułatwi konstruowanie i rozszerzanie aplikacji.
Zadania do wykonania powiązane będą z użytkownikami, dlatego na początku zajmiemy się blueprintem, który pozwoli na logowanie, rejestrację i usuwanie użytkownika.
W katalogu aplikacji tworzymy plik users.py i dodajemy do niego poniższy kod:
1import functools
2from flask import (
3 Blueprint, flash, g, redirect, render_template, request, session, url_for
4)
5from werkzeug.security import check_password_hash, generate_password_hash
6from db import get_db, query_db
7
8bp = Blueprint('users', __name__, template_folder='templates', url_prefix='/users')
Blueprint jest instancją klasy (obiektem typu) Blueprint. Do konstruktora przekazujemy:
users– nazwę blueprinta,__name__– nazwa pliku, w którym blueprint jest definiowany,template_folder– podkatalog katalogu aplikacji, w którym należy szukać szablonów,url_prefix– część adresu URL, który blueprint będzie obsługiwał, w tym przypadku będzie to adreshttp://nazwa_serwera/users/.
6.2.2.1. (Wy)Logowanie
W utworzonym blueprincie zdefiniujemy widok loguj() powiązany z adresem URL http://nazwa_serwera/users/loguj,
który obsłuży żądania GET i POST:
10@bp.route('/loguj', methods=['GET', 'POST'])
11def loguj():
12 if request.method == 'POST':
13 login = request.form['login'].strip()
14 haslo = request.form['haslo'].strip()
15
16 error = None
17 user = query_db('SELECT * FROM user WHERE login = ?', [login], one=True)
18
19 if user is None:
20 error = 'Błędny login.'
21 elif not check_password_hash(user['haslo'], haslo):
22 error = 'Błędne hasło.'
23
24 if error is None:
25 session.clear()
26 session['user_id'] = user['id']
27 flash(f'Zalogowano użytkownika {user['login']}!')
28 return redirect(url_for('index'))
29 flash(error)
30
31 return render_template('users/user_loguj.html')
Jeżeli serwer otrzyma żądanie typu GET oraz w przypadku błędów logowania,
funkcja zwróci szablon users/loguj.html. Natomiast kiedy serwer otrzyma dane z formularza,
wykonamy następujące operacje:
request.form[]– odczytanie loginu i hasłu z przesłanego formularza;query_db('SELECT...', lista)– wykonanie zapytania SQL pobierającego dane użytkownika o podanym loginie, znak zapytania zostanie zastąpiony wartością z listy;przygotowanie informacji o błędzie w zmiennej
error, jeżeli w bazie nie ma użytkownika o podanym loginie (user is None) lub jeżeli podano błędne hasło (not check_password_hash(user['haslo'], haslo));session['user_id'] = user['id']– zapisanie identyfikatora użytkownika w sesji (zob. sesja), jeżeli dane logowania są poprawne;redirect(url_for('index'))przekierowanie zalogowanego użytkownika na stronę główną;flash()– zapisanie komunikatów dla użytkownika, które będzie można później odczytać w szablonie.
Po poprawnym zalogowaniu identyfikator użytkownika będzie dostępny w sesji podczas przetwarzania
kolejnych żądań. Będzie on odczytywany przez funkcję load_user(), którą umieszczamy w blueprincie:
33@bp.before_app_request
34def load_user():
35 user_id = session.get('user_id')
36 if user_id is None:
37 g.user = None
38 else:
39 g.user = query_db('SELECT * FROM user WHERE id = ?', [user_id], one=True)
Funkcja wykonywana będzie przed każdym żądaniem, ponieważ poprzedzamy ją dekoratorem @bp.before_app_request.
Zadaniem funkcji będzie pobranie danych zalogowanego użytkownika z bazy i zapisanie ich w kontekście g.user.
Adres http://nazwa_serwera/users/wyloguj będzie obsługiwany przez widok wyloguj():
41@bp.route('/wyloguj')
42def wyloguj():
43 session.clear()
44 flash(f'Wylogowano użytkownika {g.user['login']}.')
45 return redirect(url_for('index'))
Wylogowanie polega na usunięciu danych użytkownika z sesji, po czym nastąpi przekierowanie na stronę główną.
Na koniec dodamy jeszcze jedną funkcję login_required(), której użyjemy do zabezpieczenia dostępu niektórych widoków:
47def login_required(view):
48 @functools.wraps(view)
49 def wrapped_view(**kwargs):
50 if g.user is None:
51 return redirect(url_for('users.loguj'))
52 return view(**kwargs)
53 return wrapped_view
Dodana funkcja to dekorator, który jeżeli użytkownik nie będzie zalogowany, przekieruje go na stronę logowania, w przeciwnym razie zwróci żądany widok.
6.2.2.2. Rejestracja blueprinta
Użycie blueprinta wymaga zarejestrowania go w aplikacji.
Na początku pliku app.py importujemy moduł users, następnie poniżej komentarza
dodajemy kod:
import users
...
# rejestracja blueprintów
app.register_blueprint(users.bp)
6.2.3. Szablony
W rozbudowanych aplikacjach zawierających wiele blueprintów i widoków zwracających
szablony, część kodu HTML powtarza się na każdej stronie, co zapewnia spójność wyglądu.
Tę wspólną część, aby jej nie powielać, umieścimy w szablonie bazowym.
Użyjemy dotychczasowego pliku templates/index.html, w którym
umieszczamy poniższy kod:
1<!-- projekty_flask/todo/templates/index.html -->
2<html>
3 <head>
4 <title>{{ config.SITE_NAME }}</title>
5 </head>
6<body>
7 <h1>{% block h1 %} {{ config.SITE_NAME }} {% endblock %}</h1>
8
9 <nav>
10 <ul>
11 <li><a href="{{ url_for('index') }}">Strona główna »</a></li>
12 {% if g.user %}
13 <li><a href="{{ url_for('users.wyloguj') }}">Wyloguj się »</a></li>
14 <li><span>Zalogowany: {{ g.user['login'] }}</span></li>
15 {% else %}
16 <li><a href="{{ url_for('users.loguj') }}">Zaloguj się »</a></li>
17 {% endif %}
18 </ul>
19 </nav>
20
21 {% with komunikaty = get_flashed_messages() %}
22 {% if komunikaty %}
23 {% for komunikat in komunikaty %}
24 <p> {{ komunikat }} </p>
25 {% endfor %}
26 {% endif %}
27 {% endwith %}
28
29 {% block body %} {% endblock %}
30
31</body>
32</html>
W szablonach wykorzystujemy specjalne tagi dwóch rodzajów:
{% instrukcja %}– pozwalają używać instrukcji sterujących, np. warunkowych lub pętli, oraz definiować bloki, które będą uzupełniane przez szablony dziedziczące,{{ zmienna }}– służą wyświetlaniu wartości zmiennych lub wywoływaniu metod obiektów przekazanych do szablonu.
Zastosowanie tagów w szablonach:
{{ config.SITE_NAME }}– wstawienie nazwy serwisu zdefiniowanego w słowniku ustawień aplikacjiconfig;{% block nazwa %} {% endblock %}– zdefiniowanie bloku o podanej nazwie, którego zawartość może zostać nadpisana w szablonach dziedziczących;{% if g.user %} ... {% else %} ... {% endif %}– jeżeli użytkownik jest zalogowany, wstawiamy odnośnik „Wyloguj się” oraz informację, kto jest zalogowany, w przeciwnym razie wstawiamy odnośnik „Zaloguj się”;{{ url_for('index') }}– funkcjaurl_for()zwraca adres URL powiązany z podanym jako argument widokiem, poprzedzonym ewentualna nazwą blueprintu, w którym został zdefiniowany, np.{{ url_for('users.loguj') }};{% with komunikaty = get_flashed_messages() %} {% endwith %}– odczytanie komunikatów dla użytkownika utworzonych w widokach, jeżeli zostały utworzone ({% if komunikaty %}), odczytujemy je w pętli ({% for komunikat in komunikaty %}) i wstawiamy do szablonu w osobnych akapitach<p> {{ komunikat }} </p>.
Szablony blueprintów mogą być zapisywane w osobnych podkatalogach. W katalogu
projekty_flask/todo/templates tworzymy podkatalog o takiej samej nazwie
jak nasz blueprint, tj. users, a w nim plik user_loguj.html.
W utworzonym szablonie umieszczamy kod:
1<!-- projekty_flask/todo/templates/users/loguj.html -->
2{% extends "index.html" %}
3{% block h1 %} Logowanie użytkownika {% endblock %}
4{% block body %}
5 <form method="POST" action="{{ url_for('users.loguj') }}">
6 <label for="login">Login:</label>
7 <input type="text" id="login" name="login" value="" required>
8 <br>
9 <label for="haslo">Hasło:</label>
10 <input type="password" id="haslo" name="haslo" value="" required>
11 <br>
12 <button type="submit">Zaloguj</button>
13 </form>
14{% endblock %}
Tag {% extends "index.html" %} wskazuje szablon bazowy, z którego dziedziczymy kod.
W tagach {% block nazwa %} {% endblock %} wstawiamy kod charakterystyczny dla bieżącego szablonu.
W tym przypadku tworzymy formularz HTML pozwalający na wpisanie loginu i hasła i przesłanie
tych danych na adres zdefiniowany w atrybucie action obsługiwany przez widok loguj()
z blueprinta users.
Po uruchomieniu serwera deweloperskiego i wejściu na adres http://127.0.0.1:5000/users/loguj
powinniśmy zobaczyć stronę logowania:
6.2.3.1. Ćwiczenie
Spróbuj zalogować się na konto użytkownika utworzonego podczas dodawania do bazy przykładowych
danych. W formularzu podaj login adam i hasło zaq1@WSX. Po zalogowaniu
powinieneś zobaczyć stronę główną z odpowiednim komunikatem:
Po wylogowaniu podobnie:
6.2.4. Dodawanie i usuwanie kont
W blueprincie users.py umieścimy jeszcze dwa widoki, które umożliwią zarejestrowanie się
użytkownika oraz usuwanie konta:
55@bp.route('/dodaj', methods=['GET', 'POST'])
56def dodaj():
57 if request.method == 'POST':
58 login = request.form['login'].strip()
59 haslo = request.form['haslo'].strip()
60 db = get_db()
61 try:
62 db.execute('INSERT INTO user VALUES (?, ?, ?)',
63 [None, login, generate_password_hash(haslo)])
64 db.commit()
65 except db.IntegrityError:
66 flash(f'Podany login {login} jest już używany.')
67 else:
68 flash(f'Dodano konto {login}')
69 return redirect(url_for('index'))
70
71 return render_template('users/user_dodaj.html')
72
73@bp.route('/usun', methods=['GET', 'POST'])
74@login_required
75def usun():
76 if request.method == 'POST':
77 login = g.user['login']
78 user_id = g.user['id']
79 db = get_db()
80 db.execute('DELETE FROM user WHERE id = ?', [user_id])
81 db.commit()
82 flash(f'Usunięto użytkownika {login}!')
83 return redirect(url_for('index'))
84
85 return render_template('users/user_usun.html')
Po wysłaniu żądania GET na adres http://nazwa_serwera/users/dodaj widok dodaj()
zwróci szablon user_dodaj.html zawierający formularz. Po wypełnieniu i wysłaniu formularza,
czyli w przypadku żądania typu POST odczytujemy login oraz hasło i próbujemy dodać
do bazy nowego użytkownika wykonując klauzulę SQL INSERT INTO user ....
Warto zwrócić uwagę, że hasło jest szyfrowane za pomocą funkcji skrótu generate_password_hash().
Jeżeli podany login istnieje w bazie, zwrócony zostanie wyjątek db.IntegrityError,
tj. błąd integralności, wtedy przygotowujemy komunikat dla użytkownika i ponownie zawracamy
szablon z formularzem. Pod udanym dodaniu konta użytkownik zostanie przekierowany na stronę główną
z komunikatem potwierdzającym dodanie konta.
Zalogowany użytkownik będzie mógł usunąć konto po wejściu na stronę o adresie
http://nazwa_serwera/users/usun. Widok usun() obsługujący ten adres zabezpieczamy dodanym
wcześniej dekoratorem login_required. Jeżeli użytkownik potwierdzi chęć usunięcia konta
oraz wszystkich jego zadań przesyłając formularz z szablonu user_usun.html,
wykonamy klauzulę SQL DELETE FROM users ..., która usunie jego konto z bazy oraz kaskadowo
wszystkie powiązane zadania. Na koniec przekierujemy użytkownika na stronę główną.
Pozostaje dodanie szablonów zwracanych przez omówione widoki. W katalogu
projekty_flask/todo/templates/users:
dodajemy plik
user_dodaj.html:Plik user_dodaj.html Kod nr1<!-- projekty_flask/todo/templates/users/dodaj.html --> 2{% extends "index.html" %} 3{% block h1 %} Dodawanie konta {% endblock %} 4{% block body %} 5 <form method="POST" action="{{ url_for('users.dodaj') }}"> 6 <label for="login">Login:</label> 7 <input type="text" id="login" name="login" required> 8 <br> 9 <label for="haslo">Hasło:</label> 10 <input type="password" id="haslo" name="haslo" required> 11 <br> 12 <button type="submit">Dodaj</button> 13 </form> 14{% endblock %}
– oraz plik user_usun.html:
1<!-- projekty_flask/todo/templates/users/usun.html --> 2{% extends "index.html" %} 3{% block h1 %} Usuwanie użytkownika {% endblock %} 4{% block body %} 5 <form method="POST" action="{{ url_for('users.usun') }}"> 6 <p>Czy na pewno chcesz usunąć swoje konto oraz wszystkie zadania?</p> 7 <br> 8 <button type="submit">Usuń</button> 9 </form> 10{% endblock %}
6.2.4.1. Ćwiczenie
Dodaj do szablonu bazowego we właściwych sekcjach odnośniki pozwalające na dodawanie i usuwanie konta przez użytkownika.
Dodaj koto użytkownika
ewa, a następnie je usuń.
6.2.5. Lista zadań
Obsługę zadań umieścimy w osobnym blueprincie. W katalogu aplikacji projekty_flask/todo
tworzymy plik todo.py i wypełniamy kodem:
1from flask import (
2 Blueprint, flash, g, render_template, request, redirect, url_for
3)
4from db import get_db, query_db
5from users import login_required
6
7bp = Blueprint('todo', __name__, template_folder='templates', url_prefix='/todo')
8
9@bp.route('/')
10@login_required
11def index():
12 sql = 'SELECT * FROM zadanie WHERE user_id=? ORDER BY data_pub DESC'
13 zadania = query_db(sql, [g.user['id']])
14 return render_template('todo/index.html', zadania=zadania)
Widok index() wywoływany będzie po wejściu na adres http://nazwa_serwera/todo.
Jego zadaniem jest pobranie z bazy wszystkich zadań zalogowanego użytkownika i przekazanie ich do szablonu
w zmiennej zadania.
Szablon tworzymy w pliku projekty_flask/todo/templates/todo/index.html:
1<!-- projekty_flask/todo/templates/todo/index.html -->
2{% extends "index.html" %}
3{% block h1 %} Lista zadań {% endblock %}
4{% block body %}
5 <ol>
6 <!-- wypisujemy kolejno wszystkie zadania -->
7 {% for zadanie in zadania %}
8 <li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em></li>
9 {% endfor %}
10 </ol>
11{% endblock %}
W pętli {% for zadanie in zadania %} odczytujemy zadania z listy przekazanej
do szablonu, wypisujemy treść zadania i datę dodania.
6.2.5.1. Ćwiczenie
W pliku
app.pyzaimportuj modułtodoi zarejestruj blueprinttodo.bpw aplikacji.Dodaj do szablonu bazowego odnośnik do listy zadań.
Zaloguj się podając login
adami hasłozaq1@WSX.Wejdź na stronę z listą zadań.
6.2.6. Dodawanie zadań
W pliku todo.py dopisujemy widoki:
16@bp.route('/dodaj', methods=['GET', 'POST'])
17@login_required
18def dodaj():
19 """Dodawanie nowego zadania"""
20 error = None
21 if request.method == 'POST':
22 zadanie = request.form['zadanie'].strip()
23 if len(zadanie):
24 db = get_db()
25 db.execute('INSERT INTO zadanie (user_id, zadanie, zrobione) VALUES (?, ?, ?)',
26 [g.user['id'], zadanie, 0])
27 db.commit()
28 flash('Dodano nowe zadanie.')
29 return redirect(url_for('todo.index'))
30 else:
31 flash('Nie możesz dodać pustego zadania!') # komunikat o błędzie
32
33 return render_template('todo/zadanie_dodaj.html')
34
35@bp.route('/zrobione', methods=['POST'])
36def zrobione():
37 """Zmiana statusu zadania na wykonane."""
38 zadanie_id = request.form['id']
39 db = get_db()
40 db.execute('UPDATE zadanie SET zrobione=1 WHERE id=? AND user_id=?',
41 [zadanie_id, g.user['id']])
42 db.commit()
43 flash('Zmieniono status zadania.')
44 return redirect(url_for('todo.index'))
Widok dodaj() w odpowiedzi na żądanie typu GET zwróci szablon zadanie_dodaj.html,
który będzie zawierał formularz pozwalający na wpisanie treści zadania.
Po przesłaniu formularza na serwer odczytamy treść zadania i sprawdzimy,
czy zawiera jakieś znaki. Jeżeli tak, wykonamy zapytanie SQL INSERT INTO ...,
które do tabeli zadanie doda nowy rekord zawierający identyfikator użytkownika,
treść zadania oraz wartość 0 oznaczającą, że zadanie nie jest wykonane.
Warto zwrócić uwagę, że nie podajemy daty publikacji, ponieważ zostanie ona utworzona
automatycznie przez bazę danych dzięki zdefiniowaniu wartości domyślnej
pola data_pub w modelu danych: DEFAULT CURRENT_TIMESTAMP.
Widok zrobione() obsługiwał będzie tylko żądania typu POST. Po otrzymaniu
identyfikatora zadania przesłanego z formularza wykonamy zapytanie SQL UPDATE,
które oznaczy zadanie wskazane w klauzuli WHERE jako zrobione: SET zrobione=1.
W szablonie projekty_flask/todo/templates/todo/zadanie_dodaj.html umieszczamy kod
HTML formularza pozwalającego dodać zadanie:
1<!-- projekty_flask/todo/templates/todo/zadanie_dodaj.html -->
2{% extends "index.html" %}
3{% block h1 %} Dodawanie zadań {% endblock %}
4{% block body %}
5 <form method="POST" action="{{ url_for('todo.dodaj') }}">
6 <input name="zadanie" value=""> <br>
7 <button type="submit">Dodaj zadanie</button>
8 </form>
9{% endblock %}
Szablon projekty_flask/todo/templates/todo/index.html uzupełniamy, tzn.
kod w pętli {% for zadanie in zadania %} zmieniamy na:
1 {% for zadanie in zadania %}
2 <li>{{ zadanie.zadanie }} – <em>{{ zadanie.data_pub }}</em>
3
4 <!-- formularz zmiany statusu zadania -->
5 {% if not zadanie.zrobione %}
6 <form method="POST" action="{{ url_for('todo.zrobione') }}">
7 <!-- wysyłamy tylko id zadania -->
8 <input type="hidden" name="id" value="{{ zadanie.id }}"/>
9 <button type="submit">Zrobione</button>
10 </form>
11 {% else %}
12 <!-- tu wstawisz kod formularza do usuwania zadania -->
13 {% endif %}
14
15 </li>
16 {% endfor %}
{% if not zadanie.zrobione %}– jeżeli zadanie nie jest wykonane, dodajemy formularz zmiany statusu zadania zawierający ukryte pole z identyfikatorem zadania. Jeżeli użytkownik kliknie przyciskZrobione, do serwera zostanie wysłane żądaniePOSTz identyfikatorem zadania, które zostanie obsłużone przez widokzrobione().
6.2.6.1. Ćwiczenie
Do szablonu wyświetlającego listę zadań dodaj na końcu odnośnik umożliwiający dodawanie zadań.
Dodaj konto dla użytkownika o loginie
ewai zaloguj się na nie.Dodaj dwa zadania i oznacz jedno z nich jako wykonane.
6.2.7. Style CSS
O wyglądzie aplikacji decydują arkusze stylów CSS. Tego typu zasoby, podobnie jak np. obrazy,
nie zmieniają się zbyt często, dlatego umieszczamy je w specjalnym podkatalogu static
w folderze aplikacji.
Tworzymy więc podkatalog projekty_flask/todo/static, a w nim plik style.css.
W pliku umieszczamy przykładowe definicje:
1/* todo/static/style.css */
2body { margin-top: 20px; background-color: lightgreen; }
3h1, p { margin-left: 20px; }
4ol { text-align: left; }
5form { display: inline-block; margin: 5px; }
6input {
7 width: 100%;
8 padding: 12px 20px;
9 margin: 8px 0;
10 box-sizing: border-box
11 border-color: blue;
12 border-radius: 5px;
13}
14input:focus {
15 background-color: lightgray;
16}
17ul {
18 list-style-type: none;
19 margin: 0;
20 padding: 0;
21 overflow: hidden;
22 background-color: #333333;
23}
24ul li { float: left; }
25ul li a, ul li span{
26 display: block;
27 color: white;
28 text-align: center;
29 padding: 16px;
30 text-decoration: none;
31}
32ul li a:hover { background-color: #111111; }
33li { margin-bottom: 5px; }
34button {
35 padding: 8px 16px;
36 cursor: pointer;
37 color: blue;
38 font-size: 14px/1.5em;
39 background: white;
40 border: 1px solid grey;
41}
42.error { color: red; }
43.success { color: green; }
44.done { text-decoration: line-through; }
Arkusz CSS dołączamy do szablonu bazowego projekty_flask/todo/templates/index.html w sekcji head:
1<!-- projekty_flask/todo/templates/index.html -->
2<html>
3 <head>
4 <title>{{ config.SITE_NAME }}</title>
5 <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
6 </head>
6.2.7.1. Ćwiczenie
Uruchom aplikację lub odśwież stroną z listą zadań zalogowanego użytkownika po dołączeniu stylów CSS.
6.2.8. Zadania dodatkowe
Dodaj możliwość usuwania zadań, czyli:
w szablonie listy zadań dodaj formularz podobny do formularza oznaczania zadań jako wykonane, z którego identyfikator zadania wysyłany jest do widoku
usun,w blueprincie
todo.pydodaj wymagający zalogowania się widokusun()obsługujący adres URL/usun, który przy użyciu klauzuli SQLDELETE FROMusuwa z bazy zadanie o odczytanym z formularza identyfikatorze należące do zalogowanego użytkownika,usuń pierwsze zadanie użytkownika
ewa.
6.2.9. Materiały
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:
2026-04-19 o 17:42 w Sphinx 7.3.7
- Autorzy: