7.3. Quiz ORM
Realizacja aplikacji internetowej Quiz w oparciu o framework Flask 0.12.x i bazę danych SQLite zarządzaną systemem ORM Peewee lub SQLAlchemy.
Zalecamy zapoznanie się z materiałami zawartymi w scenariuszach:
Wykorzystywane biblioteki instalujemy przy użyciu instalatora pip
:
~$ sudo pip install peewee flask-wtf
Informacja
W budowanym poniżej kodzie wykorzystamy ORM Peewee, na końcu omówimy różnice w przypadku użycia SQLAlchemy.
7.3.1. Modularyzacja
Scenariusze Quiz i ToDo pokazują możliwość umieszczenia całego kodu aplikacji obsługiwanej przez Flaska w jednym pliku. Dla celów szkoleniowych to dobre rozwiązanie, ale w bardziej rozbudowanych projektach wygodniej umieścić poszczególne części aplikacji w osobnych plikach.
Kod rozmieścimy więc następująco:
app.py
– konfiguracja aplikacji Flaska i połączeń z bazą,models.py
– klasy opisujące tabele, pola i relacje w bazie,views.py
– widoki, czyli funkcje, powiązane z adresami URL, obsługujące żądania użytkownika,forms.py
– definicje formularza wykorzystywanego w aplikacji,main.py
– główny plik naszej aplikacji wiążący wszystkie powyższe, odpowiada za utworzenie początkowej bazy,dane.py
– moduł opcjonalny, odczytanie przykładowych danych z plikupytania.csv
i dodanie ich do bazy.
Wszystkie pliki muszą znajdować się w katalogu aplikacji quiz-orm
,
który zawierać będzie również podkatalogi:
templates
– tu umieścimy szablony html,static
– to miejsce dla arkuszy stylów, obrazki i/lub skryptów js.
Ściągamy przygotowane przez nas archiwum quiz-orm_skel.zip
i rozpakowujemy w wybranym katalogu. Początkowy kod pozwoli uruchomić aplikację
i wyświetlić zawartość strony głównej. Aplikację uruchamiamy wydając
w katalogu quiz-orm
polecenie:
~/quiz-orm$ python3 main.py

