Stwórz własny walidator URL

Spójrz na poniższy fragment kodu:

path('article/<int:pk>', ArticleDetailsView.as_view()),
re_path(r'^articles/(?P<year>[0-9]{4})/$', ArticlesListView.as_view()), 
  1. Czy zdarzyło Ci się definiować podobne adresy url?
  2. Czy chociaż raz narzekałeś na “niezrozumiałe wyrażenia regularne”, których musiałeś użyć, żeby ograniczyć zakres dozwolonych parametrów?
  3. Czy zastanawiałeś się, co tak naprawdę kryje w sobie zapis: <int:pk> ?

Jeśli przynajmniej na jedno pytanie odpowiedziałeś TAK, to ten artykuł jest dla Ciebie! 

Domyślne definiowanie parametrów url

W większości przypadków podczas definiowania adresów url z dodatkowymi parametrami wykorzystujemy schemat z pytania nr 2: nazwa parametru (np. pk) poprzedzona dwukropkiem i typem zmiennej, a całość opakowana w znaczniki <>, np: <int:pk>, <uuid:pk> .

Pierwszy przykład pozwala nam podać jako parametr pk liczbę, zaś drugi – identyfikator UUID.

Co ciekawe, Django oferuje nam w takiej sytuacji (tylko i aż) 5 typów zmiennych, które powinny spełniać większość wymagań programistów:

  • int – dowolna liczba całkowita,
  • str – dowolny niepusty string z wyłączeniem separatora ścieżki '/',
  • slug – dowolny ciąg znaków zawierający litery, cyfry oraz myślnik i podkreślnik, np: przykladowy-slug,
  • uuid – ciąg znaków typu UUID, czyli zawierający grupy znaków rozdzielone myślnikami. Traktowany później przez Django jak zmienna typu UUID (w odróżnieniu od typu slug),
  • path – dowolny niepusty string, z uwględnieniem separatora ścieżki '/'.

Okazuje się jednak, że w niektórych sytuacjach musimy ograniczyć dopuszczalny zakres danych i żadna z powyższych opcji nie jest wystarczająca.

Co zrobić w takim przypadku?

Wyrażenia regularne wewnątrz adresu url

Załóżmy, że w aplikacji stworzymy widok zwracający listę artykułów opublikowanych tylko w wybranym przez użytkownika roku – podanym jako parametr ścieżki url. Jako pierwsza do głowy przyjdzie nam zapewne taka ścieżka:

path('articles/<int:year>', ArticlesListView.as_view()) 

Wiemy, że rok to liczba całkowita, więc informujemy Django, że parametr year będzie typem int.

Ale moment! Jesteśmy przecież zapobiegawczymi programistami i nie chcemy, żeby jakiś użytkownik wpisał nam wartość 123. Wiedząc, że rok to liczba czterocyfrowa, ulepszymy nieco poprzedni url:

re_path(r'^articles/(?P<year>[0-9]{4})/$', ArticlesListView.as_view()), 

Pochwaliliśmy się jednocześnie znajomością wyrażeń regularnych i funkcji re_path!
Brawo my! 🙂 

Ale hola, hola, nie tak szybko. Przecież liczbą czterocyfrową jest też 1234, a zwracane artykuły nie pochodzą z XIII wieku…

Czas na kolejne podejście! Ograniczmy zakres wpisywanych dat tak, aby były to wartości większe od 1900.

re_path(r'^articles/(?P<year>[1-2]{1}[0-9][3])/$', ArticlesListView.as_view()), 

Chyba się udało. Ale co będzie, jeśli zechcemy jeszcze bardziej ograniczyć ten zakres, np. do przedziału 1996 – 2020? Nie wiem jak Wy, ale mnie takie skomplikowane regexy przyprawiają o zawrót głowy. Przecież nasz kod nie powienien wyglądać jak tajny szyfr, który zrozumieją tylko nieliczni!

Jak więc nie zwariować, a jednocześnie osiągnąć cel?

Customowe klasy PathConverter

Okazuje się, że istnieje bardzo proste, choć nie tak oczywiste, rozwiązanie. Zdefiniowane przez Django typy zmiennych, które wymieniłam wyżej, to tzw. konwertery ścieżek, zaimplementowane w pakiecie django.url.converters. Odpowiedzialne są za nie odpowiednio klasy: IntConverter, StringConverter, SlugConverter, UUIDConverter oraz PathConverter.

Spójrzmy, jak wygląda pierwsza z powyższych klas:

class IntConverter:
    regex = '[0-9]+'

    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return str(value) 

Nie ma tu żadnej magii! Zdefiniowany jest tylko regex, który waliduje poprawność parametru ścieżki oraz dwie metody:

  • to_python(self, value) – wykonująca konwersję parametru do zmiennej pythonowej, przetwarzanej dalej w kodzie (czyli w tym przypadku ze stringa do inta).
  • to_url(self, value)– operacja odwrotna, czyli konwersja wartości wykorzystywanej w kodzie do tej, która znajdzie się w adresie url (w tym przypadku z inta do stringa).

Jeśli któraś z powyższych metod nie będzie w stanie wykonać konwersji, powinna ona wyrzucić wyjątek ValueError, który w rezultacie zwróci nam błąd 404 (w przypadku to_python) lub NoReverseMatch (w przypadku to_url).

No dobra, skoro twórcy Django dali radę, możemy i my. Nadpiszmy więc powyższą klasę tak, żeby spełniony był założony wcześniej warunek:

class YearConverter(IntConverter):

    def to_python(self, value):
        value = int(value)
        if value >= 1996 and value <= 2020:
            return value
        raise ValueError 

W ten sposób dodaliśmy walidację, której warunek jest jasny i zrozumiały dla każdego: wartość parametru nie może być mniejsza od 1996 ani większa od 2020.

Teraz zostało jeszcze zarejestrowanie nowego konwertera.

W pliku urls.py dodajmy linijkę:

register_converter(YearConverter, 'yyyy') 

W ten sposób stworzyliśmy nowy typ parametru url – yyyy, który możemy wykorzystać w następującej ścieżce:

path('articles/<yyyy:year>', ArticlesListView.as_view()) 

Kod wygląda teraz czytelnie, a jednocześnie spełnia założenia 😉 .

Podsumowanie

Możliwości konwerterów ścieżek są ograniczone tylko i wyłącznie naszą wyobraźnią. Możemy dowolnie nadpisywać istniejące lub tworzyć nowe klasy i definiować w nich własną logikę, a także (jeśli ktoś bardzo je lubi) bardzo skomplikowane regexy. 

Dzięki użyciu własnych konwerterów wynosimy walidację parametrów do oddzielnych klas, co daje nam możliwość użycia ich w wielu ścieżkach, przez co nie duplikujmey kodu, a zapewniamy jego przejrzystość i czytelność 😉 .

A może zdarzyło Ci się już tworzyć własne konwertery? Jeśli tak, to podziel się w komentarzu swoimi doświadczeniami, a może nawet kawałkiem kodu, z którego jesteś dumny!

5 2 votes
Article Rating
guest
4 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Adrian
6 miesięcy temu

Bardzo ciekawy blog!

Tomasz
Tomasz
5 miesięcy temu

W warunku if value >= 1996 or value <= 2020 nie powinno być and?