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 pliku pytania.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:

Terminal. Kod nr
~/quiz-orm$ python3 main.py
../../_images/quiz-orm_skel.png

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:

Szablon szkielet.html. Kod nr
 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 %} &#8211; {{ 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') }} – funkcja url_for() pozwala wygenerować ścieżkę do zasobów umieszczonych w podkatalogu static;

  • {% 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:

Szablon index.html. Kod nr
 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:

../../_images/quiz-orm_glowna.png

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:

Peewee app.py. Kod nr
 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.

Peewee models.py. Kod nr
 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:

Peewee dane.py. Kod nr
 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:

Plik pytania.csv. Kod nr
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:

Peewee main.py. Kod nr
 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ą:

Peewee views.py. Kod nr
 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:

Szablon lista.html. Kod nr
 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 &#8211; 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ń:

../../_images/quiz-orm_lista.png

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

Peewee views.py. Kod nr
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, metoda scalar() 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:

Szablon quiz.html. Kod nr
 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 &#8211; 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 %}
../../_images/quiz-orm_quiz.png

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:

Peewee forms.py. Kod nr
 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 listy choices zawierającej pary wartość - etykieta,

  • HiddenField() – pole ukryte.

Funkcja pomocnicza i widok obsługujący dodawanie:

Peewee views.py. Kod nr
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łowniku form.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:

Szablon dodaj.html. Kod nr
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 &#8211; 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:

Szablon pytanie_form.html. Kod nr
 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:

../../_images/quiz-orm_dodawanie.png

7.3.9. Edycja

Zaczniemy od dodania w pliku views.py funkcji pomocniczych i widoku edytuj():

Peewee views.py. Kod nr
 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 pomocnicza get_or_404() zwróci obiekt, a jeżeli nie będzie to możliwe, wywoła błąd abort(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 szablon 404.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 parametru obj.

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

Szablon 404.html. Kod nr
 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 &#8211; 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:

Szablon edytuj.html. Kod nr
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 &#8211; 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:

Szablon lista.html. Kod nr
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.

../../_images/quiz-orm_edycja.png
../../_images/quiz-orm_edycja2.png

7.3.10. Usuwanie

Pozostaje umożliwienie usuwania pytań i odpowiedzi. W pliku views.py dodajemy widok usun():

Peewee views.py. Kod nr
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 (opcja recursive).

W przypadku żądania typu GET zwracamy formularz potwierdzenia usunięcia pytanie_usun.html:

Szablon pytanie_usun.html. Kod nr
 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 &#8211; 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:

Szablon lista.html. Kod nr
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>
../../_images/quiz-orm_usuwanie.png
../../_images/quiz-orm_usuwanie2.png

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.

SQLAlchemy app.py. Kod nr
 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():

SQLAlchemy dane.py. Kod nr
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ą:

SQLAlchemy main.py. Kod nr
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:

SQLAlchemy models.py. Kod nr
 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 obiekt baza 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:

SQLAlchemy views.py. Kod nr
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


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:

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

Autorzy:

Patrz plik „Autorzy”