Nowości w Django 5.2 Beta 1 – przegląd zmian i praktyczne przykłady

Nowości w Django 5.2 Beta 1 – przegląd zmian i praktyczne przykłady
Photo by Liam Charmer / Unsplash

Django 5.2 (wersja beta 1) wprowadza szereg usprawnień i nowych funkcjonalności, na które czekali deweloperzy. W poniższym wpisie szczegółowo omówimy najważniejsze zmiany, zilustrujemy je praktycznymi fragmentami kodu oraz porównamy z wersją Django 5.1. Skupimy się na tym, jak wykorzystać nowe funkcje w codziennej pracy z Django, a także jak przygotować istniejące projekty do migracji.

1. Przegląd zmian – Co nowego w Django 5.2?

Najważniejsze nowości i zmiany w Django 5.2 Beta 1:

W kolejnych sekcjach przyjrzymy się szczegółowo tym zmianom, prezentując konkretne przykłady użycia i porównując zachowanie z Django 5.1.

2. Szczegółowe omówienie nowości

Automatyczny import modeli w manage.py shell

Polecenie Django manage.py shell stało się wygodniejsze – teraz automatycznie importuje modele ze wszystkich zainstalowanych aplikacji. Oznacza to, że po wejściu do shella nie musimy już ręcznie pisać from myapp.models import MyModel dla każdego modelu; będą one dostępne od razu. Na przykład, uruchomienie shella z wyższą szczegółowością (--verbosity=2) wyświetli komunikat o zaimportowanych obiektach:

$ python manage.py shell --verbosity=2
6 objects imported automatically, including:

  from django.contrib.auth.models import Group, Permission, User
  from django.contrib.contenttypes.models import ContentType
  ...

Jak widać, m.in. modele użytkowników (User, Group) są już dostępne (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). To ułatwienie przyspiesza interaktywne testowanie. Jeśli jednak chcemy zmodyfikować tę listę automatycznych importów (np. dodać własne moduły lub pominąć niektóre modele), Django 5.2 na to pozwala. Możemy nadpisać metodę get_auto_imports() we własnym poleceniu shell, aby dostosować importy do potrzeb projektu (How to customize the shell command | Django documentation | Django) (How to customize the shell command | Django documentation | Django).

Przykład – dodanie własnych importów w shellu:

# polls/management/commands/shell.py
from django.core.management.commands import shell

class Command(shell.Command):
    def get_auto_imports(self):
        # Dodajemy funkcje reverse i resolve do automatycznych importów:
        return super().get_auto_imports() + [
            "django.urls.reverse",
            "django.urls.resolve",
        ]

Dzięki temu nasze polecenie shell dodatkowo importuje funkcje reverse i resolve z django.urls (How to customize the shell command | Django documentation | Django). Możemy także całkowicie wyłączyć automatyczne importy (np. do debugowania) flagą --no-imports lub ustawiając get_auto_imports() aby zwracało None (How to customize the shell command | Django documentation | Django). Domyślnie jednak funkcja ta jest bardzo przydatna i nie wpływa na istniejący kod – to wyłącznie wygoda dla programistów.

Obsługa złożonych kluczy głównych (Composite Primary Keys)

