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 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
 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 %} &#8211; {{ 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') }} – funkcja url_for() pozwala wygenerować scież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
 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:

../../_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
 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.

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

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

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

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

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

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

../../_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
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, 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
 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 &#8211; 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 %}
../../_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
 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 listy choices zawierającej pary wartość - etykieta,
  • HiddenField() – pole ukryte.

Funkcja pomocnicza i widok obsługujący dodawanie:

Peewee views.py. Kod nr
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ł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
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 &#8211; 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:

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

../../_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
 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 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
 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 &#8211; 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:

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

Szablon lista.html. Kod nr
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.
../../_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
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 (opcja recursive).

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

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

Szablon lista.html. Kod nr
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>
../../_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
 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():

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

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


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:2022-05-22 o 19:52 w Sphinx 1.5.3
Autorzy:Patrz plik “Autorzy”