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 przypadlku 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 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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | <!doctype html>
<!-- quiz-orm/templates/szkielet.html -->
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block tytul %}{% endblock %} – {{ config.TYTUL }}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<!-- Static navbar -->
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Przełącz nawigację</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="http://flask.pocoo.org/">
<img src="{{ url_for('static', filename='flask.png') }}" style="max-width: 100%; max-height: 100%;">
</a>
</div>
{% set navigation_bar = [
('/', 'index', 'Strona główna'),
('/lista', 'lista', 'Lista pytań'),
('/quiz', 'quiz', 'Quiz'),
('/dodaj', 'dodaj', 'Dodaj pytania'),
] %}
{% set active_page = active_page|default('index') %}
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
{% for href, id, tekst in navigation_bar %}
<li{% if id == active_page %} class="active"{% endif %}>
<a href="{{ href|e }}">{{ tekst|e }}</a>
</li>
{% endfor %}
</ul>
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</nav>
<div class="row">
<div class="col-md-12">
<h1>{% block h1 %}{% endblock %}</h1>
{% with komunikaty = get_flashed_messages(with_categories=true) %}
{% if komunikaty %}
<div id="komunikaty" class="well">
{% for kategoria, komunikat in komunikaty %}
<span class="{{ kategoria }}">{{ komunikat }}</span><br>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div id="tresc" class="cb">
{% block tresc %}
{% endblock %}
</div>
</div>
</div> <!-- /row -->
</div> <!-- /container -->
<!-- jQuery CDN -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
crossorigin="anonymous"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
</body>
</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ć scież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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!-- quiz-orm/templates/index.html -->
{% extends "szkielet.html" %}
{% set active_page = "index" %}
{% block tytul %}Strona główna{% endblock%}
{% block h1 %}Quiz ORM{% endblock%}
{% block tresc %}
<p>
Aplikacja internetowa <i>Quiz</i> wykorzystująca framework
<a href="http://flask.pocoo.org/">Flask</a>
oraz system ORM <a href="http://docs.peewee-orm.com/en/latest/">Peewee</a>
lub <a href="https://www.sqlalchemy.org/">SQLALchemy</a>
do obsługi bazy danych.</p>
<p>
Pokazujemy, jak:
<ul>
<li>utworzyć model bazy i samą bazę</li>
<li>obsługiwać bazę z poziomu aplikacji www</li>
<li>używać szablonów do prezentacji treści</li>
<li>i wiele innych rzeczy...</li>
</ul>
</p>
{% 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 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 | # -*- coding: utf-8 -*-
# quiz-orm/app.py
import os
from flask import Flask, g
from peewee import SqliteDatabase
app = Flask(__name__)
# konfiguracja aplikacji
app.config.update(dict(
SECRET_KEY='bardzosekretnawartosc',
TYTUL='Quiz ORM Peewee',
DATABASE=os.path.join(app.root_path, 'quiz.db'),
))
# tworzymy instancję bazy używanej przez modele
baza = SqliteDatabase(app.config['DATABASE'])
@app.before_request
def before_request():
g.db = baza
g.db.get_conn()
@app.after_request
def after_request(response):
g.db.close()
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 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 | # -*- coding: utf-8 -*-
# quiz-orm/models.py
from peewee import *
from app import baza
class BaseModel(Model):
class Meta:
database = baza
class Pytanie(BaseModel):
pytanie = CharField(unique=True)
odpok = CharField()
def __str__(self):
return self.pytanie
class Odpowiedz(BaseModel):
pnr = ForeignKeyField(
Pytanie, related_name='odpowiedzi', on_delete='CASCADE')
odpowiedz = CharField()
def __str__(self):
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 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 | # -*- coding: utf-8 -*-
# quiz-orm/dane.py
import os
import csv
from models import Pytanie, Odpowiedz
def pobierz_dane(plikcsv):
"""Funkcja zwraca tuplę zawierającą tuple z danymi z pliku csv."""
dane = []
if os.path.isfile(plikcsv):
with open(plikcsv, newline='') as plikcsv:
tresc = csv.reader(plikcsv, delimiter='#')
for rekord in tresc:
dane.append(tuple(rekord))
else:
print("Plik z danymi", plikcsv, "nie istnieje!")
return tuple(dane)
def dodaj_pytania(dane):
"""Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
for pytanie, odpowiedzi, odpok in dane:
p = Pytanie(pytanie=pytanie, odpok=odpok)
p.save()
for o in odpowiedzi.split(","):
odp = Odpowiedz(pnr=p.id, odpowiedz=o.strip())
odp.save()
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
:
1 2 3 | Stolica Hiszpani, to:#Madryt, Warszawa, Barcelona#Madryt
Objętość sześcianu o boku 6 cm, wynosi:#36, 216, 18#216
Symbol 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
:
4 5 6 7 8 9 10 11 12 13 14 | import os
from app import app, baza
from models import *
from views import *
from dane import *
if __name__ == '__main__':
if not os.path.exists(app.config['DATABASE']):
baza.create_tables([Pytanie, Odpowiedz], True) # tworzymy tabele
dodaj_pytania(pobierz_dane('pytania.csv'))
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ą:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from flask import render_template, request, redirect, url_for, abort, flash
from app import app
from models import Pytanie, Odpowiedz
from forms import *
@app.route('/')
def index():
"""Strona główna"""
return render_template('index.html')
@app.route('/lista')
def lista():
"""Pobranie wszystkich pytań z bazy i zwrócenie szablonu z listą pytań"""
pytania = Pytanie().select().annotate(Odpowiedz)
if not pytania.count():
flash('Brak pytań w bazie.', 'kom')
return redirect(url_for('index'))
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- quiz-orm/templates/lista.html -->
{% extends "szkielet.html" %}
{% set active_page = "lista" %}
{% block tytul %}Lista pytań{% endblock%}
{% block h1 %}Quiz ORM – lista pytań{% endblock%}
{% block tresc %}
<ol>
<!-- pętla odczytująca kolejne pytania z listy -->
{% for p in pytania %}
<li>
<!-- wypisujemy pytanie -->
{{ p.pytanie }}
</li>
{% endfor %}
</ol>
{% 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @app.route('/quiz', methods=['GET', 'POST'])
def quiz():
"""Wyświetlenie pytań i odpowiedzi w formie quizu oraz ocena poprawności
przesłanych odpowiedzi"""
if request.method == 'POST':
wynik = 0
for pid, odp in request.form.items():
odpok = Pytanie.select(Pytanie.odpok).where(
Pytanie.id == int(pid)).scalar()
if odp == odpok:
wynik += 1
flash('Liczba poprawnych odpowiedzi, to: {0}'.format(wynik), 'sukces')
return redirect(url_for('index'))
# GET, wyświetl pytania
pytania = Pytanie().select().annotate(Odpowiedz)
if not pytania.count():
flash('Brak pytań w bazie.', 'kom')
return redirect(url_for('index'))
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 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 | <!-- quiz-orm/templates/quiz.html -->
{% extends "szkielet.html" %}
{% set active_page = "quiz" %}
{% block tytul %}Pytania{% endblock%}
{% block h1 %}Quiz ORM – pytania{% endblock%}
{% block tresc %}
<h2>Odpowiedz na pytania:</h2>
<!-- formularz z quizem -->
<form method="POST">
<!-- pętla odczytująca kolejne pytania z listy -->
{% for p in pytania %}
<p>
<!-- wypisujemy pytanie -->
{{ p.pytanie }}
<br>
<!-- pętla odczytująca możliwe odpowiedzi dla danego pytania -->
{% for o in p.odpowiedzi %}
<label>
<!-- pole radio button aby można było zaznaczyć odpowiedź -->
<input type="radio" value="{{ o.odpowiedz }}" name="{{ p.id }}">
{{ o.odpowiedz }}
</label>
<br>
{% endfor %}
</p>
{% endfor %}
<!-- przycisk wysyłający wypełniony formularz -->
<button type="submit" class="btn btn-default">Sprawdź odpowiedzi</button>
</form>
{% 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # -*- coding: utf-8 -*-
# quiz-orm/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, RadioField, HiddenField, FieldList
from wtforms.validators import Required
blad1 = 'To pole jest wymagane'
blad2 = 'Brak zaznaczonej poprawnej odpowiedzi'
class DodajForm(FlaskForm):
pytanie = StringField('Treść pytania:',
validators=[Required(message=blad1)])
odpowiedzi = FieldList(StringField(
'Odpowiedź',
validators=[Required(message=blad1)]),
min_entries=3,
max_entries=3)
odpok = RadioField(
'Poprawna odpowiedź',
validators=[Required(message=blad2)],
choices=[('0', 'o0'), ('1', 'o1'), ('2', 'o2')]
)
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:
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | def flash_errors(form):
"""Odczytanie wszystkich błędów formularza i przygotowanie komunikatów"""
for field, errors in form.errors.items():
for error in errors:
if type(error) is list:
error = error[0]
flash("Błąd: {}. Pole: {}".format(
error,
getattr(form, field).label.text))
@app.route('/dodaj', methods=['GET', 'POST'])
def dodaj():
"""Dodawanie pytań i odpowiedzi"""
form = DodajForm()
if form.validate_on_submit():
odp = form.odpowiedzi.data
p = Pytanie(pytanie=form.pytanie.data, odpok=odp[int(form.odpok.data)])
p.save()
for o in odp:
inst = Odpowiedz(pnr=p.id, odpowiedz=o)
inst.save()
flash("Dodano pytanie: {}".format(form.pytanie.data))
return redirect(url_for("lista"))
elif request.method == 'POST':
flash_errors(form)
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 2 3 4 5 6 7 8 | <!-- quiz-orm/templates/dodaj.html -->
{% extends "szkielet.html" %}
{% set active_page = "dodaj" %}
{% block tytul %}Dodawanie{% endblock%}
{% block h1 %}Quiz ORM – dodawanie pytań{% endblock%}
{% block tresc %}
{% include "pytanie_form.html" %}
{% 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <form method="POST" class="form-inline" action="">
{{ form.csrf_token }}
{{ form.pytanie.label }}<br>
{{ form.pytanie(class="form-control") }}<br>
<br>
<label>Podaj odpowiedzi i zaznacz poprawną:</label><br>
<ol>
{% for o in form.odpowiedzi %}
<li>{{ radio[loop.index0] }} {{ o(class="form-control") }}</li>
{% endfor %}
</ol>
<button type="submit" class="btn btn-default">Zapisz pytanie</button>
</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()
:
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | def get_or_404(pid):
"""Pobranie i zwrócenie obiektu z bazy lub wywołanie szablonu 404.html"""
try:
p = Pytanie.select().annotate(Odpowiedz).where(Pytanie.id == pid).get()
return p
except Pytanie.DoesNotExist:
abort(404)
@app.errorhandler(404)
def page_not_found(e):
"""Zwrócenie szablonu 404.html w przypadku nie odnalezienia strony"""
return render_template('404.html'), 404
@app.route('/edytuj/<int:pid>', methods=['GET', 'POST'])
def edytuj(pid):
"""Edycja pytania o identyfikatorze pid i odpowiedzi"""
p = get_or_404(pid)
form = DodajForm()
if form.validate_on_submit():
odp = form.odpowiedzi.data
p.pytanie = form.pytanie.data
p.odpok = odp[int(form.odpok.data)]
p.save()
for i, o in enumerate(p.odpowiedzi):
o.odpowiedz = odp[i]
o.save()
flash("Zaktualizowano pytanie: {}".format(form.pytanie.data))
return redirect(url_for("lista"))
elif request.method == 'POST':
flash_errors(form)
for i in range(3):
if p.odpok == p.odpowiedzi[i].odpowiedz:
p.odpok = i
break
form = DodajForm(obj=p)
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 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 | <!-- quiz-orm/templates/dodaj.html -->
{% extends "szkielet.html" %}
{% set active_page = "index" %}
{% block tytul %}Błąd: strony nie znaleziono{% endblock%}
{% block h1 %}Quiz ORM – błąd{% endblock%}
{% block tresc %}
<style type="text/css">
.error-template {padding: 40px 15px;text-align: center;}
.error-actions {margin-top:15px;margin-bottom:15px;}
.error-actions .btn { margin-right:10px; }
</style>
<div class="container">
<div class="row">
<div class="error-template">
<h2>404 Nie znaleziono</h2>
<div class="error-details">
Przepraszamy, wystąpił błąd, żądanej strony nie znaleziono!<br>
</div>
<div class="error-actions">
<a href="{{ url_for('index') }}" class="btn btn-primary">
<i class="icon-home icon-white"></i> Strona główna</a>
</div>
</div>
</div>
</div>
{% endblock %}
|
Szablon edycji jest bardzo podobny do szablonu dodawania, ponieważ wykorzystujemy
ten sam formularz. Tworzymy więc plik edytuj.html
:
1 2 3 4 5 6 7 8 | <!-- quiz-orm/templates/edytuj.html -->
{% extends "szkielet.html" %}
{% set active_page = "edytuj" %}
{% block tytul %}Edycja{% endblock%}
{% block h1 %}Quiz ORM – edycja pytań{% endblock%}
{% block tresc %}
{% include "pytanie_form.html" %}
{% endblock %}
|
Linki umożliwiające edycję pytań wygenerujemy w na liście pytań. W pliku
lista.html
po kodzie {{ pytanie }}
wstawiamy:
12 13 | {{ p.pytanie }}
<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 124 125 126 127 128 129 130 131 | @app.route('/usun/<int:pid>', methods=['GET', 'POST'])
def usun(pid):
"""Usunięcie pytania o identyfikatorze pid"""
p = get_or_404(pid)
if request.method == 'POST':
flash('Usunięto pytanie {0}'.format(p.pytanie), 'sukces')
p.delete_instance(recursive=True)
return redirect(url_for('index'))
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 2 3 4 5 6 7 8 9 10 11 12 13 | <!-- quiz-orm/templates/pytanie_usun.html -->
{% extends "szkielet.html" %}
{% set active_page = "usun" %}
{% block tytul %}Edycja{% endblock%}
{% block h1 %}Quiz ORM – usuwanie pytania{% endblock%}
{% block tresc %}
<form method="POST" action="{{ url_for('usun', pid=pytanie.id) }}">
<!-- wstawiamy id pytania -->
<p class="lead">Czy na pewno chcesz usunąć pytanie:</p>
<p>{{ pytanie.pytanie }}</p>
<button id="btn-delete" type="submit" class="btn btn-danger">Usuń</button>
</form>
{% 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 14 | <a href="{{ url_for('edytuj', pid=p.id ) }}" class="btn btn-default">Edytuj</a>
<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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # -*- coding: utf-8 -*-
# quiz-orm/app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
# konfiguracja aplikacji
app.config.update(dict(
SECRET_KEY='bardzosekretnawartosc',
DATABASE=os.path.join(app.root_path, 'quiz.db'),
SQLALCHEMY_DATABASE_URI='sqlite:///' +
os.path.join(app.root_path, 'quiz.db'),
SQLALCHEMY_TRACK_MODIFICATIONS=False,
TYTUL='Quiz ORM SQLAlchemy'
))
# tworzymy instancję bazy używanej przez modele
baza = 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
24 25 26 27 28 29 30 31 32 33 34 | def dodaj_pytania(dane):
"""Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
for pytanie, odpowiedzi, odpok in dane:
p = Pytanie(pytanie=pytanie, odpok=odpok)
baza.session.add(p)
baza.session.commit()
for o in odpowiedzi.split(","):
odp = Odpowiedz(pnr=p.id, odpowiedz=o.strip())
baza.session.add(odp)
baza.session.commit()
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 12 | if not os.path.exists(app.config['DATABASE']):
baza.create_all() # tworzymy tabele
|
create_all()
– funkcja tworzy wszystkie tabele na podstawie zadeklarowanych modeli:
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 | # -*- coding: utf-8 -*-
# quiz-orm/models.py
from app import baza
class Pytanie(baza.Model):
id = baza.Column(baza.Integer, primary_key=True)
pytanie = baza.Column(baza.Unicode(255), unique=True)
odpok = baza.Column(baza.Unicode(100))
odpowiedzi = baza.relationship(
'Odpowiedz', backref=baza.backref('pytanie'),
cascade="all, delete, delete-orphan")
def __str__(self):
return self.pytanie
class Odpowiedz(baza.Model):
id = baza.Column(baza.Integer, primary_key=True)
pnr = baza.Column(baza.Integer, baza.ForeignKey('pytanie.id'))
odpowiedz = baza.Column(baza.Unicode(100))
def __str__(self):
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
:
4 5 | from flask import render_template, request, redirect, url_for, flash
from app import app, baza
|
Funkcja lista()
– zmieniamy instrukcje odczytujące pytania z bazy:
18 19 20 | """Pobranie wszystkich pytań z bazy i zwrócenie szablonu z listą pytań"""
pytania = Pytanie.query.all()
if not pytania:
|
pytania = Pytanie.query.all()
– pobranie z bazy wszystkich pytań w formie listy.
Funkcja quiz()
– zmieniamy zapytanie odczytujące poprawną odpowiedź:
34 35 | odpok = baza.session.query(Pytanie.odpok).filter(
Pytanie.id == int(pid)).scalar()
|
– a także zapytanie odczytujące pytania oraz odpowiedzi z bazy:
42 43 | # GET, wyświetl pytania
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | p = Pytanie(pytanie=form.pytanie.data, odpok=odp[int(form.odpok.data)])
baza.session.add(p)
baza.session.commit()
for o in odp:
inst = Odpowiedz(pnr=p.id, odpowiedz=o)
baza.session.add(inst)
baza.session.commit()
flash("Dodano pytanie: {}".format(form.pytanie.data))
return redirect(url_for("lista"))
elif request.method == 'POST':
flash_errors(form)
return render_template("dodaj.html", form=form, radio=list(form.odpok))
@app.errorhandler(404)
def 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 99 100 101 | p.odpok = odp[int(form.odpok.data)]
for i, o in enumerate(p.odpowiedzi):
o.odpowiedz = odp[i]
baza.session.commit()
|
Funkcja usun()
– kod usuwający obiekty z bazy przyjmuje postać:
120 121 122 | flash('Usunięto pytanie {0}'.format(p.pytanie), 'sukces')
baza.session.delete(p)
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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |