Wykorzystanie baz danych w Django: podstawy
Tym razem w naszej lekcji cofniemy się o jeden krok, zanim wykonamy dwa kolejne. Kiedy skończyliśmy ostatnią lekcję, oba formularze w naszej aplikacji, formularz do głosowania oraz do tworzenia nowych ankiet były obsługiwane przez klasy pomocnicze Django.
Wróćmy na chwilę do koncepcji obsługi formularza bez wykorzystania klas pomocniczych Django i spróbujmy dodać do naszej aplikacji obsługę połączenia z bazą danych.
Skąd wziąć bazę danych i dlaczego już ją mam?
Po pierwsze, jeżeli ktoś czytał ostrzeżenia podczas każdego uruchamiania aplikacji, to prawdopodobnie podejrzewa, że Django już zakładało jakieś połączenie z bazą danych. To fakt, framework Django zakłada, że nasza aplikacja będzie korzystała z bazy danych. Domyślnie utworzony projekt zawiera aplikację admin
, która już sama w sobie zawiera pewne typy danych, które autorzy Django uznali na tyle powszechne we wszystkich aplikacjach internetowych, że z góry zakładają, że programista będzie chciał z nich korzystać.
Co do wyboru domyślnego silnika bazy danych, to Django też ma konrytną, domyślną propozycję dla użytkowników, jest to mianowicie darmowy i prosty SQLite. Należy zaznaczyć, że SQLite przez lata nie był traktowany poważnie jako baza danych, która może być używana “na produkcji”, ale po latach rozwoju SQLite okazuje się zupełnie wystarczającym rozwiązaniem dla wielu projektów. Oczywiście Django wspiera wiele innych silników bazodanowych, ale na chwilę obecną wystarczy nam wybór domyślny.
Kontakt z bazą (danych)
Do kontaktu z bazą danych Django używa tzw. klas modeli nazwanych tak ze względu na zastosowaną architekturę typu Model-View-Controller. Zaczniemy od stworzenia prostego zestwu dwóch klas opisujących dane, na których działa nasza aplikacja: ankiet oraz przypisanych do nich zestawów odpowiedzi wraz z ilością oddanych na te odpowiedzi głosów.
Wyedytujmy w tym celu nieruszany dotychczas plik models.py
:
|
|
Jeżeli pracowaliście wcześniej z bazami danych, to taki opis może być dla Was naturalny, ale przejdźmy przez niego pokolei:
- W liniach 4-9 definiujemy klasę opisującą ankietę, która zawiera tekst pytania jako pole tekstowe o długości 200 znaków oraz datę publikacji.
- W liniach 11-17 definiujemy klasę odpowiedzi na ankietę
- W linii 12 określamy, że po pierwsze odpowiedź jest powiązana z ankietą poprzez pole
question
.
Warto zwrócić uwagę na powiązanie dwóch klas poprzez użycie pola typuForeignKey
.
Dodatkowo określamy, że w przypadku usunięcia pytania, chcemy żeby baza danych sama usunęła powiązane z pytaniem odpowiedzi. To właśnie określa argumenton_delete=models.CASCADE
. - W linii 13 i 14 definiujemy pozostałe informacje dotyczące odpowiedzi, czyli jak odpowiedź brzmi i ile zebrała głosów.
- W linii 12 określamy, że po pierwsze odpowiedź jest powiązana z ankietą poprzez pole
- W liniach 8-9 oraz 16-17 pojawia się nadpisanie funkcji specjalnej w języku Python, która służy do generowania reprezentacji tekstowej obiektu danej klasy.
Póki co niech nam wystarczy informacja, że dzięki temu nasze dane będą lepiej widoczne podczas debugowania.
Po dodaniu tych modeli możemy użyć skryptu manage.py
do stworzenia dla nas skryptów SQL, które stworzą w bazie danych odpowiednie tabele do przechowywania naszcy danych, a następnie do uruchomienia tych skryptów. Zrobimy to używając komend:
$ ./manage.py makemigrations
$ ./manage.py migrate
Po uruchomieniu tych skryptów zasadniczo nic się w naszej aplikacji nie zmieni, ale możemy zacząć już pracować z bazą danyc. Zacznijmy od wykorzystania panelu administracyjnego Django.
Integracja modeli z wbudowaną aplikacją administracyjną
Zacznijmy od dodania naszego modelu do strony admin
.
from django.contrib import admin
from .models import Poll
admin.site.register(Poll)
Teraz w terminalu stwórz nowe konto administratorskie dla swojej aplikacji, a następnie wejdź na stronę na ścieżkę admin/ i zaloguj się używając danych stworzonego przed chwilą konta użytkownika.
$ ./manage.py createsuperuser
Teraz nasza strona powinna pozwolić na tworzenie nowych ankiet, przeglądanie tych już obecnych w bazie danych i usuwanie ich. Oczywiście na początku lista ta będzie pusta.
Stwórz przynajmniej dwie ankiety, a potem zabierzemy się za wydobycie danych z bazy w naszej aplikacji.
Czytanie z bazy danych
Zastąp swój obecny widok index
następującym kodem:
from .models import Poll
def index(request):
context = {
"latest_polls": Poll.objects.order_by("-pub_date")[:5]
}
return render(request, "polls/index.html", context)
Dużo się dzieje w tej jednej linijce kodu! Czyli do danych odpowiadających klasie Polls mogę się dostać przez pole Polls.objects
. Następnie sortuję te dane po dacie publikacji (od najnowszych), a potem wybieram pierwsze pięć z nich.
Skoro mamy dane, to jeszcze trzeba je jakoś obsłużyć w szablonie HTML.
Zastąp obecną zawartość pliku następującą treścią:
{% extends "polls/base.html" %}
{% block content %}
Recent polls:
<ul>
{% for poll in latest_polls %}
<li><a href="{% url 'polls:poll' poll.id %}">{{ poll.question_text }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Zwróć uwagę, że tworząc url do ankiety posłużyliśmy się zmienną poll.id
. Nasza ankieta nie ma już krótkiej nazwy, do której możemy się odnieść, ale w zamian za to Django stworzyło dla nas unikatowy identyfikator, po którym możemy odszukać ankietę. Poszukaj tego w identyfikatora używając panelu administracyjnego Django.
Strefy czasowe w Django
Jeżeli daty lub godziny nie zgadzają się do końca z tym, czego się spodziewasz, upewnij się, że zarówno Django jak i Twoja maszyna wirtualna korzystają z tej samej strefy czasowej.
Ustawienie strefy czasowej na systemie Ubuntu Linux:
$ sudo timedatectl set-timezone Europe/Warsaw
Ustawienie tej samej strefy w aplikacji Django:
# ...
TIME_ZONE = 'Europe/Warsaw'
# ...
Wyświetlanie formularzy do głosowania w ankietech
Aby wyświetlić naszą ankietę i umożliwić głosowanie w niej, musimy znów dostosować odpowiednio widok, wykorzystywany przez niego szablon i upewnić się, że aplikacja poprawnie obsłuży ścieżki. Zacznijmy od ostatniego punktu.
Ponieważ nasze ankiety nie mają obecnie nazw, ale mają identyfikatory w bazie danych, zmienimy nazwę widoku i jego argumentu na bardziej adekwatną:
app_name = "polls"
urlpatterns = [
path('', views.index, name="index"),
path('<int:poll_id>/', views.by_id, name="details"),
path('vote/<str:poll_name>/', views.vote, name="vote"),
path('results/<str:poll_name>/', views.results, name="results"),
]
Następnie zmodyfikujemy sam widok:
# ...
def by_id(request, poll_id):
context = {}
retcode = 200
try:
context['poll'] = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
retcode = 404
return render(request, "polls/poll.html", context, status=retcode)
Poll.objects.get(pk=poll_id)
jest tu kluczowe. pk
to skrót od primary key, a metoda get
zakłada, że w bazie danych będzie jeden i tylko jeden obiekt spełniający warunki wyszukiwania. Skoro szukamy ankiety o konkretnym, unikalnym identyfikatorze, to powinno tak właśnie być.
A na sam koniec wyświtlimy dane używając naszego szablonu HTML:
{% extends "polls/base.html" %}
{% block content %}
{% if poll %}
<form action="{% url 'polls:vote' poll.id%}" method="post">
{% csrf_token %}
<fieldset>
<legend><h3>{{ poll.question_text }}</h3></legend>
{% for choice in poll.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ forloop.counter0 }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
{% else %}
<p>No such poll!</p>
{% endif %}
{% endblock %}
Dodawanie danych z konsoli Pythona
Co z tego, że mamy możlwiość wyświetlenia ankiet, skoro nasze ankiety nie mają żadnych przypisanych opcji odpowiedzi?
Moglibyśmy stworzyć znów interface do tego celu w panelu administracyjnym, ale jest inna, potencjalnie szybsza opcja. Używając wiersza poleceń możemy użyć skryptu manage.py do stworzenia dla nas Pythonowego shella, w którym będziemy mogli ręcznie modyfikować bazę danych korzystając z istniejących klas naszej aplikacji.
$ ./manage.py shell
>>> from polls.models import Poll
>>> q = Poll.objects.get(pk=1)
>>> q
<Poll: What is the best ice-cream flavour?>
>>> q.choice_set.create(choice_text="chocolate")
>>> q.choice_set.create(choice_text="vanilla")
W mojej aplikacji zacząłem od stworzenia pytania o najlepszy smak lodów. Django automatycznie nadał tej ankiecie id z numerem 1. W powyższej sesji, stworzyłem następnie dwie możliwe odpowiedzi. Po wejściu na stronę ankiety, mogłem zobaczyć czy faktycznie pojawiły się one w bazie.
Przywrócenie funkcjonalności głosowania
O ile udało nam się wyświetlić poprawnie dane z bazy danych, o tyle samo głosowanie oraz walidacja danych od użytkownika znów nie działa. Wróćmy do koncepcji użycia klas pomocniczych Django do obsługi formularza za nas:
Zacznijmy od stworzenia samej klasy formularza. Utwórz nowy plik forms.py
from django import forms
class VoteForm(forms.Form):
choice = forms.ChoiceField(
widget=forms.RadioSelect,
required=True,
)
def __init__(self, poll, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['choice'].choices = [(choice.id, choice.choice_text) for choice in poll.choice_set.all()]
self.fields['choice'].label = poll.question_text
W linii 11 używamy dość skomplikowanego wyrażenia:
[(choice.id, choice.choice_text) for choice in poll.choice_set.all()]
W tym miejscu Django oczekuje listy dostępnych wartości w formie [(wartosc, etykieta), (wartosc, etykieta), ...]
więc korzystając z konstrukcji list comprehensions przygotowujemy listę w dokładnie takim formacie.
Skoro mamy już formularz, to użyjmy go w naszym widoku wyświetlającym ankietę:
from .forms import VoteForm
# ...
def by_id(request, poll_id):
context = {}
retcode = 200
try:
poll = Poll.objects.get(pk=poll_id)
context['poll'] = poll
context['vote_form'] = VoteForm(poll)
except Poll.DoesNotExist:
retcode = 404
return render(request, "polls/poll.html", context, status=retcode)
Oraz użyjmy go w naszym szablonie HTML:
{% extends "polls/base.html" %}
{% block content %}
{% if poll and vote_form %}
<form action="{% url 'polls:vote' poll.id%}" method="post">
{% csrf_token %}
{{ vote_form }}
<input type="submit" value="Vote">
</form>
{% else %}
<p>No such poll!</p>
{% endif %}
{% endblock %}
Jeżeli szablon nadal poprawnie się wyświetla, to możemy przejść do obsłużenia głosowania w widoku vote
:
from django.db.models import Sum
from .models import Poll, Choice
def vote(request, poll_id):
if request.method == 'GET':
return HttpResponseRedirect(reverse("polls:details", args=[poll_id]))
try:
poll = Poll.objects.get(pk=poll_id)
vote_form = VoteForm(poll, data=request.POST)
if vote_form.is_valid():
selected_choice = Choice.objects.get(pk = vote_form.cleaned_data['choice'])
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse("polls:results", args=[poll_id]))
else:
context = {
'poll': poll,
'vote_form': vote_form
}
return render(request, "polls/poll.html", context)
except Poll.DoesNotExist:
return render(request, "polls/poll.html", status=404)
Jak widać obsługa nie zmieniła się znacząco od poprzedniej implementacji. Tworzymy wypełniony formularz na bazie danych przesłanych przez użytkownika w request.POST
, a następnie jeżeli dane są prawidłowe, odnotowujemy oddany głos i zapisujemy informację w bazie danych.
Wyświetlanie wyników będzie działało podobnie jak ostatnio, oto jak będzie wyglądał nasz kod:
def results(request, poll_id):
context = {}
retcode = 200
try:
poll = Poll.objects.get(pk=poll_id)
context['poll'] = poll
votes = poll.choice_set.aggregate(total=Sum('votes'))
context['total_votes'] = max(1, votes['total'])
except Poll.DoesNotExist:
retcode = 404
return render(request, "polls/results.html", context, status=retcode)
W tym wypadku wykorzystamy magię bazy danych aby to ona za nas podliczyła, ile łącznie głosów zostało oddanych w ankiecie.
Pozostaje nam jeszcze przygotować szablon HTML do wyświetlenia wyników:
{% extends "polls/base.html" %}
{% block content %}
{% if poll %}
<p>{{ poll.question_text }}</p>
<ul>
{% for choice in poll.choice_set.all %}
<li>
<span class="poll-option">{{ choice.choice_text }}: {{choice.votes}} vote(s)</span><br>
<span class="poll-votes" style="--votes: {{choice.votes}}; --total-votes: {{total_votes}};"></span>
</li>
{% endfor %}
</ul>
{% else %}
<p>No such poll!</p>
{% endif %}
{% endblock %}
Tym sposobem nasza aplikacja powinna być w stanie obsłużyć znów głosowanie w ankietach.
Wyzwanie: sam dodaj opcję tworzenia nowych ankiet
W poprzedniej lekcji dodaliśmy też możliwość tworzenia własnych ankiet.
Używając tamtych instrukcji i wszystkiego, czego się nauczyłeś w czasie przerabiania tego materiału, spróbuj odtworzyć tamtą funkcjonalność z wykorzystaniem bazy danych. Powodzenia!
Gdyby zadanie jednak okazało się za trudne, możesz zawsze sprawdzić rozwiązanie w repozytorium z kodem z lekcji.