7.3.2. Szablon podstawowy
W omówionych do tej pory, wspomnianych wyżej, scenariuszach aplikacji internetowych
każdy szablon zawierał kompletny kod strony. W praktyce jednak duża część kodu HTML
powtarza się na każdej stronie w ramach danego serwisu. Tę wspólną część kodu
umieścimy w szablonie podstawowym templates/szkielet.html
:
1<!doctype html>
2<!-- quiz-orm/templates/szkielet.html -->
3<html>
4 <head>
5 <meta charset="utf-8">
6 <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 <title>{% block tytul %}{% endblock %} – {{ config.TYTUL }}</title>
9 <!-- Latest compiled and minified CSS -->
10 <link rel="stylesheet"
11 href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
12 integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
13 crossorigin="anonymous">
14 <!-- Optional theme -->
15 <link rel="stylesheet"
16 href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
17 integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
18 crossorigin="anonymous">
19 <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
20 </head>
21 <body>
22 <div class="container">
23 <!-- Static navbar -->
24 <nav class="navbar navbar-default">
25 <div class="container-fluid">
26 <div class="navbar-header">
27 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
28 <span class="sr-only">Przełącz nawigację</span>
29 <span class="icon-bar"></span>
30 <span class="icon-bar"></span>
31 <span class="icon-bar"></span>
32 </button>
33 <a class="navbar-brand" href="http://flask.pocoo.org/">
34 <img src="{{ url_for('static', filename='flask.png') }}" style="max-width: 100%; max-height: 100%;">
35 </a>
36 </div>
37
38{% set navigation_bar = [
39 ('/', 'index', 'Strona główna'),
40 ('/lista', 'lista', 'Lista pytań'),
41 ('/quiz', 'quiz', 'Quiz'),
42 ('/dodaj', 'dodaj', 'Dodaj pytania'),
43] %}
44{% set active_page = active_page|default('index') %}
45
46 <div id="navbar" class="navbar-collapse collapse">
47 <ul class="nav navbar-nav">
48
49 {% for href, id, tekst in navigation_bar %}
50 <li{% if id == active_page %} class="active"{% endif %}>
51 <a href="{{ href|e }}">{{ tekst|e }}</a>
52 </li>
53 {% endfor %}
54
55 </ul>
56 </div><!--/.nav-collapse -->
57 </div><!--/.container-fluid -->
58 </nav>
59
60 <div class="row">
61 <div class="col-md-12">
62 <h1>{% block h1 %}{% endblock %}</h1>
63
64 {% with komunikaty = get_flashed_messages(with_categories=true) %}
65 {% if komunikaty %}
66 <div id="komunikaty" class="well">
67 {% for kategoria, komunikat in komunikaty %}
68 <span class="{{ kategoria }}">{{ komunikat }}</span><br>
69 {% endfor %}
70 </div>
71 {% endif %}
72 {% endwith %}
73
74 <div id="tresc" class="cb">
75 {% block tresc %}
76 {% endblock %}
77 </div>
78
79 </div>
80 </div> <!-- /row -->
81 </div> <!-- /container -->
82
83 <!-- jQuery CDN -->
84 <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
85 integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
86 crossorigin="anonymous"></script>
87 <!-- Latest compiled and minified JavaScript -->
88 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
89 integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
90 crossorigin="anonymous"></script>
91
92 </body>
93</html>
Szablon oparty jest na frameworku Bootstrap. Odpowiednie linki do stylów CSS, pobieranych z systemu CDN zostały skopiowane ze strony Getting started i wklejone w podświetlonych liniach. Do szablonu dołączono również wymaganą przez Bootstrapa bibliotekę jQuery.
{{ url_for('static', filename='style.css') }}
– funkcjaurl_for()
pozwala wygenerować ścieżkę do zasobów umieszczonych w podkatalogustatic
;{% tag %}...{% endtag %}
– tagi sterujące, wymagają zamknięcia(!),{% block nazwa_bloku %}
– tag pozwala definiować miejsca, w których szablony dziedziczące mogą wstawiać swój kod,{{ zmienna }}
– tagi pozwalające wstawiać wartości zmiennych dostępnych domyślnie i przekazanych do szablonu,container
,row
,navbar
itd. – klasy Bootstrapa tworzące podstawowy układ (ang. layout) strony,navigation_bar
– lista na podstawie której generowane są pozycje menu,active_page
– zmienna zawierająca identyfikator aktywnej strony,get_flashed_messages(with_categories=true)
– funkcja zwracająca komunikaty dla użytkownika oznaczone kategoriami, wykorzystywanymi jako klasy CSS.
Dodatkowo szablon wykorzystuje zawarty w początkowym archiwum
plik static/style.css
.
Szablon strony głównej z pliku index.html
zmieniamy
następująco:
1<!-- quiz-orm/templates/index.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "index" %}
4{% block tytul %}Strona główna{% endblock%}
5{% block h1 %}Quiz ORM{% endblock%}
6{% block tresc %}
7 <p>
8 Aplikacja internetowa <i>Quiz</i> wykorzystująca framework
9 <a href="http://flask.pocoo.org/">Flask</a>
10 oraz system ORM <a href="http://docs.peewee-orm.com/en/latest/">Peewee</a>
11 lub <a href="https://www.sqlalchemy.org/">SQLALchemy</a>
12 do obsługi bazy danych.</p>
13 <p>
14 Pokazujemy, jak:
15 <ul>
16 <li>utworzyć model bazy i samą bazę</li>
17 <li>obsługiwać bazę z poziomu aplikacji www</li>
18 <li>używać szablonów do prezentacji treści</li>
19 <li>i wiele innych rzeczy...</li>
20 </ul>
21 </p>
22{% endblock %}
{% extends "szkielet.html" %}
– wskazanie dziedziczenia z szablonu podstawowego;{% block tresc %} treść {% endblock %}
– zastąpienie lub uzupełnienie treści bloków zdefiniowanych w szablonie podstawowym.
Po odświeżeniu strony powinniśmy zobaczyć w przeglądarce nowy wygląd strony:

7.3.3. Baza danych
Konfigurację bazy danych obsługiwanej przez wybrany system ORM umieścimy w pliku
app.py
. Zaczynamy od uzupełnienia ustawień w słowniku config
i
utworzenia obiektu bazy danych:
1# -*- coding: utf-8 -*-
2# quiz-orm/app.py
3
4import os
5from flask import Flask, g
6from peewee import SqliteDatabase
7
8app = Flask(__name__)
9
10# konfiguracja aplikacji
11app.config.update(dict(
12 SECRET_KEY='bardzosekretnawartosc',
13 TYTUL='Quiz ORM Peewee',
14 DATABASE=os.path.join(app.root_path, 'quiz.db'),
15))
16
17# tworzymy instancję bazy używanej przez modele
18baza = SqliteDatabase(app.config['DATABASE'])
19
20
21@app.before_request
22def before_request():
23 g.db = baza
24 g.db.get_conn()
25
26
27@app.after_request
28def after_request(response):
29 g.db.close()
30 return response
before_request()
,after_request()
– funkcje wykorzystywane do otwierania i zamykania połączenia z bazą SQLite przed żądaniem i po żądaniu (ang. request),g
– specjalny obiekt Flaska do przechowywania danych kontekstowych aplikacji.
7.3.4. Modele
Modele pozwalają opisać strukturę naszej bazy danych w postaci definicji klas
i ich właściwości. Na podstawie tych definicji system ORM utworzy odpowiednie
tabele i kolumny. Wykorzystamy tabelę Pytanie
, zawierającą treść pytania
i poprawną odpowiedź, oraz tabelę Odpowiedź
, która przechowywać będzie
wszystkie możliwe odpowiedzi. Relację jeden-do-wielu między tabelami
tworzyć będzie pole pnr
, czyli klucz obcy,
przechowujący identyfikator pytania.
1# -*- coding: utf-8 -*-
2# quiz-orm/models.py
3
4from peewee import *
5from app import baza
6
7
8class BaseModel(Model):
9 class Meta:
10 database = baza
11
12
13class Pytanie(BaseModel):
14 pytanie = CharField(unique=True)
15 odpok = CharField()
16
17 def __str__(self):
18 return self.pytanie
19
20
21class Odpowiedz(BaseModel):
22 pnr = ForeignKeyField(
23 Pytanie, related_name='odpowiedzi', on_delete='CASCADE')
24 odpowiedz = CharField()
25
26 def __str__(self):
27 return self.odpowiedz
BaseModel
– klasa określająca obiekt bazy,unique=True
– właściwość wymagająca niepowtarzalnej zawartości pola,ForeignKeyField()
– definicja klucza obcego, tworzenie relacji,on_delete = 'CASCADE'
– usuwanie rekordów z powiązanych tabel.
Identyfikatory pytań i odpowiedzi, czyli pola id
w każdej tabeli
tworzone są automatycznie.
Metody __str__(self)
służą „autoprezentacji” obiektów utworzonych na podstawie
danego modelu, są wykorzystywane np. podczas używania funkcji print()
.
7.3.5. Dane początkowe
Moduł dane.py
:
1# -*- coding: utf-8 -*-
2# quiz-orm/dane.py
3
4import os
5import csv
6from models import Pytanie, Odpowiedz
7
8
9def pobierz_dane(plikcsv):
10 """Funkcja zwraca tuplę zawierającą tuple z danymi z pliku csv."""
11 dane = []
12 if os.path.isfile(plikcsv):
13 with open(plikcsv, newline='') as plikcsv:
14 tresc = csv.reader(plikcsv, delimiter='#')
15 for rekord in tresc:
16 dane.append(tuple(rekord))
17 else:
18 print("Plik z danymi", plikcsv, "nie istnieje!")
19
20 return tuple(dane)
21
22
23def dodaj_pytania(dane):
24 """Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
25 for pytanie, odpowiedzi, odpok in dane:
26 p = Pytanie(pytanie=pytanie, odpok=odpok)
27 p.save()
28 for o in odpowiedzi.split(","):
29 odp = Odpowiedz(pnr=p.id, odpowiedz=o.strip())
30 odp.save()
31 print("Dodano przykładowe pytania")
pobierz_dane()
– funkcja wykorzystuje modułcsv
, który ułatwia odczytywanie danych zapisanych w tym formacie, zobacz format CSV, zwraca tuplę 3-elementowych tupli (:-));dodaj_pytania()
– funkcja dodaje przykładowe pytania i odpowiedzi wykorzystując składnię wykorzystywanego systemu ORM;for pytanie,odpowiedzi,odpok in dane:
– pętla rozpakowuje pytanie, listę odpowiedzi i odpowiedź poprawną z przekazanych tupli;p = Pytanie(pytanie=pytanie, odpok=odpok)
– utworzenie obiektu pytania;odp = Odpowiedz(pnr=p.id, odpowiedz=o.strip())
– utworzenie obiektu odpowiedzi;save()
– metoda zapisująca utworzony/zmieniony obiekt w bazie danych.
Zawartość dołączonego do archiwum pliku pytania.csv
:
1Stolica Hiszpani, to:#Madryt, Warszawa, Barcelona#Madryt
2Objętość sześcianu o boku 6 cm, wynosi:#36, 216, 18#216
3Symbol pierwiastka Helu, to:#Fe, H, He#He
Kod uruchamiający utworzenie bazy i dodanie do niej przykładowych danych umieścimy
w pliku main.py
:
4import os
5from app import app, baza
6from models import *
7from views import *
8from dane import *
9
10if __name__ == '__main__':
11 if not os.path.exists(app.config['DATABASE']):
12 baza.create_tables([Pytanie, Odpowiedz], True) # tworzymy tabele
13 dodaj_pytania(pobierz_dane('pytania.csv'))
14 app.run(debug=True)
[todo]
7.3.6. Odczyt
Skrót CRUD (Create (tworzenie), Read (odczyt), Update (aktualizacja), Delete (usuwanie)) oznacza podstawowe operacje wykonywane na bazie danych.
Zaczniemy od widoku lista()
pobierającego wszystkie pytania i zwracającego
szablon z ich listą:
1from flask import render_template, request, redirect, url_for, abort, flash
2from app import app
3from models import Pytanie, Odpowiedz
4from forms import *
5
6
7@app.route('/')
8def index():
9 """Strona główna"""
10 return render_template('index.html')
11
12
13@app.route('/lista')
14def lista():
15 """Pobranie wszystkich pytań z bazy i zwrócenie szablonu z listą pytań"""
16 pytania = Pytanie().select().annotate(Odpowiedz)
17 if not pytania.count():
18 flash('Brak pytań w bazie.', 'kom')
19 return redirect(url_for('index'))
20
21 return render_template('lista.html', pytania=pytania)
pytania = Pytanie().select()
– pobranie z bazy wszystkich pytań.redirect(url_for('index'))
– przekierowanie użytkownika na adres obsługiwany przez podany jako argument widok.
Kod szablonu lista.html
:
1<!-- quiz-orm/templates/lista.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "lista" %}
4{% block tytul %}Lista pytań{% endblock%}
5{% block h1 %}Quiz ORM – lista pytań{% endblock%}
6{% block tresc %}
7 <ol>
8 <!-- pętla odczytująca kolejne pytania z listy -->
9 {% for p in pytania %}
10 <li>
11 <!-- wypisujemy pytanie -->
12 {{ p.pytanie }}
13 </li>
14 {% endfor %}
15 </ol>
16{% endblock %}
Po uzupełnieniu kodu w przeglądarce powinniśmy zobaczyć listę pytań:

7.3.7. Quiz
Widok wyświetlający pytania i odpowiedzi w formie quizu
i sprawdzający udzielone przez użytkownika odpowiedzi to również przykład operacji
odczytu danych danych z bazy. Dodajemy funkcję quiz()
:
27@app.route('/quiz', methods=['GET', 'POST'])
28def quiz():
29 """Wyświetlenie pytań i odpowiedzi w formie quizu oraz ocena poprawności
30 przesłanych odpowiedzi"""
31 if request.method == 'POST':
32 wynik = 0
33 for pid, odp in request.form.items():
34 odpok = Pytanie.select(Pytanie.odpok).where(
35 Pytanie.id == int(pid)).scalar()
36 if odp == odpok:
37 wynik += 1
38
39 flash('Liczba poprawnych odpowiedzi, to: {0}'.format(wynik), 'sukces')
40 return redirect(url_for('index'))
41
42 # GET, wyświetl pytania
43 pytania = Pytanie().select().annotate(Odpowiedz)
44 if not pytania.count():
45 flash('Brak pytań w bazie.', 'kom')
46 return redirect(url_for('index'))
47
48 return render_template('quiz.html', pytania=pytania)
@app.route('/quiz', methods=['GET', 'POST'])
– określenie obsługiwanego adresu URL oraz akcpetowanych metod żądań,request.method
– wykorzystana metoda: GET lub POST,request.form
– formularz przesłany w żądaniu POST,for pid, odp in request.form.items():
– pętla odczytująca przesłane identyfikatory pytań i udzielone odpowiedzi.
Zapytania ORM:
Pytanie().select().annotate(Odpowiedz)
– pobranie wszystkich pytań razem z odpowiedziami,Pytanie.select(Pytanie.odpok).where(Pytanie.id == int(pid)).scalar()
– pobranie poprawnej odpowiedzi dla pytania o podanym identyfikatorze, metodascalar()
zwraca pojedynczą wartość.
Szablon quiz.html
– oparty na omówionym wcześniej wzorcu – wyświetla pytania
i możliwe odpowiedzi jako pola opcji typu radio button:
1<!-- quiz-orm/templates/quiz.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "quiz" %}
4{% block tytul %}Pytania{% endblock%}
5{% block h1 %}Quiz ORM – pytania{% endblock%}
6{% block tresc %}
7 <h2>Odpowiedz na pytania:</h2>
8 <!-- formularz z quizem -->
9 <form method="POST">
10 <!-- pętla odczytująca kolejne pytania z listy -->
11 {% for p in pytania %}
12 <p>
13 <!-- wypisujemy pytanie -->
14 {{ p.pytanie }}
15 <br>
16 <!-- pętla odczytująca możliwe odpowiedzi dla danego pytania -->
17 {% for o in p.odpowiedzi %}
18 <label>
19 <!-- pole radio button aby można było zaznaczyć odpowiedź -->
20 <input type="radio" value="{{ o.odpowiedz }}" name="{{ p.id }}">
21 {{ o.odpowiedz }}
22 </label>
23 <br>
24 {% endfor %}
25 </p>
26 {% endfor %}
27
28 <!-- przycisk wysyłający wypełniony formularz -->
29 <button type="submit" class="btn btn-default">Sprawdź odpowiedzi</button>
30 </form>
31{% endblock %}

7.3.8. Dodawanie
Dodawanie nowych pytań i odpowiedzi wymaga formularza. Gdybyśmy stworzyli go „ręcznie” w szablonie html, musielibyśmy napisać sporo kodu sprawdzającego poprawność przesyłanych danych. Dlatego skorzystamy z biblioteki Flask-wtf, pozwalającej wykorzystać formularze WTForms.
Formularz definiujemy w pliku forms.py
:
1# -*- coding: utf-8 -*-
2# quiz-orm/forms.py
3
4from flask_wtf import FlaskForm
5from wtforms import StringField, RadioField, HiddenField, FieldList
6from wtforms.validators import Required
7
8blad1 = 'To pole jest wymagane'
9blad2 = 'Brak zaznaczonej poprawnej odpowiedzi'
10
11
12class DodajForm(FlaskForm):
13 pytanie = StringField('Treść pytania:',
14 validators=[Required(message=blad1)])
15 odpowiedzi = FieldList(StringField(
16 'Odpowiedź',
17 validators=[Required(message=blad1)]),
18 min_entries=3,
19 max_entries=3)
20 odpok = RadioField(
21 'Poprawna odpowiedź',
22 validators=[Required(message=blad2)],
23 choices=[('0', 'o0'), ('1', 'o1'), ('2', 'o2')]
24 )
25 pid = HiddenField("Pytanie id")
StringField()
– definicja pola tekstowego,FieldList(StringField())
– definicja trzech pól tekstowych,Required(message=blad1)
– pole wymagane,RadioField()
– pola jednokrotnego wyboru, opcje definiuje się w postaci listychoices
zawierającej pary wartość - etykieta,HiddenField()
– pole ukryte.
Funkcja pomocnicza i widok obsługujący dodawanie:
51def flash_errors(form):
52 """Odczytanie wszystkich błędów formularza i przygotowanie komunikatów"""
53 for field, errors in form.errors.items():
54 for error in errors:
55 if type(error) is list:
56 error = error[0]
57 flash("Błąd: {}. Pole: {}".format(
58 error,
59 getattr(form, field).label.text))
60
61
62@app.route('/dodaj', methods=['GET', 'POST'])
63def dodaj():
64 """Dodawanie pytań i odpowiedzi"""
65 form = DodajForm()
66 if form.validate_on_submit():
67 odp = form.odpowiedzi.data
68 p = Pytanie(pytanie=form.pytanie.data, odpok=odp[int(form.odpok.data)])
69 p.save()
70 for o in odp:
71 inst = Odpowiedz(pnr=p.id, odpowiedz=o)
72 inst.save()
73 flash("Dodano pytanie: {}".format(form.pytanie.data))
74 return redirect(url_for("lista"))
75 elif request.method == 'POST':
76 flash_errors(form)
77
78 return render_template("dodaj.html", form=form, radio=list(form.odpok))
flash_errors()
– zadaniem funkcji jest przygotowanie komunikatów dla użytkownika zawierających ewentualne błędy walidacji formularza dostępne w słownikuform.errors
,form = DodajForm()
– utworzenie pustego formularza,form.validate_on_submit()
– funkcja zwraca prawdę, jeżeli żądanie jest typu POST i formularz zawiera poprawne dane, czyli przechodzi procedurę walidacji, funkcja automatycznie wypełnia obiekt formularza przesłanymi danymi,form.pole.data
– odczyt wartości danego pola formularza,odpok=odp[int(form.odpok.data)]
– jako poprawną odpowiedź zapisujemy tekst odpowiedzi.
Do szablonu przekazujemy formularz i osobno listę opcji odpowiedzi.
Kod szablonu dodaj.html
:
1<!-- quiz-orm/templates/dodaj.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "dodaj" %}
4{% block tytul %}Dodawanie{% endblock%}
5{% block h1 %}Quiz ORM – dodawanie pytań{% endblock%}
6{% block tresc %}
7 {% include "pytanie_form.html" %}
8{% endblock %}
{% include "pytanie_form.html" %}
– instrukcja włączania kodu z innego pliku.
Kod renderujący formularz jest taki sam podczas dodawania, jak i edycji danych. Dlatego umieścimy go w osobnym pliku:
1 <form method="POST" class="form-inline" action="">
2 {{ form.csrf_token }}
3
4 {{ form.pytanie.label }}<br>
5 {{ form.pytanie(class="form-control") }}<br>
6 <br>
7 <label>Podaj odpowiedzi i zaznacz poprawną:</label><br>
8 <ol>
9 {% for o in form.odpowiedzi %}
10 <li>{{ radio[loop.index0] }} {{ o(class="form-control") }}</li>
11 {% endfor %}
12 </ol>
13
14 <button type="submit" class="btn btn-default">Zapisz pytanie</button>
15 </form>
Formularz renderujemy „ręcznie”, aby uzyskać odpowiedni układ pól.
Po nazwie pola można opcjonalnie podawać klasy CSS, które mają
zostać użyte w kodzie HTML, np. form.pytanie(class="form-control")
.
Efekt prezentuje się następująco:

7.3.9. Edycja
Zaczniemy od dodania w pliku views.py
funkcji pomocniczych i widoku edytuj()
:
81def get_or_404(pid):
82 """Pobranie i zwrócenie obiektu z bazy lub wywołanie szablonu 404.html"""
83 try:
84 p = Pytanie.select().annotate(Odpowiedz).where(Pytanie.id == pid).get()
85 return p
86 except Pytanie.DoesNotExist:
87 abort(404)
88
89
90@app.errorhandler(404)
91def page_not_found(e):
92 """Zwrócenie szablonu 404.html w przypadku nie odnalezienia strony"""
93 return render_template('404.html'), 404
94
95
96@app.route('/edytuj/<int:pid>', methods=['GET', 'POST'])
97def edytuj(pid):
98 """Edycja pytania o identyfikatorze pid i odpowiedzi"""
99 p = get_or_404(pid)
100 form = DodajForm()
101
102 if form.validate_on_submit():
103 odp = form.odpowiedzi.data
104 p.pytanie = form.pytanie.data
105 p.odpok = odp[int(form.odpok.data)]
106 p.save()
107 for i, o in enumerate(p.odpowiedzi):
108 o.odpowiedz = odp[i]
109 o.save()
110 flash("Zaktualizowano pytanie: {}".format(form.pytanie.data))
111 return redirect(url_for("lista"))
112 elif request.method == 'POST':
113 flash_errors(form)
114
115 for i in range(3):
116 if p.odpok == p.odpowiedzi[i].odpowiedz:
117 p.odpok = i
118 break
119 form = DodajForm(obj=p)
120 return render_template("edytuj.html", form=form, radio=list(form.odpok))
Żądanie wyświetlenia aktualizowanego pytania (GET):
'/edytuj/<int:pid>'
– definicja adresu URL mówiąca, że oczekujemy wywołań w postaci /edytuj/1, przy czym końcowa liczba to identyfikator pytania,p = get_or_404(pid)
– próbujemy pobrać z bazy dane pytania o podanym identyfikatorze, funkcja pomocniczaget_or_404()
zwróci obiekt, a jeżeli nie będzie to możliwe, wywoła błądabort(404)
– co oznacza, że żądanego zasobu nie odnaleziono,page_not_found(e)
– funkcja, którą za pomocą dekoratora rejestrujemy do obsługi błędów HTTP 404, zwraca szablon404.html
,for i in range(3)
– pętla, w której ustalamy numer poprawnej odpowiedzi (p.odpok=i
), który przekażemy do formularza, aby zaznaczony został właściwy przycisk radio,form = DodajForm(obj=p)
– przed przekazaniem formularza do szablonu wypełniamy go danymi używając parametruobj
.
Żądanie zapisania danych z formularza (POST):
p.pytanie = form.pytanie.data
– aktualizujemy dane pytania po sprawdzeniu ich poprawności;for i, o in enumerate(p.odpowiedzi)
– pętla, w której aktualizujemy kolejne odpowiedzi:o.odpowiedz = odp[i]
.
Szablon 404.html
może wyglądać np. tak:
1<!-- quiz-orm/templates/dodaj.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "index" %}
4{% block tytul %}Błąd: strony nie znaleziono{% endblock%}
5{% block h1 %}Quiz ORM – błąd{% endblock%}
6{% block tresc %}
7<style type="text/css">
8.error-template {padding: 40px 15px;text-align: center;}
9.error-actions {margin-top:15px;margin-bottom:15px;}
10.error-actions .btn { margin-right:10px; }
11</style>
12<div class="container">
13 <div class="row">
14 <div class="error-template">
15 <h2>404 Nie znaleziono</h2>
16 <div class="error-details">
17 Przepraszamy, wystąpił błąd, żądanej strony nie znaleziono!<br>
18 </div>
19 <div class="error-actions">
20 <a href="{{ url_for('index') }}" class="btn btn-primary">
21 <i class="icon-home icon-white"></i> Strona główna</a>
22 </div>
23 </div>
24 </div>
25</div>
26{% endblock %}
Szablon edycji jest bardzo podobny do szablonu dodawania, ponieważ wykorzystujemy
ten sam formularz. Tworzymy więc plik edytuj.html
:
1<!-- quiz-orm/templates/edytuj.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "edytuj" %}
4{% block tytul %}Edycja{% endblock%}
5{% block h1 %}Quiz ORM – edycja pytań{% endblock%}
6{% block tresc %}
7 {% include "pytanie_form.html" %}
8{% endblock %}
Linki umożliwiające edycję pytań wygenerujemy w na liście pytań. W pliku
lista.html
po kodzie {{ pytanie }}
wstawiamy:
12 {{ p.pytanie }}
13 <a href="{{ url_for('edytuj', pid=p.id ) }}" class="btn btn-default">Edytuj</a>
{{ url_for('edytuj', pid=p.id ) }}
– funkcja generuje adres dla podanego widoku dodając na końcu identyfikator pytania.


7.3.10. Usuwanie
Pozostaje umożliwienie usuwania pytań i odpowiedzi. W pliku views.py
dodajemy widok usun()
:
123@app.route('/usun/<int:pid>', methods=['GET', 'POST'])
124def usun(pid):
125 """Usunięcie pytania o identyfikatorze pid"""
126 p = get_or_404(pid)
127 if request.method == 'POST':
128 flash('Usunięto pytanie {0}'.format(p.pytanie), 'sukces')
129 p.delete_instance(recursive=True)
130 return redirect(url_for('index'))
131 return render_template("pytanie_usun.html", pytanie=p)
'/usun/<int:pid>'
– podobnie jak w przypadku edycji widok obsłuży adres URL zawierający identyfikator pytania, który wykorzystujemy do pobrania obiektu z bazy danych,p.delete_instance(recursive=True)
– obsługując żądanie typu POST, usuwamy pytania, a także wszystkie skojarzone z nim odpowiedzi (opcjarecursive
).
W przypadku żądania typu GET zwracamy formularz potwierdzenia usunięcia
pytanie_usun.html
:
1<!-- quiz-orm/templates/pytanie_usun.html -->
2{% extends "szkielet.html" %}
3{% set active_page = "usun" %}
4{% block tytul %}Edycja{% endblock%}
5{% block h1 %}Quiz ORM – usuwanie pytania{% endblock%}
6{% block tresc %}
7 <form method="POST" action="{{ url_for('usun', pid=pytanie.id) }}">
8 <!-- wstawiamy id pytania -->
9 <p class="lead">Czy na pewno chcesz usunąć pytanie:</p>
10 <p>{{ pytanie.pytanie }}</p>
11 <button id="btn-delete" type="submit" class="btn btn-danger">Usuń</button>
12 </form>
13{% endblock %}
action="{{ url_for('usun', pid=pytanie.id) }}
– generujemy adres, pod który wysłane zostanie potwierdzenie.
Na koniec należy wstawić link umożliwiający usunięcie pytania do szablonu
lista.html
:
13 <a href="{{ url_for('edytuj', pid=p.id ) }}" class="btn btn-default">Edytuj</a>
14 <a href="{{ url_for('usun', pid=p.id ) }}" class="btn btn-danger">Usuń</a>


7.3.11. SQLAlchemy
Instalacja wymaganych modułów:
~$ sudo pip install sqlalchemy flask-sqlalchemy flask-wtf
Obsługa bazy nie wymaga w przypadku SQLAlchemy funkcji nawiązujących i kończących połączenia z bazą. Wszystko odbywa się w sesji tworzonej automatycznie.
1# -*- coding: utf-8 -*-
2# quiz-orm/app.py
3
4from flask import Flask
5from flask_sqlalchemy import SQLAlchemy
6import os
7
8app = Flask(__name__)
9
10# konfiguracja aplikacji
11app.config.update(dict(
12 SECRET_KEY='bardzosekretnawartosc',
13 DATABASE=os.path.join(app.root_path, 'quiz.db'),
14 SQLALCHEMY_DATABASE_URI='sqlite:///' +
15 os.path.join(app.root_path, 'quiz.db'),
16 SQLALCHEMY_TRACK_MODIFICATIONS=False,
17 TYTUL='Quiz ORM SQLAlchemy'
18))
19
20# tworzymy instancję bazy używanej przez modele
21baza = SQLAlchemy(app)
SQLALCHEMY_TRACK_MODIFICATIONS=False
– wyłączenie nieużywanego przez nas śledzenia modyfikacji obiektów i emitowania sygnałów.
W pliku dane.py
należy zaimportować obiekt umożliwiający zarządzanie
bazą danych, następnie modyfikujemy funkcję dodaj_pytanie()
:
from app import baza
24def dodaj_pytania(dane):
25 """Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
26 for pytanie, odpowiedzi, odpok in dane:
27 p = Pytanie(pytanie=pytanie, odpok=odpok)
28 baza.session.add(p)
29 baza.session.commit()
30 for o in odpowiedzi.split(","):
31 odp = Odpowiedz(pnr=p.id, odpowiedz=o.strip())
32 baza.session.add(odp)
33 baza.session.commit()
34 print("Dodano przykładowe pytania")
W operacjach dodawania, również w funkcji dodaj()
(zob. niżej)
korzystamy z metod obiektu sesji:
session.add()
– dodaje obiekt,session.commit()
– zatwierdza zmiany w bazie.
W pliku main.py
zmieniamy tylko jedną linią:
11 if not os.path.exists(app.config['DATABASE']):
12 baza.create_all() # tworzymy tabele
create_all()
– funkcja tworzy wszystkie tabele na podstawie zadeklarowanych modeli:
1# -*- coding: utf-8 -*-
2# quiz-orm/models.py
3
4from app import baza
5
6
7class Pytanie(baza.Model):
8 id = baza.Column(baza.Integer, primary_key=True)
9 pytanie = baza.Column(baza.Unicode(255), unique=True)
10 odpok = baza.Column(baza.Unicode(100))
11 odpowiedzi = baza.relationship(
12 'Odpowiedz', backref=baza.backref('pytanie'),
13 cascade="all, delete, delete-orphan")
14
15 def __str__(self):
16 return self.pytanie
17
18
19class Odpowiedz(baza.Model):
20 id = baza.Column(baza.Integer, primary_key=True)
21 pnr = baza.Column(baza.Integer, baza.ForeignKey('pytanie.id'))
22 odpowiedz = baza.Column(baza.Unicode(100))
23
24 def __str__(self):
25 return self.odpowiedz
from app import baza
– jedyny import, którego potrzebujemy, to obiektbaza
udostępniający wszystkie klasy i metody SQLAlchemy,primary_key=True
– definicja klucza podstawowego, czyli identyfikatora pytania i odpowiedzi,ForeignKey()
– określenie klucza obcego, czyli relacji,relationship()
– relacja zwrotna, właściwośćPytanie.odpowiedzi
,backref=baza.backref('pytanie')
– relacja zwrotna, właściwośćOdpowiedz.pytanie
,cascade="all, delete, delete-orphan"
– usuwanie rekordów z powiązanych tabel.
Zmiany w pliku views.py
dotyczą głównie innej składni zapytań do bazy.
Na początku drobne zmiany w importach: usuwamy obiekt abort
i dodajemy
import obiektu baza
:
4from flask import render_template, request, redirect, url_for, flash
5from app import app, baza
Funkcja lista()
– zmieniamy instrukcje odczytujące pytania z bazy:
18 """Pobranie wszystkich pytań z bazy i zwrócenie szablonu z listą pytań"""
19 pytania = Pytanie.query.all()
20 if not pytania:
pytania = Pytanie.query.all()
– pobranie z bazy wszystkich pytań w formie listy.
Funkcja quiz()
– zmieniamy zapytanie odczytujące poprawną odpowiedź:
34 odpok = baza.session.query(Pytanie.odpok).filter(
35 Pytanie.id == int(pid)).scalar()
– a także zapytanie odczytujące pytania oraz odpowiedzi z bazy:
42 # GET, wyświetl pytania
43 pytania = Pytanie.query.join(Odpowiedz).all()
.join()
– metoda pozwala odczytać odpowiedzi powiązane relacją z pytaniem.
Funkcja dodaj()
– zmieniamy polecenia dodające obiekty do bazy:
68 p = Pytanie(pytanie=form.pytanie.data, odpok=odp[int(form.odpok.data)])
69 baza.session.add(p)
70 baza.session.commit()
71 for o in odp:
72 inst = Odpowiedz(pnr=p.id, odpowiedz=o)
73 baza.session.add(inst)
74 baza.session.commit()
75 flash("Dodano pytanie: {}".format(form.pytanie.data))
76 return redirect(url_for("lista"))
77 elif request.method == 'POST':
78 flash_errors(form)
79
80 return render_template("dodaj.html", form=form, radio=list(form.odpok))
81
82
83@app.errorhandler(404)
84def page_not_found(e):
Funkcje edytuj()
i usun()
– zmieniamy kod pobierający obiekt o podanym
identyfikatorze z bazy:
92 p = Pytanie.query.get_or_404(pid)
Funkcja get_or_404()
– jest niepotrzebna i należy ją usunąć. Zamiast niej
używamy metody dostępnej w SQLAlchemy.
Funkcja edytuj()
– upraszczamy kod aktualizujący obiekty w bazie, :
98 p.odpok = odp[int(form.odpok.data)]
99 for i, o in enumerate(p.odpowiedzi):
100 o.odpowiedz = odp[i]
101 baza.session.commit()
Funkcja usun()
– kod usuwający obiekty z bazy przyjmuje postać:
120 flash('Usunięto pytanie {0}'.format(p.pytanie), 'sukces')
121 baza.session.delete(p)
122 baza.session.commit()
7.3.12. Ź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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: