7.3. Quiz ORM¶
Realizacja aplikacji internetowej Quiz w oparciu o framework Flask i bazę danych SQLite zarządzaną systemem ORM Peewee lub SQLAlchemy.
7.3.1. Wymagania¶
Dobre zrozumienie omawianych tu zagadnień wymaga przyswojenia podstaw Pythona
omówionych w scenariuszu “Python w przykładach” (tematy 2-6), obsługi bazy
danych przedstawionej w scenariuszu “Bazy danych w Pythonie” oraz scenariusza
wprowadzającego do użycia frameworka Flask w aplikacjach internetowych
pt. “Quiz”. Zalecamy również zapoznanie się ze scenariuszem “ToDo”, który
ilustruje użycie bazy danych obsługiwanej za pomocą wbudowanego modułu sqlite3
z aplikacją internetową.
Wykorzystywane biblioteki instalujemy przy użyciu instalatora pip
:
~$ sudo pip install peewee sqlalchemy flask-sqlalchemy
7.3.2. Modularyzacja¶
Scenariusze “Quiz” i “ToDo” pokazują możliwość umieszczenia całego kodu aplikacji obsługiwanej przez Flaska w jednym pliku. O ile dla celów szkoleniowych jest to dobre rozwiązanie, o tyle w praktycznych realizacjach wygodniej logicznie rozdzielić poszczególne części aplikacji i umieścić je w osobnych plikach, których nazwy określają ich przeznaczenie. Podejście takie usprawnia rozwijanie aplikacji, ale również ułatwia poznawanie bardziej rozbudowanych systemów, takich jak Django, przedstawione w scenariuszu “Czat”.
Tak więc kod rozmieścimy następująco:
app.py
– konfiguracja aplikacji Flaska i obiektu służącego do łączenia się z bazą;models.py
– klasy opisujące tabele, pola i relacje w bazie;views.py
– widoki obsługujące udostępnione użytkownikowi akcje, typu “rozwiąż quiz”, “dodaj pytanie”, “edytuj pytania” itp.main.py
– główny plik naszej aplikacji wiążący wszystkie powyższe, odpowiada za utworzenie tabel w bazie, wypełnienie ich danymi początkowymi i uruchomienie aplikacji, czyli serwera www;dane.py
– moduł opcjonalny, którego zadaniem jest odczytanie wstępnych danych z pliku csv i dodanie ich do bazy.
Wszystkie powyższe pliki muszą znajdować się w katalogu aplikacji quiz2
.
W podkatalogach templates
umieścimy wszystkie szablony, czyli pliki
z rozszerzeniem html, arkusz stylów o nazwie style.css
znajdzie się
w podkatalogu static
. Potrzebną strukturę katalogów można utworzyć poleceniami:
~$ mkdir quiz2
~$ cd quiz2
~$ mkdir templates; mkdir static
~$ touch app.py
Komendę z ostatniej linii, która tworzy pusty plik o podanej nazwie, wydajemy w miarę rozbudowywania aplikacji. Można oczywiście korzystać z wybranego edytora.
7.3.3. Aplikacja i baza¶
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 | # -*- coding: utf-8 -*-
# quiz_pw/app.py
from flask import Flask, g
from peewee import *
app = Flask(__name__)
# konfiguracja aplikacji, m.in. klucz do obsługi sesji HTTP wymaganej
# przez funkcję flash
app.config.update(dict(
SECRET_KEY='bardzosekretnawartosc',
TYTUL='Quiz 2 Peewee'
))
# tworzymy instancję bazy używanej przez modele
baza = SqliteDatabase('quiz.db')
@app.before_request
def before_request():
g.db = baza
g.db.connect()
@app.after_request
def after_request(response):
g.db.close()
return response
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # -*- coding: utf-8 -*-
# quiz_sa/app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# konfiguracja aplikacji, m.in. klucz do obsługi sesji HTTP wymaganej
# przez funkcję flash
app.config.update(dict(
SECRET_KEY='bardzosekretnawartosc',
SQLALCHEMY_DATABASE_URI='sqlite:///quiz.db',
SQLALCHEMY_TRACK_MODIFICATIONS=False,
TYTUL='Quiz 2 SQLAlchemy'
))
# tworzymy instancję bazy używanej przez modele
baza = SQLAlchemy(app)
|
Moduł app.py
, jak wskazuje sama nazwa, służy zainicjowaniu aplikacji
Flaska (app = Flask(__name__)
).
Jej ustawienia przechowywane są w słowniku .config
. Oprócz klucza
używanego do obsługi sesji (SECRET_KEY
), a także nazwy wykorzystywanej
w szablonach (TYTUL
), w przypadku SQLAlchemy definiujemy tu nazwę pliku
bazy danych (SQLALCHEMY_DATABASE_URI='sqlite:///quiz.db'
)
i wyłączamy śledzenie modyfikacji (SQLALCHEMY_TRACK_MODIFICATIONS=False
).
Następnie tworzymy instancję obiektu reprezentującego bazę.
Peewee wykorzystuje specjalną zmienną g
, w której możemy przechowywać
różne zasoby składające się na kontekst aplikacji, np. instancję bazy.
Tworzymy ją przekazując konstruktorowi nazwę pliku (SqliteDatabase('quiz.db')
).
Następnie przy użyciu odpowiednich dekoratorów Flaska definiujemy funkcje
otwierające i zamykające połączenie w ramach każdego cyklu żądanie-odpowiedź,
co stanowi specyficzny wymóg bazy SQLite.
SQLAlchemy będziemy obsługiwać za pomocą rozszerzenia flask_sqlalchemy
,
które ułatwia używanie tego systemu ORM. Dzięki niemu tworzymy instancję
bazy powiązaną z konkretną aplikacją Flaska dzięki prostemu wywołaniu
odpowiedniego konstruktora (baza = SQLAlchemy(app)
).
7.3.4. Modele¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # -*- coding: utf-8 -*-
# quiz_pw/models.py
from app import baza
from peewee import *
class BaseModel(Model):
class Meta:
database = baza
class Pytanie(BaseModel):
pytanie = CharField(unique=True)
odpok = CharField()
class Odpowiedz(BaseModel):
pnr = ForeignKeyField(
Pytanie, related_name='odpowiedzi', on_delete='CASCADE')
odpowiedz = CharField()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # -*- coding: utf-8 -*-
# quiz_sa/models.py
from app import baza
class Pytanie(baza.Model):
id = baza.Column(baza.Integer, primary_key=True)
pytanie = baza.Column(baza.String(255), unique=True)
odpok = baza.Column(baza.String(100))
odpowiedzi = baza.relationship(
'Odpowiedz', backref=baza.backref('pytanie'),
cascade="all, delete, delete-orphan")
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.String(100))
|
Modele to miejsce, w którym opisujemy strukturę naszej bazy danych,
a więc definiujemy klasy – odpowiadające tabelom i ich właściwości -
odpowiadające kolumnom. Jak widać, 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 (ForeignKey
),
przechowujący identyfikator pytania. W obu systemach nieco inaczej definiujemy
to powiązanie, w Peewee podajemy nazwę klasy (Pytanie
), w SQLAlchemy
nazwę konkretnego pola (pytani.id
). W obu przypadkach inaczej też określamy
relacje zwrotne w postaci pola odpowiedzi
, za pomocą którego w obiekcie
Pytanie
będziemy mieli dostęp do przypisanych mu odpowiedzi.
Na uwagę zasługują atrybuty dodatkowe, dzięki którym po usunięciu pytania,
usunięte również zostaną wszystkie przypisane mu odpowiedzi. W Peewee
podajemy: on_delete = 'CASCADE'
; w SQLAlchemy: cascade="all, delete, delete-orphan"
.
Warto zauważyć również, że w SQLAlchemy dzięki rozszerzeniu flask.ext.sqlalchemy
jedyny import, którego potrzebujemy, to obiekt baza
, który udostępnia
wszystkie klasy i metody SQLAlchemy. Druga rzecz to miejsce, w którym określamy
relację zwrotną. Inaczej niż w Peewee robimy to w klasie Pytanie
.
7.3.5. Widoki¶
Przypomnijmy, że widoki to funkcje obsługujące przypisane im adresy url. Najczęściej po wykonaniu określonych operacji zawierają również wywołanie szablonu html, który uzupełniony o ewentualne dane zostaje odesłany użytkownikowi. Zawartość tych funkcji jest w dużej mierze niezależna od obsługi bazy, dlatego poniżej prezentować będziemy kompletny kod dla Peewee, a potrzebne zmiany dla SQLAlchemy będziemy wskazywać w komentarzu lub przywoływać we fragmentach. Warto również zaznaczyć, że wykorzystywane szablony dla obu systemów są takie same.
7.3.5.1. Strona główna i szablony¶
Widok obsługujący stronę główną w obu przypadkach jest prawie taki sam,
w Peewee linia from app import baza
nie jest potrzebna:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # -*- coding: utf-8 -*-
# quiz_sa/views.py
from flask import render_template, request, redirect, url_for, flash
from app import app
from app import baza
from models import Pytanie, Odpowiedz
@app.route('/')
def index():
return render_template('index.html')
|
Zadaniem funkcji index()
jest tylko wywołanie renderowania szablonu
index.html
, który zostanie zwrócony użytkownikowi. W omówionych
do tej pory scenariuszach aplikacji internetowych (Quiz, ToDo) opartych
na Flasku każdy szablon zawierał kompletny kod strony. W praktyce jednak
spora część kodu HTML powtarza się na każdej stronie w ramach danego serwisu.
W związku z tym nasze szablony będą oparte o wzorzec zawierający stałe
elementy i bloki oznaczające fragmenty, które będzie można dostosować
do danego widoku. Wzorzec umieszczamy w katalogu templates
pod nazwą
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 | <!doctype html>
<!-- quiz2pw/templates/szkielet.html -->
<html>
<head>
<title>{% block tytul %}{% endblock %} – {{ config.TYTUL }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<h1>{% block h1 %}{% endblock %}</h1>
<div id="menu" class="cb">
<ul>
<li><a href="{{ url_for('index') }}">Strona główna</a></li>
<li><a href="{{ url_for('quiz') }}">Rozwiąż quiz</a></li>
<li><a href="{{ url_for('dodaj') }}">Dodaj pytanie</a></li>
<li><a href="{{ url_for('edytuj') }}">Edytuj pytania</a></li>
</ul>
</div>
<div id="komunikaty" class="cb">
{% for kategoria, komunikat in get_flashed_messages(with_categories=true) %}
<span class="{{ kategoria }}">{{ komunikat }}</span>
{% endfor %}
</div>
<div id="tresc" class="cb">
{% block tresc %}
{% endblock %}
</div>
</body>
</html>
|
Przypomnijmy i uzupełnijmy składnię. Instrukcje sterujące otoczone znacznikami
{% %}
wymagają otwarcia i zamknięcia, np.: {% for %} {% endfor %}
.
Nowy znacznik {% block nazwa_bloku %}
pozwala definiować nazwane miejsca,
w których szablony dziedziczące mogą wstawiać swój kod. Jeżeli chcemy
umieścić w kodzie konkretne wartości używamy znaczników {{ zmienna }}
.
We wzorcu szablonów zawarliśmy więc elementy stałe, takie jak dołączane style css
w nagłówku strony, menu nawigacyjne wyświetlane na każdej stronie
(<div id="menu" class="cb">...</div>
) oraz wyświetlanie komunikatów
(<div id="komunikaty" class="cb">
). W każdym szablonie zwracanym przez
zdefiniowane widoki możemy natomiast zmienić tytuł strony
({% block tytul %}{% endblock %}
), nagłówek strony
({% block h1 %}{% endblock %}
) i przede wszystkim treść
({% block tresc %}{% endblock %}
). Tak właśnie robimy w szablonie
index.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- quiz2pw/templates/index.html -->
{% extends "szkielet.html" %}
{% block tytul %}Strona główna{% endblock%}
{% block h1 %}Quiz 2 – Peewee{% endblock%}
{% block tresc %}
<p>
Przykład aplikacji internetowej wykorzystującej framework Flask
do tworzenia m.in. serwisów WWW oraz system
<strong>ORM <a href="http://peewee.readthedocs.org/en/latest/">Peewee</a></strong> 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 %}
|
Każdy szablon dziedziczący z wzorca musi zawierać znacznik {% extends "szkielet.html" %}
,
a jeżeli coś zmienia, umieszcza odpowiednią treść w znaczniku typu
{% block tresc %} treść {% endblock %}
.
Dla porządku spójrzmy jeszcze na zawartość pliku style.css
zapisanego
w katalogu static
i określającego wygląd naszej aplikacji.
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 | /* quiz2pw/static/style.css */
body {
margin-top: 2em;
font: 1em/1.3em arial,tahoma,sans-serif;
background-color: #E6E6FA;
color: #000;
}
#menu { padding-bottom: 0.5em; }
#menu li { float: left; margin-left: 2em; }
#komunikaty {
width: 80%;
margin: 0.5em 2em 0 2em;
padding: 1em;
font-family: verdana,sans-serif;
border-radius: 5px;
background-color: #cecece;
}
#tresc { width: 80%; margin: 0.5em 2em 0 2em; }
h1, p { margin: 0 0 1em 2em; }
.add-form { margin-left: 2em; }
ol { text-align: left; }
form { display: inline-block; margin-bottom: 0;}
input[type=text] { width: 300px; margin-bottom: 0.5em; }
input:focus {
border-color: light-blue;
border-radius: 5px;
}
li { margin-bottom: 5px; }
button {
margin-top: 0.5em;
padding: 0;
cursor: pointer;
font-size: 1em;
background: white;
border: 1px solid gray;
border-radius: 3px;
color: blue;
}
span.blad { color: red; }
span.sukces { color: green; }
span.kom { color: blue; }
.cb { clear: both; }
.fb { font-weight: bold; }
|
7.3.6. Powiązanie modułów¶
Po zdefiniowaniu aplikacji, bazy, modelu, widoków i wykorzystywanych
przez nie szablonów, trzeba wszystkie moduły połączyć w całość.
Posłuży nam do tego plik main.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # -*- coding: utf-8 -*-
# quiz_pw/main.py
from app import app, baza
from models import *
from views import *
from dane import *
import os
if __name__ == '__main__':
if not os.path.exists('quiz.db'):
baza.create_tables([Pytanie, Odpowiedz], True) # tworzymy tabele
dodaj_pytania(pobierz_dane('pytania.csv'))
app.run(debug=True)
|
Żeby zrozumieć rolę tego modułu, wystarczy prześledzić źródła importów,
które w Pythonie odpowiadają nazwom plików. Tak więc z pliku (modułu)
app.py
importujemy instancję aplikacji i bazy, z models.py
klasy opisujące schemat bazy, a z views.py
zdefiniowane widoki.
W podanym kodzie najważniejsze jest polecenie tworzące bazę i tabele:
baza.create_tables([Pytanie, Odpowiedz],True)
; w SQLAlchemy
trzeba zastąpić je wywołaniem baza.create_all()
. Zostanie ono wykonane,
o ile na dysku nie istnieje już plik bazy quiz.db
.
Ostatnie polecenie app.run(debug=True)
ma uruchomić naszą aplikację
w trybie debugowania. Czas więc uruchomić nasz testowy serwer:
~/quiz2$ python main.py
Po wpisaniu w przeglądarce adresu 127.0.0.1:5000 powinniśmy zobaczyć:
7.3.7. Widoki CRUD¶
Skrót CRUD (Create (tworzenie), Read (odczyt), Update (aktualizacja), Delete (usuwanie)) oznacza, jak wyjaśniono, podstawowe operacje wykonywane na bazie danych.
7.3.7.1. 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 | # -*- coding: utf-8 -*-
# quiz_sa/dane.py
from app import baza
from models import Pytanie, Odpowiedz
import os
def pobierz_dane(plikcsv):
"""Funkcja zwraca tuplę tupli zawierających dane pobrane z pliku csv."""
dane = []
if os.path.isfile(plikcsv):
with open(plikcsv, "r") as sCsv:
for line in sCsv:
line = line.replace("\n", "") # usuwamy znaki końca linii
line = line.decode("utf-8") # format kodowania znaków
dane.append(tuple(line.split("#")))
else:
print "Plik z danymi", plikcsv, "nie istnieje!"
return tuple(dane)
|
W Peewee linia from app import baza
nie jest potrzebna.
Plik z danymi:
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
|
Pierwsza funkcja pobierz_dane('pytania.csv')
odczytuje z podanego pliku kolejne
linie zawierające pytanie, odpowiedzi i odpowiedź prawidłową oddzielone znakiem
“#”. Z odczytanych linii usuwamy znaki końca linii, następnie ustawiamy
kodowanie znaków, a na koniec rozbijamy je na trzy elementy (line.split("#")
),
z których tworzymy tuple i dodajemy ją do listy dane.append(tuple(...))
.
Na koniec listę tupli zwracamy jako tuplę, która trafia do wywołania
drugiej funkcji dodaj_pytania()
.
24 25 26 27 28 29 30 31 32 33 | def dodaj_pytania(dane):
"""Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
for pytanie, odpowiedzi, odpok in dane:
pyt = Pytanie(pytanie=pytanie, odpok=odpok)
pyt.save()
for o in odpowiedzi.split(","):
odp = Odpowiedz(pnr=pyt.id, odpowiedz=o.strip())
odp.save()
print "Dodano przykładowe pytania"
|
25 26 27 28 29 30 31 32 33 34 35 36 | def dodaj_pytania(dane):
"""Funkcja dodaje pytania i odpowiedzi przekazane w tupli do bazy."""
for pytanie, odpowiedzi, odpok in dane:
pyt = Pytanie(pytanie=pytanie, odpok=odpok)
baza.session.add(pyt)
baza.session.commit()
for o in odpowiedzi.split(","):
odp = Odpowiedz(pnr=pyt.id, odpowiedz=o.strip())
baza.session.add(odp)
baza.session.commit()
print "Dodano przykładowe pytania"
|
Pętla for pytanie,odpowiedzi,odpok in dane:
do oddzielonych przecinkami
zmiennych odczytuje z przekazanych tupli kolejne dane. Następnie
tworzymy obiekty reprezentujące rekordy w tablicy pytanie
(pyt = Pytanie(pytanie = pytanie, odpok = odpok)
) i wywołujemy
odpowiednie dla danego ORM-u polecenia zapisujące je w bazie. Podobnie
postępujemy w pętli wewnętrznej, przy czym tworząc obiekty odpowiedzi
wykorzystujemy identyfikatory zapisanych wcześniej pytań
(odp = Odpowiedz(pnr = pyt.id, odpowiedz = o.strip())
).
7.3.7.2. Odczyt¶
Zaczniemy od widoku wyświetlającego pobrane z bazy dane w formie quizu i sprawdzającego udzielone przez użytkownika odpowiedzi.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | @app.route('/quiz', methods=['GET', 'POST'])
def quiz():
# POST, sprawdź odpowiedzi
if request.method == 'POST':
wynik = 0 # liczba poprawnych odpowiedzi
# odczytujemy słownik z odpowiedziami
for pid, odp in request.form.items():
# pobieramy z bazy poprawną odpowiedź
odpok = Pytanie.select(Pytanie.odpok).where(
Pytanie.id == int(pid)).scalar()
if odp == odpok: # porównujemy odpowiedzi
wynik += 1 # zwiększamy wynik
# przygotowujemy informacje o wyniku
flash(u'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(u'Brak pytań w bazie.', 'kom')
return redirect(url_for('index'))
return render_template('quiz.html', pytania=pytania)
|
Wyświetlenie pytań wymaga odczytania ich wraz z możliwymi odpowiedziami z bazy.
W Peewee korzystamy z kodu: Pytanie().select().annotate(Odpowiedz)
,
w SQLAlchemy: Pytanie.query.join(Odpowiedz)
(metoda .join()
zwiększa efektywność, bo wymusza pobranie możliwych odpowiedzi
w jednym zapytaniu). Po sprawdzeniu, czy mamy jakiekolwiek pytania za pomocą
metody .count()
, zwracamy użytkownikowi szablon quiz.html
, któremu
przekazujemy w zmiennej pytania
dane w odpowiedniej formie. W SQLALchemy
korzystamy z metody .all()
zwracającej pasujące rekordy jako listę.
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 32 | <!-- quiz2pw/templates/quiz.html -->
{% extends "szkielet.html" %}
{% block tytul %}Pytania{% endblock%}
{% block h1 %}Quiz 2 – pytania{% endblock%}
{% block tresc %}
<p class="fb">
Odpowiedz na pytania:
</p>
<!-- 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>
<!-- odpowiedź wyświetlamy jako pole radio button -->
<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">Sprawdź odpowiedzi</button>
</form>
{% endblock %}
|
Użytkownik po wybraniu odpowiedzi naciska przycisk Sprawdź... i przesyła
do naszego widoku dane w żądaniu typu POST. W funkcji quiz()
uwzględniamy taką sytuację i w pętli for pid, odp in request.form.items():
odczytujemy identyfikator pytania i udzieloną odpowiedź. Następnie
pobieramy odpowiedź prawidłową w Peewee za pomocą kodu
odpok = Pytanie.select(Pytanie.odpok).where(Pytanie.id == int(pid)).scalar()
,
a w SQLALchemy odpok = baza.session.query(Pytanie.odpok).filter(Pytanie.id == int(pid)).scalar()
.
W obu przypadkach metody .scalar()
zwracają pojedyncze wartości, które
porównujemy z odpowiedziami użytkownika (if odp == odpok:
) i w przypadku
poprawności zwiększamy wynik.
7.3.7.3. Dodawanie i aktualizacja¶
Możliwość dodawania nowych pytań i odpowiedzi wymaga stworzenia nowego widoku powiązanego z określonym adresem url, jak i szablonu, który wyświetli użytkownikowi właściwy formularz. Na początku zajmiemy się właśnie nim.
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 | <!-- quiz2pw/templates/dodaj.html -->
{% extends "szkielet.html" %}
{% block tytul %}Dodawanie{% endblock%}
{% block h1 %}Quiz 2 – dodawanie pytań{% endblock%}
{% block tresc %}
<form method="POST" class="add-form" action="{{ url_for('dodaj') }}">
<label>Wpisz pytanie:</label><br />
{% if pytanie %}
<!-- wstawiamy id pytania -->
<input type="hidden" name="id" value="{{ pytanie.id }}" /><br />
<input type="text" name="pytanie" value="{{ pytanie.pytanie }}" /><br />
{% else %}
<input type="text" name="pytanie" value="" /><br />
{% endif %}
<label>Podaj odpowiedzi:</label><br />
<ol>
{% if pytanie %}
{% for o in pytanie.odpowiedzi %}
<li><input type="text" name="odp[]" value="{{ o.odpowiedz }}" /></li>
{% endfor %}
{% else %}
<li><input type="text" name="odp[]" value="" /></li>
<li><input type="text" name="odp[]" value="" /></li>
<li><input type="text" name="odp[]" value="" /></li>
{% endif %}
</ol>
<label>Podaj numer poprawnej odpowiedzi:</label><br />
{% if pytanie %}
{% for o in pytanie.odpowiedzi %}
{% if o.odpowiedz == pytanie.odpok %}
<input type="text" name="odpok" value="{{ loop.index }}" /><br />
{% endif %}
{% endfor %}
{% else %}
<input type="text" name="odpok" value="" /><br />
{% endif %}
<button type="submit">Zapisz pytanie</button>
</form>
{% endblock %}
|
Powyższy kod umieszczamy w pliku dodaj.html
w katalogu szablonów, czyli
templates
. Jak widać najważniejszym elementem jest tu formularz.
Zawiera on pola tekstowe przeznaczone na pytanie, trzy odpowiedzi
i numer odpowiedzi poprawnej. Takiego formularza możemy użyć zarówno do dodawania nowych,
jak i edycji istniejących już pytań. Jedyna różnica będzie taka, że
przy edycji musimy w formularzu wyświetlić dane wybranego pytania.
Dlatego w kodzie szablonu stosujemy instrukcję warunkową {% if pytanie %}
,
która decyduje o tym, czy wyświetlamy puste pola, czy wypełniamy je
przekazanymi danymi. W tym ostatnim przypadku umieszczamy w
formularzu dodatkowe ukryte pole, w którym zapisujemy id edytowanego pytania.
Załóżmy, że użytkownik wpisał lub zmienił pytanie i nacisnął przycisk
typu submit, czyli wysłał dane do serwera. Co dzieje się dalej? Takie
żądanie POST trafi do widoku dodaj()
, co określone zostało
w atrybucie formularza: action="{{ url_for('dodaj') }}"
. Zobaczmy, jak
wygląda ten widok:
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 | @app.route('/dodaj', methods=['GET', 'POST'])
def dodaj():
error = []
# POST, zapisz pytanie
if request.method == 'POST':
# sprawdzanie poprawności przesłanych danych
if len(request.form['pytanie']) == 0:
error.append(u'Błąd: pytanie nie może być puste!')
odpowiedzi = list(request.form.getlist('odp[]'))
for odp in odpowiedzi:
if len(odp) == 0:
error.append(u'Odpowiedź nie może być pusta!')
if len(request.form['odpok']) == 0:
error.append(u'Brak numeru poprawnej odpowiedzi!')
elif int(request.form['odpok']) > len(odpowiedzi):
error.append(u'Błędny numer poprawnej odpowiedzi!')
if not error: # jeżeli nie ma błędów dodajemy pytanie
pytanie = request.form['pytanie'].strip()
odpok = odpowiedzi[(int(request.form['odpok']) - 1)]
try:
if request.form['id']: # aktualizujemy pytanie
p = Pytanie.select(Pytanie, Odpowiedz).join(Odpowiedz).\
where(Pytanie.id == int(request.form['id'])).get()
p.pytanie = pytanie.strip()
p.odpok = odpok.strip()
p.save()
for i, o in enumerate(list(p.odpowiedzi)):
o.odpowiedz = odpowiedzi[i].strip()
o.save()
flash(u'Zmieniono pytanie:', 'sukces')
except KeyError: # dodajemy nowe pytanie, brak id pytania!
p = Pytanie(pytanie=pytanie.strip(), odpok=odpok.strip())
p.save()
for odp in odpowiedzi:
o = Odpowiedz(pnr=p, odpowiedz=odp.strip())
o.save()
flash(u'Dodano pytanie:', 'sukces')
flash("\n" + pytanie + " " + odpok.strip() +
" (" + ", ".join(odpowiedzi) + ")", 'kom')
return redirect(url_for('index'))
else:
for e in error:
flash(e, 'blad')
# GET, wyświetl formularz
return render_template('dodaj.html')
|
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | try:
if request.form['id']: # aktualizujemy pytanie
p = Pytanie.query.get(request.form['id'])
p.pytanie = pytanie.strip()
p.odpok = odpok.strip()
for i, odp in enumerate(odpowiedzi):
p.odpowiedzi[i].odpowiedz = odp.strip()
baza.session.commit()
flash(u'Zmieniono pytanie:', 'sukces')
except KeyError: # dodajemy nowe pytanie, brak id pytania!
p = Pytanie(pytanie=pytanie.strip(), odpok=odpok.strip())
baza.session.add(p)
baza.session.commit()
for odp in odpowiedzi:
o = Odpowiedz(pnr=p.id, odpowiedz=odp.strip())
baza.session.add(o)
baza.session.commit()
flash(u'Dodano pytanie:', 'sukces')
|
Po otworzeniu adresu /dodaj
otrzymujemy żądanie GET,
na które odpowiadamy zwróceniem omówionego wyżej szablonu dodaj.html
.
Jeżeli jednak otrzymujemy dane z formularza, na początku dokonujemy prostej
walidacji, tj. sprawdzamy, czy użytkownik nie przesyła pustego pytania lub odpowiedzi,
dodatkowo, czy podał odpowiedni numer odpowiedzi poprawnej.
Obiekt request.form
zawiera wszystkie dane przesłane w ramach
żądania. Jeżeli wśród nich nie ma identyfikatora pytania, co oznaczałoby
edycję, generowany jest wyjątek, który przechwytujemy za pomocą konstrukcji
try: ... except KeyError:
i dodajemy nowe pytanie.
Tworzymy więc nowy obiekt pytania
(p = Pytanie(pytanie = pytanie.strip(), odpok = odpok.strip())
) i używając
odpowiednich metod zapisujemy. Podobnie dalej odczytujemy w pętli przesłane odpowiedzi,
dla każdej tworzymy nowy obiekt (o = Odpowiedz(pnr = p, odpowiedz = odp.strip())
)
i zapisujemy.
Trochę więcej zachodu wymaga aktualizacja danych. Na początku pobieramy
obiekt reprezentujemy edytowane pytanie i odpowiedzi na nie. W Peewee kod jest cokolwiek
rozbudowany: p = Pytanie.select(Pytanie,Odpowiedz).join(Odpowiedz).where(Pytanie.id == int(request.form['id'])).get()
,
w SQLAlchemy jest krócej: p = Pytanie.query.get(request.form['id'])
.
Później odpowiednim polom przypisujemy nowe dane. Więcej różnic występuje
dalej. W Peewee przeglądamy listę obiektów reprezentujących odpowiedzi,
w każdym zmieniamy odpowiednią właściwość (o.odpowiedz = odpowiedzi[i].strip()
)
i zapisujemy zmiany. w SQLAlchemy iterujemy po przesłanych odpowiedziach,
które zapisujemy w obiektach odpowiedzi odczytywanych bezpośrednio
z obiektu reprezentującego pytanie (p.odpowiedzi[i].odpowiedz = odp.strip()
).
Zapisywanie lub aktualizacja danych kończy się wygenerowaniem odpowiedniego
komunikatu dla użytkownika, np. flash(u'Dodano pytanie:','sukces')
.
Podobnie wcześniej, jeżeli podczas walidacji otrzymanych danych pojawi
się błąd, komunikat o nim zostanie zapisany w liście error[]
,
a później przekazany użytkownikowi w kodzie: for e in error: flash(e, 'blad')
.
Warto zwrócić tu uwagę na dodatkowe argumenty w funkcji flash
, wskazują
one rodzaj przekazywanych informacji, co wykorzystujemy we wzorcu
szkielet.html
. Pętla {% for kategoria, komunikat in get_flashed_messages(with_categories=true) %}
w zmiennej kategoria
odczytuje omawiane dodatkowe argumenty
i używa jej do oznaczenia klasy CSS decydującej o sposobie wyświetlenia
danej informacji: <span class="{{ kategoria }}">{{ komunikat }}</span>
.
7.3.7.4. Widok edycji i usuwanie¶
Można zadać pytanie, jak do szablonu dodaj.html
trafiają pytania, które
chcemy edytować. Odpowiada za to widok edytuj()
90 91 92 93 94 95 96 97 98 99 100 101 102 103 | @app.route('/edytuj', methods=['GET', 'POST'])
def edytuj():
pytania = Pytanie().select().annotate(Odpowiedz)
if not pytania.count():
flash(u'Brak pytań w bazie.', 'kom')
return redirect(url_for('index'))
if request.method == 'POST':
pid = request.form['id']
pytanie = Pytanie.select(Pytanie, Odpowiedz).join(
Odpowiedz).where(Pytanie.id == int(pid)).get()
return render_template('dodaj.html', pytanie=pytanie)
return render_template('edytuj.html', pytania=pytania)
|
Na początku pobieramy wszystkie pytania przy użyciu takiego samego kodu
jak w widoku quiz()
i sprawdzamy, czy w ogóle jakieś są. Jeżeli tak,
przekazujemy pytania do szablonu edytuj.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 | <!-- quiz2pw/templates/edytuj.html -->
{% extends "szkielet.html" %}
{% block tytul %}Edycja{% endblock%}
{% block h1 %}Quiz 2 – edycja pytań{% endblock%}
{% block tresc %}
<!-- pętla odczytująca kolejne pytania z listy -->
<ol>
{% for p in pytania %}
<li>
<!-- wypisujemy pytanie -->
<input type="text" value="{{ p.pytanie }}" name="pyt[]" />
<form method="POST" action="{{ url_for('edytuj') }}">
<!-- wstawiamy id pytania -->
<input type="hidden" name="id" value="{{ p.id }}"/>
<button type="submit">Edytuj</button>
</form>
<form method="POST" action="{{ url_for('usun') }}">
<!-- wstawiamy id pytania -->
<input type="hidden" name="id" value="{{ p.id }}"/>
<button type="submit">Usuń</button>
</form>
</li>
{% endfor %}
</ol>
{% endblock %}
|
Zadaniem szablonu jest wyświetlenie treści pytań i dwóch przycisków typu
submit, umożliwiających edycję lub usunięcie pytania. Przyciski te
są częścią formularzy, które zawierają tylko jedno ukryte pole przechowujące
id pytania. O tym, gdzie trafia identyfikator decyduje atrybutu action
w formularzu: {{ url_for('edytuj') }}
lub {{ url_for('usun') }}
.
Używamy tu funkcji url_for
, która na podstawie podanego widoku generuje
odpowiadający mu adres url.
Jeżeli użytkownik wybierze edycję, do omawianego widoku edytuj()
trafia
żądanie POST, które obsługujemy w ten sposób, że na podstawie
odebranego identyfikatora tworzymy obiekt z żądanym pytaniem i odpowiedziami
(w SQLAlchemy stosujemy tu polecenie: Pytanie.query.get(pid)
), a następnie
każemy go wyrenderować w szablonie dodaj.html
. Działanie tego szablonu
omówiono wyżej. Jeżeli użytkownik kliknie przycisk Usuń jego żądanie
trafia do widoku usun()
. Funkcja ta przedstawia się następująco:
106 107 108 109 110 111 112 113 | @app.route('/usun', methods=['POST'])
def usun():
"""Usunięcie pytania o identyfikatorze pid"""
pid = request.form['id']
pytanie = Pytanie.get(Pytanie.id == int(pid))
pytanie.delete_instance(recursive=True)
flash(u'Usunięto pytanie {0}'.format(pid), 'sukces')
return redirect(url_for('index'))
|
Działanie jest proste. Tworzymy obiekt reprezentujący pytanie o przesłanym
identyfikatorze i wywołujemy metodę, która go usuwa.. W Peewee korzystamy z polecenia:
Pytanie.get(Pytanie.id == int(pid))
i metody delete_instance(recursive = True)
;
dodatkowy argument recursive
zapewnia kaskadowe usunięcie wszystkich odpowiedzi.
W SQLAlchemy pozyskany obiekt p = Pytanie.query.get(pid)
usuwamy za pomocą
metody sesji baza.session.delete(p)
, którą finalnie zapisujemy baza.session.commit()
.
Na koniec wywołujemy za pomocą tzw. przekierowania widok strony głównej
(return redirect(url_for('index'))
), który wyświetli przygotowane dla użytkownika komunikaty.
Nota bene, podobnie postąpiliśmy również w innych omówionych wyżej widokach.
7.3.8. Źródła¶
Kompletne wersje kodu znajdziesz w powyższym archiwum w podkatalogach quiz2_pw
i quiz2_sa
. Uruchamiamy je poleceniami:
~/quiz2/quiz2_orm$ python main.py
- gdzie orm jest oznaczeniem modułu obsługi bazy danych, pw dla Peewee, sa dla SQLALchemy.
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: | 2017-09-08 o 19:38 w Sphinx 1.4.5 |
---|---|
Autorzy: | Patrz plik “Autorzy” |