Poznawanie klas modeli w Django i wstęp do sesji użytkownika
Wykorzystanie klas modeli poza widokami Django
Na ostatniej lekcji przenieśliśmy dane naszej aplikacji z pamięci RAM na dysk, a konkretnie do bazy danych z wykorzystaniem klas modeli.
O ile korzystanie z klas modeli jako wygodniejszego interfejsu do bazy danych już samo w sobie jest mocnym argumentem za ich stosowaniem, zamiast pisania własnego kodu obsługującego tworzenie tabel, relacji i przesyłania danych w jedną i w drugą stronę.
Dodatkową zaletą jest możliwość tworzenia widoków na podstawie informacji o klasie modelu, który chcemy zaprezentować.
Klasy generycznych widoków
Wyświetlanie listy elementów czy strony zawierającej szczegóły dotyczące jednej z pozycji z listy to podstawa tworzenia aplikacji internetowych. Przykładami takich zadań mogą być:
- Lista przepisów na blogu i szczegółowy widok przepisu
- Galeria zdjęć i ekran zdjęcia z menu reakcji czy sekcją komentarzy
- Galeria produktów w sklepie internetowym z widokiem szczegółowym pokazującym różne warianty produktu
- Menu potraw restauracji i widok poszczególnych dań ze szczegółowymi informacjami żywieniowymi
- itp., itd.
Właściwie są to zadania tak powszechne w programowaniu aplikacji webowych, że można oczekiwać, że framework da nam jakieś narzędzia ułatwiające ich realizacje. Tak też jest w przypadku Django.
Zacznijmy od dodania następującego importu do naszego pliku z widokami
# ...
from django.views import generic
# ...
W naszej aplikacji mamy jeden widok prezentujący listę otwartych ankiet, konkretnie index
. Możemy uzyskać taką samą funkcjonalność w taki sposób:
def index(request):
context = {
"latest_polls": Poll.objects.order_by("-pub_date")[:5]
}
return render(request, "polls/index.html", context)
class IndexView(generic.ListView)
template_name = "polls/index.html"
context_object_name = "latest_polls"
queryset = Poll.objects.order_by("-pub_date")[:5]
Na tym etapie nie bardzo widać oszczędność w liniach kodu, ale spróbujmy poszukać dalej. Wyświetlanie strony do głosowania czy widoku z wynikami to z kolei przykłady wyświetlania szczegółów pojedynczego elementu. Na początek zobaczmy ekwiwalent dla widoku ankiety z szablonem do głosowania.
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)
class DetailView(generic.DetailView):
model = Poll
template_name = "polls/poll.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['vote_form'] = VoteForm(self.object)
return context;
Tu już jest trochę ciekawiej. Nigdzie w kodzie klasy DetailView nie dodajemy obsługi nieistnienia ankiety o podanym id. Nawet nie ma tu mowy o jakichkolwiek identyfikatorach. Gdyby nie to, że chcemy dodać do kontekstu formularz do głosowania, to w zasadzie widok sprowadziłby się do trzech linijek kodu Pythona
A co z widokiem widoków głosowania?
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)
class ResultsView(generic.DetailView):
model = Poll
template_name = "polls/results.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
votes = self.object.choice_set.aggregate(total=Sum('votes'))
context['total_votes'] = max(1, votes['total'])
return context
Sytuacja wygląda bardzo podobnie jak dla poprzedniego widoku. Django oferuje również inne wbudowane funkcjonalności związane z generycznymi widokami, o których możecie więcej przeczytać w dokumentacji online
Aby skorzystać z nowych widoków, trzeba je podpiąć w konfiguracji ścieżek aplikacji, co w naszym przypadku może wyglądać tak:
app_name = "polls"
urlpatterns = [
path('', views.IndexView.as_view(), name="index"),
path('new/', views.new_poll, name="new"),
path('<int:pk>/', views.DetailView.as_view(), name="details"),
path('vote/<int:poll_id>/', views.vote, name="vote"),
path('results/<int:pk>/', views.ResultsView.as_view(), name="results"),
]
Łatwiejsze edytowanie ankiet w widoku admin
Dodatkową zaletą korzystania z klas modeli jest możliwość dość prostego dodania wygodnych szablonów administracyjnych.
Nasz własny szablon dodawania nowej ankiety jest dość toporny, szczególnie jeżeli chodzi o definiowanie dostępnych odpowiedzi na pytania w ankiecie. Nie mamy natomiast w ogóle możliwości edycji wyborów dla już utworzonych ankiet.
Zmodyfikujmy plik odpowiedzialny za dodanie interfejsów administracyjnych dla naszego modelu Poll w następujący sposób:
from django.contrib import admin
from .models import Poll, Choice
# derive from admin.StackedInline for a more vertical representation
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
# readonly_fields = ['votes']
class PollAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {
"fields": ["pub_date"],
"classes": ["collapse"]}),
]
inlines = [ChoiceInline]
admin.site.register(Poll, PollAdmin)
Po przejściu do panelu administracyjnego możemy teraz nie tylko dodawać nowe ankiety razem z odpowiedziami, ale możemy też edytować już istniejące ankiety, a także liczbę oddanych na poszczególne opcje… chyba, że chcemy być sprawiedliwi w liczeniu głosów.
Sesje użytkownika, czyli jak odrobinę zabezpieczyć ankietę
Zazwyczaj tworząc ankiety, nawet anonimowe, chcemy żeby jedna osoba mogła oddać głos tylko raz, w przeciwnym wypadku nawet jedna osoba może zupełnie zaburzyć wynik ankiety.
Django zapewnia domyślnie wbudowane wsparcie dla kont użytkowników i wkrótce z niego skorzystamy, ale póki co pozostańmy w sferze obsługi użytkowników anonimowych. Nawet jeżeli nie jesteśmy w stanie powiązać użytkownika z kontem, mamy pewne narzędzie identyfikowania użytkownika pomiędzy żądaniami wysyłanymi do naszej aplikacji. Ten mechanizm to tzw. sesje użytkownika, które zazwyczaj opierają się na plikach cookies.
Jak dokładnie działają cookies omówimy w następnej lekcji a póki co użyjmy ich po prostu w naszym formularzu do głosowania.
Kiedy oryginalnie stworzyliśmy naszą aplikację mysite
, Django automatycznie dodało do listy zainstalowanych aplikacji naszego projektu kilka pozycji, takich jak np. panel administracyjny admin
. Inną domyślnie włączoną aplikacją jest ta odpowiedzialna za sesje użytkownika, django.contrib.sessions
. Dzięki temu do każdego z naszych widoków trafia obiekt request
z dodanym polem session
. Pole request.session
oferuje interfejs podobny do słownika Pythona, dołączając do tego parę innych funkcjonalności.
Załóżmy, że dla każdego użytkownika naszej strony stworzymy listę id
ankiet, na które oddał już głos. Czyli użytkownik, który zagłosował w ankietach o id
1, 2 i 3, powinien mieć przypisaną listę [1, 2, 3]
. Czyli podczas kolejnego głosowania np. na ankietę z id
5, dopiszemy je do listy i lista będzie wyglądała tak [1, 2, 3, 5]
Jeżeli podczas próby oddania głosu okaże się, że użytkownik już głosował w tej ankiecie, wyświetlimy mu odpowiedni komunikat błędu i odrzucimy jego głos.
Należy pamiętać, że może się zdarzyć, że użytkownik głosuje w jakiejkolwiek ankiecie na naszej stronie po raz pierwszy. W takim wypadku jego sesja użytkownika nie będzie zawierać listy z oddanymi głosami i trzeba stworzyć dla niego taką pustą listę
def vote(request, poll_id):
if request.method == 'GET':
return HttpResponseRedirect(reverse("polls:details", args=[poll_id]))
try:
# first time voter/new user handling
if not 'voted' in request.session:
request.session['voted'] = [];
poll = Poll.objects.get(pk=poll_id)
vote_form = VoteForm(poll, data=request.POST)
# already voted
if poll.id in request.session['voted']:
context = {
'poll': poll,
'vote_form': vote_form,
'error_message': "You already voted!"
}
return render(request, "polls/poll.html", context)
elif vote_form.is_valid():
selected_choice = Choice.objects.get(pk = vote_form.cleaned_data['choice'])
selected_choice.votes += 1
selected_choice.save()
# remember to record the vote took place
request.session['voted'].append(poll.id)
request.session.modified = True
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)
Ten system nie jest idealnym zabezpieczeniem. Wystarczy, że użytkownik skorzysta z innej przeglądarki, albo z przeglądarki w trybie incognito, albo nawet ręcznie usunie swój identyfikator sesji, a będzie mógł ponownie zagłosować.
Na następnej lekcji przyglądniemy się dokładniej temu, jak działają sesje użytkownika pod maską.
W międzyczasie możesz się zastanowić, jak Ty zaimplementowałbyś mechanizm sesji użytkownika gdybyś miał go napisać od zera?