Jedną z najważniejszych nowości Django 5.2 jest natywne wsparcie dla złożonych kluczy głównych, czyli klucza głównego opartego na więcej niż jednym polu tabeli (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dotychczas Django zakładało istnienie pojedynczego klucza głównego (np. domyślnego pola id). Teraz możemy zdefiniować w modelu pole pk jako instancję klasy CompositePrimaryKey, wskazując nazwy pól składających się na złożony klucz:

from django.db import models

class Order(models.Model):
    reference = models.CharField(max_length=20, primary_key=True)

class Product(models.Model):
    name = models.CharField(max_length=100)

class OrderLineItem(models.Model):
    pk = models.CompositePrimaryKey("product_id", "order_id")
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    order   = models.ForeignKey(Order, on_delete=models.CASCADE)
    quantity = models.IntegerField()

W powyższym przykładzie klasa OrderLineItem używa CompositePrimaryKey, definiując klucz główny złożony z pola product_id i order_id (Composite primary keys | Django documentation | Django). W efekcie w bazie danych powstanie klucz główny PRIMARY KEY(product_id, order_id) (Composite primary keys | Django documentation | Django).

Jak to działa w praktyce? Atrybut pk obiektu takiego modelu jest krotką zawierającą wartości obu pól. Dla stworzonych obiektów możemy to zobaczyć:

>>> product = Product.objects.create(name="Apple")
>>> order = Order.objects.create(reference="A755H")
>>> item = OrderLineItem.objects.create(product=product, order=order, quantity=5)
>>> item.pk  
(1, "A755H")

Tutaj item.pk zwraca krotkę (1, "A755H"), odpowiadającą kluczowi złożonemu (Composite primary keys | Django documentation | Django). Możemy również filtrować i wyszukiwać po złożonym kluczu używając krotki, np. OrderLineItem.objects.get(pk=(1, "A755H")) (Composite primary keys | Django documentation | Django).

Wskazówka: Nie można zmieniać zdefiniowanego klucza złożonego w trakcie migracji. Django 5.2 nie wspiera migracji istniejącej tabeli z pojedynczego klucza na klucz złożony (ani odwrotnie) – takie operacje należy przeprowadzić ręcznie na bazie danych (Composite primary keys | Django documentation | Django). Najlepiej zaprojektować klucz złożony od razu przy tworzeniu modelu. Ponadto aktualnie Django admin nie obsługuje modeli ze złożonym kluczem głównym – nie można ich zarejestrować w panelu admina (to ograniczenie ma zostać usunięte w przyszłości) (Composite primary keys | Django documentation | Django). Warto też wiedzieć, że relacje typu ForeignKey nie mogą wskazywać na model ze złożonym kluczem (np. nie stworzymy prostego ForeignKey(OrderLineItem)), choć można to obejść używając niższego poziomu API (ForeignObject) kosztem utraty pewnych udogodnień (Composite primary keys | Django documentation | Django) (Composite primary keys | Django documentation | Django). Mimo tych ograniczeń, dodanie Composite Primary Key to duży krok naprzód dla projektów wymagających takiej struktury danych.

Jiri Sifalda - Unsplash

Uproszczone nadpisywanie BoundField (renderowanie pól formularzy)

Kolejną nowością jest ułatwienie w dostosowywaniu sposobu renderowania pól formularzy. Do tej pory, aby zmodyfikować zachowanie obiektu BoundField (reprezentującego związane z danymi pole formularza), należało nadpisać metodę Field.get_bound_field() – była to mało intuicyjna i globalna zmiana. Django 5.2 wprowadza prostszy mechanizm: możemy określić własną klasę BoundField na poziomie całego projektu, formularza lub pojedynczego pola poprzez atrybut bound_field_class (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Dostępne są trzy poziomy:

  • Globalnie: ustawienie bound_field_class w globalnym rendererze formularzy (BaseRenderer.bound_field_class).
  • Na poziomie formularza: atrybut klasy formularza Form.bound_field_class.
  • Na poziomie pola: atrybut pola formularza Field.bound_field_class.

Ustawienia bardziej szczegółowe mają pierwszeństwo (pole > formularz > globalne) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Rozważmy przykład: chcemy, by każde pole formularza renderowało się w otoczeniu diva z dodatkową klasą CSS. Możemy stworzyć własną klasę BoundField:

from django import forms

class CustomBoundField(forms.BoundField):
    custom_class = "custom"
    def css_classes(self, extra_classes=None):
        result = super().css_classes(extra_classes)
        # Dodajemy naszą klasę CSS jeśli jeszcze jej nie ma:
        if self.custom_class not in result:
            result += f" {self.custom_class}"
        return result.strip()

class CustomForm(forms.Form):
    bound_field_class = CustomBoundField  # użyj naszej klasy BoundField
    name = forms.CharField(label="Your Name", max_length=100, required=False,
                           widget=forms.TextInput(attrs={"class": "name-input"}))
    email = forms.EmailField(label="Your Email")

W powyższym kodzie definiujemy CustomBoundField dodający klasę "custom" do atrybutów CSS każdego pola podczas renderowania (poprzez nadpisanie metody css_classes) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Następnie przypisujemy tę klasę do formularza CustomForm za pomocą bound_field_class (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Gdy teraz wygenerujemy HTML takiego formularza, każdy element pola będzie zawierał naszą dodatkową klasę CSS:

<div class="custom">
  <label for="id_name">Your Name:</label>
  <input type="text" name="name" class="name-input" id="id_name" maxlength="100">
</div>
<div class="custom">
  <label for="id_email">Your Email:</label>
  <input type="email" name="email" id="id_email" required maxlength="320">
</div>

Wygenerowany kod HTML pokazuje <div class="custom"> otaczający każde pole wraz z etykietą (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Udało się to osiągnąć bez grzebania w metodach wewnętrznych pól – wystarczyło użyć nowego mechanizmu bound_field_class. Ten sam efekt można też osiągnąć ustawiając Field.bound_field_class dla konkretnego pola, jeśli chcemy aby tylko wybrane pola korzystały z niestandardowego renderowania.

Nowy sposób jest wstecznie zgodny – jeśli ktoś nadpisywał get_bound_field() w starym kodzie, będzie to nadal działać. Jednak korzystanie z bound_field_class jest znacznie prostsze i bardziej czytelne. Więcej szczegółów i możliwości (np. hierarchię priorytetów) znajdziemy w dokumentacji (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Asynchroniczne metody w uwierzytelnianiu (django.contrib.auth)

Django od wersji 3.1 stopniowo rozszerza wsparcie dla asynchronicznych widoków i operacji. W Django 5.2 zrobiono kolejny krok – dodano szereg asynchronicznych metod w ramach modułu uwierzytelniania (django.contrib.auth). Dotyczy to zarówno menedżerów użytkowników, jak i obiektów użytkownika oraz backendów autoryzacji.

Przykłady nowych metod (asynchroniczne odpowiedniki istniejących metod, z prefiksem a):

Dzięki temu, jeżeli piszemy widoki asynchroniczne (async def) i musimy np. utworzyć użytkownika lub sprawdzić uprawnienia, możemy to zrobić nie blokując pętli zdarzeń. Przykładowo, asynchroniczny widok rejestracji użytkownika może teraz wyglądać tak:

from django.contrib.auth import get_user_model

User = get_user_model()

async def register(request):
    # ... (walidacja danych)
    user = await User.objects.acreate_user(username="jan", email="jan@example.com", password="p@ssword123")
    # user utworzony asynchronicznie, bez blokowania
    # ... (dalsza logika)
    return JsonResponse({"status": "ok"})

Metoda acreate_user utworzy użytkownika korzystając z nowo dodanej asynchronicznej implementacji i zwróci obiekt User – możemy na niego poczekać słowem kluczowym await. Analogicznie skorzystamy z await user.ahas_perm('app.label_permission') aby sprawdzić uprawnienie w sposób nieblokujący.

Ważne jest, że customowe backendy autoryzacji również mogą teraz definiować swoje asynchroniczne odpowiedniki metod. Jeśli podamy je w klasie backendu, to Django użyje ich automatycznie w kontekście asynchronicznym (co redukuje liczbę przełączeń kontekstu i poprawia wydajność) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Innymi słowy, możemy dodać np. async def aauthenticate() do własnego backendu. Jeśli natomiast backend nie ma zaimplementowanych wersji asynchronicznych, Django i tak zadziała (będzie wykonywać operacje w wątku synchronizacyjnym), ale implementacja własnych async może dać zysk wydajności.

Na koniec, drobna zmiana w walidatorach haseł: klasa bazowa walidatora (UserAttributeSimilarityValidator, MinimumLengthValidator itd.) zyskała nową metodę get_error_message() (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Można ją nadpisać, aby w prosty sposób zdefiniować własną treść komunikatu błędu walidacji hasła (zamiast nadpisywać np. atrybuty czy całe validate). To ułatwia spójne dostosowanie komunikatów błędów podczas sprawdzania złożoności hasła.

Nowe widżety formularzy: ColorInput, SearchInput, TelInput

W HTML5 dostępnych jest kilka specyficznych typów pól <input>, które do tej pory wymagały ręcznego tworzenia atrybutu input_type lub użycia widgetu TextInput z odpowiednim parametrem. Django 5.2 dodaje dedykowane klasy widżetów formularza dla trzech popularnych typów:

Użycie tych widżetów jest proste. Możemy przypisać je do pola formularza tak jak inne widgety:

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(label="Imię")
    email = forms.EmailField(label="Email")
    phone = forms.CharField(label="Telefon", widget=forms.TelInput)
    query = forms.CharField(label="Szukaj", required=False, widget=forms.SearchInput)
    favorite_color = forms.CharField(label="Ulubiony kolor", required=False, widget=forms.ColorInput)

W powyższym formularzu pole phone będzie renderowane jako <input type="tel" ...>, query jako <input type="search">, a favorite_color jako <input type="color">. Nie musimy ustawiać ręcznie żadnych atrybutów – wystarczy użyć odpowiedniego widżetu. Oczywiście można dalej dodawać atrybuty attrs (np. klasę CSS) w razie potrzeby.

To drobna zmiana, ale pomocna – kod staje się bardziej semantyczny i zgodny ze standardami, a my piszemy mniej własnego kodu.

Usprawnienia dostępności (ARIA): W temacie formularzy warto wspomnieć o zmianach związanych z dostępnością. Django 5.2 automatycznie łączy komunikaty błędów pól formularza z polami poprzez atrybut aria-describedby. Każdy BoundField ma teraz właściwość aria_describedby, która generuje odpowiednie ID elementu błędu (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Jeśli korzystamy z standardowego renderowania błędów przez ErrorList, to Django zadba o ustawienie aria-describedby na polu, wskazującym na element <ul class="errorlist"> z komunikatem błędu. Co więcej, klasa ErrorList posiada nowy parametr field_id, który pozwala nadać własne id wygenerowanej liście błędów (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dzięki temu poprawiono dostępność formularzy dla czytników ekranu i ułatwiono deweloperom tworzenie formularzy zgodnych ze standardami WCAG (powiązanie komunikatu błędu z polem jest automatyczne). W codziennej pracy nie musimy nic zmieniać – po prostu otrzymujemy bardziej accessible formularze za darmo.

Mathew Schwartz - Unsplash

Dekorator method_decorator dla widoków asynchronicznych

Django od dawna posiada dekorator django.utils.decorators.method_decorator ułatwiający stosowanie funkcji-dekoratorów (np. login_required) do metod klasy (np. metody dispatch czy get w Class-Based Views). W Django 5.2 rozszerzono jego możliwości – teraz obsługuje on również asynchroniczne metody widoków (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Co to oznacza? Jeśli tworzymy klasę widoku opartą o View lub inne klasy CBV i definiujemy w niej metody asynchroniczne (async def), to możemy je dekorować tak samo jak metody synchroniczne. Przykład:

from django.views import View
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required

class UserDataView(View):
    @method_decorator(login_required)
    async def get(self, request):
        # ta metoda jest asynchroniczna, ale udekorowana login_required
        data = await fetch_user_data_async(request.user)  # przykładowe async I/O
        return JsonResponse({"data": data})

W powyższym kodzie metoda get jest asynchroniczna, ale dzięki @method_decorator(login_required) nadal wymagamy, by użytkownik był zalogowany. We wcześniejszych wersjach Django mogły być z tym problemy (dekorator mógł nie rozpoznawać coroutiny poprawnie). Teraz method_decorator wykrywa, że dekorowana metoda jest asynchroniczna i odpowiednio dostosuje dekorator (o ile sam dekorator potrafi obsłużyć async, a login_required potrafi). Ta zmiana jest wewnętrzna, dla nas niewidoczna, ale umożliwia szersze wykorzystanie asynchronicznych CBV bez utraty funkcjonalności dekoratorów.

Funkcja reverse() z parametrami query i fragment

Generowanie URL-i na podstawie nazw widoków (funkcja django.urls.reverse) zostało ulepszone. Często potrzebujemy nie tylko odtworzyć główny URL, ale również dodać parametry zapytania (query string) lub fragment URL (część za #). Do tej pory robiliśmy to ręcznie, np. reverse('search') + '?q=abc#section'. Django 5.2 pozwala zrobić to wprost, przekazując do reverse() dodatkowe argumenty query i fragment (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Przykład użycia reverse z nowymi parametrami:

from django.urls import reverse

# Przypuśćmy, że mamy route o nazwie "search-results"
url = reverse("search-results", kwargs={"term": "django"}, query={"page": 2, "lang": "pl"}, fragment="top")
print(url)
# Wynik: "/search/django/?page=2&lang=pl#top"

Powyżej reverse() wygeneruje URL do widoku "search-results" (np. /search/django/), a następnie doda do niego automatycznie ?page=2&lang=pl oraz fragment #top. Nie musimy martwić się samodzielnym kodowaniem tych elementów ani ich poprawnym escapingiem.

Parametr query przyjmuje słownik (lub listę krotek) z parametrami, a fragment to string (bez znaku #). Uwaga: parametr query nie zastępuje przekazywania args/kwargs do reverse – nadal używamy args lub kwargs do dynamicznych części URL (np. term: "django" w powyższym przykładze), a query wyłącznie do części zapytania. Ta funkcjonalność poprawia czytelność kodu i zmniejsza ryzyko błędów przy doklejaniu query stringów ręcznie (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).

Nowe właściwości obiektów HttpResponse i HttpRequest

W odpowiedziach HTTP i obiektach requestu pojawiło się kilka nowych udogodnień:

Przekierowania z zachowaniem metody (307/308) – Wprowadzono nowy argument preserve_request dla klas HttpResponseRedirect i HttpResponsePermanentRedirect (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django) oraz analogiczną opcję dla skrótu redirect() (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Normalnie przekierowanie HTTP (302 lub 301) powoduje zmianę metody żądania na GET. Jednak kody 307 (Temporary Redirect) i 308 (Permanent Redirect) pozwalają zachować metodę i body oryginalnego żądania. Jeśli więc chcemy przekierować np. metodę POST na inny adres bez zmiany na GET (co by porzuciło dane formularza), możemy teraz zrobić:

return redirect('new-endpoint', preserve_request=True)

lub równoważnie:

return HttpResponseRedirect("/new-url", preserve_request=True)

Ustawienie preserve_request=True spowoduje wysłanie kodu 307 (lub 308 w przypadku permanentnego przekierowania) zamiast 302/301 (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). To sygnał dla klienta, by ponowił żądanie pod nowym adresem używając tej samej metody i danych. Jest to szczególnie przydatne przy przekierowaniach w API REST (gdzie np. chcemy przekierować POST na inny endpoint), lub przy wymuszaniu HTTPS/WWW gdzie chcemy przekierować również nietypowe metody bez utraty ich payloadu. Z punktu widzenia programisty Django zmiana jest prosta – dodatkowy argument – ale warto być świadomym różnicy. Wcześniej osiągnięcie tego wymagało ręcznego tworzenia odpowiedzi z kodem 307.

HttpRequest.get_preferred_type(media_types) – Nowa metoda obiektu request, która pomaga w negocjacji typu odpowiedzi na podstawie nagłówka Accept wysyłanego przez klienta (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Przydaje się to w API lub widokach obsługujących różne formaty (HTML, JSON, XML). Metoda przyjmuje listę typów mediów (MIME), które nasz widok może zwrócić, i zwraca ten z nich, który jest najwyżej preferowany przez klienta lub None, jeśli żaden nie jest akceptowany (Request and response objects — Django 5.2.dev20241127161328 documentation) (Request and response objects — Django 5.2.dev20241127161328 documentation).Przykład użycia:

def my_view(request):
    best_type = request.get_preferred_type(["text/html", "application/json"])
    if best_type == "application/json":
        return JsonResponse({"msg": "Hello"})
    else:
        return HttpResponse("<p>Hello</p>", content_type="text/html")

Jeśli przeglądarka (klient) w nagłówku Accept faworyzuje HTML, get_preferred_type zwróci "text/html"; jeśli np. klientem jest skrypt preferujący JSON, zwróci "application/json". To wygodny sposób na obsługę content negotiation – Django wykonuje za nas analizę skomplikowanego nagłówka Accept. Wcześniej deweloper musiał to robić ręcznie lub użyć bibliotek zewnętrznych. Teraz wbudowana metoda upraszcza ten kod. Pamiętajmy tylko, by przekazać listę obsługiwanych typów w odpowiedniej kolejności priorytetów.

HttpResponse.text – W obiekcie odpowiedzi HTTP (HttpResponse i jego pochodnych) dodano właściwość (property) .text, która zwraca zawartość odpowiedzi jako ciąg tekstowy (string) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dotychczas, aby odczytać treść odpowiedzi, korzystaliśmy z response.content (co zwraca bajty) i ewentualnie dekodowaliśmy do tekstu. Teraz response.text zrobi to za nas, zakładając poprawne kodowanie UTF-8 (lub inne ustawione). Przykład:

response = HttpResponse("Witaj Django")
response.content  # b"Witaj Django"
response.text     # "Witaj Django"

Jest to bardziej Pythonowe i wygodne, zwłaszcza przy pisaniu testów lub debugowaniu odpowiedzi. Jeśli odpowiedź jest binarna lub nie da się zdekodować, property to może oczywiście rzucić wyjątek lub zwrócić nieczytelne dane – więc używamy go tam, gdzie spodziewamy się tekstu.

Nowy dekorator simple_block_tag dla szablonów

Django 5.2 wprowadza nowy dekorator ułatwiający tworzenie własnych znaczników szablonowych (template tags), które otaczają blok kodu. Do tej pory istniał dekorator simple_tag do szybkiego tworzenia tagów prostych (zwracających wartość), ale tagi blokowe (takie jak np. {% cache %}...{% endcache %} czy {% if %}...{% endif %}) wymagały bardziej złożonej implementacji. Teraz możemy skorzystać z @register.simple_block_tag.

Dekorator simple_block_tag pozwala łatwo zdefiniować tag przyjmujący fragment renderowanego już szablonu jako wejście. Funkcja dekorowana otrzyma pierwszy argument o nazwie content, zawierający treść między tagiem otwierającym a zamykającym, już wyrenderowaną do łańcucha tekstowego (How to create custom template tags and filters — Django 5.2.dev20241127161328 documentation). Możemy następnie przetworzyć lub użyć tej treści wedle potrzeb i zwrócić wynik, który zostanie wstawiony w miejsce oryginalnego tagu wraz z blokiem.

Przykład:

Załóżmy, że chcemy stworzyć tag {% highlight %}...{% endhighlight %}, który opakowuje dany blok HTML w dodatkowy znacznik (np. <div class="highlight">...</div>). Z simple_block_tag zrobimy to tak:

# myapp/templatetags/mytags.py
from django import template
register = template.Library()

@register.simple_block_tag
def highlight(content):
    # content to już zrenderowany tekst wewnątrz taga
    return f'<div class="highlight">{ content }</div>'

Nasz tag highlight przyjmuje jeden argument content i zwraca łańcuch HTML z owiniętą zawartością. Użycie w szablonie:

{% load mytags %}
{% highlight %}
    <p>Ten tekst będzie podświetlony.</p>
{% endhighlight %}

Po wyrenderowaniu, w miejscu tego bloku pojawi się:

<div class="highlight">
    <p>Ten tekst będzie podświetlony.</p>
</div>

Django zadbało o przekazanie zawartości bloku do naszej funkcji highlight(content), a my mogliśmy ją wykorzystać jak zwykły string i zwrócić opakowany w dodatkowy kod. Uwaga: w definicji takiej funkcji pierwszy parametr musi nazywać się content – to konwencja wymagana przez mechanizm simple_block_tag (How to create custom template tags and filters — Django 5.2.dev20241127161328 documentation). Możemy definiować też dodatkowe parametry dla tagu (np. atrybuty), jak w normalnej funkcji, oraz użyć opcji takich jak takes_context czy zdefiniować alternatywną nazwę zamykającego tagu przez argument end_name dekoratora (How to create custom template tags and filters — Django 5.2.dev20241127161328 documentation). simple_block_tag dba też o prawidłowe escape'owanie i bezpieczeństwo (analogicznie do simple_tag) (How to create custom template tags and filters — Django 5.2.dev20241127161328 documentation).

Dzięki temu nowemu dekoratorowi pisanie własnych znaczników szablonowych staje się prostsze i mniej podatne na błędy. Możemy łatwo tworzyć np. niestandardowe kontenery, elementy interfejsu czy integracje z bibliotekami JS (np. tag generujący elementy potrzebne dla biblioteki chart/wykresów, gdzie zawartość tagu to dane lub konfiguracja – już pojawiły się pomysły użycia simple_block_tag do integracji z HTMX i innymi narzędziami (Tim Schilling: "I'm pretty excited for #Django…" - Fosstodon)). To kolejna funkcjonalność, która nie wpływa na stary kod (stare tagi działają bez zmian), ale daje nowe możliwości.

Usprawnienia w migracjach i ORM

Django 5.2 przynosi też kilka zmian, które choć mniejsze, mogą być istotne dla pracy z bazą danych i migracjami:

  • Nowa operacja migracji AlterConstraint: Pozwala zmienić istniejące ograniczenie (constraint) bazy danych bez jego usuwania i tworzenia od nowa (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). W poprzednich wersjach zmiana np. warunku (condition) w UniqueConstraint czy zmiana flagi DEFERRABLE wymagała faktycznego usunięcia constraint i dodania nowego, co w przypadku dużych tabel bywa kosztowne. AlterConstraint to operacja typu no-op na poziomie migratora Django – sygnalizuje zmianę atrybutów ograniczenia, ale stara się ją zastosować bez pełnej przebudowy. To oznacza krótszy czas migracji i mniej blokad tabel, jeśli nasze bazy wspierają dynamiczną zmianę constraint. Przykład: jeśli mamy unikalność z warunkiem na kolumnie, i chcemy zmodyfikować ten warunek, migrator wygeneruje AlterConstraint zamiast RemoveConstraint + AddConstraint. Warto zajrzeć do dokumentacji migracji po szczegóły, bo użycie tej operacji może zależeć od konkretnego typu bazy.
  • Kolejność pól przy QuerySet.values() zgodna z podaną: Metody QuerySet values() i values_list() od teraz zachowują dokładnie kolejność pól/wyrażeń, w jakiej je określimy w kodzie (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Poprzednio istniały pewne nieintuicyjne reguły ustalania kolejności kolumn (np. przy łączeniu zapytań czy dodawaniu adnotacji), co mogło powodować zamieszanie podczas łączenia QS przez union() itp. Teraz kolejność SELECT-a jest przewidywalna, co ułatwia pracę np. z wynikami jako krotki. Dla większości deweloperów będzie to niezauważalna zmiana (chyba że ktoś polegał na starej, dziwnej kolejności), ale poprawia spójność API.
  • Wsparcie set-returning functions (funkcje zwracające tabelę) w ORM: Dodano atrybut Expression.set_returning = True, który można ustawić w niestandardowych wyrażeniach ORM jeśli reprezentują one funkcje zwracające wiele wierszy (tzw. set-returning, np. niektóre funkcje PG) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Użycie takiej funkcji zwykle wymaga specjalnego traktowania (np. włączenia jej w subzapytanie). Ten mechanizm jest raczej dla zaawansowanych użytkowników i twórców bibliotek, ale pozwoli łatwiej korzystać z tych funkcji w Django. Przykład: jeśli napisaliśmy custom Expression reprezentujący unnest() Postgresa, powinniśmy ustawić mu set_returning=True, aby Django ujął go w subselect zamiast w SELECT głównym.
  • CharField.max_length opcjonalny na SQLite: SQLite nie wymusza limitu długości tekstu w kolumnach VARCHAR, dlatego teraz Django nie będzie wymagać ustawienia max_length dla pól tekstowych przy użyciu SQLite (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Możemy teoretycznie zdefiniować CharField() bez podania max_length (będzie traktowany jako nieograniczony na SQLite). Jednak dla przenaszalności kodu nadal warto ten max_length podawać – na innych bazach jest on potrzebny. Ta zmiana raczej eliminuje pewną niespójność (wcześniej choć SQLite ignoruje limit, Django wciąż wymuszało jego podanie). Użytkownicy SQLite mogą więc zobaczyć ostrzeżenie tylko jeśli naprawdę brakowało max_length.
  • QuerySet.explain() rozszerzony: Jeśli używamy funkcji .explain() do uzyskania planu zapytania SQL, to na PostgreSQL 17+ można teraz przekazać dodatkowe opcje MEMORY i SERIALIZE do komendy EXPLAIN (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Pozwala to uzyskać więcej informacji (np. o wykorzystanej pamięci) w planie zapytania. To ukłon w stronę osób optymalizujących zapytania – jeśli nasz projekt działa na najnowszym Postgresie, mamy pełniejszy dostęp do możliwości EXPLAIN.

Nowa funkcja bazodanowa JSONArray: W module django.db.models.functions pojawiła się funkcja JSONArray, która zwraca tablicę JSON z wartości wybranych pól (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Działa podobnie do innych funkcji agregujących JSON dostępnych w PostgreSQL. Możemy jej użyć np. tak:

from django.db.models import JSONArray, F
qs = Order.objects.annotate(items_array=JSONArray('items__name', 'items__quantity'))

Jeśli model Order ma powiązane items, powyższe zapytanie zwróci pole items_array będące listą JSON zawierającą nazwy i ilości pozycji. W praktyce Django wygeneruje SQL używający funkcji JSON (PostgreSQL) lub odpowiednika w danej bazie. To przydatne do pobierania zagregowanych danych w formacie JSON bezpośrednio z bazy (np. listy wszystkich powiązanych elementów). Wcześniej trzeba było to robić ręcznie przez Value i Concat lub raw SQL. Teraz mamy wygodny wbudowany mechanizm.

Podsumowując, zmiany w warstwie ORM/migracji to głównie rozszerzenie funkcjonalności i drobne korekty poprawiające developer experience. Większość z nich nie wymaga żadnych zmian w kodzie istniejących aplikacji, a jedynie daje nowe narzędzia do wykorzystania w razie potrzeby.

Michael LaRosa - Unsplash

Ulepszenia w systemie e-mail (django.core.mail)

Moduł wysyłania maili otrzymał parę drobnych ulepszeń, które mogą ułatwić obsługę zaawansowanych scenariuszy:

  • Załączniki jako namedtuple: W obiektach EmailMessage i EmailMultiAlternatives dotychczas lista załączników (attachments) była listą krotek (nazwa pliku, zawartość, MIME type). W Django 5.2 elementy tej listy to namedtuple zamiast zwykłych krotek (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dzięki temu możemy odwoływać się do pól załącznika po nazwie (np. attachment.name, attachment.content) zamiast pamiętać indeksy 0,1,2. Podobnie EmailMultiAlternatives.alternatives (lista alternatywnych treści, np. wersja HTML e-maila) jest teraz listą namedtuple zamiast krotek (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Ta zmiana nie wpływa na wysyłkę, ale jeśli gdzieś w kodzie iterujemy po email.attachments i odwołujemy się przez indeks, możemy również użyć czytelniejszych atrybutów.
  • Dodawanie alternatyw tylko przez metodę: Związana z powyższym jest zmiana backwards-incompatible: właściwość EmailMultiAlternatives.alternatives stała się tylko do odczytu – dodawanie pozycji do alternatywnych treści maila jest obsługiwane teraz wyłącznie przez metodę attach_alternative() (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Innymi słowy, nie powinniśmy modyfikować bezpośrednio listy email.alternatives. Jeśli ktoś tak robił (np. email.alternatives.append(...)), powinien zmienić kod na użycie w/w metody. W zamian zyskujemy gwarancję spójności typów (namedtuple) i być może przyszłe rozszerzenia.

Nowa metoda body_contains(): Klasa EmailMessage (i jej pochodne) ma teraz wygodną metodę body_contains(substring) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Zwraca ona True/False informując, czy dany ciąg znaków występuje w treści e-maila lub w którejkolwiek z alternatywnych części typu text/ (czyli np. w wersji HTML maila). Przykład użycia:

email = EmailMessage(subject="Test", body="Hello **world**")
email.attach_alternative("<p>Hello <strong>world</strong></p>", "text/html")
assert email.body_contains("world")  # True, znaleziono w body i w HTML
assert email.body_contains("<strong>")  # False, szuka w czystym tekście

Ta funkcja jest przydatna zwłaszcza w testach – możemy łatwo sprawdzić, czy wygenerowany email zawiera spodziewaną treść, bez ręcznego parsowania MIME i części. Wewnątrz sprawdza zarówno email.body (zakładając że to text/plain) jak i wszystkie załączone części o typie tekstowym (np. text/html) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). To drobne ułatwienie poprawia czytelność testów dotyczących wysyłki maili.

Drobne zmiany i usprawnienia w innych obszarach

Na koniec warto wymienić kilka pomniejszych zmian w Django 5.2, które mogą mieć wpływ na specyficzne obszary:

  • Admin: Szablon bazowy admina (admin/base.html) posiada nowy block extrabody umożliwiający wstrzyknięcie własnego kodu tuż przed zamknięciem tagu </body> (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dzięki temu rozszerzenia admina lub własne szablony mogą łatwiej dodawać np. dodatkowe skrypty JS na końcu strony. Ponadto, pola typu URLField wyświetlane w adminie będą teraz automatycznie renderowane jako klikalne linki (anchor <a href="...">) zamiast czystego tekstu (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), co poprawia użyteczność interfejsu administratora.
  • Admindocs: Dokumentacja modeli generowana przez django.contrib.admindocs została ograniczona tylko do użytkowników mających uprawnienia view lub change dla danego modelu (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Wcześniej potencjalnie każdy zalogowany w adminie mógł podejrzeć dokumentację modeli; teraz jest to bardziej restrykcyjne. Dodatkowo, składnia linków w docstringach obsługuje teraz własny tekst linku przez format :role:\tekst `` (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), umożliwiając ładniejsze formatowanie odnośników.
  • Bezpieczeństwo haseł: Domyślna liczba iteracji algorytmu PBKDF2 dla hashowania haseł wzrosła z  870 000 do 1 000 000 (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). To zwiększa bezpieczeństwo (utrudnia ataki brute-force), ale nie wymaga od nas żadnej zmiany – Django automatycznie zadba o przepisanie hashy przy najbliższej zmianie hasła przez użytkownika. Wydajność nie powinna ucierpieć znacząco na współczesnych serwerach.
  • Filtracja danych w raportach błędów: Obiekt SafeExceptionReporterFilter (stosowany np. do wycinania wrażliwych danych w debugu) uznaje teraz każdą ustawienie/zmienną, której nazwa zawiera AUTH, za wrażliwą (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Czyli np. jeśli mamy jakiś AUTH_TOKEN w settings, to jego wartość zostanie ukryta w raportach błędów. To drobna zmiana zwiększająca bezpieczeństwo wyświetlanych logów.
  • Zarządzanie plikami statycznymi: Funkcja django.contrib.staticfiles.finders.find(path, all=True) dostała zmienioną sygnaturę – parametr all jest deprecate (będzie usunięty) i zastąpiony przez find_all o tej samej funkcjonalności (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Jeśli w projekcie gdzieś wywołujemy find(..., all=True), warto już teraz zmienić na find_all=True aby uniknąć ostrzeżeń.
  • Ostrzeżenie przy runserver: Uruchamiając serwer deweloperski komendą manage.py runserver, zobaczymy teraz w konsoli ostrzeżenie, że ten serwer nie nadaje się do środowiska produkcyjnego (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Wielu początkujących błędnie używa go w produkcji, stąd Django postanowiło bardziej stanowczo to komunikować. Jeśli wiemy co robimy i chcemy to wyłączyć (np. w testach integracyjnych), można ustawić zmienną środowiskową HIDE_PRODUCTION_WARNING=true (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), by ukryć to ostrzeżenie.
  • Inne zmiany w tle: Warto odnotować, że typy bazodanowe PostGIS 3.0 oraz GDAL 3.0 nie są już wspierane (należy używać nowszych wersji tych bibliotek GIS) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Django 5.2 porzuca wsparcie dla PostgreSQL 13 – minimalna wspierana wersja to 14 (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Nowe projekty Django domyślnie używają dla MySQL zestawu znaków utf8mb4 zamiast utf8/utf8mb3 (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django) (to ukłon w stronę obsługi emoji i pełnego Unicode – jeśli ktoś bardzo potrzebuje starego utf8mb3, musi to teraz wymusić w konfiguracji bazy, ale nie jest to zalecane).
  • Kontekst szablonu debug: Kontekstowy procesor debug() nie jest już domyślnie dodawany do ustawień nowo tworzonych projektów (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). W praktyce oznacza to, że w świeżym projekcie settings.py w sekcji TEMPLATES[...]OPTIONS['context_processors'] nie znajdziemy wpisu "django.template.context_processors.debug". Dla działających projektów po aktualizacji nic to nie zmienia (zostaje tak jak było), ale jeśli generujemy nowy projekt 5.2 i chcemy używać zmiennych debug w szablonach, musimy dodać ten processor.
  • alters_data=True dla metod tworzących obiekty: Kilka metod wysokiego poziomu, które zmieniają stan bazy (np. UserManager.create_user(), QuerySet.create(), QuerySet.bulk_create() i ich asynchroniczne odpowiedniki) otrzymało flagę alters_data=True (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Flaga ta jest używana przez mechanizm szablonów, by ostrzec lub zablokować wywołanie takich metod w kontekście renderowania (zapobiega to przypadkowemu tworzeniu danych np. przez wstawienie {{ model.objects.create(...) }} w szablonie). Dla zwykłego kodu aplikacji nie ma to wpływu, ale zapewnia dodatkową ochronę przed niechcianym efektem ubocznym w warstwie prezentacji.

Lista drobnych zmian jest dłuższa (np. w GIS dodano obsługę typów geometrycznych curved, w syndication feedy mogą teraz deklarować style XSL przez atrybut stylesheets (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), itp.), ale powyżej skupiliśmy się na tych, które mogą dotyczyć większości programistów Django na co dzień.

3. Porównanie z wcześniejszą wersją (Django 5.1) – wpływ zmian na istniejące aplikacje

Większość zmian wprowadzonych w Django 5.2 jest kompatybilna wstecznie lub dodaje nowe możliwości bez naruszania istniejącego kodu. Oznacza to, że aktualizacja z Django 5.1 do 5.2 powinna przebiec dość gładko, o ile nasz projekt nie korzysta z funkcjonalności, które zostały wycofane lub zmienione. Poniżej podsumowujemy, jak nowe funkcje wpływają na projekty napisane pod Django 5.1:

  • Nowe funkcjonalności są opcjonalne: Takie dodatki jak composite primary keys, automatyczne importy w shellu, dekorator simple_block_tag, nowe widgety formularzy czy asynchroniczne metody w auth nie wymagają od nas żadnych zmian – możemy zacząć z nich korzystać w kodzie, ale stary kod będzie działał jak dotychczas. Np. jeśli nie używamy composite PK, nic się nie zmienia. Jeśli nie wprowadzamy własnego BoundField, domyślne zachowanie jest identyczne jak w 5.1.
  • Zmiany w ustawieniach baz danych: Jeśli nasz projekt działa na PostgreSQL 13 lub starszym, należy zaplanować aktualizację bazy do wersji 14+, ponieważ Django 5.2 nie wspiera już PG13 (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Próba użycia z niewspieraną bazą może skutkować błędami przy migracjach lub uruchamianiu połączenia. Podobnie w przypadku integracji GIS – upewnijmy się, że korzystamy z PostGIS >= 3.1 i GDAL >= 3.1 (starsze 3.0 zostały porzucone) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django).
  • Zachowanie MySQL: Zmiana domyślnego charsetu na utf8mb4 jest z reguły zmianą pozytywną (lepsza obsługa Unicode). Jeśli jednak nasz stary projekt zakładał utf8 (alias dla utf8mb3), warto upewnić się, że migracja schematu bazy przebiegnie poprawnie. W razie potrzeby można wymusić stary charset w ustawieniach DB (opcja OPTIONS {'charset': 'utf8mb3'}) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), choć zaleca się migrację danych na utf8mb4. Dla nowych tabel Django 5.2 i tak użyje utf8mb4 domyślnie.
  • Deprecacje i zmiany w API: Sprawdźmy w kodzie, czy nie używamy funkcji/parametrów oznaczonych do usunięcia:
  • Potencjalne subtelne różnice: Choć starano się unikać regresji, pewne poprawki mogą zmienić zachowanie, na którym nieświadomie polegał nasz kod:
    • Kolejność wyników z QuerySet.values() teraz jest deterministyczna i zgodna z kolejnością pól w kodzie (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Jeśli gdzieś zakładaliśmy konkretny porządek kolumn wynikowych w oparciu o poprzednie heurystyki (mało prawdopodobne), możemy dostać dane w innej kolejności – warto to przetestować, np. jeśli budujemy na nich jakieś struktury.
    • Zmiana flagi alters_data=True w pewnych metodach (create(), create_user() itd.) oznacza, że nie można ich już wywoływać w szablonach (wcześniej było to możliwe, choć stanowczo niewskazane). Jeśli z jakiegoś powodu mieliśmy logikę tworzącą obiekty w trakcie renderowania szablonu, po aktualizacji do 5.2 to podejście przestanie działać (co jest zmianą na plus – tego i tak nie powinno się robić).
    • Domyślny context processor debug jest wyłączony w nowych projektach. W istniejących projektach po prostu pozostanie, dopóki go sami nie usuniemy. Ale jeżeli np. porównujemy ustawienia nowego projektu 5.2 ze starym 5.1, zauważymy tę różnicę. Nie wpływa to jednak na działanie aktualizowanego projektu.
    • Jeżeli korzystaliśmy z niestandardowych obejść braku composite PK (np. własne mechanizmy), teraz warto rozważyć użycie natywnej obsługi. Jednak migracja istniejącego modelu na composite PK nie jest wspierana automatycznie (Composite primary keys | Django documentation | Django), więc jeśli planujemy refactoring kluczy, trzeba to zrobić ostrożnie (np. poprzez utworzenie nowej tabeli i przeniesienie danych, albo manualne operacje DB i --fake migracje (Composite primary keys | Django documentation | Django)).
    • Modele ze złożonym kluczem nie zadziałają w adminie – jeśli planujemy je dodać do istniejącego projektu i korzystać z admin, napotkamy ograniczenie (admin wyświetli błąd). Trzeba poczekać na pełne wsparcie lub ewentualnie zaimplementować własne mechanizmy admin dla takich modeli.

Ogólnie, aktualizacja z 5.1 do 5.2 nie powinna wymagać dużych zmian w kodzie aplikacji. Zaleca się oczywiście uruchomienie pełnej suite testów po migracji oraz przejrzenie ostrzeżeń (Django przy starcie i w testach wypisuje RemovedInDjango60Warning dla rzeczy planowanych do usunięcia). Zwróćmy uwagę na ostrzeżenia dotyczące deprecacji wymienionych wyżej elementów i poprawmy je zawczasu, aby mieć święty spokój przed wydaniem Django 6.0.

Warto dodać, że Django 5.2 jest wydaniem Long-Term Support (LTS) (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django), co oznacza, że po stabilnym wydaniu będzie otrzymywać poprawki bezpieczeństwa przez następne ~3 lata. Dlatego tym bardziej opłaca się z czasem zaktualizować projekty z poprzedniego LTS (4.2) czy 5.1 do wersji 5.2.

4. Nowe API i funkcjonalności – lista i zastosowanie

Dla czytelności, zebraliśmy w jednym miejscu kluczowe nowe elementy API Django 5.2, które zostały opisane powyżej. Ta lista może służyć jako szybkie podsumowanie tego, co doszło w tej wersji:

forms.Media – klasa Script: Nowy typ obiektu do deklaracji plików JS wewnątrz formularzy z możliwością dodania atrybutów HTML. Przykład użycia:

class MyForm(forms.Form):
    class Media:
        js = [forms.widgets.Script('myapp/script.js', defer=True)]

Spowoduje to wyrenderowanie tagu <script src="myapp/script.js" defer></script> (Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django). Dzięki temu możemy dodawać defer, async, type czy inne atrybuty do naszych skryptów osadzanych przez formularze, co wcześniej wymagało niestandardowych rozwiązań.

Jak widać, lista jest długa – Django 5.2 dostarcza sporo nowości. W codziennej pracy najpewniej najbardziej odczujemy takie rzeczy jak automatyczne importy w shellu, obsługa composite PK (jeśli mieliśmy taką potrzebę, to przełomowa zmiana), wygodniejsze reverse() z query, czy asynchroniczne ulepszenia jeśli tworzymy async views. Inne zmiany choć mniejsze, sumują się na bardziej dopracowany framework.

Ihor Malytskyi - Unsplash

5. Możliwe problemy i wskazówki dotyczące migracji do Django 5.2

Przy planowaniu migracji istniejącego projektu do Django 5.2 Beta (a wkrótce stabilnej wersji) warto wziąć pod uwagę kilka kwestii:

Na koniec, przy aktualizacji do Django 5.2 Beta 1 pamiętajmy, że to nadal wersja testowa – nie zaleca się wrzucania jej na produkcję. Warto jednak już teraz wypróbować nowe funkcje lokalnie czy na branchu testowym i zgłosić ewentualne błędy społeczności Django, aby finalne wydanie w kwietniu 2025 było jak najbardziej dopracowane (Django 5.2 beta 1 released | Weblog | Django) (Django 5.2 beta 1 released | Weblog | Django). Migracja istniejącego projektu powinna być dość prosta, a nowe funkcje – jak widać – mogą zaoszczędzić nam sporo czasu i kodu w przyszłości. Powodzenia w eksplorowaniu Django 5.2!