Django 5.0
Najważniejsze zmiany.
Wprowadzenie do Django 5.0
W grudniu 2023 miał miejsce release nowej major wersji Django, tj. 5.0. Jako programiści świadomi i muszący nadążać za zmieniającymi się technologiami na rynku, zapoznajmy się z konceptami, które zostały zaprezentowane w najnowszym standardzie frameworka. Choć ta nowa wersja nie wprowadza kompletnie nowych konceptów do technologii i nie jest dużym game changerem dla tego, jak piszemy kod z wykorzystaniem Django, to udostępniono nam kilka ciekawych funkcjonalności, które mogą przydać się w codziennej pracy z tym web frameworkiem.
Na rzecz efektywności nauki, pominiemy drobne zmiany i skupimy się na tych najistotniejszych, które można wykorzystywać najczęściej i najbardziej pragmatycznie.
Feature 1. Nowe Funkcje w Panelu Admina
Filtry fasetowe (ang. facet filters) w Django 5.0 są nową funkcjonalnością panelu admina, która pozwala na bardziej intuicyjną i szybszą analizę danych w tabelach poprzez wyświetlanie liczby rekordów dla każdego aktywnego filtru. Dzięki temu użytkownicy widzą, ile wyników odpowiada każdemu kryterium, co ułatwia poruszanie się po dużych zbiorach danych i analizowanie ich bez potrzeby wielokrotnego odświeżania wyników.
Szczegóły i Zastosowanie Filtrów Fasetowych
Co to są filtry fasetowe?
Filtry fasetowe pokazują liczbę wystąpień (liczbę rekordów) dla każdej wartości filtra, który jest zastosowany w panelu admina. Filtry są szczególnie użyteczne w dużych zestawach danych, gdy chcemy szybko zorientować się, jakie wartości występują najczęściej lub jak rozkładają się rekordy według wybranego kryterium. Filtry fasetowe pozwalają na szybkie odnalezienie wyników, które spełniają określone kryteria.
Przykład Filtru Fasetowego w Panelu Admina
Przypuśćmy, że mamy model Product, który zawiera wiele pól, takich jak category, price, availability itp. W panelu admina możemy ustawić fasetowe filtrowanie według kategorii, aby zobaczyć, ile produktów przypada na każdą kategorię.
Konfiguracja fasetowego filtra dla modelu Product: W pliku admin.py możemy włączyć filtry fasetowe za pomocą ustawienia show_facets = True w klasie ProductAdmin. Umożliwia to wyświetlenie liczby produktów dla każdej kategorii.
from django.contrib import admin
from .models import Product
class ProductAdmin(admin.ModelAdmin):
list_filter = ('category', 'availability') # Włączamy filtrowanie po kategorii i dostępności
show_facets = True # Włączenie fasetowego wyświetlania liczby rekordów
admin.site.register(Product, ProductAdmin)
Efekt w panelu admina: Po tej konfiguracji, gdy przejdziemy do listy produktów (Product) w panelu admina, obok każdej wartości filtra (np. każdej kategorii) zobaczymy liczbę produktów przypadających na daną kategorię. Na przykład:
- Elektronika (35)
- Odzież (50)
- Artykuły spożywcze (20)
Kiedy używać filtrów fasetowych?
- Przegląd duże zbiorów danych: Filtry fasetowe są szczególnie przydatne, gdy mamy do czynienia z dużymi zbiorami danych, takimi jak lista produktów, użytkowników czy transakcji. Pozwalają na szybkie ustalenie liczby rekordów przypadających na określone wartości filtra.
- Analiza rozkładu danych: Umożliwiają przeglądanie rozkładu danych według określonego kryterium, co może być pomocne w analizie, np. liczby dostępnych produktów w różnych kategoriach lub liczby zamówień w różnych statusach.
- Optymalizacja nawigacji: Fasetowe filtry zmniejszają liczbę kroków potrzebnych do znalezienia interesujących rekordów, co przekłada się na oszczędność czasu administratora.
Feature 2. Usprawnienia formularzy - grupy pól.
Czym są Grupy Pól?
Grupy pól (ang. field groups) to nowa koncepcja w Django 5.0, która pozwala zebrać wszystkie elementy związane z pojedynczym polem formularza (np. etykietę, pole pomocy, błąd walidacji i sam widget) w jednym miejscu. Wcześniej, renderowanie każdego z tych elementów wymagało napisania osobnego kodu HTML dla każdego pola, co mogło być czasochłonne i trudne do zarządzania.
Funkcja as_field_group() automatycznie generuje odpowiedni kod HTML, który zawiera wszystkie elementy pola, dzięki czemu formularze są bardziej przejrzyste i prostsze w utrzymaniu.
Przykład użycia as_field_group()
Załóżmy, że mamy prosty formularz rejestracyjny zawierający pola: name, email i password. Wcześniej, aby wyświetlić te pola z wszystkimi ich elementami (etykieta, pomoc, błędy i widget), musielibyśmy napisać osobny kod HTML dla każdego pola, co wyglądałoby mniej więcej tak:
<form>
<div>
{{ form.name.label_tag }}
{% if form.name.help_text %}
<div class="helptext" id="{{ form.name.auto_id }}_helptext">
{{ form.name.help_text|safe }}
</div>
{% endif %}
{{ form.name.errors }}
{{ form.name }}
<div class="row">
<div class="col">
{{ form.email.label_tag }}
{% if form.email.help_text %}
<div class="helptext" id="{{ form.email.auto_id }}_helptext">
{{ form.email.help_text|safe }}
</div>
{% endif %}
{{ form.email.errors }}
{{ form.email }}
</div>
<div class="col">
{{ form.password.label_tag }}
{% if form.password.help_text %}
<div class="helptext" id="{{ form.password.auto_id }}_helptext">
{{ form.password.help_text|safe }}
</div>
{% endif %}
{{ form.password.errors }}
{{ form.password }}
</div>
</div>
</div>
</form>
Dzięki as_field_group() możemy teraz uzyskać ten sam efekt w znacznie uproszczony sposób:
<form>
<div>{{ form.name.as_field_group }}</div>
<div class="row">
<div class="col">{{ form.email.as_field_group }}</div>
<div class="col">{{ form.password.as_field_group }}</div>
</div>
</form>
Zalety Użycia as_field_group()
- Zmniejszenie Ilości Kodowania: as_field_group() znacznie skraca kod HTML, usuwając konieczność osobnego renderowania każdego elementu pola. Dzięki temu formularze stają się bardziej czytelne, a kod łatwiejszy w utrzymaniu.
- Standaryzacja Szablonów: as_field_group() korzysta domyślnie z szablonu "django/forms/field.html", który można dostosować do potrzeb całego projektu lub indywidualnych pól. Daje to większą kontrolę nad wyglądem pól formularzy w skali całej aplikacji.
- Lepsza Organizacja Wizualna: Grupy pól ułatwiają zarządzanie formularzami o złożonym układzie, np. gdy chcemy wyświetlić pola w różnych kolumnach lub rzędach, co jest częste w bardziej zaawansowanych formularzach (np. formularzach rejestracyjnych, ankietach, formularzach płatności).
- Skalowalność: Funkcja ta jest elastyczna – możemy używać as_field_group() dla wybranych pól, a pozostałe renderować w standardowy sposób. W ten sposób można dostosować wygląd formularzy do potrzeb różnych części aplikacji bez potrzeby nadmiernego powielania kodu HTML.
Kiedy stosować as_field_group()?
- Formularze wielopolowe: W przypadku formularzy, które zawierają wiele pól wymagających jednolitego stylu i formatowania.
- Zaawansowane układy: Przy bardziej złożonych układach formularzy, gdzie pola są umieszczane w różnych kolumnach lub sekcjach.
- Długoterminowe projekty: Kiedy chcemy mieć skalowalny i łatwy w utrzymaniu system szablonów dla formularzy, szczególnie w projektach o długim cyklu życia.
Feature 3. Nowe Funkcje dla pól modelu
a) Parametr db_default dla domyślnych wartości obliczanych przez bazę
db_default pozwala ustawić domyślną wartość obliczaną na poziomie bazy danych. Oznacza to, że baza danych sama przypisze wartość, jeśli pole pozostanie puste przy tworzeniu rekordu.
Działa to odwrotnie do znanego Ci default, gdzie wartość ta ustawiana jest na poziomie kodu aplikacji i przekazywana do bazy danych podczas zapisu nowego rekordu. Dzięki temu db_default może być bardziej wydajny dla dużej ilości danych, ponieważ pozwala uniknąć przetwarzania wartości domyślnej po stronie aplikacji.
Jednak z drugiej strony, db_default wymaga, aby baza danych obsługiwała funkcje, które generują wartości domyślne, co jest bardziej złożone i ogranicza przenośność niektórych rozwiązań między różnymi typami baz. Dlatego musimy korzystać z tego rozwiązania ostrożnie i z rozwagą, szczególnie migrując między różnymi typami baz danych.
from django.db import models
from django.db.models.functions import Now
class Event(models.Model):
created = models.DateTimeField(db_default=Now())
Nowe pole GeneratedField
Pole to pozwala tworzyć kolumny, które są zawsze obliczane na podstawie innych pól.
GeneratedField to nowe pole w Django 5.0, które pozwala na tworzenie kolumn w bazie danych obliczanych dynamicznie na podstawie innych pól tego samego rekordu. Jest to tzw. „kolumna generowana”, co oznacza, że jej wartość nie jest przechowywana bezpośrednio, lecz jest obliczana na bieżąco przy każdym odczycie lub może być zapisana jako wartość pochodna, jeśli ustawimy db_persist=True. Tego typu pola są użyteczne w sytuacjach, gdy wynik można uzyskać z istniejących danych i nie trzeba go przechowywać osobno.
Dlaczego warto używać GeneratedField?
- Redukcja redundancji danych: Dzięki GeneratedField możemy obliczać wartość bezpośrednio na podstawie innych pól, co eliminuje potrzebę przechowywania dodatkowych danych.
- Optymalizacja bazy danych: Zamiast wykonywać obliczenia w kodzie aplikacji, pole GeneratedField pozwala bazie danych wykonywać obliczenia przy zapytaniach, co jest często bardziej wydajne.
- Zgodność z różnymi typami baz danych: Django obsługuje GeneratedField na wszystkich głównych backendach baz danych, co czyni tę funkcję uniwersalną i łatwą w implementacji.
Przykład użycia GeneratedField.
Przyjrzyjmy się modelowi Rectangle, który posiada dwa pola: width i height, oznaczające szerokość i wysokość prostokąta. Chcemy, aby Django automatycznie obliczało pole prostokąta (area) na podstawie szerokości i wysokości. Dzięki GeneratedField możemy to zrobić bezpośrednio na poziomie bazy danych.
from django.db import models
from django.db.models import F
class Rectangle(models.Model):
width = models.IntegerField()
height = models.IntegerField()
area = models.GeneratedField(
expression=F("width") * F("height"), # Wyrażenie obliczające pole na podstawie szerokości i wysokości
output_field=models.BigIntegerField(), # Typ wyniku
db_persist=True, # Ustawienie na True oznacza, że wartość będzie przechowywana w bazie
)
W powyższym przykładzie:
- expression: Parametr ten określa wyrażenie, które jest używane do obliczenia wartości pola area. W tym przypadku korzystamy z wyrażenia F("width") * F("height"), co oznacza, że pole jest obliczane jako szerokość razy wysokość, gdzie wartości te są innymi polami w tabeli.
- output_field: Ten parametr określa typ danych wynikowego pola (BigIntegerField), co jest wymagane, ponieważ Django musi wiedzieć, jakiego typu jest wynik wyrażenia.
- db_persist: Ustawienie tego parametru na True oznacza, że wynik wyrażenia zostanie zapisany w bazie danych jako rzeczywista wartość w kolumnie. Jeśli ustawimy db_persist=False, pole będzie obliczane przy każdym zapytaniu, ale nie będzie przechowywane w bazie danych.
Większa elastyczność w choices.
Przed wersjami Django 5.0, pole “choices” mogło wykorzystywać tylko listę tupli lub typ Enum, aby móc zadeklarować możliwe wartości do wystąpienia w polu (przypominam, że pole “choices” umożliwia wskazanie, jakie konkretne wartości dopuszczamy, aby znalazły się pod konkretnym polem w modelu lub formularzu). Od Django 5.0, możemy przekazywać wartości do choices w wariantach takich jak poniżej:
Medal = models.TextChoices("Medal", "GOLD SILVER BRONZE")
SPORT_CHOICES = {
"Martial Arts": {"judo": "Judo", "karate": "Karate"},
"Racket": {"badminton": "Badminton", "tennis": "Tennis"},
}
def get_scores():
return [(i, f"Score {i}") for i in range(1, 11)]
class Competition(models.Model):
medal = models.CharField(choices=Medal)
sport = models.CharField(choices=SPORT_CHOICES)
score = models.IntegerField(choices=get_scores)
Feature 4. Nowe funkcje asynchroniczne.
Django 5.0 dodaje kilka nowych funkcji asynchronicznych, które stanowią asynchroniczne odpowiedniki istniejących funkcji uwierzytelniania (jeśli nie wiesz, jaki jest sens użycia funkcji asynchronicznych w kodzie, umieściłem krótkie podsumowanie na końcu tej sekcji).
Oto one:
aauthenticate():
Asynchroniczny odpowiednik funkcji authenticate(), która sprawdza dane logowania użytkownika. Funkcja ta może zostać użyta do asynchronicznego uwierzytelniania, co jest szczególnie przydatne w aplikacjach działających na ASGI, np. z FastAPI lub przy użyciu WebSocketów.
Przykład użycia:
from django.contrib.auth import aauthenticate
async def login_view(request):
user = await aauthenticate(request, username='user', password='password')
if user is not None:
# Użytkownik został uwierzytelniony
...
else:
# Niepoprawne dane logowania
...
alogin() i alogout():
Asynchroniczne odpowiedniki login() i logout(), które umożliwiają zalogowanie lub wylogowanie użytkownika w kontekście widoku asynchronicznego. Jest to szczególnie użyteczne w aplikacjach, które muszą reagować na zmiany statusu sesji użytkownika bez blokowania wątków.
Przykład użycia alogin():
from django.contrib.auth import aauthenticate, alogin
from django.http import HttpResponse
async def async_login_view(request):
user = await aauthenticate(request, username='username', password='password')
if user is not None:
await alogin(request, user)
return HttpResponse("Zalogowano!")
else:
return HttpResponse("Niepoprawne dane logowania")
Przykład użycia alogout():
from django.contrib.auth import alogout
from django.http import HttpResponse
async def async_logout_view(request):
await alogout(request)
return HttpResponse("Wylogowano!")
aget_user():
Asynchroniczny odpowiednik get_user(), który pobiera obiekt użytkownika aktualnie zalogowanego w danej sesji. Funkcja jest przydatna, gdy chcemy uzyskać informacje o zalogowanym użytkowniku w kontekście asynchronicznym.
Przykład użycia:
from django.contrib.auth import aget_user
async def profile_view(request):
user = await aget_user(request)
return HttpResponse(f"Witaj, {user.username}")
aupdate_session_auth_hash():
Asynchroniczny odpowiednik update_session_auth_hash(), który jest używany do aktualizacji hashu sesji użytkownika po zmianie hasła. Dzięki temu użytkownik pozostaje zalogowany po zmianie hasła, bez konieczności ponownego logowania się.
Przykład użycia:
from django.contrib.auth import aauthenticate, aupdate_session_auth_hash
async def change_password_view(request):
user = await aauthenticate(request, username='user', password='old_password')
if user:
user.set_password('new_password')
await aupdate_session_auth_hash(request, user)
return HttpResponse("Hasło zmienione")
else:
return HttpResponse("Niepoprawne dane logowania")
acheck_password():
Asynchroniczny odpowiednik check_password(), który sprawdza poprawność hasła użytkownika bez blokowania. Dzięki acheck_password() aplikacja może sprawdzać hasła użytkowników asynchronicznie, co jest przydatne w przypadku dużej liczby równoczesnych zapytań uwierzytelniających.
Przykład użycia:
from django.contrib.auth.hashers import acheck_password
from django.http import HttpResponse
async def check_password_view(request):
correct = await acheck_password('wprowadzone_hasło', 'hash_zapisanego_hasła')
if correct:
return HttpResponse("Poprawne hasło")
else:
return HttpResponse("Niepoprawne hasło")
Kiedy stosować asynchroniczne funkcje uwierzytelniania?
Asynchroniczne funkcje w django.contrib.auth są przydatne, gdy aplikacja:
- Obsługuje dużą liczbę równoczesnych użytkowników: np. w przypadku API, systemów chatowych lub aplikacji mobilnych.
- Wymaga integracji z zewnętrznymi systemami uwierzytelniania: gdzie każde wywołanie funkcji autoryzacyjnych może blokować wątek przez dłuższy czas.
- Jest oparta na ASGI: asynchroniczność jest kluczowa dla środowisk takich jak ASGI, gdzie obsługa widoków i żądań jest wykonywana asynchronicznie.
Feature 5. Usprawnienia dekoratorów dla widoków asynchronicznych
Wiele dekoratorów (np. cache_control(), csrf_exempt()) teraz obsługuje widoki asynchroniczne i mogą być bez problemu używane właśnie w implementacjach asynchronicznych. Pełną listę dekoratorów wspierających asynchroniczność, znajdziesz tutaj: Django 5.0 release notes
Poniżej Użycie dekoratora csrf_exempt z widokiem asynchronicznym.
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
async def my_async_view(request):
# Obsługa widoku
pass
Feature 6. Ulepszenia w Paginatorze
W Django 5.0 Paginator został ulepszony o nową funkcję, która pozwala na dostosowanie komunikatów błędów przy paginacji. Teraz możemy korzystać z argumentu error_messages, aby definiować własne, bardziej spersonalizowane komunikaty dla błędów paginacji, takich jak próba dostępu do nieistniejącej strony.
Dlaczego dostosowanie komunikatów błędów w Paginatorze jest przydatne?
- Lepsze doświadczenie użytkownika: Dzięki własnym komunikatom możemy przekazać bardziej zrozumiałe i spersonalizowane informacje o błędach, np. „Brak wyników” zamiast domyślnych komunikatów.
- Spójność interfejsu: Możliwość dostosowania komunikatów pozwala na lepsze dopasowanie ich do stylu językowego i interfejsu aplikacji, co wpływa na spójność całej aplikacji.
- Ułatwienie pracy programistom: Deweloperzy mogą teraz kontrolować komunikaty błędów bez potrzeby modyfikowania kodu Django, co przyspiesza rozwój i testowanie aplikacji.
Przykład użycia error_messages w Paginatorze
Poniżej znajduje się przykład konfiguracji Paginator, który ustawia niestandardowy komunikat błędu dla sytuacji, gdy użytkownik próbuje przejść do strony, która nie zawiera wyników (czyli np. strony numer 1000 przy niewielkiej liczbie rekordów).
from django.core.paginator import Paginator
# Przykładowa lista obiektów, które chcemy paginować
objects = range(30) # Lista 30 obiektów
# Tworzenie paginatora z 10 obiektami na stronę i dostosowanymi komunikatami błędów
paginator = Paginator(objects, 10, error_messages={'empty_page': 'Brak wyników'})
# Próba uzyskania strony, która jest poza zakresem
try:
page = paginator.page(5) # Zakładamy, że mamy mniej niż 5 stron
except paginator.EmptyPage:
print("Brak wyników") # Wyświetli dostosowany komunikat "Brak wyników"
Argument error_messages i dostępne błędy
W error_messages możemy dostosować komunikaty dla następujących typów błędów:
- empty_page: Błąd pojawia się, gdy użytkownik próbuje uzyskać dostęp do strony spoza zakresu (np. gdy strona nie istnieje).
- page_not_an_integer: Błąd pojawia się, gdy wartość strony nie jest liczbą całkowitą (np. jeśli użytkownik wprowadził literę zamiast liczby).
Przykład pełnej konfiguracji:
paginator = Paginator(
objects,
10,
error_messages={
'empty_page': 'Przepraszamy, ale ta strona nie zawiera wyników.',
'page_not_an_integer': 'Numer strony musi być liczbą całkowitą.'
}
)
Kiedy warto stosować error_messages w Paginatorze?
- W aplikacjach publicznych: Gdy aplikacja jest używana przez szeroką grupę użytkowników, dostosowane komunikaty błędów mogą poprawić zrozumienie sytuacji, np. gdy użytkownik wpisze błędny numer strony.
- W aplikacjach wielojęzycznych: Możliwość zmiany komunikatów pozwala na ich łatwe tłumaczenie, co jest kluczowe w aplikacjach obsługujących wiele języków.
- W interfejsach typu SPA (Single Page Application): W aplikacjach SPA możemy łatwo przekazać dostosowane komunikaty do frontendu, co wpływa na lepsze doświadczenie użytkownika.