Aplikacja webowa w Pythonie – Flask – Routing – #3

W poprzednich odcinkach tutorialu stworzyliśmy strukturę aplikacji, do której podłączony został layout strony głównej. Jak sprawić, aby mogło zaistnieć więcej podstron niż jedna? Zaraz się dowiecie :)
Ładne URL-e
Dla twórców Flaska od początku ważne były estetyczne URL-e, dlatego utworzenie ich jest zaskakująco proste. Tak naprawdę wystarczy nam teraz dostęp do pliku views.py.
Skrypt, który pozwalał nam wyświetlić zawartość strony głównej wyglądał mniej więcej tak:
1 2 3 |
@app.route('/') def index(): return 'Strona główna' |
Skupmy się teraz na pierwszej linijce tego krótkiego kodu. Czym jest route()? Według dokumentacji – dekoratorem. Jeśli chciałbyś dowiedzieć się, czym są we Flasku dekoratory, to ta sama dokumentacja ma dość dziwną ich definicję, a mianowicie:
Python has a really interesting feature called function decorators. This allows some really neat things for web applications. Because each view in Flask is a function, decorators can be used to inject additional functionality to one or more functions.
Czyli krótko mówiąc, chodzi o wstrzykiwanie pewnych dodatkowych opcji w funkcję wyświetlającą konkretny view. Funkcją wyświetlającą jest w tym przypadku oczywiście index(), a użyte wcześniej app.route("/") pozwala przekierować to, co jest zwracane przez funkcję pod konkretny adres – w tym wypadku macierzysty. Co w takim razie wydarzy się, jeżeli "/" zamienimy na "/post"? Jak łatwo się domyślić, zawartość strony głównej zniknie (wyświetli się 404 Not Found), a napis “Strona główna” pojawi się dopiero, gdy dopiszemy do urla adres podstrony.
Wykorzystajmy to w sensowny sposób.
Weźmy pod lupę kod, który stworzyliśmy w poprzednim wpisie. W layoucie wyświetliliśmy menu z dwoma obiektami – stroną “o mnie” oraz kontaktem. Postarajmy się, aby każda z nich mogła zostać samodzielnie wyświetlona.
Wcześniej plik views.py wyglądał tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from flask import render_template from hello_world import app @app.route('/') def index(): user = {'name': 'Alicja'} navigation = [ { 'href': 'omnie', 'caption': 'O mnie' }, { 'href': 'kontakt', 'caption': 'Kontakt' } ] return render_template('index.html', user=user, navigation=navigation) |
Musimy do niego dopisać dwa kolejne route’y.
Pierwszy z nich, ten dla strony o autorze, mógłby wyglądać tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@app.route('/omnie') def about(): user = {'name': 'Alicja'} navigation = [ { 'href': 'omnie', 'caption': 'O mnie' }, { 'href': 'kontakt', 'caption': 'Kontakt' } ] return render_template('about.html', user=user, navigation=navigation) |
Od razu można zauważyć, że tego typu kod będzie działać (jeśli tylko stworzymy template about.html oparty na base.html), ale niestety jest sprzeczny z zasadą DRY. Jak go ulepszyć? Flask proponuje ciekawe rozwiązanie – context processors. Zdefiniowane w nich zmienne (w postaci słownika) zostają automatycznie dodane podczas tworzenia się każdego z widoków. Warto pamiętać, że wykonuje się to przy generowaniu każdego template’u – w tym przypadku będzie to jednak ich zaleta.
Kod naszego procesora dodajmy w pliku views.py (pod routingiem) i usuńmy definicję zmiennych ze skryptów dla poszczególnych podstron. Całość powinna się teraz prezentować tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from flask import render_template from hello_world import app @app.route('/') def index(): return render_template('index.html') @app.route('/omnie') def about(): return render_template('about.html') @app.route('/kontakt') def contact(): return render_template('contact.html') @app.context_processor def inject_variables(): return dict( user = {'name': 'Alicja'}, navigation = [ { 'href': '/', 'caption': 'Home' }, { 'href': 'omnie', 'caption': 'O mnie' }, { 'href': 'kontakt', 'caption': 'Kontakt' } ]) |
Jak widać, context processor, zdefiniowany funkcją inject_variables() pozwala nam uniknąć powtarzania tego samego kodu przy każdej podstronie. Zauważyłeś zapewne, że dodałam do nawigacji również zmienną z opisem “Home”, która przekierowuje nas na stronę główną – gdybyśmy nie skorzystali z context processora, taki dodatek musielibyśmy dopisać aż trzy razy – przy każdej podstronie.
Stwórz teraz template’y dla pozostałych podstron.
about.html:
1 2 3 4 5 6 7 8 9 10 11 |
{% extends "base.html" %} {% block title %}O mnie{% endblock %} {% block content %} <h2>O mnie</h2> <p> Witaj na podstronie o autorze {% if user %} {{ user.name | striptags }} {% endif %} </p> {% endblock %} |
contact.html:
1 2 3 4 5 6 7 8 9 10 11 |
{% extends "base.html" %} {% block title %}Kontakt{% endblock %} {% block content %} <h2>Kontakt</h2> <p> Witaj na podstronie kontakt {% if user %} {{ user.name | striptags }} {% endif %} </p> {% endblock %} |
Nadszedł czas na uruchomienie aplikacji – wszystko działa jak trzeba, a flaskowy routing pozwala na wyświetlenie konkretnego layoutu na każdej podstronie :).
URL-e w HTMLu
Zwróciłeś już pewnie uwagę, że urle w naszej nawigacji nie są zahardkodowane, tylko umieszczone w zmiennych, które wywołujemy w bazowej templatce:
1 2 3 4 5 |
<ul id="navigation"> {% for item in navigation %} <li><a href="{{ item.href }}">{{ item.caption }}</a></li> {% endfor %} </ul> |
To dobre podejście, ale Flask proponuje również inne. Możemy skorzystać z tego, że w views.py każdy routing ma swoją funkcję i w base.html odwołać się do niej przez url_for:
1 2 3 4 5 |
<ul id="navigation"> <li><a href="{{url_for('index')}}">Home</a></li> <li><a href="{{url_for('about')}}">O mnie</a></li> <li><a href="{{url_for('contact')}}">Kontakt</a></li> </ul> |
Wtedy na spokojnie możemy pozbyć się definicji navigation z context processora…
1 2 3 4 5 |
@app.context_processor def inject_variables(): return dict( user = {'name': 'Alicja'} ) |
… i całość wciąż będzie śmigać :)
Zmienne w adresie URL?
Ostatnim tematem, który poruszę w tym poście są zmienne w adresie URL. O co mi chodzi? Wyobraź sobie, że tworzysz bloga, w którym wyświetlane będą posty. Niezależnie od tego, ile tych postów masz, chciałbyś móc wyświetlić każdy z nich, bez tworzenia osobnych routingów. Tutaj przydadzą się właśnie zmienne.
Przyjmijmy, że na naszej stronie chcemy wyświetlić właśnie posty. W views.py potrzebujemy dodać routing dla strony z listą postów:
1 2 3 |
@app.route('/posty') def posts(): return render_template('posts.html') |
Oraz dla pojedynczego posta:
1 2 3 |
@app.route('/post/<int:post_id>') def post(post_id): return render_template('post.html', post_id = post_id) |
Jak widzisz, w funkcji route() można zdefiniować również zmienne z URL-a. Wykorzystujemy do tego nawiasy trójkątne: <variable>. Opcjonalny jest również tak zwany konwerter, czyli w tym przypadku int:. Możliwe konwertery to:
- string – domyślny, akceptuje dowolny tekst bez “/”
- int – akceptuje integery
- float – liczby zmiennoprzecinkowe
- path – podobny do stringa, ale akceptuje również “/”
- UUID – dla łańcuchów UUID
Jeżeli wybierzemy, tak jak w przykładzie, konwerter int, to wpisując do URL-a coś innego niż integera (np. /post/fajny-post), wywołamy 404.
Aby nasza aplikacja mogła działać poprawnie, musimy zdefiniować również same posty. Dodajmy je do context processora:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@app.context_processor def inject_variables(): return dict( user = {'name': 'Alicja'}, posts = [ { 'post_id': 0, 'title': 'Post numer 0' }, { 'post_id': 1, 'title': 'Post numer 1' }, { 'post_id': 2, 'title': 'Post numer 2' }] ) |
Utwórzmy również template’y, czyli post.html, wyświetlający pojedynczy post:
1 2 3 4 5 6 7 8 9 10 11 12 |
{% extends "base.html" %} {% block title %}Posty{% endblock %} {% block content %} <h2>Post</h2> <p> {% if posts[post_id] %} Oto post - {{ posts[post_id].title }}! {% else %} Posta nie znaleziono :( {% endif %} </p> {% endblock %} |
Oraz posts.html, wyświetlający listę postów:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{% extends "base.html" %} {% block title %}Posty{% endblock %} {% block content %} <h2>Posty</h2> <p> Witaj na podstronie o postach {% if user %} {{ user.name | striptags }} {% endif %} {% for item in posts %} <li><a href="{{url_for('post', post_id = item.post_id)}}">{{ item.title }}</a></li> {% endfor %} </p> {% endblock %} |
Zauważ ciekawą konstrukcją związaną z url_for, a mianowicie: {{url_for('post', post_id = item.post_id)}}. Okazuje się, że funkcja ta również może przyjmować parametry, dzięki czemu podlinkowanie wszystkich postów po kolei staje się bardzo proste i czytelne. Do każdego wywołania pętli for tworzymy odpowiedni link, prowadzący do konkretnego posta.
Na koniec wystarczy dodać do nawigacji w base.html link do podstrony ze wszystkimi postami:
1 2 3 4 5 6 |
<ul id="navigation"> <li><a href="{{url_for('index')}}">Home</a></li> <li><a href="{{url_for('posts')}}">Posty</a></li> <li><a href="{{url_for('about')}}">O mnie</a></li> <li><a href="{{url_for('contact')}}">Kontakt</a></li> </ul> |
Wtedy całość prezentuje się poprawnie :). Kolejny odcinek – o upiększaniu naszej aplikacji – już wkrótce!